001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.projection; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.awt.GridBagLayout; 008import java.awt.event.ActionEvent; 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 = (new ArrayList<>(SystemOfMeasurement.ALL_SYSTEMS.keySet())).toArray(new String[0]); 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<>(projectionChoices.toArray(new ProjectionChoice[0])); 286 287 /** 288 * Combobox with all coordinate display possibilities 289 */ 290 private final JosmComboBox<CoordinateFormat> coordinatesCombo = new JosmComboBox<>(CoordinateFormat.values()); 291 292 private final JosmComboBox<String> unitsCombo = new JosmComboBox<>(unitsValuesTr); 293 294 /** 295 * This variable holds the JPanel with the projection's preferences. If the 296 * selected projection does not implement this, it will be set to an empty 297 * Panel. 298 */ 299 private JPanel projSubPrefPanel; 300 private final JPanel projSubPrefPanelWrapper = new JPanel(new GridBagLayout()); 301 302 private final JLabel projectionCodeLabel = new JLabel(tr("Projection code")); 303 private final Component projectionCodeGlue = GBC.glue(5, 0); 304 private final JLabel projectionCode = new JLabel(); 305 private final JLabel projectionNameLabel = new JLabel(tr("Projection name")); 306 private final Component projectionNameGlue = GBC.glue(5, 0); 307 private final JLabel projectionName = new JLabel(); 308 private final JLabel bounds = new JLabel(); 309 310 /** 311 * This is the panel holding all projection preferences 312 */ 313 private final VerticallyScrollablePanel projPanel = new VerticallyScrollablePanel(new GridBagLayout()); 314 315 /** 316 * The GridBagConstraints for the Panel containing the ProjectionSubPrefs. 317 * This is required twice in the code, creating it here keeps both occurrences 318 * in sync 319 */ 320 private static final GBC projSubPrefPanelGBC = GBC.std().fill(GBC.BOTH).weight(1.0, 1.0); 321 322 @Override 323 public void addGui(PreferenceTabbedPane gui) { 324 ProjectionChoice pc = setupProjectionCombo(); 325 326 for (int i = 0; i < coordinatesCombo.getItemCount(); ++i) { 327 if (coordinatesCombo.getItemAt(i).name().equals(PROP_COORDINATES.get())) { 328 coordinatesCombo.setSelectedIndex(i); 329 break; 330 } 331 } 332 333 for (int i = 0; i < unitsValues.length; ++i) { 334 if (unitsValues[i].equals(PROP_SYSTEM_OF_MEASUREMENT.get())) { 335 unitsCombo.setSelectedIndex(i); 336 break; 337 } 338 } 339 340 projPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); 341 projPanel.add(new JLabel(tr("Projection method")), GBC.std().insets(5, 5, 0, 5)); 342 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 343 projPanel.add(projectionCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 344 projPanel.add(projectionCodeLabel, GBC.std().insets(25, 5, 0, 5)); 345 projPanel.add(projectionCodeGlue, GBC.std().fill(GBC.HORIZONTAL)); 346 projPanel.add(projectionCode, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 347 projPanel.add(projectionNameLabel, GBC.std().insets(25, 5, 0, 5)); 348 projPanel.add(projectionNameGlue, GBC.std().fill(GBC.HORIZONTAL)); 349 projPanel.add(projectionName, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 350 projPanel.add(new JLabel(tr("Bounds")), GBC.std().insets(25, 5, 0, 5)); 351 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 352 projPanel.add(bounds, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 353 projPanel.add(projSubPrefPanelWrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 5, 5, 5)); 354 355 projectionCodeLabel.setLabelFor(projectionCode); 356 projectionNameLabel.setLabelFor(projectionName); 357 358 projPanel.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 10)); 359 projPanel.add(new JLabel(tr("Display coordinates as")), GBC.std().insets(5, 5, 0, 5)); 360 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 361 projPanel.add(coordinatesCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 362 projPanel.add(new JLabel(tr("System of measurement")), GBC.std().insets(5, 5, 0, 5)); 363 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 364 projPanel.add(unitsCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 365 projPanel.add(GBC.glue(1, 1), GBC.std().fill(GBC.HORIZONTAL).weight(1.0, 1.0)); 366 367 gui.getMapPreference().addSubTab(this, tr("Map Projection"), projPanel.getVerticalScrollPane()); 368 369 selectedProjectionChanged(pc); 370 } 371 372 private void updateMeta(ProjectionChoice pc) { 373 pc.setPreferences(pc.getPreferences(projSubPrefPanel)); 374 Projection proj = pc.getProjection(); 375 projectionCode.setText(proj.toCode()); 376 projectionName.setText(proj.toString()); 377 Bounds b = proj.getWorldBoundsLatLon(); 378 CoordinateFormat cf = CoordinateFormat.getDefaultFormat(); 379 bounds.setText(b.getMin().lonToString(cf) + ", " + b.getMin().latToString(cf) + " : " + 380 b.getMax().lonToString(cf) + ", " + b.getMax().latToString(cf)); 381 boolean showCode = true; 382 boolean showName = false; 383 if (pc instanceof SubPrefsOptions) { 384 showCode = ((SubPrefsOptions) pc).showProjectionCode(); 385 showName = ((SubPrefsOptions) pc).showProjectionName(); 386 } 387 projectionCodeLabel.setVisible(showCode); 388 projectionCodeGlue.setVisible(showCode); 389 projectionCode.setVisible(showCode); 390 projectionNameLabel.setVisible(showName); 391 projectionNameGlue.setVisible(showName); 392 projectionName.setVisible(showName); 393 } 394 395 @Override 396 public boolean ok() { 397 ProjectionChoice pc = (ProjectionChoice) projectionCombo.getSelectedItem(); 398 399 String id = pc.getId(); 400 Collection<String> prefs = pc.getPreferences(projSubPrefPanel); 401 402 setProjection(id, prefs); 403 404 if (PROP_COORDINATES.put(((CoordinateFormat) coordinatesCombo.getSelectedItem()).name())) { 405 CoordinateFormat.setCoordinateFormat((CoordinateFormat) coordinatesCombo.getSelectedItem()); 406 } 407 408 int i = unitsCombo.getSelectedIndex(); 409 SystemOfMeasurement.setSystemOfMeasurement(unitsValues[i]); 410 411 return false; 412 } 413 414 public static void setProjection() { 415 setProjection(PROP_PROJECTION.get(), PROP_SUB_PROJECTION.get()); 416 } 417 418 public static void setProjection(String id, Collection<String> pref) { 419 ProjectionChoice pc = projectionChoicesById.get(id); 420 421 if (pc == null) { 422 JOptionPane.showMessageDialog( 423 Main.parent, 424 tr("The projection {0} could not be activated. Using Mercator", id), 425 tr("Error"), 426 JOptionPane.ERROR_MESSAGE 427 ); 428 pref = null; 429 pc = mercator; 430 } 431 id = pc.getId(); 432 PROP_PROJECTION.put(id); 433 PROP_SUB_PROJECTION.put(pref); 434 Main.pref.putCollection("projection.sub."+id, pref); 435 pc.setPreferences(pref); 436 Projection proj = pc.getProjection(); 437 Main.setProjection(proj); 438 } 439 440 /** 441 * Handles all the work related to update the projection-specific 442 * preferences 443 * @param pc the choice class representing user selection 444 */ 445 private void selectedProjectionChanged(final ProjectionChoice pc) { 446 // Don't try to update if we're still starting up 447 int size = projPanel.getComponentCount(); 448 if (size < 1) 449 return; 450 451 final ActionListener listener = new ActionListener() { 452 @Override 453 public void actionPerformed(ActionEvent e) { 454 updateMeta(pc); 455 } 456 }; 457 458 // Replace old panel with new one 459 projSubPrefPanelWrapper.removeAll(); 460 projSubPrefPanel = pc.getPreferencePanel(listener); 461 projSubPrefPanelWrapper.add(projSubPrefPanel, projSubPrefPanelGBC); 462 projPanel.revalidate(); 463 projSubPrefPanel.repaint(); 464 updateMeta(pc); 465 } 466 467 /** 468 * Sets up projection combobox with default values and action listener 469 * @return the choice class for user selection 470 */ 471 private ProjectionChoice setupProjectionCombo() { 472 ProjectionChoice pc = null; 473 for (int i = 0; i < projectionCombo.getItemCount(); ++i) { 474 ProjectionChoice pc1 = projectionCombo.getItemAt(i); 475 pc1.setPreferences(getSubprojectionPreference(pc1)); 476 if (pc1.getId().equals(PROP_PROJECTION.get())) { 477 projectionCombo.setSelectedIndex(i); 478 selectedProjectionChanged(pc1); 479 pc = pc1; 480 } 481 } 482 // If the ProjectionChoice from the preferences is not available, it 483 // should have been set to Mercator at JOSM start. 484 if (pc == null) 485 throw new RuntimeException("Couldn't find the current projection in the list of available projections!"); 486 487 projectionCombo.addActionListener(new ActionListener() { 488 @Override 489 public void actionPerformed(ActionEvent e) { 490 ProjectionChoice pc = (ProjectionChoice) projectionCombo.getSelectedItem(); 491 selectedProjectionChanged(pc); 492 } 493 }); 494 return pc; 495 } 496 497 private static Collection<String> getSubprojectionPreference(ProjectionChoice pc) { 498 return Main.pref.getCollection("projection.sub."+pc.getId(), null); 499 } 500 501 @Override 502 public boolean isExpert() { 503 return false; 504 } 505 506 @Override 507 public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) { 508 return gui.getMapPreference(); 509 } 510 511 /** 512 * Selects the given projection. 513 * @param projection The projection to select. 514 * @since 5604 515 */ 516 public void selectProjection(ProjectionChoice projection) { 517 if (projectionCombo != null && projection != null) { 518 projectionCombo.setSelectedItem(projection); 519 } 520 } 521}