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