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