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