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>" +
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></ul>" +
105                        "<li>48 deg 42&#39; 52.13\" N, 21 deg 11&#39; 47.60\" E</li></ul>"
106                        )),
107                GBC.eol().fill().weight(1.0, 1.0));
108
109        // parse and verify input on the fly
110        //
111        LatLonInputVerifier inputVerifier = new LatLonInputVerifier();
112        tfLatLon.getDocument().addDocumentListener(inputVerifier);
113
114        // select the text in the field on focus
115        //
116        TextFieldFocusHandler focusHandler = new TextFieldFocusHandler();
117        tfLatLon.addFocusListener(focusHandler);
118        return pnl;
119    }
120
121    private JPanel buildEastNorth() {
122        JPanel pnl = new JPanel(new GridBagLayout());
123        pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
124
125        pnl.add(new JLabel(tr("Projected coordinates:")), GBC.std().insets(0,10,5,0));
126        tfEastNorth = new JosmTextField(24);
127
128        pnl.add(tfEastNorth, GBC.eol().insets(0,10,0,0).fill(GBC.HORIZONTAL).weight(1.0, 0.0));
129
130        pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0,5,0,5));
131
132        pnl.add(new HtmlPanel(
133                tr("Enter easting and northing (x and y) separated by space, comma or semicolon.")),
134                GBC.eol().fill(GBC.HORIZONTAL));
135
136        pnl.add(GBC.glue(1, 1), GBC.eol().fill().weight(1.0, 1.0));
137
138        EastNorthInputVerifier inputVerifier = new EastNorthInputVerifier();
139        tfEastNorth.getDocument().addDocumentListener(inputVerifier);
140
141        TextFieldFocusHandler focusHandler = new TextFieldFocusHandler();
142        tfEastNorth.addFocusListener(focusHandler);
143
144        return pnl;
145    }
146
147    protected void build() {
148        tabs = new JTabbedPane();
149        tabs.addTab(tr("Lat/Lon"), buildLatLon());
150        tabs.addTab(tr("East/North"), buildEastNorth());
151        tabs.getModel().addChangeListener(new ChangeListener() {
152            @Override
153            public void stateChanged(ChangeEvent e) {
154                switch (tabs.getModel().getSelectedIndex()) {
155                    case 0: parseLatLonUserInput(); break;
156                    case 1: parseEastNorthUserInput(); break;
157                    default: throw new AssertionError();
158                }
159            }
160        });
161        setContent(tabs, false);
162    }
163
164    public LatLonDialog(Component parent, String title, String help) {
165        super(parent, title, new String[] { tr("Ok"), tr("Cancel") });
166        setButtonIcons(new String[] { "ok", "cancel" });
167        configureContextsensitiveHelp(help, true);
168
169        build();
170        setCoordinates(null);
171    }
172
173    public boolean isLatLon() {
174        return tabs.getModel().getSelectedIndex() == 0;
175    }
176
177    public void setCoordinates(LatLon ll) {
178        if (ll == null) {
179            ll = new LatLon(0,0);
180        }
181        this.latLonCoordinates = ll;
182        tfLatLon.setText(ll.latToString(CoordinateFormat.getDefaultFormat()) + " " + ll.lonToString(CoordinateFormat.getDefaultFormat()));
183        EastNorth en = Main.getProjection().latlon2eastNorth(ll);
184        tfEastNorth.setText(en.east()+" "+en.north());
185        setOkEnabled(true);
186    }
187
188    public LatLon getCoordinates() {
189        if (isLatLon()) {
190            return latLonCoordinates;
191        } else {
192            if (eastNorthCoordinates == null) return null;
193            return Main.getProjection().eastNorth2latlon(eastNorthCoordinates);
194        }
195    }
196
197    public LatLon getLatLonCoordinates() {
198        return latLonCoordinates;
199    }
200
201    public EastNorth getEastNorthCoordinates() {
202        return eastNorthCoordinates;
203    }
204
205    protected void setErrorFeedback(JosmTextField tf, String message) {
206        tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1));
207        tf.setToolTipText(message);
208        tf.setBackground(BG_COLOR_ERROR);
209    }
210
211    protected void clearErrorFeedback(JosmTextField tf, String message) {
212        tf.setBorder(UIManager.getBorder("TextField.border"));
213        tf.setToolTipText(message);
214        tf.setBackground(UIManager.getColor("TextField.background"));
215    }
216
217    protected Double parseDoubleFromUserInput(String input) {
218        if (input == null) return null;
219        // remove white space and an optional degree symbol
220        //
221        input = input.trim();
222        input = input.replaceAll(DEG, "");
223
224        // try to parse using the current locale
225        //
226        NumberFormat f = NumberFormat.getNumberInstance();
227        Number n=null;
228        ParsePosition pp = new ParsePosition(0);
229        n = f.parse(input,pp);
230        if (pp.getErrorIndex() >= 0 || pp.getIndex()<input.length()) {
231            // fall back - try to parse with the english locale
232            //
233            pp = new ParsePosition(0);
234            f = NumberFormat.getNumberInstance(Locale.ENGLISH);
235            n = f.parse(input, pp);
236            if (pp.getErrorIndex() >= 0 || pp.getIndex()<input.length())
237                return null;
238        }
239        return n== null ? null : n.doubleValue();
240    }
241
242    protected void parseLatLonUserInput() {
243        LatLon latLon;
244        try {
245            latLon = parseLatLon(tfLatLon.getText());
246            if (!LatLon.isValidLat(latLon.lat()) || !LatLon.isValidLon(latLon.lon())) {
247                latLon = null;
248            }
249        } catch (IllegalArgumentException e) {
250            latLon = null;
251        }
252        if (latLon == null) {
253            setErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates"));
254            latLonCoordinates = null;
255            setOkEnabled(false);
256        } else {
257            clearErrorFeedback(tfLatLon,tr("Please enter a GPS coordinates"));
258            latLonCoordinates = latLon;
259            setOkEnabled(true);
260        }
261    }
262
263    protected void parseEastNorthUserInput() {
264        EastNorth en;
265        try {
266            en = parseEastNorth(tfEastNorth.getText());
267        } catch (IllegalArgumentException e) {
268            en = null;
269        }
270        if (en == null) {
271            setErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing"));
272            latLonCoordinates = null;
273            setOkEnabled(false);
274        } else {
275            clearErrorFeedback(tfEastNorth,tr("Please enter a Easting and Northing"));
276            eastNorthCoordinates = en;
277            setOkEnabled(true);
278        }
279    }
280
281    private void setOkEnabled(boolean b) {
282        if (buttons != null && !buttons.isEmpty()) {
283            buttons.get(0).setEnabled(b);
284        }
285    }
286
287    @Override
288    public void setVisible(boolean visible) {
289        if (visible) {
290            WindowGeometry.centerInWindow(Main.parent, getSize()).applySafe(this);
291        }
292        super.setVisible(visible);
293    }
294
295    class LatLonInputVerifier implements DocumentListener {
296        @Override
297        public void changedUpdate(DocumentEvent e) {
298            parseLatLonUserInput();
299        }
300
301        @Override
302        public void insertUpdate(DocumentEvent e) {
303            parseLatLonUserInput();
304        }
305
306        @Override
307        public void removeUpdate(DocumentEvent e) {
308            parseLatLonUserInput();
309        }
310    }
311
312    class EastNorthInputVerifier implements DocumentListener {
313        @Override
314        public void changedUpdate(DocumentEvent e) {
315            parseEastNorthUserInput();
316        }
317
318        @Override
319        public void insertUpdate(DocumentEvent e) {
320            parseEastNorthUserInput();
321        }
322
323        @Override
324        public void removeUpdate(DocumentEvent e) {
325            parseEastNorthUserInput();
326        }
327    }
328
329    static class TextFieldFocusHandler implements FocusListener {
330        @Override
331        public void focusGained(FocusEvent e) {
332            Component c = e.getComponent();
333            if (c instanceof JosmTextField) {
334                JosmTextField tf = (JosmTextField)c;
335                tf.selectAll();
336            }
337        }
338        @Override
339        public void focusLost(FocusEvent e) {}
340    }
341
342    public static LatLon parseLatLon(final String coord) {
343        final Matcher m = p.matcher(coord);
344
345        final StringBuilder sb = new StringBuilder();
346        final List<Object> list = new ArrayList<Object>();
347
348        while (m.find()) {
349            if (m.group(1) != null) {
350                sb.append('R');     // floating point number
351                list.add(Double.parseDouble(m.group(1).replace(',', '.')));
352            } else if (m.group(2) != null) {
353                sb.append('Z');     // integer number
354                list.add(Double.parseDouble(m.group(2)));
355            } else if (m.group(3) != null) {
356                sb.append('o');     // degree sign
357            } else if (m.group(4) != null) {
358                sb.append('\'');    // seconds sign
359            } else if (m.group(5) != null) {
360                sb.append('"');     // minutes sign
361            } else if (m.group(6) != null) {
362                sb.append(',');     // separator
363            } else if (m.group(7) != null) {
364                sb.append("x");     // cardinal direction
365                String c = m.group(7).toUpperCase();
366                if (c.equals("N") || c.equals("S") || c.equals("E") || c.equals("W")) {
367                    list.add(c);
368                } else {
369                    list.add(c.replace(N_TR, 'N').replace(S_TR, 'S')
370                            .replace(E_TR, 'E').replace(W_TR, 'W'));
371                }
372            } else if (m.group(8) != null) {
373                throw new IllegalArgumentException("invalid token: " + m.group(8));
374            }
375        }
376
377        final String pattern = sb.toString();
378
379        final Object[] params = list.toArray();
380        final LatLonHolder latLon = new LatLonHolder();
381
382        if (pattern.matches("Ro?,?Ro?")) {
383            setLatLonObj(latLon,
384                    params[0], ZERO, ZERO, "N",
385                    params[1], ZERO, ZERO, "E");
386        } else if (pattern.matches("xRo?,?xRo?")) {
387            setLatLonObj(latLon,
388                    params[1], ZERO, ZERO, params[0],
389                    params[3], ZERO, ZERO, params[2]);
390        } else if (pattern.matches("Ro?x,?Ro?x")) {
391            setLatLonObj(latLon,
392                    params[0], ZERO, ZERO, params[1],
393                    params[2], ZERO, ZERO, params[3]);
394        } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) {
395            setLatLonObj(latLon,
396                    params[0], params[1], ZERO, "N",
397                    params[2], params[3], ZERO, "E");
398        } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) {
399            setLatLonObj(latLon,
400                    params[1], params[2], ZERO, params[0],
401                    params[4], params[5], ZERO, params[3]);
402        } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) {
403            setLatLonObj(latLon,
404                    params[0], params[1], ZERO, params[2],
405                    params[3], params[4], ZERO, params[5]);
406        } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) {
407            setLatLonObj(latLon,
408                    params[0], params[1], params[2], params[3],
409                    params[4], params[5], params[6], params[7]);
410        } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) {
411            setLatLonObj(latLon,
412                    params[1], params[2], params[3], params[0],
413                    params[5], params[6], params[7], params[4]);
414        } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) {
415            setLatLonObj(latLon,
416                    params[0], params[1], params[2], "N",
417                    params[3], params[4], params[5], "E");
418        } else {
419            throw new IllegalArgumentException("invalid format: " + pattern);
420        }
421
422        return new LatLon(latLon.lat, latLon.lon);
423    }
424
425    public static EastNorth parseEastNorth(String s) {
426        String[] en = s.split("[;, ]+");
427        if (en.length != 2) return null;
428        try {
429            double east = Double.parseDouble(en[0]);
430            double north = Double.parseDouble(en[1]);
431            return new EastNorth(east, north);
432        } catch (NumberFormatException nfe) {
433            return null;
434        }
435    }
436
437    private static class LatLonHolder {
438        double lat, lon;
439    }
440
441    private static void setLatLonObj(final LatLonHolder latLon,
442            final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1,
443            final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) {
444
445        setLatLon(latLon,
446                (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1,
447                (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2);
448    }
449
450    private static void setLatLon(final LatLonHolder latLon,
451            final double coord1deg, final double coord1min, final double coord1sec, final String card1,
452            final double coord2deg, final double coord2min, final double coord2sec, final String card2) {
453
454        setLatLon(latLon, coord1deg, coord1min, coord1sec, card1);
455        setLatLon(latLon, coord2deg, coord2min, coord2sec, card2);
456    }
457
458    private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec, final String card) {
459        if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) {
460            throw new IllegalArgumentException("out of range");
461        }
462
463        double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600);
464        coord = card.equals("N") || card.equals("E") ? coord : -coord;
465        if (card.equals("N") || card.equals("S")) {
466            latLon.lat = coord;
467        } else {
468            latLon.lon = coord;
469        }
470    }
471
472    public String getLatLonText() {
473        return tfLatLon.getText();
474    }
475
476    public void setLatLonText(String text) {
477        tfLatLon.setText(text);
478    }
479
480    public String getEastNorthText() {
481        return tfEastNorth.getText();
482    }
483
484    public void setEastNorthText(String text) {
485        tfEastNorth.setText(text);
486    }
487
488}