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