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