001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.projection; 003 004import static org.openstreetmap.josm.data.SystemOfMeasurement.ALL_SYSTEMS; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.awt.Component; 008import java.awt.GridBagLayout; 009import java.awt.event.ActionListener; 010import java.util.ArrayList; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.HashMap; 014import java.util.List; 015import java.util.Map; 016 017import javax.swing.BorderFactory; 018import javax.swing.JLabel; 019import javax.swing.JOptionPane; 020import javax.swing.JPanel; 021import javax.swing.JSeparator; 022 023import org.openstreetmap.josm.Main; 024import org.openstreetmap.josm.data.Bounds; 025import org.openstreetmap.josm.data.SystemOfMeasurement; 026import org.openstreetmap.josm.data.coor.CoordinateFormat; 027import org.openstreetmap.josm.data.preferences.CollectionProperty; 028import org.openstreetmap.josm.data.preferences.StringProperty; 029import org.openstreetmap.josm.data.projection.CustomProjection; 030import org.openstreetmap.josm.data.projection.Projection; 031import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 032import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 033import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 034import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting; 035import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting; 036import org.openstreetmap.josm.gui.widgets.JosmComboBox; 037import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel; 038import org.openstreetmap.josm.tools.GBC; 039 040/** 041 * Projection preferences. 042 * 043 * How to add new Projections: 044 * - Find EPSG code for the projection. 045 * - Look up the parameter string for Proj4, e.g. on http://spatialreference.org/ 046 * and add it to the file 'data/projection/epsg' in JOSM trunk 047 * - Search for official references and verify the parameter values. These 048 * documents are often available in the local language only. 049 * - Use {@link #registerProjectionChoice}, to make the entry known to JOSM. 050 * 051 * In case there is no EPSG code: 052 * - override {@link AbstractProjectionChoice#getProjection()} and provide 053 * a manual implementation of the projection. Use {@link CustomProjection} 054 * if possible. 055 */ 056public class ProjectionPreference implements SubPreferenceSetting { 057 058 /** 059 * Factory used to create a new {@code ProjectionPreference}. 060 */ 061 public static class Factory implements PreferenceSettingFactory { 062 @Override 063 public PreferenceSetting createPreferenceSetting() { 064 return new ProjectionPreference(); 065 } 066 } 067 068 private static List<ProjectionChoice> projectionChoices = new ArrayList<>(); 069 private static Map<String, ProjectionChoice> projectionChoicesById = new HashMap<>(); 070 071 /** 072 * WGS84: Directly use latitude / longitude values as x/y. 073 */ 074 public static final ProjectionChoice wgs84 = registerProjectionChoice(tr("WGS84 Geographic"), "core:wgs84", 4326, "epsg4326"); 075 076 /** 077 * Mercator Projection. 078 * 079 * The center of the mercator projection is always the 0 grad coordinate. 080 * 081 * See also USGS Bulletin 1532 (http://pubs.usgs.gov/bul/1532/report.pdf) 082 * initially EPSG used 3785 but that has been superseded by 3857, see https://www.epsg-registry.org/ 083 */ 084 public static final ProjectionChoice mercator = registerProjectionChoice(tr("Mercator"), "core:mercator", 3857); 085 086 /** 087 * Lambert conic conform 4 zones using the French geodetic system NTF. 088 * 089 * This newer version uses the grid translation NTF<->RGF93 provided by IGN for a submillimetric accuracy. 090 * (RGF93 is the French geodetic system similar to WGS84 but not mathematically equal) 091 * 092 * Source: http://geodesie.ign.fr/contenu/fichiers/Changement_systeme_geodesique.pdf 093 */ 094 public static final ProjectionChoice lambert = new LambertProjectionChoice(); 095 096 /** 097 * French departements in the Caribbean Sea and Indian Ocean. 098 * 099 * Using the UTM transvers Mercator projection and specific geodesic settings. 100 */ 101 public static final ProjectionChoice utm_france_dom = new UTMFranceDOMProjectionChoice(); 102 103 /** 104 * Lambert Conic Conform 9 Zones projection. 105 * 106 * As specified by the IGN in this document 107 * http://geodesie.ign.fr/contenu/fichiers/documentation/rgf93/cc9zones.pdf 108 */ 109 public static final ProjectionChoice lambert_cc9 = new LambertCC9ZonesProjectionChoice(); 110 111 static { 112 113 /************************ 114 * Global projections. 115 */ 116 117 /** 118 * UTM. 119 */ 120 registerProjectionChoice(new UTMProjectionChoice()); 121 122 /************************ 123 * Regional - alphabetical order by country code. 124 */ 125 126 /** 127 * Belgian Lambert 72 projection. 128 * 129 * As specified by the Belgian IGN in this document: 130 * http://www.ngi.be/Common/Lambert2008/Transformation_Geographic_Lambert_FR.pdf 131 * 132 * @author Don-vip 133 */ 134 registerProjectionChoice(tr("Belgian Lambert 1972"), "core:belgianLambert1972", 31370); // BE 135 136 /** 137 * Belgian Lambert 2008 projection. 138 * 139 * As specified by the Belgian IGN in this document: 140 * http://www.ngi.be/Common/Lambert2008/Transformation_Geographic_Lambert_FR.pdf 141 * 142 * @author Don-vip 143 */ 144 registerProjectionChoice(tr("Belgian Lambert 2008"), "core:belgianLambert2008", 3812); // BE 145 146 /** 147 * SwissGrid CH1903 / L03, see https://en.wikipedia.org/wiki/Swiss_coordinate_system. 148 * 149 * Actually, what we have here, is CH1903+ (EPSG:2056), but without 150 * the additional false easting of 2000km and false northing 1000 km. 151 * 152 * To get to CH1903, a shift file is required. So currently, there are errors 153 * up to 1.6m (depending on the location). 154 */ 155 registerProjectionChoice(new SwissGridProjectionChoice()); // CH 156 157 registerProjectionChoice(new GaussKruegerProjectionChoice()); // DE 158 159 /** 160 * Estonian Coordinate System of 1997. 161 * 162 * Thanks to Johan Montagnat and its geoconv java converter application 163 * (https://www.i3s.unice.fr/~johan/gps/ , published under GPL license) 164 * from which some code and constants have been reused here. 165 */ 166 registerProjectionChoice(tr("Lambert Zone (Estonia)"), "core:lambertest", 3301); // EE 167 168 /** 169 * Lambert conic conform 4 zones using the French geodetic system NTF. 170 * 171 * This newer version uses the grid translation NTF<->RGF93 provided by IGN for a submillimetric accuracy. 172 * (RGF93 is the French geodetic system similar to WGS84 but not mathematically equal) 173 * 174 * Source: http://geodesie.ign.fr/contenu/fichiers/Changement_systeme_geodesique.pdf 175 * @author Pieren 176 */ 177 registerProjectionChoice(lambert); // FR 178 179 /** 180 * Lambert 93 projection. 181 * 182 * As specified by the IGN in this document 183 * http://geodesie.ign.fr/contenu/fichiers/documentation/rgf93/Lambert-93.pdf 184 * @author Don-vip 185 */ 186 registerProjectionChoice(tr("Lambert 93 (France)"), "core:lambert93", 2154); // FR 187 188 /** 189 * Lambert Conic Conform 9 Zones projection. 190 * 191 * As specified by the IGN in this document 192 * http://geodesie.ign.fr/contenu/fichiers/documentation/rgf93/cc9zones.pdf 193 * @author Pieren 194 */ 195 registerProjectionChoice(lambert_cc9); // FR 196 197 /** 198 * French departements in the Caribbean Sea and Indian Ocean. 199 * 200 * Using the UTM transvers Mercator projection and specific geodesic settings. 201 */ 202 registerProjectionChoice(utm_france_dom); // FR 203 204 /** 205 * LKS-92/ Latvia TM projection. 206 * 207 * Based on data from spatialreference.org. 208 * http://spatialreference.org/ref/epsg/3059/ 209 * 210 * @author Viesturs Zarins 211 */ 212 registerProjectionChoice(tr("LKS-92 (Latvia TM)"), "core:tmerclv", 3059); // LV 213 214 /** 215 * Netherlands RD projection 216 * 217 * @author vholten 218 */ 219 registerProjectionChoice(tr("Rijksdriehoekscoördinaten (Netherlands)"), "core:dutchrd", 28992); // NL 220 221 /** 222 * PUWG 1992 and 2000 are the official cordinate systems in Poland. 223 * 224 * They use the same math as UTM only with different constants. 225 * 226 * @author steelman 227 */ 228 registerProjectionChoice(new PuwgProjectionChoice()); // PL 229 230 /** 231 * SWEREF99 13 30 projection. Based on data from spatialreference.org. 232 * http://spatialreference.org/ref/epsg/3008/ 233 * 234 * @author Hanno Hecker 235 */ 236 registerProjectionChoice(tr("SWEREF99 13 30 / EPSG:3008 (Sweden)"), "core:sweref99", 3008); // SE 237 238 /************************ 239 * Projection by Code. 240 */ 241 registerProjectionChoice(new CodeProjectionChoice()); 242 243 /************************ 244 * Custom projection. 245 */ 246 registerProjectionChoice(new CustomProjectionChoice()); 247 } 248 249 public static void registerProjectionChoice(ProjectionChoice c) { 250 projectionChoices.add(c); 251 projectionChoicesById.put(c.getId(), c); 252 } 253 254 public static ProjectionChoice registerProjectionChoice(String name, String id, Integer epsg, String cacheDir) { 255 ProjectionChoice pc = new SingleProjectionChoice(name, id, "EPSG:"+epsg, cacheDir); 256 registerProjectionChoice(pc); 257 return pc; 258 } 259 260 private static ProjectionChoice registerProjectionChoice(String name, String id, Integer epsg) { 261 ProjectionChoice pc = new SingleProjectionChoice(name, id, "EPSG:"+epsg); 262 registerProjectionChoice(pc); 263 return pc; 264 } 265 266 public static List<ProjectionChoice> getProjectionChoices() { 267 return Collections.unmodifiableList(projectionChoices); 268 } 269 270 private static final StringProperty PROP_PROJECTION = new StringProperty("projection", mercator.getId()); 271 private static final StringProperty PROP_COORDINATES = new StringProperty("coordinates", null); 272 private static final CollectionProperty PROP_SUB_PROJECTION = new CollectionProperty("projection.sub", null); 273 public static final StringProperty PROP_SYSTEM_OF_MEASUREMENT = new StringProperty("system_of_measurement", "Metric"); 274 private static final String[] unitsValues = ALL_SYSTEMS.keySet().toArray(new String[ALL_SYSTEMS.size()]); 275 private static final String[] unitsValuesTr = new String[unitsValues.length]; 276 static { 277 for (int i = 0; i < unitsValues.length; ++i) { 278 unitsValuesTr[i] = tr(unitsValues[i]); 279 } 280 } 281 282 /** 283 * Combobox with all projections available 284 */ 285 private final JosmComboBox<ProjectionChoice> projectionCombo = new JosmComboBox<>( 286 projectionChoices.toArray(new ProjectionChoice[projectionChoices.size()])); 287 288 /** 289 * Combobox with all coordinate display possibilities 290 */ 291 private final JosmComboBox<CoordinateFormat> coordinatesCombo = new JosmComboBox<>(CoordinateFormat.values()); 292 293 private final JosmComboBox<String> unitsCombo = new JosmComboBox<>(unitsValuesTr); 294 295 /** 296 * This variable holds the JPanel with the projection's preferences. If the 297 * selected projection does not implement this, it will be set to an empty 298 * Panel. 299 */ 300 private JPanel projSubPrefPanel; 301 private final JPanel projSubPrefPanelWrapper = new JPanel(new GridBagLayout()); 302 303 private final JLabel projectionCodeLabel = new JLabel(tr("Projection code")); 304 private final Component projectionCodeGlue = GBC.glue(5, 0); 305 private final JLabel projectionCode = new JLabel(); 306 private final JLabel projectionNameLabel = new JLabel(tr("Projection name")); 307 private final Component projectionNameGlue = GBC.glue(5, 0); 308 private final JLabel projectionName = new JLabel(); 309 private final JLabel bounds = new JLabel(); 310 311 /** 312 * This is the panel holding all projection preferences 313 */ 314 private final VerticallyScrollablePanel projPanel = new VerticallyScrollablePanel(new GridBagLayout()); 315 316 /** 317 * The GridBagConstraints for the Panel containing the ProjectionSubPrefs. 318 * This is required twice in the code, creating it here keeps both occurrences 319 * in sync 320 */ 321 private static final GBC projSubPrefPanelGBC = GBC.std().fill(GBC.BOTH).weight(1.0, 1.0); 322 323 @Override 324 public void addGui(PreferenceTabbedPane gui) { 325 ProjectionChoice pc = setupProjectionCombo(); 326 327 for (int i = 0; i < coordinatesCombo.getItemCount(); ++i) { 328 if (coordinatesCombo.getItemAt(i).name().equals(PROP_COORDINATES.get())) { 329 coordinatesCombo.setSelectedIndex(i); 330 break; 331 } 332 } 333 334 for (int i = 0; i < unitsValues.length; ++i) { 335 if (unitsValues[i].equals(PROP_SYSTEM_OF_MEASUREMENT.get())) { 336 unitsCombo.setSelectedIndex(i); 337 break; 338 } 339 } 340 341 projPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); 342 projPanel.add(new JLabel(tr("Projection method")), GBC.std().insets(5, 5, 0, 5)); 343 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 344 projPanel.add(projectionCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 345 projPanel.add(projectionCodeLabel, GBC.std().insets(25, 5, 0, 5)); 346 projPanel.add(projectionCodeGlue, GBC.std().fill(GBC.HORIZONTAL)); 347 projPanel.add(projectionCode, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 348 projPanel.add(projectionNameLabel, GBC.std().insets(25, 5, 0, 5)); 349 projPanel.add(projectionNameGlue, GBC.std().fill(GBC.HORIZONTAL)); 350 projPanel.add(projectionName, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 351 projPanel.add(new JLabel(tr("Bounds")), GBC.std().insets(25, 5, 0, 5)); 352 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 353 projPanel.add(bounds, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 354 projPanel.add(projSubPrefPanelWrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 5, 5, 5)); 355 356 projectionCodeLabel.setLabelFor(projectionCode); 357 projectionNameLabel.setLabelFor(projectionName); 358 359 projPanel.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 10)); 360 projPanel.add(new JLabel(tr("Display coordinates as")), GBC.std().insets(5, 5, 0, 5)); 361 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 362 projPanel.add(coordinatesCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 363 projPanel.add(new JLabel(tr("System of measurement")), GBC.std().insets(5, 5, 0, 5)); 364 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 365 projPanel.add(unitsCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 366 projPanel.add(GBC.glue(1, 1), GBC.std().fill(GBC.HORIZONTAL).weight(1.0, 1.0)); 367 368 gui.getMapPreference().addSubTab(this, tr("Map Projection"), projPanel.getVerticalScrollPane()); 369 370 selectedProjectionChanged(pc); 371 } 372 373 private void updateMeta(ProjectionChoice pc) { 374 pc.setPreferences(pc.getPreferences(projSubPrefPanel)); 375 Projection proj = pc.getProjection(); 376 projectionCode.setText(proj.toCode()); 377 projectionName.setText(proj.toString()); 378 Bounds b = proj.getWorldBoundsLatLon(); 379 CoordinateFormat cf = CoordinateFormat.getDefaultFormat(); 380 bounds.setText(b.getMin().lonToString(cf) + ", " + b.getMin().latToString(cf) + " : " + 381 b.getMax().lonToString(cf) + ", " + b.getMax().latToString(cf)); 382 boolean showCode = true; 383 boolean showName = false; 384 if (pc instanceof SubPrefsOptions) { 385 showCode = ((SubPrefsOptions) pc).showProjectionCode(); 386 showName = ((SubPrefsOptions) pc).showProjectionName(); 387 } 388 projectionCodeLabel.setVisible(showCode); 389 projectionCodeGlue.setVisible(showCode); 390 projectionCode.setVisible(showCode); 391 projectionNameLabel.setVisible(showName); 392 projectionNameGlue.setVisible(showName); 393 projectionName.setVisible(showName); 394 } 395 396 @Override 397 public boolean ok() { 398 ProjectionChoice pc = (ProjectionChoice) projectionCombo.getSelectedItem(); 399 400 String id = pc.getId(); 401 Collection<String> prefs = pc.getPreferences(projSubPrefPanel); 402 403 setProjection(id, prefs); 404 405 if (PROP_COORDINATES.put(((CoordinateFormat) coordinatesCombo.getSelectedItem()).name())) { 406 CoordinateFormat.setCoordinateFormat((CoordinateFormat) coordinatesCombo.getSelectedItem()); 407 } 408 409 int i = unitsCombo.getSelectedIndex(); 410 SystemOfMeasurement.setSystemOfMeasurement(unitsValues[i]); 411 412 return false; 413 } 414 415 public static void setProjection() { 416 setProjection(PROP_PROJECTION.get(), PROP_SUB_PROJECTION.get()); 417 } 418 419 public static void setProjection(String id, Collection<String> pref) { 420 ProjectionChoice pc = projectionChoicesById.get(id); 421 422 if (pc == null) { 423 JOptionPane.showMessageDialog( 424 Main.parent, 425 tr("The projection {0} could not be activated. Using Mercator", id), 426 tr("Error"), 427 JOptionPane.ERROR_MESSAGE 428 ); 429 pref = null; 430 pc = mercator; 431 } 432 id = pc.getId(); 433 PROP_PROJECTION.put(id); 434 PROP_SUB_PROJECTION.put(pref); 435 Main.pref.putCollection("projection.sub."+id, pref); 436 pc.setPreferences(pref); 437 Projection proj = pc.getProjection(); 438 Main.setProjection(proj); 439 } 440 441 /** 442 * Handles all the work related to update the projection-specific 443 * preferences 444 * @param pc the choice class representing user selection 445 */ 446 private void selectedProjectionChanged(final ProjectionChoice pc) { 447 // Don't try to update if we're still starting up 448 int size = projPanel.getComponentCount(); 449 if (size < 1) 450 return; 451 452 final ActionListener listener = e -> updateMeta(pc); 453 454 // Replace old panel with new one 455 projSubPrefPanelWrapper.removeAll(); 456 projSubPrefPanel = pc.getPreferencePanel(listener); 457 projSubPrefPanelWrapper.add(projSubPrefPanel, projSubPrefPanelGBC); 458 projPanel.revalidate(); 459 projSubPrefPanel.repaint(); 460 updateMeta(pc); 461 } 462 463 /** 464 * Sets up projection combobox with default values and action listener 465 * @return the choice class for user selection 466 */ 467 private ProjectionChoice setupProjectionCombo() { 468 ProjectionChoice pc = null; 469 for (int i = 0; i < projectionCombo.getItemCount(); ++i) { 470 ProjectionChoice pc1 = projectionCombo.getItemAt(i); 471 pc1.setPreferences(getSubprojectionPreference(pc1)); 472 if (pc1.getId().equals(PROP_PROJECTION.get())) { 473 projectionCombo.setSelectedIndex(i); 474 selectedProjectionChanged(pc1); 475 pc = pc1; 476 } 477 } 478 // If the ProjectionChoice from the preferences is not available, it 479 // should have been set to Mercator at JOSM start. 480 if (pc == null) 481 throw new RuntimeException("Couldn't find the current projection in the list of available projections!"); 482 483 projectionCombo.addActionListener(e -> { 484 ProjectionChoice pc1 = (ProjectionChoice) projectionCombo.getSelectedItem(); 485 selectedProjectionChanged(pc1); 486 }); 487 return pc; 488 } 489 490 private static Collection<String> getSubprojectionPreference(ProjectionChoice pc) { 491 return Main.pref.getCollection("projection.sub."+pc.getId(), null); 492 } 493 494 @Override 495 public boolean isExpert() { 496 return false; 497 } 498 499 @Override 500 public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) { 501 return gui.getMapPreference(); 502 } 503 504 /** 505 * Selects the given projection. 506 * @param projection The projection to select. 507 * @since 5604 508 */ 509 public void selectProjection(ProjectionChoice projection) { 510 if (projectionCombo != null && projection != null) { 511 projectionCombo.setSelectedItem(projection); 512 } 513 } 514}