001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.BorderLayout;
008import java.awt.GridBagLayout;
009import java.awt.event.ActionEvent;
010import java.awt.event.KeyEvent;
011import java.util.Optional;
012
013import javax.swing.JLabel;
014import javax.swing.JOptionPane;
015import javax.swing.JPanel;
016import javax.swing.event.DocumentEvent;
017import javax.swing.event.DocumentListener;
018
019import org.openstreetmap.josm.Main;
020import org.openstreetmap.josm.data.Bounds;
021import org.openstreetmap.josm.data.coor.LatLon;
022import org.openstreetmap.josm.data.coor.conversion.LatLonParser;
023import org.openstreetmap.josm.gui.ExtendedDialog;
024import org.openstreetmap.josm.gui.MainApplication;
025import org.openstreetmap.josm.gui.MapView;
026import org.openstreetmap.josm.gui.datatransfer.ClipboardUtils;
027import org.openstreetmap.josm.gui.widgets.JosmTextField;
028import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
029import org.openstreetmap.josm.spi.preferences.Config;
030import org.openstreetmap.josm.tools.GBC;
031import org.openstreetmap.josm.tools.ImageProvider;
032import org.openstreetmap.josm.tools.Logging;
033import org.openstreetmap.josm.tools.OsmUrlToBounds;
034import org.openstreetmap.josm.tools.Shortcut;
035
036/**
037 * Allows to jump to a specific location.
038 * @since 2575
039 */
040public class JumpToAction extends JosmAction {
041
042    private final JosmTextField url = new JosmTextField();
043    private final JosmTextField lat = new JosmTextField();
044    private final JosmTextField lon = new JosmTextField();
045    private final JosmTextField zm = new JosmTextField();
046
047    /**
048     * Constructs a new {@code JumpToAction}.
049     */
050    public JumpToAction() {
051        super(tr("Jump To Position"), (ImageProvider) null, tr("Opens a dialog that allows to jump to a specific location"),
052                Shortcut.registerShortcut("tools:jumpto", tr("Tool: {0}", tr("Jump To Position")),
053                        KeyEvent.VK_J, Shortcut.CTRL), true, "action/jumpto", true);
054        putValue("help", ht("/Action/JumpToPosition"));
055    }
056
057    static class JumpToPositionDialog extends ExtendedDialog {
058        JumpToPositionDialog(String[] buttons, JPanel panel) {
059            super(Main.parent, tr("Jump to Position"), buttons);
060            setContent(panel);
061            setCancelButton(2);
062        }
063    }
064
065    class OsmURLListener implements DocumentListener {
066        @Override
067        public void changedUpdate(DocumentEvent e) {
068            parseURL();
069        }
070
071        @Override
072        public void insertUpdate(DocumentEvent e) {
073            parseURL();
074        }
075
076        @Override
077        public void removeUpdate(DocumentEvent e) {
078            parseURL();
079        }
080    }
081
082    class OsmLonLatListener implements DocumentListener {
083        @Override
084        public void changedUpdate(DocumentEvent e) {
085            updateUrl(false);
086        }
087
088        @Override
089        public void insertUpdate(DocumentEvent e) {
090            updateUrl(false);
091        }
092
093        @Override
094        public void removeUpdate(DocumentEvent e) {
095            updateUrl(false);
096        }
097    }
098
099    /**
100     * Displays the "Jump to" dialog.
101     */
102    public void showJumpToDialog() {
103        if (!MainApplication.isDisplayingMapView()) {
104            return;
105        }
106        MapView mv = MainApplication.getMap().mapView;
107
108        final Optional<Bounds> boundsFromClipboard = Optional
109                .ofNullable(ClipboardUtils.getClipboardStringContent())
110                .map(OsmUrlToBounds::parse);
111        if (boundsFromClipboard.isPresent() && Config.getPref().getBoolean("jumpto.use.clipboard", true)) {
112            setBounds(boundsFromClipboard.get());
113        } else {
114            setBounds(mv.getState().getViewArea().getCornerBounds());
115        }
116        updateUrl(true);
117
118        JPanel panel = new JPanel(new BorderLayout());
119        panel.add(new JLabel("<html>"
120                              + tr("Enter Lat/Lon to jump to position.")
121                              + "<br>"
122                              + tr("You can also paste an URL from www.openstreetmap.org")
123                              + "<br>"
124                              + "</html>"),
125                  BorderLayout.NORTH);
126
127        OsmLonLatListener x = new OsmLonLatListener();
128        lat.getDocument().addDocumentListener(x);
129        lon.getDocument().addDocumentListener(x);
130        zm.getDocument().addDocumentListener(x);
131        url.getDocument().addDocumentListener(new OsmURLListener());
132
133        SelectAllOnFocusGainedDecorator.decorate(lat);
134        SelectAllOnFocusGainedDecorator.decorate(lon);
135        SelectAllOnFocusGainedDecorator.decorate(zm);
136        SelectAllOnFocusGainedDecorator.decorate(url);
137
138        JPanel p = new JPanel(new GridBagLayout());
139        panel.add(p, BorderLayout.NORTH);
140
141        p.add(new JLabel(tr("Latitude")), GBC.eol());
142        p.add(lat, GBC.eol().fill(GBC.HORIZONTAL));
143
144        p.add(new JLabel(tr("Longitude")), GBC.eol());
145        p.add(lon, GBC.eol().fill(GBC.HORIZONTAL));
146
147        p.add(new JLabel(tr("Zoom (in metres)")), GBC.eol());
148        p.add(zm, GBC.eol().fill(GBC.HORIZONTAL));
149
150        p.add(new JLabel(tr("URL")), GBC.eol());
151        p.add(url, GBC.eol().fill(GBC.HORIZONTAL));
152
153        String[] buttons = {tr("Jump there"), tr("Cancel")};
154        LatLon ll = null;
155        double zoomLvl = 100;
156        while (ll == null) {
157            final int option = new JumpToPositionDialog(buttons, panel).showDialog().getValue();
158
159            if (option != 1) return;
160            try {
161                zoomLvl = Double.parseDouble(zm.getText());
162                ll = new LatLon(Double.parseDouble(lat.getText()), Double.parseDouble(lon.getText()));
163            } catch (NumberFormatException ex) {
164                try {
165                    ll = LatLonParser.parse(lat.getText() + "; " + lon.getText());
166                } catch (IllegalArgumentException ex2) {
167                    JOptionPane.showMessageDialog(Main.parent,
168                            tr("Could not parse Latitude, Longitude or Zoom. Please check."),
169                            tr("Unable to parse Lon/Lat"), JOptionPane.ERROR_MESSAGE);
170                }
171            }
172        }
173
174        double zoomFactor = 1/ mv.getDist100Pixel();
175        mv.zoomToFactor(mv.getProjection().latlon2eastNorth(ll), zoomFactor * zoomLvl);
176    }
177
178    private void parseURL() {
179        if (!url.hasFocus()) return;
180        String urlText = url.getText();
181        Bounds b = OsmUrlToBounds.parse(urlText);
182        setBounds(b);
183    }
184
185    private void setBounds(Bounds b) {
186        if (b != null) {
187            final LatLon center = b.getCenter();
188            lat.setText(Double.toString(center.lat()));
189            lon.setText(Double.toString(center.lon()));
190            zm.setText(Double.toString(OsmUrlToBounds.getZoom(b)));
191        }
192    }
193
194    private void updateUrl(boolean force) {
195        if (!lat.hasFocus() && !lon.hasFocus() && !zm.hasFocus() && !force) return;
196        try {
197            double dlat = Double.parseDouble(lat.getText());
198            double dlon = Double.parseDouble(lon.getText());
199            double zoomLvl = Double.parseDouble(zm.getText());
200            url.setText(OsmUrlToBounds.getURL(dlat, dlon, (int) zoomLvl));
201        } catch (NumberFormatException e) {
202            Logging.debug(e.getMessage());
203        }
204    }
205
206    @Override
207    public void actionPerformed(ActionEvent e) {
208        showJumpToDialog();
209    }
210
211    @Override
212    protected void updateEnabledState() {
213        setEnabled(MainApplication.isDisplayingMapView());
214    }
215
216    @Override
217    protected void installAdapters() {
218        super.installAdapters();
219        // make this action listen to mapframe change events
220        MainApplication.addMapFrameListener((o, n) -> updateEnabledState());
221    }
222}