001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Color;
007import java.awt.Component;
008import java.awt.GridBagLayout;
009import java.awt.event.FocusEvent;
010import java.awt.event.FocusListener;
011import java.awt.event.WindowAdapter;
012import java.awt.event.WindowEvent;
013import java.util.Arrays;
014
015import javax.swing.BorderFactory;
016import javax.swing.JLabel;
017import javax.swing.JPanel;
018import javax.swing.JSeparator;
019import javax.swing.JTabbedPane;
020import javax.swing.UIManager;
021import javax.swing.event.DocumentEvent;
022import javax.swing.event.DocumentListener;
023
024import org.openstreetmap.josm.Main;
025import org.openstreetmap.josm.data.coor.CoordinateFormat;
026import org.openstreetmap.josm.data.coor.EastNorth;
027import org.openstreetmap.josm.data.coor.LatLon;
028import org.openstreetmap.josm.gui.ExtendedDialog;
029import org.openstreetmap.josm.gui.widgets.HtmlPanel;
030import org.openstreetmap.josm.gui.widgets.JosmTextField;
031import org.openstreetmap.josm.tools.GBC;
032import org.openstreetmap.josm.tools.Utils;
033import org.openstreetmap.josm.tools.WindowGeometry;
034
035public class LatLonDialog extends ExtendedDialog {
036    private static final Color BG_COLOR_ERROR = new Color(255, 224, 224);
037
038    public JTabbedPane tabs;
039    private JosmTextField tfLatLon, tfEastNorth;
040    private LatLon latLonCoordinates;
041    private EastNorth eastNorthCoordinates;
042
043    protected JPanel buildLatLon() {
044        JPanel pnl = new JPanel(new GridBagLayout());
045        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
046
047        pnl.add(new JLabel(tr("Coordinates:")), GBC.std().insets(0, 10, 5, 0));
048        tfLatLon = new JosmTextField(24);
049        pnl.add(tfLatLon, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL).weight(1.0, 0.0));
050
051        pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
052
053        pnl.add(new HtmlPanel(
054                Utils.join("<br/>", Arrays.asList(
055                        tr("Enter the coordinates for the new node."),
056                        tr("You can separate longitude and latitude with space, comma or semicolon."),
057                        tr("Use positive numbers or N, E characters to indicate North or East cardinal direction."),
058                        tr("For South and West cardinal directions you can use either negative numbers or S, W characters."),
059                        tr("Coordinate value can be in one of three formats:")
060                      )) +
061                Utils.joinAsHtmlUnorderedList(Arrays.asList(
062                        tr("<i>degrees</i><tt>&deg;</tt>"),
063                        tr("<i>degrees</i><tt>&deg;</tt> <i>minutes</i><tt>&#39;</tt>"),
064                        tr("<i>degrees</i><tt>&deg;</tt> <i>minutes</i><tt>&#39;</tt> <i>seconds</i><tt>&quot</tt>")
065                      )) +
066                Utils.join("<br/><br/>", Arrays.asList(
067                        tr("Symbols <tt>&deg;</tt>, <tt>&#39;</tt>, <tt>&prime;</tt>, <tt>&quot;</tt>, <tt>&Prime;</tt> are optional."),
068                        tr("You can also use the syntax <tt>lat=\"...\" lon=\"...\"</tt> or <tt>lat=''...'' lon=''...''</tt>."),
069                        tr("Some examples:")
070                      )) +
071                "<table><tr><td>" +
072                Utils.joinAsHtmlUnorderedList(Arrays.asList(
073                        "49.29918&deg; 19.24788&deg;",
074                        "N 49.29918 E 19.24788",
075                        "W 49&deg;29.918&#39; S 19&deg;24.788&#39;",
076                        "N 49&deg;29&#39;04&quot; E 19&deg;24&#39;43&quot;",
077                        "49.29918 N, 19.24788 E",
078                        "49&deg;29&#39;21&quot; N 19&deg;24&#39;38&quot; E",
079                        "49 29 51, 19 24 18",
080                        "49 29, 19 24",
081                        "E 49 29, N 19 24"
082                      )) +
083                "</td><td>" +
084                Utils.joinAsHtmlUnorderedList(Arrays.asList(
085                        "49&deg; 29; 19&deg; 24",
086                        "N 49&deg; 29, W 19&deg; 24",
087                        "49&deg; 29.5 S, 19&deg; 24.6 E",
088                        "N 49 29.918 E 19 15.88",
089                        "49 29.4 19 24.5",
090                        "-49 29.4 N -19 24.5 W",
091                        "48 deg 42&#39; 52.13\" N, 21 deg 11&#39; 47.60\" E",
092                        "lat=\"49.29918\" lon=\"19.24788\"",
093                        "lat='49.29918' lon='19.24788'"
094                    )) +
095                "</td></tr></table>"),
096                GBC.eol().fill().weight(1.0, 1.0));
097
098        // parse and verify input on the fly
099        //
100        LatLonInputVerifier inputVerifier = new LatLonInputVerifier();
101        tfLatLon.getDocument().addDocumentListener(inputVerifier);
102
103        // select the text in the field on focus
104        //
105        TextFieldFocusHandler focusHandler = new TextFieldFocusHandler();
106        tfLatLon.addFocusListener(focusHandler);
107        return pnl;
108    }
109
110    private JPanel buildEastNorth() {
111        JPanel pnl = new JPanel(new GridBagLayout());
112        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
113
114        pnl.add(new JLabel(tr("Projected coordinates:")), GBC.std().insets(0, 10, 5, 0));
115        tfEastNorth = new JosmTextField(24);
116
117        pnl.add(tfEastNorth, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL).weight(1.0, 0.0));
118
119        pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
120
121        pnl.add(new HtmlPanel(
122                tr("Enter easting and northing (x and y) separated by space, comma or semicolon.")),
123                GBC.eol().fill(GBC.HORIZONTAL));
124
125        pnl.add(GBC.glue(1, 1), GBC.eol().fill().weight(1.0, 1.0));
126
127        EastNorthInputVerifier inputVerifier = new EastNorthInputVerifier();
128        tfEastNorth.getDocument().addDocumentListener(inputVerifier);
129
130        TextFieldFocusHandler focusHandler = new TextFieldFocusHandler();
131        tfEastNorth.addFocusListener(focusHandler);
132
133        return pnl;
134    }
135
136    protected void build() {
137        tabs = new JTabbedPane();
138        tabs.addTab(tr("Lat/Lon"), buildLatLon());
139        tabs.addTab(tr("East/North"), buildEastNorth());
140        tabs.getModel().addChangeListener(e -> {
141            switch (tabs.getModel().getSelectedIndex()) {
142                case 0: parseLatLonUserInput(); break;
143                case 1: parseEastNorthUserInput(); break;
144                default: throw new AssertionError();
145            }
146        });
147        setContent(tabs, false);
148        addWindowListener(new WindowAdapter() {
149            @Override
150            public void windowOpened(WindowEvent e) {
151                tfLatLon.requestFocusInWindow();
152            }
153        });
154    }
155
156    public LatLonDialog(Component parent, String title, String help) {
157        super(parent, title, new String[] {tr("Ok"), tr("Cancel")});
158        setButtonIcons(new String[] {"ok", "cancel"});
159        configureContextsensitiveHelp(help, true);
160
161        build();
162        setCoordinates(null);
163    }
164
165    public boolean isLatLon() {
166        return tabs.getModel().getSelectedIndex() == 0;
167    }
168
169    public void setCoordinates(LatLon ll) {
170        if (ll == null) {
171            ll = LatLon.ZERO;
172        }
173        this.latLonCoordinates = ll;
174        tfLatLon.setText(ll.latToString(CoordinateFormat.getDefaultFormat()) + ' ' + ll.lonToString(CoordinateFormat.getDefaultFormat()));
175        EastNorth en = Main.getProjection().latlon2eastNorth(ll);
176        tfEastNorth.setText(Double.toString(en.east()) + ' ' + Double.toString(en.north()));
177        setOkEnabled(true);
178    }
179
180    public LatLon getCoordinates() {
181        if (isLatLon()) {
182            return latLonCoordinates;
183        } else {
184            if (eastNorthCoordinates == null) return null;
185            return Main.getProjection().eastNorth2latlon(eastNorthCoordinates);
186        }
187    }
188
189    public LatLon getLatLonCoordinates() {
190        return latLonCoordinates;
191    }
192
193    public EastNorth getEastNorthCoordinates() {
194        return eastNorthCoordinates;
195    }
196
197    protected void setErrorFeedback(JosmTextField tf, String message) {
198        tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1));
199        tf.setToolTipText(message);
200        tf.setBackground(BG_COLOR_ERROR);
201    }
202
203    protected void clearErrorFeedback(JosmTextField tf, String message) {
204        tf.setBorder(UIManager.getBorder("TextField.border"));
205        tf.setToolTipText(message);
206        tf.setBackground(UIManager.getColor("TextField.background"));
207    }
208
209    protected void parseLatLonUserInput() {
210        LatLon latLon;
211        try {
212            latLon = LatLon.parse(tfLatLon.getText());
213            if (!LatLon.isValidLat(latLon.lat()) || !LatLon.isValidLon(latLon.lon())) {
214                latLon = null;
215            }
216        } catch (IllegalArgumentException e) {
217            Main.trace(e);
218            latLon = null;
219        }
220        if (latLon == null) {
221            setErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates"));
222            latLonCoordinates = null;
223            setOkEnabled(false);
224        } else {
225            clearErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates"));
226            latLonCoordinates = latLon;
227            setOkEnabled(true);
228        }
229    }
230
231    protected void parseEastNorthUserInput() {
232        EastNorth en;
233        try {
234            en = parseEastNorth(tfEastNorth.getText());
235        } catch (IllegalArgumentException e) {
236            Main.trace(e);
237            en = null;
238        }
239        if (en == null) {
240            setErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing"));
241            latLonCoordinates = null;
242            setOkEnabled(false);
243        } else {
244            clearErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing"));
245            eastNorthCoordinates = en;
246            setOkEnabled(true);
247        }
248    }
249
250    private void setOkEnabled(boolean b) {
251        if (buttons != null && !buttons.isEmpty()) {
252            buttons.get(0).setEnabled(b);
253        }
254    }
255
256    @Override
257    public void setVisible(boolean visible) {
258        final String preferenceKey = getClass().getName() + ".geometry";
259        if (visible) {
260            new WindowGeometry(
261                    preferenceKey,
262                    WindowGeometry.centerInWindow(getParent(), getSize())
263            ).applySafe(this);
264        } else {
265            new WindowGeometry(this).remember(preferenceKey);
266        }
267        super.setVisible(visible);
268    }
269
270    class LatLonInputVerifier implements DocumentListener {
271        @Override
272        public void changedUpdate(DocumentEvent e) {
273            parseLatLonUserInput();
274        }
275
276        @Override
277        public void insertUpdate(DocumentEvent e) {
278            parseLatLonUserInput();
279        }
280
281        @Override
282        public void removeUpdate(DocumentEvent e) {
283            parseLatLonUserInput();
284        }
285    }
286
287    class EastNorthInputVerifier implements DocumentListener {
288        @Override
289        public void changedUpdate(DocumentEvent e) {
290            parseEastNorthUserInput();
291        }
292
293        @Override
294        public void insertUpdate(DocumentEvent e) {
295            parseEastNorthUserInput();
296        }
297
298        @Override
299        public void removeUpdate(DocumentEvent e) {
300            parseEastNorthUserInput();
301        }
302    }
303
304    static class TextFieldFocusHandler implements FocusListener {
305        @Override
306        public void focusGained(FocusEvent e) {
307            Component c = e.getComponent();
308            if (c instanceof JosmTextField) {
309                JosmTextField tf = (JosmTextField) c;
310                tf.selectAll();
311            }
312        }
313
314        @Override
315        public void focusLost(FocusEvent e) {
316            // Not used
317        }
318    }
319
320    /**
321     * Parses the given string as lat/lon.
322     * @param coord String to parse
323     * @return parsed lat/lon
324     * @deprecated use {@link LatLon#parse(String)} instead
325     */
326    @Deprecated
327    public static LatLon parseLatLon(final String coord) {
328        return LatLon.parse(coord);
329    }
330
331    public static EastNorth parseEastNorth(String s) {
332        String[] en = s.split("[;, ]+");
333        if (en.length != 2) return null;
334        try {
335            double east = Double.parseDouble(en[0]);
336            double north = Double.parseDouble(en[1]);
337            return new EastNorth(east, north);
338        } catch (NumberFormatException nfe) {
339            return null;
340        }
341    }
342
343    public String getLatLonText() {
344        return tfLatLon.getText();
345    }
346
347    public void setLatLonText(String text) {
348        tfLatLon.setText(text);
349    }
350
351    public String getEastNorthText() {
352        return tfEastNorth.getText();
353    }
354
355    public void setEastNorthText(String text) {
356        tfEastNorth.setText(text);
357    }
358}