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