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