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