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