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