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