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}