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