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