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>" + 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></ul>" + 105 "<li>48 deg 42' 52.13\" N, 21 deg 11' 47.60\" E</li></ul>" 106 )), 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(new ChangeListener() { 152 @Override 153 public void stateChanged(ChangeEvent e) { 154 switch (tabs.getModel().getSelectedIndex()) { 155 case 0: parseLatLonUserInput(); break; 156 case 1: parseEastNorthUserInput(); break; 157 default: throw new AssertionError(); 158 } 159 } 160 }); 161 setContent(tabs, false); 162 } 163 164 public LatLonDialog(Component parent, String title, String help) { 165 super(parent, title, new String[] { tr("Ok"), tr("Cancel") }); 166 setButtonIcons(new String[] { "ok", "cancel" }); 167 configureContextsensitiveHelp(help, true); 168 169 build(); 170 setCoordinates(null); 171 } 172 173 public boolean isLatLon() { 174 return tabs.getModel().getSelectedIndex() == 0; 175 } 176 177 public void setCoordinates(LatLon ll) { 178 if (ll == null) { 179 ll = new LatLon(0,0); 180 } 181 this.latLonCoordinates = ll; 182 tfLatLon.setText(ll.latToString(CoordinateFormat.getDefaultFormat()) + " " + ll.lonToString(CoordinateFormat.getDefaultFormat())); 183 EastNorth en = Main.getProjection().latlon2eastNorth(ll); 184 tfEastNorth.setText(en.east()+" "+en.north()); 185 setOkEnabled(true); 186 } 187 188 public LatLon getCoordinates() { 189 if (isLatLon()) { 190 return latLonCoordinates; 191 } else { 192 if (eastNorthCoordinates == null) return null; 193 return Main.getProjection().eastNorth2latlon(eastNorthCoordinates); 194 } 195 } 196 197 public LatLon getLatLonCoordinates() { 198 return latLonCoordinates; 199 } 200 201 public EastNorth getEastNorthCoordinates() { 202 return eastNorthCoordinates; 203 } 204 205 protected void setErrorFeedback(JosmTextField tf, String message) { 206 tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1)); 207 tf.setToolTipText(message); 208 tf.setBackground(BG_COLOR_ERROR); 209 } 210 211 protected void clearErrorFeedback(JosmTextField tf, String message) { 212 tf.setBorder(UIManager.getBorder("TextField.border")); 213 tf.setToolTipText(message); 214 tf.setBackground(UIManager.getColor("TextField.background")); 215 } 216 217 protected Double parseDoubleFromUserInput(String input) { 218 if (input == null) return null; 219 // remove white space and an optional degree symbol 220 // 221 input = input.trim(); 222 input = input.replaceAll(DEG, ""); 223 224 // try to parse using the current locale 225 // 226 NumberFormat f = NumberFormat.getNumberInstance(); 227 Number n=null; 228 ParsePosition pp = new ParsePosition(0); 229 n = f.parse(input,pp); 230 if (pp.getErrorIndex() >= 0 || pp.getIndex()<input.length()) { 231 // fall back - try to parse with the english locale 232 // 233 pp = new ParsePosition(0); 234 f = NumberFormat.getNumberInstance(Locale.ENGLISH); 235 n = f.parse(input, pp); 236 if (pp.getErrorIndex() >= 0 || pp.getIndex()<input.length()) 237 return null; 238 } 239 return n== null ? null : n.doubleValue(); 240 } 241 242 protected void parseLatLonUserInput() { 243 LatLon latLon; 244 try { 245 latLon = parseLatLon(tfLatLon.getText()); 246 if (!LatLon.isValidLat(latLon.lat()) || !LatLon.isValidLon(latLon.lon())) { 247 latLon = null; 248 } 249 } catch (IllegalArgumentException e) { 250 latLon = null; 251 } 252 if (latLon == null) { 253 setErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates")); 254 latLonCoordinates = null; 255 setOkEnabled(false); 256 } else { 257 clearErrorFeedback(tfLatLon,tr("Please enter a GPS coordinates")); 258 latLonCoordinates = latLon; 259 setOkEnabled(true); 260 } 261 } 262 263 protected void parseEastNorthUserInput() { 264 EastNorth en; 265 try { 266 en = parseEastNorth(tfEastNorth.getText()); 267 } catch (IllegalArgumentException e) { 268 en = null; 269 } 270 if (en == null) { 271 setErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing")); 272 latLonCoordinates = null; 273 setOkEnabled(false); 274 } else { 275 clearErrorFeedback(tfEastNorth,tr("Please enter a Easting and Northing")); 276 eastNorthCoordinates = en; 277 setOkEnabled(true); 278 } 279 } 280 281 private void setOkEnabled(boolean b) { 282 if (buttons != null && !buttons.isEmpty()) { 283 buttons.get(0).setEnabled(b); 284 } 285 } 286 287 @Override 288 public void setVisible(boolean visible) { 289 if (visible) { 290 WindowGeometry.centerInWindow(Main.parent, getSize()).applySafe(this); 291 } 292 super.setVisible(visible); 293 } 294 295 class LatLonInputVerifier implements DocumentListener { 296 @Override 297 public void changedUpdate(DocumentEvent e) { 298 parseLatLonUserInput(); 299 } 300 301 @Override 302 public void insertUpdate(DocumentEvent e) { 303 parseLatLonUserInput(); 304 } 305 306 @Override 307 public void removeUpdate(DocumentEvent e) { 308 parseLatLonUserInput(); 309 } 310 } 311 312 class EastNorthInputVerifier implements DocumentListener { 313 @Override 314 public void changedUpdate(DocumentEvent e) { 315 parseEastNorthUserInput(); 316 } 317 318 @Override 319 public void insertUpdate(DocumentEvent e) { 320 parseEastNorthUserInput(); 321 } 322 323 @Override 324 public void removeUpdate(DocumentEvent e) { 325 parseEastNorthUserInput(); 326 } 327 } 328 329 static class TextFieldFocusHandler implements FocusListener { 330 @Override 331 public void focusGained(FocusEvent e) { 332 Component c = e.getComponent(); 333 if (c instanceof JosmTextField) { 334 JosmTextField tf = (JosmTextField)c; 335 tf.selectAll(); 336 } 337 } 338 @Override 339 public void focusLost(FocusEvent e) {} 340 } 341 342 public static LatLon parseLatLon(final String coord) { 343 final Matcher m = p.matcher(coord); 344 345 final StringBuilder sb = new StringBuilder(); 346 final List<Object> list = new ArrayList<Object>(); 347 348 while (m.find()) { 349 if (m.group(1) != null) { 350 sb.append('R'); // floating point number 351 list.add(Double.parseDouble(m.group(1).replace(',', '.'))); 352 } else if (m.group(2) != null) { 353 sb.append('Z'); // integer number 354 list.add(Double.parseDouble(m.group(2))); 355 } else if (m.group(3) != null) { 356 sb.append('o'); // degree sign 357 } else if (m.group(4) != null) { 358 sb.append('\''); // seconds sign 359 } else if (m.group(5) != null) { 360 sb.append('"'); // minutes sign 361 } else if (m.group(6) != null) { 362 sb.append(','); // separator 363 } else if (m.group(7) != null) { 364 sb.append("x"); // cardinal direction 365 String c = m.group(7).toUpperCase(); 366 if (c.equals("N") || c.equals("S") || c.equals("E") || c.equals("W")) { 367 list.add(c); 368 } else { 369 list.add(c.replace(N_TR, 'N').replace(S_TR, 'S') 370 .replace(E_TR, 'E').replace(W_TR, 'W')); 371 } 372 } else if (m.group(8) != null) { 373 throw new IllegalArgumentException("invalid token: " + m.group(8)); 374 } 375 } 376 377 final String pattern = sb.toString(); 378 379 final Object[] params = list.toArray(); 380 final LatLonHolder latLon = new LatLonHolder(); 381 382 if (pattern.matches("Ro?,?Ro?")) { 383 setLatLonObj(latLon, 384 params[0], ZERO, ZERO, "N", 385 params[1], ZERO, ZERO, "E"); 386 } else if (pattern.matches("xRo?,?xRo?")) { 387 setLatLonObj(latLon, 388 params[1], ZERO, ZERO, params[0], 389 params[3], ZERO, ZERO, params[2]); 390 } else if (pattern.matches("Ro?x,?Ro?x")) { 391 setLatLonObj(latLon, 392 params[0], ZERO, ZERO, params[1], 393 params[2], ZERO, ZERO, params[3]); 394 } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) { 395 setLatLonObj(latLon, 396 params[0], params[1], ZERO, "N", 397 params[2], params[3], ZERO, "E"); 398 } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) { 399 setLatLonObj(latLon, 400 params[1], params[2], ZERO, params[0], 401 params[4], params[5], ZERO, params[3]); 402 } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) { 403 setLatLonObj(latLon, 404 params[0], params[1], ZERO, params[2], 405 params[3], params[4], ZERO, params[5]); 406 } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) { 407 setLatLonObj(latLon, 408 params[0], params[1], params[2], params[3], 409 params[4], params[5], params[6], params[7]); 410 } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) { 411 setLatLonObj(latLon, 412 params[1], params[2], params[3], params[0], 413 params[5], params[6], params[7], params[4]); 414 } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) { 415 setLatLonObj(latLon, 416 params[0], params[1], params[2], "N", 417 params[3], params[4], params[5], "E"); 418 } else { 419 throw new IllegalArgumentException("invalid format: " + pattern); 420 } 421 422 return new LatLon(latLon.lat, latLon.lon); 423 } 424 425 public static EastNorth parseEastNorth(String s) { 426 String[] en = s.split("[;, ]+"); 427 if (en.length != 2) return null; 428 try { 429 double east = Double.parseDouble(en[0]); 430 double north = Double.parseDouble(en[1]); 431 return new EastNorth(east, north); 432 } catch (NumberFormatException nfe) { 433 return null; 434 } 435 } 436 437 private static class LatLonHolder { 438 double lat, lon; 439 } 440 441 private static void setLatLonObj(final LatLonHolder latLon, 442 final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1, 443 final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) { 444 445 setLatLon(latLon, 446 (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1, 447 (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2); 448 } 449 450 private static void setLatLon(final LatLonHolder latLon, 451 final double coord1deg, final double coord1min, final double coord1sec, final String card1, 452 final double coord2deg, final double coord2min, final double coord2sec, final String card2) { 453 454 setLatLon(latLon, coord1deg, coord1min, coord1sec, card1); 455 setLatLon(latLon, coord2deg, coord2min, coord2sec, card2); 456 } 457 458 private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec, final String card) { 459 if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) { 460 throw new IllegalArgumentException("out of range"); 461 } 462 463 double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600); 464 coord = card.equals("N") || card.equals("E") ? coord : -coord; 465 if (card.equals("N") || card.equals("S")) { 466 latLon.lat = coord; 467 } else { 468 latLon.lon = coord; 469 } 470 } 471 472 public String getLatLonText() { 473 return tfLatLon.getText(); 474 } 475 476 public void setLatLonText(String text) { 477 tfLatLon.setText(text); 478 } 479 480 public String getEastNorthText() { 481 return tfEastNorth.getText(); 482 } 483 484 public void setEastNorthText(String text) { 485 tfEastNorth.setText(text); 486 } 487 488}