001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.projection;
003
004import java.io.BufferedReader;
005import java.io.IOException;
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.Collections;
009import java.util.HashMap;
010import java.util.HashSet;
011import java.util.LinkedHashMap;
012import java.util.List;
013import java.util.Locale;
014import java.util.Map;
015import java.util.Set;
016import java.util.regex.Matcher;
017import java.util.regex.Pattern;
018
019import org.openstreetmap.josm.Main;
020import org.openstreetmap.josm.data.coor.EastNorth;
021import org.openstreetmap.josm.data.coor.LatLon;
022import org.openstreetmap.josm.data.projection.datum.Datum;
023import org.openstreetmap.josm.data.projection.datum.GRS80Datum;
024import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
025import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum;
026import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum;
027import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
028import org.openstreetmap.josm.data.projection.proj.AlbersEqualArea;
029import org.openstreetmap.josm.data.projection.proj.CassiniSoldner;
030import org.openstreetmap.josm.data.projection.proj.ClassProjFactory;
031import org.openstreetmap.josm.data.projection.proj.DoubleStereographic;
032import org.openstreetmap.josm.data.projection.proj.LambertAzimuthalEqualArea;
033import org.openstreetmap.josm.data.projection.proj.LambertConformalConic;
034import org.openstreetmap.josm.data.projection.proj.LonLat;
035import org.openstreetmap.josm.data.projection.proj.Mercator;
036import org.openstreetmap.josm.data.projection.proj.ObliqueMercator;
037import org.openstreetmap.josm.data.projection.proj.PolarStereographic;
038import org.openstreetmap.josm.data.projection.proj.Proj;
039import org.openstreetmap.josm.data.projection.proj.ProjFactory;
040import org.openstreetmap.josm.data.projection.proj.Sinusoidal;
041import org.openstreetmap.josm.data.projection.proj.SwissObliqueMercator;
042import org.openstreetmap.josm.data.projection.proj.TransverseMercator;
043import org.openstreetmap.josm.gui.preferences.projection.ProjectionChoice;
044import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
045import org.openstreetmap.josm.io.CachedFile;
046import org.openstreetmap.josm.tools.Utils;
047
048/**
049 * Class to manage projections.
050 *
051 * Use this class to query available projection or register new projections
052 * from a plugin.
053 */
054public final class Projections {
055
056    /**
057     * Class to hold information about one projection.
058     */
059    public static class ProjectionDefinition {
060        public String code;
061        public String name;
062        public String definition;
063
064        public ProjectionDefinition(String code, String name, String definition) {
065            this.code = code;
066            this.name = name;
067            this.definition = definition;
068        }
069    }
070
071    private static final Set<String> allCodes = new HashSet<>();
072    private static final Map<String, ProjectionChoice> allProjectionChoicesByCode = new HashMap<>();
073    private static final Map<String, Projection> projectionsByCode_cache = new HashMap<>();
074
075    /*********************************
076     * Registry for custom projection
077     *
078     * should be compatible to PROJ.4
079     */
080    static final Map<String, ProjFactory> projs = new HashMap<>();
081    static final Map<String, Ellipsoid> ellipsoids = new HashMap<>();
082    static final Map<String, Datum> datums = new HashMap<>();
083    static final Map<String, NTV2GridShiftFileWrapper> nadgrids = new HashMap<>();
084    static final Map<String, ProjectionDefinition> inits;
085
086    static {
087        registerBaseProjection("aea", AlbersEqualArea.class, "core");
088        registerBaseProjection("cass", CassiniSoldner.class, "core");
089        registerBaseProjection("laea", LambertAzimuthalEqualArea.class, "core");
090        registerBaseProjection("lcc", LambertConformalConic.class, "core");
091        registerBaseProjection("lonlat", LonLat.class, "core");
092        registerBaseProjection("merc", Mercator.class, "core");
093        registerBaseProjection("omerc", ObliqueMercator.class, "core");
094        registerBaseProjection("somerc", SwissObliqueMercator.class, "core");
095        registerBaseProjection("sinu", Sinusoidal.class, "core");
096        registerBaseProjection("stere", PolarStereographic.class, "core");
097        registerBaseProjection("sterea", DoubleStereographic.class, "core");
098        registerBaseProjection("tmerc", TransverseMercator.class, "core");
099
100        ellipsoids.put("airy", Ellipsoid.Airy);
101        ellipsoids.put("mod_airy", Ellipsoid.AiryMod);
102        ellipsoids.put("aust_SA", Ellipsoid.AustSA);
103        ellipsoids.put("bessel", Ellipsoid.Bessel1841);
104        ellipsoids.put("bess_nam", Ellipsoid.BesselNamibia);
105        ellipsoids.put("clrk66", Ellipsoid.Clarke1866);
106        ellipsoids.put("clrk80", Ellipsoid.Clarke1880);
107        ellipsoids.put("clrk80ign", Ellipsoid.ClarkeIGN);
108        ellipsoids.put("evrstSS", Ellipsoid.EverestSabahSarawak);
109        ellipsoids.put("intl", Ellipsoid.Hayford);
110        ellipsoids.put("helmert", Ellipsoid.Helmert);
111        ellipsoids.put("krass", Ellipsoid.Krassowsky);
112        ellipsoids.put("GRS67", Ellipsoid.GRS67);
113        ellipsoids.put("GRS80", Ellipsoid.GRS80);
114        ellipsoids.put("WGS66", Ellipsoid.WGS66);
115        ellipsoids.put("WGS72", Ellipsoid.WGS72);
116        ellipsoids.put("WGS84", Ellipsoid.WGS84);
117
118        datums.put("WGS84", WGS84Datum.INSTANCE);
119        datums.put("NAD83", GRS80Datum.INSTANCE);
120        datums.put("carthage", new ThreeParameterDatum(
121                "Carthage 1934 Tunisia", "carthage",
122                Ellipsoid.ClarkeIGN, -263.0, 6.0, 431.0));
123        datums.put("GGRS87", new ThreeParameterDatum(
124                "Greek Geodetic Reference System 1987", "GGRS87",
125                Ellipsoid.GRS80, -199.87, 74.79, 246.62));
126        datums.put("hermannskogel", new SevenParameterDatum(
127                "Hermannskogel", "hermannskogel",
128                Ellipsoid.Bessel1841, 577.326, 90.129, 463.919, 5.137, 1.474, 5.297, 2.4232));
129        datums.put("ire65", new SevenParameterDatum(
130                "Ireland 1965", "ire65",
131                Ellipsoid.AiryMod, 482.530, -130.596, 564.557, -1.042, -0.214, -0.631, 8.15));
132        datums.put("nzgd49", new SevenParameterDatum(
133                "New Zealand Geodetic Datum 1949", "nzgd49",
134                Ellipsoid.Hayford, 59.47, -5.04, 187.44, 0.47, -0.1, 1.024, -4.5993));
135        datums.put("OSGB36", new SevenParameterDatum(
136                "Airy 1830", "OSGB36",
137                Ellipsoid.Airy, 446.448, -125.157, 542.060, 0.1502, 0.2470, 0.8421, -20.4894));
138        datums.put("potsdam", new SevenParameterDatum(
139                "Potsdam Rauenberg 1950 DHDN", "potsdam",
140                Ellipsoid.Bessel1841, 598.1, 73.7, 418.2, 0.202, 0.045, -2.455, 6.7));
141
142        nadgrids.put("BETA2007.gsb", NTV2GridShiftFileWrapper.BETA2007);
143        nadgrids.put("ntf_r93_b.gsb", NTV2GridShiftFileWrapper.ntf_rgf93);
144
145        List<ProjectionDefinition> pds;
146        try {
147            pds = loadProjectionDefinitions("resource://data/projection/custom-epsg");
148        } catch (IOException ex) {
149            throw new RuntimeException(ex);
150        }
151        inits = new LinkedHashMap<>();
152        for (ProjectionDefinition pd : pds) {
153            inits.put(pd.code, pd);
154        }
155
156        for (ProjectionChoice pc : ProjectionPreference.getProjectionChoices()) {
157            for (String code : pc.allCodes()) {
158                allProjectionChoicesByCode.put(code, pc);
159            }
160        }
161        allCodes.addAll(inits.keySet());
162        allCodes.addAll(allProjectionChoicesByCode.keySet());
163    }
164
165    private Projections() {
166        // Hide default constructor for utils classes
167    }
168
169    /**
170     * Convert from lat/lon to easting/northing using the current projection.
171     *
172     * @param ll the geographical point to convert (in WGS84 lat/lon)
173     * @return the corresponding east/north coordinates
174     */
175    public static EastNorth project(LatLon ll) {
176        if (ll == null) return null;
177        return Main.getProjection().latlon2eastNorth(ll);
178    }
179
180    /**
181     * Convert from easting/norting to lat/lon using the current projection.
182     *
183     * @param en the geographical point to convert (in projected coordinates)
184     * @return the corresponding lat/lon (WGS84)
185     */
186    public static LatLon inverseProject(EastNorth en) {
187        if (en == null) return null;
188        return Main.getProjection().eastNorth2latlon(en);
189    }
190
191    /**
192     * Plugins can register additional base projections.
193     *
194     * @param id The "official" PROJ.4 id. In case the projection is not supported
195     * by PROJ.4, use some prefix, e.g. josm:myproj or gdal:otherproj.
196     * @param fac The base projection factory.
197     * @param origin Multiple plugins may implement the same base projection.
198     * Provide plugin name or similar string, so it be differentiated.
199     */
200    public static void registerBaseProjection(String id, ProjFactory fac, String origin) {
201        projs.put(id, fac);
202    }
203
204    public static void registerBaseProjection(String id, Class<? extends Proj> projClass, String origin) {
205        registerBaseProjection(id, new ClassProjFactory(projClass), origin);
206    }
207
208    /**
209     * Get a base projection by id.
210     *
211     * @param id the id, for example "lonlat" or "tmerc"
212     * @return the corresponding base projection if the id is known, null otherwise
213     */
214    public static Proj getBaseProjection(String id) {
215        ProjFactory fac = projs.get(id);
216        if (fac == null) return null;
217        return fac.createInstance();
218    }
219
220    /**
221     * Get an ellipsoid by id.
222     *
223     * @param id the id, for example "bessel" or "WGS84"
224     * @return the corresponding ellipsoid if the id is known, null otherwise
225     */
226    public static Ellipsoid getEllipsoid(String id) {
227        return ellipsoids.get(id);
228    }
229
230    /**
231     * Get a geodetic datum by id.
232     *
233     * @param id the id, for example "potsdam" or "WGS84"
234     * @return the corresponding datum if the id is known, null otherwise
235     */
236    public static Datum getDatum(String id) {
237        return datums.get(id);
238    }
239
240    /**
241     * Get a NTV2 grid database by id.
242     * @param id the id
243     * @return the corresponding NTV2 grid if the id is known, null otherwise
244     */
245    public static NTV2GridShiftFileWrapper getNTV2Grid(String id) {
246        return nadgrids.get(id);
247    }
248
249    /**
250     * Get the projection definition string for the given code.
251     * @param code the code
252     * @return the string that can be processed by #{link CustomProjection}.
253     * Null, if the code isn't supported.
254     */
255    public static String getInit(String code) {
256        ProjectionDefinition pd = inits.get(code.toUpperCase(Locale.ENGLISH));
257        if (pd == null) return null;
258        return pd.definition;
259    }
260
261    /**
262     * Load projection definitions from file.
263     *
264     * @param path the path
265     * @return projection definitions
266     * @throws IOException in case of I/O error
267     */
268    public static List<ProjectionDefinition> loadProjectionDefinitions(String path) throws IOException {
269        try (
270            CachedFile cf = new CachedFile(path);
271            BufferedReader r = cf.getContentReader()
272        ) {
273            return loadProjectionDefinitions(r);
274        }
275    }
276
277    /**
278     * Load projection definitions from file.
279     *
280     * @param r the reader
281     * @return projection definitions
282     * @throws IOException in case of I/O error
283     */
284    public static List<ProjectionDefinition> loadProjectionDefinitions(BufferedReader r) throws IOException {
285        List<ProjectionDefinition> result = new ArrayList<>();
286        Pattern epsgPattern = Pattern.compile("<(\\d+)>(.*)<>");
287        String line, lastline = "";
288        while ((line = r.readLine()) != null) {
289            line = line.trim();
290            if (!line.startsWith("#") && !line.isEmpty()) {
291                if (!lastline.startsWith("#")) throw new AssertionError("EPSG file seems corrupted");
292                String name = lastline.substring(1).trim();
293                Matcher m = epsgPattern.matcher(line);
294                if (m.matches()) {
295                    String code = "EPSG:" + m.group(1);
296                    String definition = m.group(2).trim();
297                    result.add(new ProjectionDefinition(code, name, definition));
298                } else {
299                    Main.warn("Failed to parse line from the EPSG projection definition: "+line);
300                }
301            }
302            lastline = line;
303        }
304        return result;
305    }
306
307    /**
308     * Get a projection by code.
309     * @param code the code, e.g. "EPSG:2026"
310     * @return the corresponding projection, if the code is known, null otherwise
311     */
312    public static Projection getProjectionByCode(String code) {
313        Projection proj = projectionsByCode_cache.get(code);
314        if (proj != null) return proj;
315        ProjectionChoice pc = allProjectionChoicesByCode.get(code);
316        if (pc != null) {
317            Collection<String> pref = pc.getPreferencesFromCode(code);
318            pc.setPreferences(pref);
319            try {
320                proj = pc.getProjection();
321            } catch (RuntimeException e) {
322                Main.warn(e, "Unable to get projection "+code+" with "+pc+':');
323            }
324        }
325        if (proj == null) {
326            ProjectionDefinition pd = inits.get(code);
327            if (pd == null) return null;
328            proj = new CustomProjection(pd.name, code, pd.definition, null);
329        }
330        projectionsByCode_cache.put(code, proj);
331        return proj;
332    }
333
334    /**
335     * Get a list of all supported projection codes.
336     *
337     * @return all supported projection codes
338     * @see #getProjectionByCode(java.lang.String)
339     */
340    public static Collection<String> getAllProjectionCodes() {
341        return Collections.unmodifiableCollection(allCodes);
342    }
343
344    /**
345     * Get a list of ids of all registered base projections.
346     *
347     * @return all registered base projection ids
348     * @see #getBaseProjection(java.lang.String)
349     */
350    public static Collection<String> getAllBaseProjectionIds() {
351        return projs.keySet();
352    }
353
354    private static String listKeys(Map<String, ?> map) {
355        List<String> keys = new ArrayList<>(map.keySet());
356        Collections.sort(keys);
357        return Utils.join(", ", keys);
358    }
359
360    /**
361     * Replies the list of projections as string (comma separated).
362     * @return the list of projections as string (comma separated)
363     * @since 8533
364     */
365    public static String listProjs() {
366        return listKeys(projs);
367    }
368
369    /**
370     * Replies the list of ellipsoids as string (comma separated).
371     * @return the list of ellipsoids as string (comma separated)
372     * @since 8533
373     */
374    public static String listEllipsoids() {
375        return listKeys(ellipsoids);
376    }
377
378    /**
379     * Replies the list of datums as string (comma separated).
380     * @return the list of datums as string (comma separated)
381     * @since 8533
382     */
383    public static String listDatums() {
384        return listKeys(datums);
385    }
386
387    /**
388     * Replies the list of nadgrids as string (comma separated).
389     * @return the list of nadgrids as string (comma separated)
390     * @since 8533
391     */
392    public static String listNadgrids() {
393        return listKeys(nadgrids);
394    }
395}