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