source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/LatLonDialog.java@ 3656

Last change on this file since 3656 was 3656, checked in by bastiK, 13 years ago

applied #5604 (patch by m.zdila) - support different formats of GPS coordinates in Add Node

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