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.JScrollPane; 022import javax.swing.JSeparator; 023 024import org.openstreetmap.josm.Main; 025import org.openstreetmap.josm.data.Bounds; 026import org.openstreetmap.josm.data.SystemOfMeasurement; 027import org.openstreetmap.josm.data.coor.CoordinateFormat; 028import org.openstreetmap.josm.data.preferences.CollectionProperty; 029import org.openstreetmap.josm.data.preferences.StringProperty; 030import org.openstreetmap.josm.data.projection.CustomProjection; 031import org.openstreetmap.josm.data.projection.Projection; 032import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 033import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 034import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 035import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting; 036import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting; 037import org.openstreetmap.josm.gui.widgets.JosmComboBox; 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 * PUWG 1992 and 2000 are the official cordinate systems in Poland. 197 * 198 * They use the same math as UTM only with different constants. 199 * 200 * @author steelman 201 */ 202 registerProjectionChoice(new PuwgProjectionChoice()); // PL 203 204 /** 205 * SWEREF99 13 30 projection. Based on data from spatialreference.org. 206 * http://spatialreference.org/ref/epsg/3008/ 207 * 208 * @author Hanno Hecker 209 */ 210 registerProjectionChoice(tr("SWEREF99 13 30 / EPSG:3008 (Sweden)"), "core:sweref99", 3008); // SE 211 212 /************************ 213 * Projection by Code. 214 */ 215 registerProjectionChoice(new CodeProjectionChoice()); 216 217 /************************ 218 * Custom projection. 219 */ 220 registerProjectionChoice(new CustomProjectionChoice()); 221 } 222 223 public static void registerProjectionChoice(ProjectionChoice c) { 224 projectionChoices.add(c); 225 projectionChoicesById.put(c.getId(), c); 226 } 227 228 public static ProjectionChoice registerProjectionChoice(String name, String id, Integer epsg, String cacheDir) { 229 ProjectionChoice pc = new SingleProjectionChoice(name, id, "EPSG:"+epsg, cacheDir); 230 registerProjectionChoice(pc); 231 return pc; 232 } 233 234 private static ProjectionChoice registerProjectionChoice(String name, String id, Integer epsg) { 235 ProjectionChoice pc = new SingleProjectionChoice(name, id, "EPSG:"+epsg); 236 registerProjectionChoice(pc); 237 return pc; 238 } 239 240 public static List<ProjectionChoice> getProjectionChoices() { 241 return Collections.unmodifiableList(projectionChoices); 242 } 243 244 private static final StringProperty PROP_PROJECTION = new StringProperty("projection", mercator.getId()); 245 private static final StringProperty PROP_COORDINATES = new StringProperty("coordinates", null); 246 private static final CollectionProperty PROP_SUB_PROJECTION = new CollectionProperty("projection.sub", null); 247 public static final StringProperty PROP_SYSTEM_OF_MEASUREMENT = new StringProperty("system_of_measurement", "Metric"); 248 private static final String[] unitsValues = (new ArrayList<>(SystemOfMeasurement.ALL_SYSTEMS.keySet())).toArray(new String[0]); 249 private static final String[] unitsValuesTr = new String[unitsValues.length]; 250 static { 251 for (int i = 0; i < unitsValues.length; ++i) { 252 unitsValuesTr[i] = tr(unitsValues[i]); 253 } 254 } 255 256 /** 257 * Combobox with all projections available 258 */ 259 private final JosmComboBox<ProjectionChoice> projectionCombo = new JosmComboBox<>(projectionChoices.toArray(new ProjectionChoice[0])); 260 261 /** 262 * Combobox with all coordinate display possibilities 263 */ 264 private final JosmComboBox<CoordinateFormat> coordinatesCombo = new JosmComboBox<>(CoordinateFormat.values()); 265 266 private final JosmComboBox<String> unitsCombo = new JosmComboBox<>(unitsValuesTr); 267 268 /** 269 * This variable holds the JPanel with the projection's preferences. If the 270 * selected projection does not implement this, it will be set to an empty 271 * Panel. 272 */ 273 private JPanel projSubPrefPanel; 274 private JPanel projSubPrefPanelWrapper = new JPanel(new GridBagLayout()); 275 276 private JLabel projectionCodeLabel; 277 private Component projectionCodeGlue; 278 private JLabel projectionCode = new JLabel(); 279 private JLabel projectionNameLabel; 280 private Component projectionNameGlue; 281 private JLabel projectionName = new JLabel(); 282 private JLabel bounds = new JLabel(); 283 284 /** 285 * This is the panel holding all projection preferences 286 */ 287 private final JPanel projPanel = new JPanel(new GridBagLayout()); 288 289 /** 290 * The GridBagConstraints for the Panel containing the ProjectionSubPrefs. 291 * This is required twice in the code, creating it here keeps both occurrences 292 * in sync 293 */ 294 private static final GBC projSubPrefPanelGBC = GBC.std().fill(GBC.BOTH).weight(1.0, 1.0); 295 296 @Override 297 public void addGui(PreferenceTabbedPane gui) { 298 ProjectionChoice pc = setupProjectionCombo(); 299 300 for (int i = 0; i < coordinatesCombo.getItemCount(); ++i) { 301 if (coordinatesCombo.getItemAt(i).name().equals(PROP_COORDINATES.get())) { 302 coordinatesCombo.setSelectedIndex(i); 303 break; 304 } 305 } 306 307 for (int i = 0; i < unitsValues.length; ++i) { 308 if (unitsValues[i].equals(PROP_SYSTEM_OF_MEASUREMENT.get())) { 309 unitsCombo.setSelectedIndex(i); 310 break; 311 } 312 } 313 314 projPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0)); 315 projPanel.setLayout(new GridBagLayout()); 316 projPanel.add(new JLabel(tr("Projection method")), GBC.std().insets(5, 5, 0, 5)); 317 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 318 projPanel.add(projectionCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 319 projPanel.add(projectionCodeLabel = new JLabel(tr("Projection code")), GBC.std().insets(25, 5, 0, 5)); 320 projPanel.add(projectionCodeGlue = GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 321 projPanel.add(projectionCode, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 322 projPanel.add(projectionNameLabel = new JLabel(tr("Projection name")), GBC.std().insets(25, 5, 0, 5)); 323 projPanel.add(projectionNameGlue = GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 324 projPanel.add(projectionName, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 325 projPanel.add(new JLabel(tr("Bounds")), GBC.std().insets(25, 5, 0, 5)); 326 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 327 projPanel.add(bounds, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 328 projPanel.add(projSubPrefPanelWrapper, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 5, 5, 5)); 329 330 projectionCodeLabel.setLabelFor(projectionCode); 331 projectionNameLabel.setLabelFor(projectionName); 332 333 projPanel.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 10)); 334 projPanel.add(new JLabel(tr("Display coordinates as")), GBC.std().insets(5, 5, 0, 5)); 335 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 336 projPanel.add(coordinatesCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 337 projPanel.add(new JLabel(tr("System of measurement")), GBC.std().insets(5, 5, 0, 5)); 338 projPanel.add(GBC.glue(5, 0), GBC.std().fill(GBC.HORIZONTAL)); 339 projPanel.add(unitsCombo, GBC.eop().fill(GBC.HORIZONTAL).insets(0, 5, 5, 5)); 340 projPanel.add(GBC.glue(1, 1), GBC.std().fill(GBC.HORIZONTAL).weight(1.0, 1.0)); 341 342 JScrollPane scrollpane = new JScrollPane(projPanel); 343 gui.getMapPreference().addSubTab(this, tr("Map Projection"), scrollpane); 344 345 selectedProjectionChanged(pc); 346 } 347 348 private void updateMeta(ProjectionChoice pc) { 349 pc.setPreferences(pc.getPreferences(projSubPrefPanel)); 350 Projection proj = pc.getProjection(); 351 projectionCode.setText(proj.toCode()); 352 projectionName.setText(proj.toString()); 353 Bounds b = proj.getWorldBoundsLatLon(); 354 CoordinateFormat cf = CoordinateFormat.getDefaultFormat(); 355 bounds.setText(b.getMin().lonToString(cf) + ", " + b.getMin().latToString(cf) + " : " + 356 b.getMax().lonToString(cf) + ", " + b.getMax().latToString(cf)); 357 boolean showCode = true; 358 boolean showName = false; 359 if (pc instanceof SubPrefsOptions) { 360 showCode = ((SubPrefsOptions) pc).showProjectionCode(); 361 showName = ((SubPrefsOptions) pc).showProjectionName(); 362 } 363 projectionCodeLabel.setVisible(showCode); 364 projectionCodeGlue.setVisible(showCode); 365 projectionCode.setVisible(showCode); 366 projectionNameLabel.setVisible(showName); 367 projectionNameGlue.setVisible(showName); 368 projectionName.setVisible(showName); 369 } 370 371 @Override 372 public boolean ok() { 373 ProjectionChoice pc = (ProjectionChoice) projectionCombo.getSelectedItem(); 374 375 String id = pc.getId(); 376 Collection<String> prefs = pc.getPreferences(projSubPrefPanel); 377 378 setProjection(id, prefs); 379 380 if (PROP_COORDINATES.put(((CoordinateFormat) coordinatesCombo.getSelectedItem()).name())) { 381 CoordinateFormat.setCoordinateFormat((CoordinateFormat) coordinatesCombo.getSelectedItem()); 382 } 383 384 int i = unitsCombo.getSelectedIndex(); 385 SystemOfMeasurement.setSystemOfMeasurement(unitsValues[i]); 386 387 return false; 388 } 389 390 public static void setProjection() { 391 setProjection(PROP_PROJECTION.get(), PROP_SUB_PROJECTION.get()); 392 } 393 394 public static void setProjection(String id, Collection<String> pref) { 395 ProjectionChoice pc = projectionChoicesById.get(id); 396 397 if (pc == null) { 398 JOptionPane.showMessageDialog( 399 Main.parent, 400 tr("The projection {0} could not be activated. Using Mercator", id), 401 tr("Error"), 402 JOptionPane.ERROR_MESSAGE 403 ); 404 pref = null; 405 pc = mercator; 406 } 407 id = pc.getId(); 408 PROP_PROJECTION.put(id); 409 PROP_SUB_PROJECTION.put(pref); 410 Main.pref.putCollection("projection.sub."+id, pref); 411 pc.setPreferences(pref); 412 Projection proj = pc.getProjection(); 413 Main.setProjection(proj); 414 } 415 416 /** 417 * Handles all the work related to update the projection-specific 418 * preferences 419 * @param pc the choice class representing user selection 420 */ 421 private void selectedProjectionChanged(final ProjectionChoice pc) { 422 // Don't try to update if we're still starting up 423 int size = projPanel.getComponentCount(); 424 if (size < 1) 425 return; 426 427 final ActionListener listener = new ActionListener() { 428 @Override 429 public void actionPerformed(ActionEvent e) { 430 updateMeta(pc); 431 } 432 }; 433 434 // Replace old panel with new one 435 projSubPrefPanelWrapper.removeAll(); 436 projSubPrefPanel = pc.getPreferencePanel(listener); 437 projSubPrefPanelWrapper.add(projSubPrefPanel, projSubPrefPanelGBC); 438 projPanel.revalidate(); 439 projSubPrefPanel.repaint(); 440 updateMeta(pc); 441 } 442 443 /** 444 * Sets up projection combobox with default values and action listener 445 * @return the choice class for user selection 446 */ 447 private ProjectionChoice setupProjectionCombo() { 448 ProjectionChoice pc = null; 449 for (int i = 0; i < projectionCombo.getItemCount(); ++i) { 450 ProjectionChoice pc1 = projectionCombo.getItemAt(i); 451 pc1.setPreferences(getSubprojectionPreference(pc1)); 452 if (pc1.getId().equals(PROP_PROJECTION.get())) { 453 projectionCombo.setSelectedIndex(i); 454 selectedProjectionChanged(pc1); 455 pc = pc1; 456 } 457 } 458 // If the ProjectionChoice from the preferences is not available, it 459 // should have been set to Mercator at JOSM start. 460 if (pc == null) 461 throw new RuntimeException("Couldn't find the current projection in the list of available projections!"); 462 463 projectionCombo.addActionListener(new ActionListener() { 464 @Override 465 public void actionPerformed(ActionEvent e) { 466 ProjectionChoice pc = (ProjectionChoice) projectionCombo.getSelectedItem(); 467 selectedProjectionChanged(pc); 468 } 469 }); 470 return pc; 471 } 472 473 private static Collection<String> getSubprojectionPreference(ProjectionChoice pc) { 474 return Main.pref.getCollection("projection.sub."+pc.getId(), null); 475 } 476 477 @Override 478 public boolean isExpert() { 479 return false; 480 } 481 482 @Override 483 public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) { 484 return gui.getMapPreference(); 485 } 486 487 /** 488 * Selects the given projection. 489 * @param projection The projection to select. 490 * @since 5604 491 */ 492 public void selectProjection(ProjectionChoice projection) { 493 if (projectionCombo != null && projection != null) { 494 projectionCombo.setSelectedItem(projection); 495 } 496 } 497}