// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.dialogs; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Color; import java.awt.Component; import java.awt.GridBagLayout; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.Arrays; import java.util.Optional; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSeparator; import javax.swing.JTabbedPane; import javax.swing.UIManager; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.coor.CoordinateFormat; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.gui.ExtendedDialog; import org.openstreetmap.josm.gui.widgets.HtmlPanel; import org.openstreetmap.josm.gui.widgets.JosmTextField; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.Utils; import org.openstreetmap.josm.tools.WindowGeometry; public class LatLonDialog extends ExtendedDialog { private static final Color BG_COLOR_ERROR = new Color(255, 224, 224); public JTabbedPane tabs; private JosmTextField tfLatLon, tfEastNorth; private LatLon latLonCoordinates; private EastNorth eastNorthCoordinates; protected JPanel buildLatLon() { JPanel pnl = new JPanel(new GridBagLayout()); pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); pnl.add(new JLabel(tr("Coordinates:")), GBC.std().insets(0, 10, 5, 0)); tfLatLon = new JosmTextField(24); pnl.add(tfLatLon, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL).weight(1.0, 0.0)); pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); pnl.add(new HtmlPanel( Utils.join("<br/>", Arrays.asList( tr("Enter the coordinates for the new node."), tr("You can separate longitude and latitude with space, comma or semicolon."), tr("Use positive numbers or N, E characters to indicate North or East cardinal direction."), tr("For South and West cardinal directions you can use either negative numbers or S, W characters."), tr("Coordinate value can be in one of three formats:") )) + Utils.joinAsHtmlUnorderedList(Arrays.asList( tr("<i>degrees</i><tt>°</tt>"), tr("<i>degrees</i><tt>°</tt> <i>minutes</i><tt>'</tt>"), tr("<i>degrees</i><tt>°</tt> <i>minutes</i><tt>'</tt> <i>seconds</i><tt>"</tt>") )) + Utils.join("<br/><br/>", Arrays.asList( tr("Symbols <tt>°</tt>, <tt>'</tt>, <tt>′</tt>, <tt>"</tt>, <tt>″</tt> are optional."), tr("You can also use the syntax <tt>lat=\"...\" lon=\"...\"</tt> or <tt>lat=''...'' lon=''...''</tt>."), tr("Some examples:") )) + "<table><tr><td>" + Utils.joinAsHtmlUnorderedList(Arrays.asList( "49.29918° 19.24788°", "N 49.29918 E 19.24788", "W 49°29.918' S 19°24.788'", "N 49°29'04" E 19°24'43"", "49.29918 N, 19.24788 E", "49°29'21" N 19°24'38" E", "49 29 51, 19 24 18", "49 29, 19 24", "E 49 29, N 19 24" )) + "</td><td>" + Utils.joinAsHtmlUnorderedList(Arrays.asList( "49° 29; 19° 24", "N 49° 29, W 19° 24", "49° 29.5 S, 19° 24.6 E", "N 49 29.918 E 19 15.88", "49 29.4 19 24.5", "-49 29.4 N -19 24.5 W", "48 deg 42' 52.13\" N, 21 deg 11' 47.60\" E", "lat=\"49.29918\" lon=\"19.24788\"", "lat='49.29918' lon='19.24788'" )) + "</td></tr></table>"), GBC.eol().fill().weight(1.0, 1.0)); // parse and verify input on the fly // LatLonInputVerifier inputVerifier = new LatLonInputVerifier(); tfLatLon.getDocument().addDocumentListener(inputVerifier); // select the text in the field on focus // TextFieldFocusHandler focusHandler = new TextFieldFocusHandler(); tfLatLon.addFocusListener(focusHandler); return pnl; } private JPanel buildEastNorth() { JPanel pnl = new JPanel(new GridBagLayout()); pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); pnl.add(new JLabel(tr("Projected coordinates:")), GBC.std().insets(0, 10, 5, 0)); tfEastNorth = new JosmTextField(24); pnl.add(tfEastNorth, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL).weight(1.0, 0.0)); pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); pnl.add(new HtmlPanel( tr("Enter easting and northing (x and y) separated by space, comma or semicolon.")), GBC.eol().fill(GBC.HORIZONTAL)); pnl.add(GBC.glue(1, 1), GBC.eol().fill().weight(1.0, 1.0)); EastNorthInputVerifier inputVerifier = new EastNorthInputVerifier(); tfEastNorth.getDocument().addDocumentListener(inputVerifier); TextFieldFocusHandler focusHandler = new TextFieldFocusHandler(); tfEastNorth.addFocusListener(focusHandler); return pnl; } protected void build() { tabs = new JTabbedPane(); tabs.addTab(tr("Lat/Lon"), buildLatLon()); tabs.addTab(tr("East/North"), buildEastNorth()); tabs.getModel().addChangeListener(e -> { switch (tabs.getModel().getSelectedIndex()) { case 0: parseLatLonUserInput(); break; case 1: parseEastNorthUserInput(); break; default: throw new AssertionError(); } }); setContent(tabs, false); addWindowListener(new WindowAdapter() { @Override public void windowOpened(WindowEvent e) { tfLatLon.requestFocusInWindow(); } }); } public LatLonDialog(Component parent, String title, String help) { super(parent, title, new String[] {tr("Ok"), tr("Cancel")}); setButtonIcons(new String[] {"ok", "cancel"}); configureContextsensitiveHelp(help, true); build(); setCoordinates(null); } public boolean isLatLon() { return tabs.getModel().getSelectedIndex() == 0; } public void setCoordinates(LatLon ll) { LatLon llc = Optional.ofNullable(ll).orElse(LatLon.ZERO); tfLatLon.setText(llc.latToString(CoordinateFormat.getDefaultFormat()) + ' ' + llc.lonToString(CoordinateFormat.getDefaultFormat())); EastNorth en = Main.getProjection().latlon2eastNorth(llc); tfEastNorth.setText(Double.toString(en.east()) + ' ' + Double.toString(en.north())); // Both latLonCoordinates and eastNorthCoordinates may have been reset to null if ll is out of the world latLonCoordinates = llc; eastNorthCoordinates = en; setOkEnabled(true); } public LatLon getCoordinates() { if (isLatLon()) { return latLonCoordinates; } else { if (eastNorthCoordinates == null) return null; return Main.getProjection().eastNorth2latlon(eastNorthCoordinates); } } public LatLon getLatLonCoordinates() { return latLonCoordinates; } public EastNorth getEastNorthCoordinates() { return eastNorthCoordinates; } protected void setErrorFeedback(JosmTextField tf, String message) { tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1)); tf.setToolTipText(message); tf.setBackground(BG_COLOR_ERROR); } protected void clearErrorFeedback(JosmTextField tf, String message) { tf.setBorder(UIManager.getBorder("TextField.border")); tf.setToolTipText(message); tf.setBackground(UIManager.getColor("TextField.background")); } protected void parseLatLonUserInput() { LatLon latLon; try { latLon = LatLon.parse(tfLatLon.getText()); if (!LatLon.isValidLat(latLon.lat()) || !LatLon.isValidLon(latLon.lon())) { latLon = null; } } catch (IllegalArgumentException e) { Main.trace(e); latLon = null; } if (latLon == null) { setErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates")); latLonCoordinates = null; setOkEnabled(false); } else { clearErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates")); latLonCoordinates = latLon; setOkEnabled(true); } } protected void parseEastNorthUserInput() { EastNorth en; try { en = parseEastNorth(tfEastNorth.getText()); } catch (IllegalArgumentException e) { Main.trace(e); en = null; } if (en == null) { setErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing")); latLonCoordinates = null; setOkEnabled(false); } else { clearErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing")); eastNorthCoordinates = en; setOkEnabled(true); } } private void setOkEnabled(boolean b) { if (buttons != null && !buttons.isEmpty()) { buttons.get(0).setEnabled(b); } } @Override public void setVisible(boolean visible) { final String preferenceKey = getClass().getName() + ".geometry"; if (visible) { new WindowGeometry( preferenceKey, WindowGeometry.centerInWindow(getParent(), getSize()) ).applySafe(this); } else { new WindowGeometry(this).remember(preferenceKey); } super.setVisible(visible); } class LatLonInputVerifier implements DocumentListener { @Override public void changedUpdate(DocumentEvent e) { parseLatLonUserInput(); } @Override public void insertUpdate(DocumentEvent e) { parseLatLonUserInput(); } @Override public void removeUpdate(DocumentEvent e) { parseLatLonUserInput(); } } class EastNorthInputVerifier implements DocumentListener { @Override public void changedUpdate(DocumentEvent e) { parseEastNorthUserInput(); } @Override public void insertUpdate(DocumentEvent e) { parseEastNorthUserInput(); } @Override public void removeUpdate(DocumentEvent e) { parseEastNorthUserInput(); } } static class TextFieldFocusHandler implements FocusListener { @Override public void focusGained(FocusEvent e) { Component c = e.getComponent(); if (c instanceof JosmTextField) { JosmTextField tf = (JosmTextField) c; tf.selectAll(); } } @Override public void focusLost(FocusEvent e) { // Not used } } /** * Parses the given string as lat/lon. * @param coord String to parse * @return parsed lat/lon * @deprecated use {@link LatLon#parse(String)} instead */ @Deprecated public static LatLon parseLatLon(final String coord) { return LatLon.parse(coord); } public static EastNorth parseEastNorth(String s) { String[] en = s.split("[;, ]+"); if (en.length != 2) return null; try { double east = Double.parseDouble(en[0]); double north = Double.parseDouble(en[1]); return new EastNorth(east, north); } catch (NumberFormatException nfe) { return null; } } public String getLatLonText() { return tfLatLon.getText(); } public void setLatLonText(String text) { tfLatLon.setText(text); } public String getEastNorthText() { return tfEastNorth.getText(); } public void setEastNorthText(String text) { tfEastNorth.setText(text); } }