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