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}