| 1 | // License: GPL. For details, see LICENSE file. |
| 2 | package utilsplugin2; |
| 3 | |
| 4 | import static org.openstreetmap.josm.tools.I18n.tr; |
| 5 | |
| 6 | import java.awt.Color; |
| 7 | import java.awt.Component; |
| 8 | import java.awt.GridBagLayout; |
| 9 | import java.awt.event.ActionEvent; |
| 10 | import java.awt.event.FocusEvent; |
| 11 | import java.awt.event.FocusListener; |
| 12 | import java.awt.event.WindowAdapter; |
| 13 | import java.awt.event.WindowEvent; |
| 14 | import java.text.NumberFormat; |
| 15 | import java.text.ParsePosition; |
| 16 | import java.util.ArrayList; |
| 17 | import java.util.List; |
| 18 | import java.util.Locale; |
| 19 | import java.util.regex.Matcher; |
| 20 | import java.util.regex.Pattern; |
| 21 | |
| 22 | import javax.swing.AbstractAction; |
| 23 | import javax.swing.BorderFactory; |
| 24 | import javax.swing.JLabel; |
| 25 | import javax.swing.JPanel; |
| 26 | import javax.swing.JSeparator; |
| 27 | import javax.swing.JTabbedPane; |
| 28 | |
| 29 | import javax.swing.text.Document; |
| 30 | import javax.swing.JTextArea; |
| 31 | import javax.swing.JScrollPane; |
| 32 | import javax.swing.ButtonGroup; |
| 33 | import javax.swing.JRadioButton; |
| 34 | |
| 35 | import javax.swing.UIManager; |
| 36 | import javax.swing.event.ChangeEvent; |
| 37 | import javax.swing.event.ChangeListener; |
| 38 | import javax.swing.event.DocumentEvent; |
| 39 | import javax.swing.event.DocumentListener; |
| 40 | |
| 41 | import org.openstreetmap.josm.Main; |
| 42 | import org.openstreetmap.josm.data.coor.CoordinateFormat; |
| 43 | import org.openstreetmap.josm.data.coor.LatLon; |
| 44 | import org.openstreetmap.josm.gui.ExtendedDialog; |
| 45 | import org.openstreetmap.josm.gui.widgets.HtmlPanel; |
| 46 | import org.openstreetmap.josm.tools.GBC; |
| 47 | import org.openstreetmap.josm.tools.ImageProvider; |
| 48 | import org.openstreetmap.josm.tools.WindowGeometry; |
| 49 | |
| 50 | public class LatLonDialog extends ExtendedDialog { |
| 51 | private static final Color BG_COLOR_ERROR = new Color(255,224,224); |
| 52 | |
| 53 | public JTabbedPane tabs; |
| 54 | private JTextArea taLatLon; |
| 55 | private JScrollPane spScroll; |
| 56 | private JRadioButton rbNodes; |
| 57 | private JRadioButton rbWay; |
| 58 | private JRadioButton rbClosedWay; |
| 59 | private ButtonGroup bgType; |
| 60 | private String geomType; |
| 61 | |
| 62 | private LatLon[] latLonCoordinates; |
| 63 | |
| 64 | private static final double ZERO = 0.0; |
| 65 | private static final String DEG = "\u00B0"; |
| 66 | private static final String MIN = "\u2032"; |
| 67 | private static final String SEC = "\u2033"; |
| 68 | |
| 69 | private static final char N_TR = LatLon.NORTH.charAt(0); |
| 70 | private static final char S_TR = LatLon.SOUTH.charAt(0); |
| 71 | private static final char E_TR = LatLon.EAST.charAt(0); |
| 72 | private static final char W_TR = LatLon.WEST.charAt(0); |
| 73 | |
| 74 | private static final Pattern p = Pattern.compile( |
| 75 | "([+|-]?\\d+[.,]\\d+)|" // (1) |
| 76 | + "([+|-]?\\d+)|" // (2) |
| 77 | + "("+DEG+"|o|deg)|" // (3) |
| 78 | + "('|"+MIN+"|min)|" // (4) |
| 79 | + "(\"|"+SEC+"|sec)|" // (5) |
| 80 | + "(,|;)|" // (6) |
| 81 | + "([NSEW"+N_TR+S_TR+E_TR+W_TR+"])|"// (7) |
| 82 | + "\\s+|" |
| 83 | + "(.+)"); |
| 84 | |
| 85 | protected JPanel buildLatLon() { |
| 86 | JPanel pnl = new JPanel(new GridBagLayout()); |
| 87 | pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5)); |
| 88 | |
| 89 | pnl.add(new JLabel(tr("Coordinates:")), GBC.std().insets(0,10,5,0)); |
| 90 | |
| 91 | taLatLon = new JTextArea(5,24); |
| 92 | taLatLon.getDocument().addDocumentListener(new CoordinateListener()); |
| 93 | spScroll = new JScrollPane(taLatLon); |
| 94 | pnl.add(spScroll, GBC.eol().insets(0,10,0,0).fill().weight(2.0, 2.0)); |
| 95 | |
| 96 | //Radio button setup |
| 97 | bgType = new ButtonGroup(); |
| 98 | |
| 99 | rbNodes = new JRadioButton("Nodes", true); |
| 100 | rbNodes.setActionCommand("nodes"); |
| 101 | bgType.add(rbNodes); |
| 102 | pnl.add(rbNodes, GBC.eol()); |
| 103 | |
| 104 | rbWay = new JRadioButton("Way"); |
| 105 | rbWay.setActionCommand("way"); |
| 106 | bgType.add(rbWay); |
| 107 | pnl.add(rbWay, GBC.eol()); |
| 108 | |
| 109 | rbClosedWay = new JRadioButton("Closed Way (Area)"); |
| 110 | rbClosedWay.setActionCommand("area"); |
| 111 | bgType.add(rbClosedWay); |
| 112 | pnl.add(rbClosedWay, GBC.eol()); |
| 113 | |
| 114 | //pnl.add(bgType, GBC.eol().insets(0,10,0,0).fill(GBC.HORIZONTAL).weight(2.0, 0.0)); |
| 115 | //pnl.add(new JRadioButton("test")); |
| 116 | //pnl.add(bgType); |
| 117 | |
| 118 | pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0,5,0,5)); |
| 119 | |
| 120 | pnl.add(new HtmlPanel( |
| 121 | tr("Enter the coordinates for the new nodes, one for each line.<br/>If you enter two lines with the same coordinates there will be generated duplicate nodes.<br/>You can separate longitude and latitude with space, comma or semicolon.<br/>" + |
| 122 | "Use positive numbers or N, E characters to indicate North or East cardinal direction.<br/>" + |
| 123 | "For South and West cardinal directions you can use either negative numbers or S, W characters.<br/>" + |
| 124 | "Coordinate value can be in one of three formats:<ul>" + |
| 125 | "<li><i>degrees</i><tt>°</tt></li>" + |
| 126 | "<li><i>degrees</i><tt>°</tt> <i>minutes</i><tt>'</tt></li>" + |
| 127 | "<li><i>degrees</i><tt>°</tt> <i>minutes</i><tt>'</tt> <i>seconds</i><tt>"</tt></li>" + |
| 128 | "</ul>" + |
| 129 | "Symbols <tt>°</tt>, <tt>'</tt>, <tt>′</tt>, <tt>"</tt>, <tt>″</tt> are optional.<br/><br/>" + |
| 130 | "Some examples:<ul>" + |
| 131 | "<li>49.29918° 19.24788°</li>" + |
| 132 | "<li>N 49.29918 E 19.24788</li>" + |
| 133 | "<li>W 49°29.918' S 19°24.788'</li>" + |
| 134 | "<li>N 49°29'04" E 19°24'43"</li>" + |
| 135 | "<li>49.29918 N, 19.24788 E</li>" + |
| 136 | "<li>49°29'21" N 19°24'38" E</li>" + |
| 137 | "<li>49 29 51, 19 24 18</li>" + |
| 138 | "<li>49 29, 19 24</li>" + |
| 139 | "<li>E 49 29, N 19 24</li>" + |
| 140 | "<li>49° 29; 19° 24</li>" + |
| 141 | "<li>N 49° 29, W 19° 24</li>" + |
| 142 | "<li>49° 29.5 S, 19° 24.6 E</li>" + |
| 143 | "<li>N 49 29.918 E 19 15.88</li>" + |
| 144 | "<li>49 29.4 19 24.5</li>" + |
| 145 | "<li>-49 29.4 N -19 24.5 W</li></ul>" + |
| 146 | "<li>48 deg 42' 52.13\" N, 21 deg 11' 47.60\" E</li></ul>" |
| 147 | )), |
| 148 | GBC.eol().fill().weight(1.0, 1.0)); |
| 149 | |
| 150 | // parse and verify input on the fly |
| 151 | // |
| 152 | LatLonInputVerifier inputVerifier = new LatLonInputVerifier(); |
| 153 | taLatLon.getDocument().addDocumentListener(inputVerifier); |
| 154 | |
| 155 | // select the text in the field on focus |
| 156 | // |
| 157 | TextFieldFocusHandler focusHandler = new TextFieldFocusHandler(); |
| 158 | taLatLon.addFocusListener(focusHandler); |
| 159 | return pnl; |
| 160 | } |
| 161 | |
| 162 | protected void build() { |
| 163 | tabs = new JTabbedPane(); |
| 164 | tabs.addTab(tr("Lat/Lon"), buildLatLon()); |
| 165 | tabs.getModel().addChangeListener(new ChangeListener() { |
| 166 | @Override |
| 167 | public void stateChanged(ChangeEvent e) { |
| 168 | switch (tabs.getModel().getSelectedIndex()) { |
| 169 | case 0: parseLatLonUserInput(); break; |
| 170 | default: throw new AssertionError(); |
| 171 | } |
| 172 | } |
| 173 | }); |
| 174 | setContent(tabs, false); |
| 175 | } |
| 176 | |
| 177 | public LatLonDialog(Component parent, String title, String help) { |
| 178 | super(Main.parent, tr("Add Node..."), new String[] { tr("Ok"), tr("Cancel") }); |
| 179 | setButtonIcons(new String[] { "ok", "cancel" }); |
| 180 | configureContextsensitiveHelp("/Action/AddNode", true); |
| 181 | |
| 182 | build(); |
| 183 | setCoordinates(null); |
| 184 | } |
| 185 | |
| 186 | public void setCoordinates(LatLon[] ll) { |
| 187 | if (ll == null) { |
| 188 | ll = new LatLon[] {}; |
| 189 | } |
| 190 | this.latLonCoordinates = ll; |
| 191 | String text = ""; |
| 192 | for (LatLon latlon : ll) { |
| 193 | text = text + latlon.latToString(CoordinateFormat.getDefaultFormat()) + " " + latlon.lonToString(CoordinateFormat.getDefaultFormat()) + "\n"; |
| 194 | } |
| 195 | taLatLon.setText(text); |
| 196 | setOkEnabled(true); |
| 197 | } |
| 198 | |
| 199 | public LatLon[] getCoordinates() { |
| 200 | return latLonCoordinates; |
| 201 | } |
| 202 | |
| 203 | public LatLon[] getLatLonCoordinates() { |
| 204 | return latLonCoordinates; |
| 205 | } |
| 206 | |
| 207 | public String getGeomType() { |
| 208 | return bgType.getSelection().getActionCommand(); |
| 209 | } |
| 210 | |
| 211 | protected void setErrorFeedback(JTextArea tf, String message) { |
| 212 | tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1)); |
| 213 | tf.setToolTipText(message); |
| 214 | tf.setBackground(BG_COLOR_ERROR); |
| 215 | } |
| 216 | |
| 217 | protected void clearErrorFeedback(JTextArea tf, String message) { |
| 218 | tf.setBorder(UIManager.getBorder("TextField.border")); |
| 219 | tf.setToolTipText(message); |
| 220 | tf.setBackground(UIManager.getColor("TextField.background")); |
| 221 | } |
| 222 | |
| 223 | protected Double parseDoubleFromUserInput(String input) { |
| 224 | if (input == null) return null; |
| 225 | // remove white space and an optional degree symbol |
| 226 | // |
| 227 | input = input.trim(); |
| 228 | input = input.replaceAll(DEG, ""); |
| 229 | |
| 230 | // try to parse using the current locale |
| 231 | // |
| 232 | NumberFormat f = NumberFormat.getNumberInstance(); |
| 233 | Number n=null; |
| 234 | ParsePosition pp = new ParsePosition(0); |
| 235 | n = f.parse(input,pp); |
| 236 | if (pp.getErrorIndex() >= 0 || pp.getIndex()<input.length()) { |
| 237 | // fall back - try to parse with the english locale |
| 238 | // |
| 239 | pp = new ParsePosition(0); |
| 240 | f = NumberFormat.getNumberInstance(Locale.ENGLISH); |
| 241 | n = f.parse(input, pp); |
| 242 | if (pp.getErrorIndex() >= 0 || pp.getIndex()<input.length()) |
| 243 | return null; |
| 244 | } |
| 245 | return n== null ? null : n.doubleValue(); |
| 246 | } |
| 247 | |
| 248 | protected void parseLatLonUserInput() { |
| 249 | LatLon[] latLons; |
| 250 | try { |
| 251 | latLons = parseLatLons(taLatLon.getText()); |
| 252 | Boolean working = true; |
| 253 | int i=0; |
| 254 | while (working && i < latLons.length) { |
| 255 | if (!LatLon.isValidLat(latLons[i].lat()) || !LatLon.isValidLon(latLons[i].lon())) { |
| 256 | latLons = null; |
| 257 | working = false; |
| 258 | } |
| 259 | i++; |
| 260 | } |
| 261 | } catch (IllegalArgumentException e) { |
| 262 | latLons = null; |
| 263 | } |
| 264 | if (latLons == null) { |
| 265 | setErrorFeedback(taLatLon, tr("Please enter a GPS coordinates")); |
| 266 | latLonCoordinates = null; |
| 267 | setOkEnabled(false); |
| 268 | } else { |
| 269 | clearErrorFeedback(taLatLon,tr("Please enter a GPS coordinates")); |
| 270 | latLonCoordinates = latLons; |
| 271 | setOkEnabled(true); |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | private void setOkEnabled(boolean b) { |
| 276 | if (buttons != null && buttons.size() > 0) { |
| 277 | buttons.get(0).setEnabled(b); |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | @Override |
| 282 | public void setVisible(boolean visible) { |
| 283 | if (visible) { |
| 284 | WindowGeometry.centerInWindow(Main.parent, getSize()).applySafe(this); |
| 285 | } |
| 286 | super.setVisible(visible); |
| 287 | } |
| 288 | |
| 289 | class LatLonInputVerifier implements DocumentListener { |
| 290 | public void changedUpdate(DocumentEvent e) { |
| 291 | parseLatLonUserInput(); |
| 292 | } |
| 293 | |
| 294 | public void insertUpdate(DocumentEvent e) { |
| 295 | parseLatLonUserInput(); |
| 296 | } |
| 297 | |
| 298 | public void removeUpdate(DocumentEvent e) { |
| 299 | parseLatLonUserInput(); |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | static class TextFieldFocusHandler implements FocusListener { |
| 304 | public void focusGained(FocusEvent e) { |
| 305 | Component c = e.getComponent(); |
| 306 | if (c instanceof JTextArea) { |
| 307 | JTextArea tf = (JTextArea)c; |
| 308 | tf.selectAll(); |
| 309 | } |
| 310 | } |
| 311 | public void focusLost(FocusEvent e) {} |
| 312 | } |
| 313 | |
| 314 | private static LatLon[] parseLatLons(final String text) { |
| 315 | String lines[] = text.split("\\r?\\n"); |
| 316 | List<LatLon> latLons = new ArrayList<LatLon>(); |
| 317 | for (String line : lines) { |
| 318 | latLons.add(parseLatLon(line)); |
| 319 | } |
| 320 | return latLons.toArray(new LatLon[]{}); |
| 321 | } |
| 322 | |
| 323 | private static LatLon parseLatLon(final String coord) { |
| 324 | final Matcher m = p.matcher(coord); |
| 325 | |
| 326 | final StringBuilder sb = new StringBuilder(); |
| 327 | final List<Object> list = new ArrayList<Object>(); |
| 328 | |
| 329 | while (m.find()) { |
| 330 | if (m.group(1) != null) { |
| 331 | sb.append('R'); // floating point number |
| 332 | list.add(Double.parseDouble(m.group(1).replace(',', '.'))); |
| 333 | } else if (m.group(2) != null) { |
| 334 | sb.append('Z'); // integer number |
| 335 | list.add(Double.parseDouble(m.group(2))); |
| 336 | } else if (m.group(3) != null) { |
| 337 | sb.append('o'); // degree sign |
| 338 | } else if (m.group(4) != null) { |
| 339 | sb.append('\''); // seconds sign |
| 340 | } else if (m.group(5) != null) { |
| 341 | sb.append('"'); // minutes sign |
| 342 | } else if (m.group(6) != null) { |
| 343 | sb.append(','); // separator |
| 344 | } else if (m.group(7) != null) { |
| 345 | sb.append("x"); // cardinal direction |
| 346 | String c = m.group(7).toUpperCase(); |
| 347 | if (c.equals("N") || c.equals("S") || c.equals("E") || c.equals("W")) { |
| 348 | list.add(c); |
| 349 | } else { |
| 350 | list.add(c.replace(N_TR, 'N').replace(S_TR, 'S') |
| 351 | .replace(E_TR, 'E').replace(W_TR, 'W')); |
| 352 | } |
| 353 | } else if (m.group(8) != null) { |
| 354 | throw new IllegalArgumentException("invalid token: " + m.group(8)); |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | final String pattern = sb.toString(); |
| 359 | |
| 360 | final Object[] params = list.toArray(); |
| 361 | final LatLonHolder latLon = new LatLonHolder(); |
| 362 | |
| 363 | if (pattern.matches("Ro?,?Ro?")) { |
| 364 | setLatLonObj(latLon, |
| 365 | params[0], ZERO, ZERO, "N", |
| 366 | params[1], ZERO, ZERO, "E"); |
| 367 | } else if (pattern.matches("xRo?,?xRo?")) { |
| 368 | setLatLonObj(latLon, |
| 369 | params[1], ZERO, ZERO, params[0], |
| 370 | params[3], ZERO, ZERO, params[2]); |
| 371 | } else if (pattern.matches("Ro?x,?Ro?x")) { |
| 372 | setLatLonObj(latLon, |
| 373 | params[0], ZERO, ZERO, params[1], |
| 374 | params[2], ZERO, ZERO, params[3]); |
| 375 | } else if (pattern.matches("Zo[RZ]'?,?Zo[RZ]'?|Z[RZ],?Z[RZ]")) { |
| 376 | setLatLonObj(latLon, |
| 377 | params[0], params[1], ZERO, "N", |
| 378 | params[2], params[3], ZERO, "E"); |
| 379 | } else if (pattern.matches("xZo[RZ]'?,?xZo[RZ]'?|xZo?[RZ],?xZo?[RZ]")) { |
| 380 | setLatLonObj(latLon, |
| 381 | params[1], params[2], ZERO, params[0], |
| 382 | params[4], params[5], ZERO, params[3]); |
| 383 | } else if (pattern.matches("Zo[RZ]'?x,?Zo[RZ]'?x|Zo?[RZ]x,?Zo?[RZ]x")) { |
| 384 | setLatLonObj(latLon, |
| 385 | params[0], params[1], ZERO, params[2], |
| 386 | params[3], params[4], ZERO, params[5]); |
| 387 | } else if (pattern.matches("ZoZ'[RZ]\"?x,?ZoZ'[RZ]\"?x|ZZ[RZ]x,?ZZ[RZ]x")) { |
| 388 | setLatLonObj(latLon, |
| 389 | params[0], params[1], params[2], params[3], |
| 390 | params[4], params[5], params[6], params[7]); |
| 391 | } else if (pattern.matches("xZoZ'[RZ]\"?,?xZoZ'[RZ]\"?|xZZ[RZ],?xZZ[RZ]")) { |
| 392 | setLatLonObj(latLon, |
| 393 | params[1], params[2], params[3], params[0], |
| 394 | params[5], params[6], params[7], params[4]); |
| 395 | } else if (pattern.matches("ZZ[RZ],?ZZ[RZ]")) { |
| 396 | setLatLonObj(latLon, |
| 397 | params[0], params[1], params[2], "N", |
| 398 | params[3], params[4], params[5], "E"); |
| 399 | } else { |
| 400 | throw new IllegalArgumentException("invalid format: " + pattern); |
| 401 | } |
| 402 | |
| 403 | return new LatLon(latLon.lat, latLon.lon); |
| 404 | } |
| 405 | |
| 406 | private static class LatLonHolder { |
| 407 | double lat, lon; |
| 408 | } |
| 409 | |
| 410 | private static void setLatLonObj(final LatLonHolder latLon, |
| 411 | final Object coord1deg, final Object coord1min, final Object coord1sec, final Object card1, |
| 412 | final Object coord2deg, final Object coord2min, final Object coord2sec, final Object card2) { |
| 413 | |
| 414 | setLatLon(latLon, |
| 415 | (Double) coord1deg, (Double) coord1min, (Double) coord1sec, (String) card1, |
| 416 | (Double) coord2deg, (Double) coord2min, (Double) coord2sec, (String) card2); |
| 417 | } |
| 418 | |
| 419 | private static void setLatLon(final LatLonHolder latLon, |
| 420 | final double coord1deg, final double coord1min, final double coord1sec, final String card1, |
| 421 | final double coord2deg, final double coord2min, final double coord2sec, final String card2) { |
| 422 | |
| 423 | setLatLon(latLon, coord1deg, coord1min, coord1sec, card1); |
| 424 | setLatLon(latLon, coord2deg, coord2min, coord2sec, card2); |
| 425 | } |
| 426 | |
| 427 | private static void setLatLon(final LatLonHolder latLon, final double coordDeg, final double coordMin, final double coordSec, final String card) { |
| 428 | if (coordDeg < -180 || coordDeg > 180 || coordMin < 0 || coordMin >= 60 || coordSec < 0 || coordSec > 60) { |
| 429 | throw new IllegalArgumentException("out of range"); |
| 430 | } |
| 431 | |
| 432 | double coord = (coordDeg < 0 ? -1 : 1) * (Math.abs(coordDeg) + coordMin / 60 + coordSec / 3600); |
| 433 | coord = card.equals("N") || card.equals("E") ? coord : -coord; |
| 434 | if (card.equals("N") || card.equals("S")) { |
| 435 | latLon.lat = coord; |
| 436 | } else { |
| 437 | latLon.lon = coord; |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | public String getLatLonText() { |
| 442 | return taLatLon.getText(); |
| 443 | } |
| 444 | |
| 445 | public void setLatLonText(String text) { |
| 446 | taLatLon.setText(text); |
| 447 | } |
| 448 | |
| 449 | private class CoordinateListener implements DocumentListener { |
| 450 | public void changedUpdate(DocumentEvent e) { |
| 451 | //not fired |
| 452 | } |
| 453 | public void insertUpdate(DocumentEvent e) { |
| 454 | updateButtons(); |
| 455 | } |
| 456 | public void removeUpdate(DocumentEvent e) { |
| 457 | updateButtons(); |
| 458 | } |
| 459 | private void updateButtons() { |
| 460 | String text = taLatLon.getText(); |
| 461 | String[] lines = text.split("\r\n|\r|\n"); |
| 462 | rbNodes.setEnabled(true); |
| 463 | rbWay.setEnabled(true); |
| 464 | rbClosedWay.setEnabled(true); |
| 465 | if (lines.length < 3) { |
| 466 | rbClosedWay.setEnabled(false); |
| 467 | bgType.setSelected(rbNodes.getModel(), true); |
| 468 | } |
| 469 | if (lines.length < 2) { |
| 470 | rbWay.setEnabled(false); |
| 471 | bgType.setSelected(rbNodes.getModel(), true); |
| 472 | } |
| 473 | } |
| 474 | } |
| 475 | } |