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.awt.event.WindowAdapter; 012import java.awt.event.WindowEvent; 013import java.util.Arrays; 014 015import javax.swing.BorderFactory; 016import javax.swing.JLabel; 017import javax.swing.JPanel; 018import javax.swing.JSeparator; 019import javax.swing.JTabbedPane; 020import javax.swing.UIManager; 021import javax.swing.event.DocumentEvent; 022import javax.swing.event.DocumentListener; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.data.coor.CoordinateFormat; 026import org.openstreetmap.josm.data.coor.EastNorth; 027import org.openstreetmap.josm.data.coor.LatLon; 028import org.openstreetmap.josm.gui.ExtendedDialog; 029import org.openstreetmap.josm.gui.widgets.HtmlPanel; 030import org.openstreetmap.josm.gui.widgets.JosmTextField; 031import org.openstreetmap.josm.tools.GBC; 032import org.openstreetmap.josm.tools.Utils; 033import org.openstreetmap.josm.tools.WindowGeometry; 034 035public class LatLonDialog extends ExtendedDialog { 036 private static final Color BG_COLOR_ERROR = new Color(255, 224, 224); 037 038 public JTabbedPane tabs; 039 private JosmTextField tfLatLon, tfEastNorth; 040 private LatLon latLonCoordinates; 041 private EastNorth eastNorthCoordinates; 042 043 protected JPanel buildLatLon() { 044 JPanel pnl = new JPanel(new GridBagLayout()); 045 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 046 047 pnl.add(new JLabel(tr("Coordinates:")), GBC.std().insets(0, 10, 5, 0)); 048 tfLatLon = new JosmTextField(24); 049 pnl.add(tfLatLon, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL).weight(1.0, 0.0)); 050 051 pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); 052 053 pnl.add(new HtmlPanel( 054 Utils.join("<br/>", Arrays.asList( 055 tr("Enter the coordinates for the new node."), 056 tr("You can separate longitude and latitude with space, comma or semicolon."), 057 tr("Use positive numbers or N, E characters to indicate North or East cardinal direction."), 058 tr("For South and West cardinal directions you can use either negative numbers or S, W characters."), 059 tr("Coordinate value can be in one of three formats:") 060 )) + 061 Utils.joinAsHtmlUnorderedList(Arrays.asList( 062 tr("<i>degrees</i><tt>°</tt>"), 063 tr("<i>degrees</i><tt>°</tt> <i>minutes</i><tt>'</tt>"), 064 tr("<i>degrees</i><tt>°</tt> <i>minutes</i><tt>'</tt> <i>seconds</i><tt>"</tt>") 065 )) + 066 Utils.join("<br/><br/>", Arrays.asList( 067 tr("Symbols <tt>°</tt>, <tt>'</tt>, <tt>′</tt>, <tt>"</tt>, <tt>″</tt> are optional."), 068 tr("You can also use the syntax <tt>lat=\"...\" lon=\"...\"</tt> or <tt>lat=''...'' lon=''...''</tt>."), 069 tr("Some examples:") 070 )) + 071 "<table><tr><td>" + 072 Utils.joinAsHtmlUnorderedList(Arrays.asList( 073 "49.29918° 19.24788°", 074 "N 49.29918 E 19.24788", 075 "W 49°29.918' S 19°24.788'", 076 "N 49°29'04" E 19°24'43"", 077 "49.29918 N, 19.24788 E", 078 "49°29'21" N 19°24'38" E", 079 "49 29 51, 19 24 18", 080 "49 29, 19 24", 081 "E 49 29, N 19 24" 082 )) + 083 "</td><td>" + 084 Utils.joinAsHtmlUnorderedList(Arrays.asList( 085 "49° 29; 19° 24", 086 "N 49° 29, W 19° 24", 087 "49° 29.5 S, 19° 24.6 E", 088 "N 49 29.918 E 19 15.88", 089 "49 29.4 19 24.5", 090 "-49 29.4 N -19 24.5 W", 091 "48 deg 42' 52.13\" N, 21 deg 11' 47.60\" E", 092 "lat=\"49.29918\" lon=\"19.24788\"", 093 "lat='49.29918' lon='19.24788'" 094 )) + 095 "</td></tr></table>"), 096 GBC.eol().fill().weight(1.0, 1.0)); 097 098 // parse and verify input on the fly 099 // 100 LatLonInputVerifier inputVerifier = new LatLonInputVerifier(); 101 tfLatLon.getDocument().addDocumentListener(inputVerifier); 102 103 // select the text in the field on focus 104 // 105 TextFieldFocusHandler focusHandler = new TextFieldFocusHandler(); 106 tfLatLon.addFocusListener(focusHandler); 107 return pnl; 108 } 109 110 private JPanel buildEastNorth() { 111 JPanel pnl = new JPanel(new GridBagLayout()); 112 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 113 114 pnl.add(new JLabel(tr("Projected coordinates:")), GBC.std().insets(0, 10, 5, 0)); 115 tfEastNorth = new JosmTextField(24); 116 117 pnl.add(tfEastNorth, GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL).weight(1.0, 0.0)); 118 119 pnl.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); 120 121 pnl.add(new HtmlPanel( 122 tr("Enter easting and northing (x and y) separated by space, comma or semicolon.")), 123 GBC.eol().fill(GBC.HORIZONTAL)); 124 125 pnl.add(GBC.glue(1, 1), GBC.eol().fill().weight(1.0, 1.0)); 126 127 EastNorthInputVerifier inputVerifier = new EastNorthInputVerifier(); 128 tfEastNorth.getDocument().addDocumentListener(inputVerifier); 129 130 TextFieldFocusHandler focusHandler = new TextFieldFocusHandler(); 131 tfEastNorth.addFocusListener(focusHandler); 132 133 return pnl; 134 } 135 136 protected void build() { 137 tabs = new JTabbedPane(); 138 tabs.addTab(tr("Lat/Lon"), buildLatLon()); 139 tabs.addTab(tr("East/North"), buildEastNorth()); 140 tabs.getModel().addChangeListener(e -> { 141 switch (tabs.getModel().getSelectedIndex()) { 142 case 0: parseLatLonUserInput(); break; 143 case 1: parseEastNorthUserInput(); break; 144 default: throw new AssertionError(); 145 } 146 }); 147 setContent(tabs, false); 148 addWindowListener(new WindowAdapter() { 149 @Override 150 public void windowOpened(WindowEvent e) { 151 tfLatLon.requestFocusInWindow(); 152 } 153 }); 154 } 155 156 public LatLonDialog(Component parent, String title, String help) { 157 super(parent, title, new String[] {tr("Ok"), tr("Cancel")}); 158 setButtonIcons(new String[] {"ok", "cancel"}); 159 configureContextsensitiveHelp(help, true); 160 161 build(); 162 setCoordinates(null); 163 } 164 165 public boolean isLatLon() { 166 return tabs.getModel().getSelectedIndex() == 0; 167 } 168 169 public void setCoordinates(LatLon ll) { 170 if (ll == null) { 171 ll = LatLon.ZERO; 172 } 173 this.latLonCoordinates = ll; 174 tfLatLon.setText(ll.latToString(CoordinateFormat.getDefaultFormat()) + ' ' + ll.lonToString(CoordinateFormat.getDefaultFormat())); 175 EastNorth en = Main.getProjection().latlon2eastNorth(ll); 176 tfEastNorth.setText(Double.toString(en.east()) + ' ' + Double.toString(en.north())); 177 setOkEnabled(true); 178 } 179 180 public LatLon getCoordinates() { 181 if (isLatLon()) { 182 return latLonCoordinates; 183 } else { 184 if (eastNorthCoordinates == null) return null; 185 return Main.getProjection().eastNorth2latlon(eastNorthCoordinates); 186 } 187 } 188 189 public LatLon getLatLonCoordinates() { 190 return latLonCoordinates; 191 } 192 193 public EastNorth getEastNorthCoordinates() { 194 return eastNorthCoordinates; 195 } 196 197 protected void setErrorFeedback(JosmTextField tf, String message) { 198 tf.setBorder(BorderFactory.createLineBorder(Color.RED, 1)); 199 tf.setToolTipText(message); 200 tf.setBackground(BG_COLOR_ERROR); 201 } 202 203 protected void clearErrorFeedback(JosmTextField tf, String message) { 204 tf.setBorder(UIManager.getBorder("TextField.border")); 205 tf.setToolTipText(message); 206 tf.setBackground(UIManager.getColor("TextField.background")); 207 } 208 209 protected void parseLatLonUserInput() { 210 LatLon latLon; 211 try { 212 latLon = LatLon.parse(tfLatLon.getText()); 213 if (!LatLon.isValidLat(latLon.lat()) || !LatLon.isValidLon(latLon.lon())) { 214 latLon = null; 215 } 216 } catch (IllegalArgumentException e) { 217 Main.trace(e); 218 latLon = null; 219 } 220 if (latLon == null) { 221 setErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates")); 222 latLonCoordinates = null; 223 setOkEnabled(false); 224 } else { 225 clearErrorFeedback(tfLatLon, tr("Please enter a GPS coordinates")); 226 latLonCoordinates = latLon; 227 setOkEnabled(true); 228 } 229 } 230 231 protected void parseEastNorthUserInput() { 232 EastNorth en; 233 try { 234 en = parseEastNorth(tfEastNorth.getText()); 235 } catch (IllegalArgumentException e) { 236 Main.trace(e); 237 en = null; 238 } 239 if (en == null) { 240 setErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing")); 241 latLonCoordinates = null; 242 setOkEnabled(false); 243 } else { 244 clearErrorFeedback(tfEastNorth, tr("Please enter a Easting and Northing")); 245 eastNorthCoordinates = en; 246 setOkEnabled(true); 247 } 248 } 249 250 private void setOkEnabled(boolean b) { 251 if (buttons != null && !buttons.isEmpty()) { 252 buttons.get(0).setEnabled(b); 253 } 254 } 255 256 @Override 257 public void setVisible(boolean visible) { 258 final String preferenceKey = getClass().getName() + ".geometry"; 259 if (visible) { 260 new WindowGeometry( 261 preferenceKey, 262 WindowGeometry.centerInWindow(getParent(), getSize()) 263 ).applySafe(this); 264 } else { 265 new WindowGeometry(this).remember(preferenceKey); 266 } 267 super.setVisible(visible); 268 } 269 270 class LatLonInputVerifier implements DocumentListener { 271 @Override 272 public void changedUpdate(DocumentEvent e) { 273 parseLatLonUserInput(); 274 } 275 276 @Override 277 public void insertUpdate(DocumentEvent e) { 278 parseLatLonUserInput(); 279 } 280 281 @Override 282 public void removeUpdate(DocumentEvent e) { 283 parseLatLonUserInput(); 284 } 285 } 286 287 class EastNorthInputVerifier implements DocumentListener { 288 @Override 289 public void changedUpdate(DocumentEvent e) { 290 parseEastNorthUserInput(); 291 } 292 293 @Override 294 public void insertUpdate(DocumentEvent e) { 295 parseEastNorthUserInput(); 296 } 297 298 @Override 299 public void removeUpdate(DocumentEvent e) { 300 parseEastNorthUserInput(); 301 } 302 } 303 304 static class TextFieldFocusHandler implements FocusListener { 305 @Override 306 public void focusGained(FocusEvent e) { 307 Component c = e.getComponent(); 308 if (c instanceof JosmTextField) { 309 JosmTextField tf = (JosmTextField) c; 310 tf.selectAll(); 311 } 312 } 313 314 @Override 315 public void focusLost(FocusEvent e) { 316 // Not used 317 } 318 } 319 320 /** 321 * Parses the given string as lat/lon. 322 * @param coord String to parse 323 * @return parsed lat/lon 324 * @deprecated use {@link LatLon#parse(String)} instead 325 */ 326 @Deprecated 327 public static LatLon parseLatLon(final String coord) { 328 return LatLon.parse(coord); 329 } 330 331 public static EastNorth parseEastNorth(String s) { 332 String[] en = s.split("[;, ]+"); 333 if (en.length != 2) return null; 334 try { 335 double east = Double.parseDouble(en[0]); 336 double north = Double.parseDouble(en[1]); 337 return new EastNorth(east, north); 338 } catch (NumberFormatException nfe) { 339 return null; 340 } 341 } 342 343 public String getLatLonText() { 344 return tfLatLon.getText(); 345 } 346 347 public void setLatLonText(String text) { 348 tfLatLon.setText(text); 349 } 350 351 public String getEastNorthText() { 352 return tfEastNorth.getText(); 353 } 354 355 public void setEastNorthText(String text) { 356 tfEastNorth.setText(text); 357 } 358}