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