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