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.util.ArrayList;
012import java.util.Arrays;
013import java.util.List;
014import java.util.Locale;
015import java.util.regex.Matcher;
016import java.util.regex.Pattern;
017
018import javax.swing.BorderFactory;
019import javax.swing.JLabel;
020import javax.swing.JPanel;
021import javax.swing.JSeparator;
022import javax.swing.JTabbedPane;
023import javax.swing.UIManager;
024import javax.swing.event.ChangeEvent;
025import javax.swing.event.ChangeListener;
026import javax.swing.event.DocumentEvent;
027import javax.swing.event.DocumentListener;
028
029import org.openstreetmap.josm.Main;
030import org.openstreetmap.josm.data.coor.CoordinateFormat;
031import org.openstreetmap.josm.data.coor.EastNorth;
032import org.openstreetmap.josm.data.coor.LatLon;
033import org.openstreetmap.josm.gui.ExtendedDialog;
034import org.openstreetmap.josm.gui.widgets.HtmlPanel;
035import org.openstreetmap.josm.gui.widgets.JosmTextField;
036import org.openstreetmap.josm.tools.GBC;
037import org.openstreetmap.josm.tools.Utils;
038import org.openstreetmap.josm.tools.WindowGeometry;
039
040public class LatLonDialog extends ExtendedDialog {
041    private static final Color BG_COLOR_ERROR = new Color(255, 224, 224);
042
043    public JTabbedPane tabs;
044    private JosmTextField tfLatLon, tfEastNorth;
045    private LatLon latLonCoordinates;
046    private EastNorth eastNorthCoordinates;
047
048    private static final Double ZERO = 0.0;
049    private static final String DEG = "\u00B0";
050    private static final String MIN = "\u2032";
051    private static final String SEC = "\u2033";
052
053    private static final char N_TR = LatLon.NORTH.charAt(0);
054    private static final char S_TR = LatLon.SOUTH.charAt(0);
055    private static final char E_TR = LatLon.EAST.charAt(0);
056    private static final char W_TR = LatLon.WEST.charAt(0);
057
058    private static final Pattern P = Pattern.compile(
059            "([+|-]?\\d+[.,]\\d+)|"             // (1)
060            + "([+|-]?\\d+)|"                   // (2)
061            + "("+DEG+"|o|deg)|"                // (3)
062            + "('|"+MIN+"|min)|"                // (4)
063            + "(\"|"+SEC+"|sec)|"               // (5)
064            + "(,|;)|"                          // (6)
065            + "([NSEW"+N_TR+S_TR+E_TR+W_TR+"])|"// (7)
066            + "\\s+|"
067            + "(.+)");
068
069    private static final Pattern P_XML = Pattern.compile(
070            "lat=[\"']([+|-]?\\d+[.,]\\d+)[\"']\\s+lon=[\"']([+|-]?\\d+[.,]\\d+)[\"']");
071
072    protected JPanel buildLatLon() {
073        JPanel pnl = new JPanel(new GridBagLayout());
074        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
075
076        pnl.add(new JLabel(tr("Coordinates:")), GBC.std().insets(0, 10, 5, 0));
077        tfLatLon = new JosmTextField(24);
078        pnl.add(tfLatLon, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL).weight(1.0, 0.0));
079
080        pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
081
082        pnl.add(new HtmlPanel(
083                Utils.join("<br/>", Arrays.asList(
084                        tr("Enter the coordinates for the new node."),
085                        tr("You can separate longitude and latitude with space, comma or semicolon."),
086                        tr("Use positive numbers or N, E characters to indicate North or East cardinal direction."),
087                        tr("For South and West cardinal directions you can use either negative numbers or S, W characters."),
088                        tr("Coordinate value can be in one of three formats:")
089                      )) +
090                Utils.joinAsHtmlUnorderedList(Arrays.asList(
091                        tr("<i>degrees</i><tt>&deg;</tt>"),
092                        tr("<i>degrees</i><tt>&deg;</tt> <i>minutes</i><tt>&#39;</tt>"),
093                        tr("<i>degrees</i><tt>&deg;</tt> <i>minutes</i><tt>&#39;</tt> <i>seconds</i><tt>&quot</tt>")
094                      )) +
095                Utils.join("<br/><br/>", Arrays.asList(
096                        tr("Symbols <tt>&deg;</tt>, <tt>&#39;</tt>, <tt>&prime;</tt>, <tt>&quot;</tt>, <tt>&Prime;</tt> are optional."),
097                        tr("You can also use the syntax <tt>lat=\"...\" lon=\"...\"</tt> or <tt>lat=''...'' lon=''...''</tt>."),
098                        tr("Some examples:")
099                      )) +
100                "<table><tr><td>" +
101                Utils.joinAsHtmlUnorderedList(Arrays.asList(
102                        "49.29918&deg; 19.24788&deg;",
103                        "N 49.29918 E 19.24788",
104                        "W 49&deg;29.918&#39; S 19&deg;24.788&#39;",
105                        "N 49&deg;29&#39;04&quot; E 19&deg;24&#39;43&quot;",
106                        "49.29918 N, 19.24788 E",
107                        "49&deg;29&#39;21&quot; N 19&deg;24&#39;38&quot; E",
108                        "49 29 51, 19 24 18",
109                        "49 29, 19 24",
110                        "E 49 29, N 19 24"
111                      )) +
112                "</td><td>" +
113                Utils.joinAsHtmlUnorderedList(Arrays.asList(
114                        "49&deg; 29; 19&deg; 24",
115                        "N 49&deg; 29, W 19&deg; 24",
116                        "49&deg; 29.5 S, 19&deg; 24.6 E",
117                        "N 49 29.918 E 19 15.88",
118                        "49 29.4 19 24.5",
119                        "-49 29.4 N -19 24.5 W",
120                        "48 deg 42&#39; 52.13\" N, 21 deg 11&#39; 47.60\" E",
121                        "lat=\"49.29918\" lon=\"19.24788\"",
122                        "lat='49.29918' lon='19.24788'"
123                    )) +
124                "</td></tr></table>"),
125                GBC.eol().fill().weight(1.0, 1.0));
126
127        // parse and verify input on the fly
128        //
129        LatLonInputVerifier inputVerifier = new LatLonInputVerifier();
130        tfLatLon.getDocument().addDocumentListener(inputVerifier);
131
132        // select the text in the field on focus
133        //
134        TextFieldFocusHandler focusHandler = new TextFieldFocusHandler();
135        tfLatLon.addFocusListener(focusHandler);
136        return pnl;
137    }
138
139    private JPanel buildEastNorth() {
140        JPanel pnl = new JPanel(new GridBagLayout());
141        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
142
143        pnl.add(new JLabel(tr("Projected coordinates:")), GBC.std().insets(0, 10, 5, 0));
144        tfEastNorth = new JosmTextField(24);
145
146        pnl.add(tfEastNorth, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL).weight(1.0, 0.0));
147
148        pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5));
149
150        pnl.add(new HtmlPanel(
151                tr("Enter easting and northing (x and y) separated by space, comma or semicolon.")),
152                GBC.eol().fill(GBC.HORIZONTAL));
153
154        pnl.add(GBC.glue(1, 1), GBC.eol().fill().weight(1.0, 1.0));
155
156        EastNorthInputVerifier inputVerifier = new EastNorthInputVerifier();
157        tfEastNorth.getDocument().addDocumentListener(inputVerifier);
158
159        TextFieldFocusHandler focusHandler = new TextFieldFocusHandler();
160        tfEastNorth.addFocusListener(focusHandler);
161
162        return pnl;
163    }
164
165    protected void build() {
166        tabs = new JTabbedPane();
167        tabs.addTab(tr("Lat/Lon"), buildLatLon());
168        tabs.addTab(tr("East/North"), buildEastNorth());
169        tabs.getModel().addChangeListener(new ChangeListener() {
170            @Override
171            public void stateChanged(ChangeEvent e) {
172                switch (tabs.getModel().getSelectedIndex()) {
173                    case 0: parseLatLonUserInput(); break;
174                    case 1: parseEastNorthUserInput(); break;
175                    default: throw new AssertionError();
176                }
177            }
178        });
179        setContent(tabs, false);
180    }
181
182    public LatLonDialog(Component parent, String title, String help) {
183        super(parent, title, new String[] {tr("Ok"), tr("Cancel")});
184        setButtonIcons(new String[] {"ok", "cancel"});
185        configureContextsensitiveHelp(help, true);
186
187        build();
188        setCoordinates(null);
189    }
190
191    public boolean isLatLon() {
192        return tabs.getModel().getSelectedIndex() == 0;
193    }
194
195    public void setCoordinates(LatLon ll) {
196        if (ll == null) {
197            ll = new LatLon(0, 0);
198        }
199        this.latLonCoordinates = ll;
200        tfLatLon.setText(ll.latToString(CoordinateFormat.getDefaultFormat()) + ' ' + ll.lonToString(CoordinateFormat.getDefaultFormat()));
201        EastNorth en = Main.getProjection().latlon2eastNorth(ll);
202        tfEastNorth.setText(en.east()+" "+en.north());
203        setOkEnabled(true);
204    }
205
206    public LatLon getCoordinates() {
207        if (isLatLon()) {
208            return latLonCoordinates;
209        } else {
210            if (eastNorthCoordinates == null) return null;
211            return Main.getProjection().eastNorth2latlon(eastNorthCoordinates);
212        }
213    }
214
215    public LatLon getLatLonCoordinates() {
216        return latLonCoordinates;
217    }
218
219    public EastNorth getEastNorthCoordinates() {
220        return eastNorthCoordinates;
221    }
222
223    protected void setErrorFeedback(JosmTextField tf, String message) {
224        tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1));
225        tf.setToolTipText(message);
226        tf.setBackground(BG_COLOR_ERROR);
227    }
228
229    protected void clearErrorFeedback(JosmTextField tf, String message) {
230        tf.setBorder(UIManager.getBorder("TextField.border"));
231        tf.setToolTipText(message);
232        tf.setBackground(UIManager.getColor("TextField.background"));
233    }
234
235    protected void parseLatLonUserInput() {
236        LatLon latLon;
237        try {
238            latLon = parseLatLon(tfLatLon.getText());
239            if (!LatLon.isValidLat(latLon.lat()) || !LatLon.isValidLon(latLon.lon())) {
240                latLon = null;
241            }
242        } catch (IllegalArgumentException e) {
243            latLon = null;
244        }
245        if (latLon == null) {
246            setErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates"));
247            latLonCoordinates = null;
248            setOkEnabled(false);
249        } else {
250            clearErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates"));
251            latLonCoordinates = latLon;
252            setOkEnabled(true);
253        }
254    }
255
256    protected void parseEastNorthUserInput() {
257        EastNorth en;
258        try {
259            en = parseEastNorth(tfEastNorth.getText());
260        } catch (IllegalArgumentException e) {
261            en = null;
262        }
263        if (en == null) {
264            setErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing"));
265            latLonCoordinates = null;
266            setOkEnabled(false);
267        } else {
268            clearErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing"));
269            eastNorthCoordinates = en;
270            setOkEnabled(true);
271        }
272    }
273
274    private void setOkEnabled(boolean b) {
275        if (buttons != null && !buttons.isEmpty()) {
276            buttons.get(0).setEnabled(b);
277        }
278    }
279
280    @Override
281    public void setVisible(boolean visible) {
282        final String preferenceKey = getClass().getName() + ".geometry";
283        if (visible) {
284            new WindowGeometry(
285                    preferenceKey,
286                    WindowGeometry.centerInWindow(getParent(), getSize())
287            ).applySafe(this);
288        } else {
289            new WindowGeometry(this).remember(preferenceKey);
290        }
291        super.setVisible(visible);
292    }
293
294    class LatLonInputVerifier implements DocumentListener {
295        @Override
296        public void changedUpdate(DocumentEvent e) {
297            parseLatLonUserInput();
298        }
299
300        @Override
301        public void insertUpdate(DocumentEvent e) {
302            parseLatLonUserInput();
303        }
304
305        @Override
306        public void removeUpdate(DocumentEvent e) {
307            parseLatLonUserInput();
308        }
309    }
310
311    class EastNorthInputVerifier implements DocumentListener {
312        @Override
313        public void changedUpdate(DocumentEvent e) {
314            parseEastNorthUserInput();
315        }
316
317        @Override
318        public void insertUpdate(DocumentEvent e) {
319            parseEastNorthUserInput();
320        }
321
322        @Override
323        public void removeUpdate(DocumentEvent e) {
324            parseEastNorthUserInput();
325        }
326    }
327
328    static class TextFieldFocusHandler implements FocusListener {
329        @Override
330        public void focusGained(FocusEvent e) {
331            Component c = e.getComponent();
332            if (c instanceof JosmTextField) {
333                JosmTextField tf = (JosmTextField) c;
334                tf.selectAll();
335            }
336        }
337
338        @Override
339        public void focusLost(FocusEvent e) {
340            // Not used
341        }
342    }
343
344    public static LatLon parseLatLon(final String coord) {
345        final LatLonHolder latLon = new LatLonHolder();
346        final Matcher mXml = P_XML.matcher(coord);
347        if (mXml.matches()) {
348            setLatLonObj(latLon,
349                    Double.valueOf(mXml.group(1).replace(',', '.')), ZERO, ZERO, "N",
350                    Double.valueOf(mXml.group(2).replace(',', '.')), ZERO, ZERO, "E");
351        } else {
352            final Matcher m = P.matcher(coord);
353
354            final StringBuilder sb = new StringBuilder();
355            final List<Object> list = new ArrayList<>();
356
357            while (m.find()) {
358                if (m.group(1) != null) {
359                    sb.append('R');     // floating point number
360                    list.add(Double.valueOf(m.group(1).replace(',', '.')));
361                } else if (m.group(2) != null) {
362                    sb.append('Z');     // integer number
363                    list.add(Double.valueOf(m.group(2)));
364                } else if (m.group(3) != null) {
365                    sb.append('o');     // degree sign
366                } else if (m.group(4) != null) {
367                    sb.append('\'');    // seconds sign
368                } else if (m.group(5) != null) {
369                    sb.append('"');     // minutes sign
370                } else if (m.group(6) != null) {
371                    sb.append(',');     // separator
372                } else if (m.group(7) != null) {
373                    sb.append('x');     // cardinal direction
374                    String c = m.group(7).toUpperCase(Locale.ENGLISH);
375                    if ("N".equals(c) || "S".equals(c) || "E".equals(c) || "W".equals(c)) {
376                        list.add(c);
377                    } else {
378                        list.add(c.replace(N_TR, 'N').replace(S_TR, 'S')
379                                .replace(E_TR, 'E').replace(W_TR, 'W'));
380                    }
381                } else if (m.group(8) != null) {
382                    throw new IllegalArgumentException("invalid token: " + m.group(8));
383                }
384            }
385
386            final String pattern = sb.toString();
387
388            final Object[] params = list.toArray();
389
390            if (pattern.matches("Ro?,?Ro?")) {
391                setLatLonObj(latLon,
392                        params[0], ZERO, ZERO, "N",
393                        params[1], ZERO, ZERO, "E");
394            } else if (pattern.matches("xRo?,?xRo?")) {
395                setLatLonObj(latLon,
396                        params[1], ZERO, ZERO, params[0],
397                        params[3], ZERO, ZERO, params[2]);
398            } else if (pattern.matches("Ro?x,?Ro?x")) {
399                setLatLonObj(latLon,
400                        params[0], ZERO, ZERO, params[1],
401                        params[2], ZERO, ZERO, params[3]);
402            } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) {
403                setLatLonObj(latLon,
404                        params[0], params[1], ZERO, "N",
405                        params[2], params[3], ZERO, "E");
406            } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) {
407                setLatLonObj(latLon,
408                        params[1], params[2], ZERO, params[0],
409                        params[4], params[5], ZERO, params[3]);
410            } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) {
411                setLatLonObj(latLon,
412                        params[0], params[1], ZERO, params[2],
413                        params[3], params[4], ZERO, params[5]);
414            } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) {
415                setLatLonObj(latLon,
416                        params[0], params[1], params[2], params[3],
417                        params[4], params[5], params[6], params[7]);
418            } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) {
419                setLatLonObj(latLon,
420                        params[1], params[2], params[3], params[0],
421                        params[5], params[6], params[7], params[4]);
422            } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) {
423                setLatLonObj(latLon,
424                        params[0], params[1], params[2], "N",
425                        params[3], params[4], params[5], "E");
426            } else {
427                throw new IllegalArgumentException("invalid format: " + pattern);
428            }
429        }
430
431        return new LatLon(latLon.lat, latLon.lon);
432    }
433
434    public static EastNorth parseEastNorth(String s) {
435        String[] en = s.split("[;, ]+");
436        if (en.length != 2) return null;
437        try {
438            double east = Double.parseDouble(en[0]);
439            double north = Double.parseDouble(en[1]);
440            return new EastNorth(east, north);
441        } catch (NumberFormatException nfe) {
442            return null;
443        }
444    }
445
446    private static class LatLonHolder {
447        private double lat;
448        private double lon;
449    }
450
451    private static void setLatLonObj(final LatLonHolder latLon,
452            final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1,
453            final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) {
454
455        setLatLon(latLon,
456                (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1,
457                (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2);
458    }
459
460    private static void setLatLon(final LatLonHolder latLon,
461            final double coord1deg, final double coord1min, final double coord1sec, final String card1,
462            final double coord2deg, final double coord2min, final double coord2sec, final String card2) {
463
464        setLatLon(latLon, coord1deg, coord1min, coord1sec, card1);
465        setLatLon(latLon, coord2deg, coord2min, coord2sec, card2);
466    }
467
468    private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec,
469            final String card) {
470        if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) {
471            throw new IllegalArgumentException("out of range");
472        }
473
474        double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600);
475        coord = "N".equals(card) || "E".equals(card) ? coord : -coord;
476        if ("N".equals(card) || "S".equals(card)) {
477            latLon.lat = coord;
478        } else {
479            latLon.lon = coord;
480        }
481    }
482
483    public String getLatLonText() {
484        return tfLatLon.getText();
485    }
486
487    public void setLatLonText(String text) {
488        tfLatLon.setText(text);
489    }
490
491    public String getEastNorthText() {
492        return tfEastNorth.getText();
493    }
494
495    public void setEastNorthText(String text) {
496        tfEastNorth.setText(text);
497    }
498}