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.List;
015import java.util.Locale;
016import java.util.Map;
017import java.util.Set;
018import java.util.regex.Matcher;
019import java.util.regex.Pattern;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.data.coor.EastNorth;
023import org.openstreetmap.josm.data.coor.LatLon;
024import org.openstreetmap.josm.data.projection.datum.Datum;
025import org.openstreetmap.josm.data.projection.datum.GRS80Datum;
026import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
027import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
028import org.openstreetmap.josm.data.projection.proj.ClassProjFactory;
029import org.openstreetmap.josm.data.projection.proj.LambertConformalConic;
030import org.openstreetmap.josm.data.projection.proj.LonLat;
031import org.openstreetmap.josm.data.projection.proj.Mercator;
032import org.openstreetmap.josm.data.projection.proj.Proj;
033import org.openstreetmap.josm.data.projection.proj.ProjFactory;
034import org.openstreetmap.josm.data.projection.proj.SwissObliqueMercator;
035import org.openstreetmap.josm.data.projection.proj.TransverseMercator;
036import org.openstreetmap.josm.gui.preferences.projection.ProjectionChoice;
037import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
038import org.openstreetmap.josm.io.CachedFile;
039import org.openstreetmap.josm.tools.Pair;
040import org.openstreetmap.josm.tools.Utils;
041
042/**
043 * Class to handle projections
044 *
045 */
046public final class Projections {
047
048    private Projections() {
049        // Hide default constructor for utils classes
050    }
051
052    public static EastNorth project(LatLon ll) {
053        if (ll == null) return null;
054        return Main.getProjection().latlon2eastNorth(ll);
055    }
056
057    public static LatLon inverseProject(EastNorth en) {
058        if (en == null) return null;
059        return Main.getProjection().eastNorth2latlon(en);
060    }
061
062    /*********************************
063     * Registry for custom projection
064     *
065     * should be compatible to PROJ.4
066     */
067    static final Map<String, ProjFactory> projs = new HashMap<>();
068    static final Map<String, Ellipsoid> ellipsoids = new HashMap<>();
069    static final Map<String, Datum> datums = new HashMap<>();
070    static final Map<String, NTV2GridShiftFileWrapper> nadgrids = new HashMap<>();
071    static final Map<String, Pair<String, String>> inits = new HashMap<>();
072
073    static {
074        registerBaseProjection("lonlat", LonLat.class, "core");
075        registerBaseProjection("josm:smerc", Mercator.class, "core");
076        registerBaseProjection("lcc", LambertConformalConic.class, "core");
077        registerBaseProjection("somerc", SwissObliqueMercator.class, "core");
078        registerBaseProjection("tmerc", TransverseMercator.class, "core");
079
080        ellipsoids.put("airy", Ellipsoid.Airy);
081        ellipsoids.put("mod_airy", Ellipsoid.AiryMod);
082        ellipsoids.put("aust_SA", Ellipsoid.AustSA);
083        ellipsoids.put("bessel", Ellipsoid.Bessel1841);
084        ellipsoids.put("clrk66", Ellipsoid.Clarke1866);
085        ellipsoids.put("clarkeIGN", Ellipsoid.ClarkeIGN);
086        ellipsoids.put("intl", Ellipsoid.Hayford);
087        ellipsoids.put("helmert", Ellipsoid.Helmert);
088        ellipsoids.put("krass", Ellipsoid.Krassowsky);
089        ellipsoids.put("GRS67", Ellipsoid.GRS67);
090        ellipsoids.put("GRS80", Ellipsoid.GRS80);
091        ellipsoids.put("WGS72", Ellipsoid.WGS72);
092        ellipsoids.put("WGS84", Ellipsoid.WGS84);
093
094        datums.put("WGS84", WGS84Datum.INSTANCE);
095        datums.put("GRS80", GRS80Datum.INSTANCE);
096
097        nadgrids.put("BETA2007.gsb", NTV2GridShiftFileWrapper.BETA2007);
098        nadgrids.put("ntf_r93_b.gsb", NTV2GridShiftFileWrapper.ntf_rgf93);
099
100        loadInits();
101    }
102
103    /**
104     * Plugins can register additional base projections.
105     *
106     * @param id The "official" PROJ.4 id. In case the projection is not supported
107     * by PROJ.4, use some prefix, e.g. josm:myproj or gdal:otherproj.
108     * @param fac The base projection factory.
109     * @param origin Multiple plugins may implement the same base projection.
110     * Provide plugin name or similar string, so it be differentiated.
111     */
112    public static void registerBaseProjection(String id, ProjFactory fac, String origin) {
113        projs.put(id, fac);
114    }
115
116    public static void registerBaseProjection(String id, Class<? extends Proj> projClass, String origin) {
117        registerBaseProjection(id, new ClassProjFactory(projClass), origin);
118    }
119
120    public static Proj getBaseProjection(String id) {
121        ProjFactory fac = projs.get(id);
122        if (fac == null) return null;
123        return fac.createInstance();
124    }
125
126    public static Ellipsoid getEllipsoid(String id) {
127        return ellipsoids.get(id);
128    }
129
130    public static Datum getDatum(String id) {
131        return datums.get(id);
132    }
133
134    public static NTV2GridShiftFileWrapper getNTV2Grid(String id) {
135        return nadgrids.get(id);
136    }
137
138    /**
139     * Get the projection definition string for the given id.
140     * @param id the id
141     * @return the string that can be processed by #{link CustomProjection}.
142     * Null, if the id isn't supported.
143     */
144    public static String getInit(String id) {
145        Pair<String, String> r = inits.get(id.toUpperCase(Locale.ENGLISH));
146        if (r == null) return null;
147        return r.b;
148    }
149
150    /**
151     * Load +init "presets" from file
152     */
153    private static void loadInits() {
154        Pattern epsgPattern = Pattern.compile("<(\\d+)>(.*)<>");
155        try (
156            InputStream in = new CachedFile("resource://data/projection/epsg").getInputStream();
157            BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
158        ) {
159            String line, lastline = "";
160            while ((line = r.readLine()) != null) {
161                line = line.trim();
162                if (!line.startsWith("#") && !line.isEmpty()) {
163                    if (!lastline.startsWith("#")) throw new AssertionError("EPSG file seems corrupted");
164                    String name = lastline.substring(1).trim();
165                    Matcher m = epsgPattern.matcher(line);
166                    if (m.matches()) {
167                        inits.put("EPSG:" + m.group(1), Pair.create(name, m.group(2).trim()));
168                    } else {
169                        Main.warn("Failed to parse line from the EPSG projection definition: "+line);
170                    }
171                }
172                lastline = line;
173            }
174        } catch (IOException ex) {
175            throw new RuntimeException(ex);
176        }
177    }
178
179    private static final Set<String> allCodes = new HashSet<>();
180    private static final Map<String, ProjectionChoice> allProjectionChoicesByCode = new HashMap<>();
181    private static final Map<String, Projection> projectionsByCode_cache = new HashMap<>();
182
183    static {
184        for (ProjectionChoice pc : ProjectionPreference.getProjectionChoices()) {
185            for (String code : pc.allCodes()) {
186                allProjectionChoicesByCode.put(code, pc);
187            }
188        }
189        allCodes.addAll(inits.keySet());
190        allCodes.addAll(allProjectionChoicesByCode.keySet());
191    }
192
193    public static Projection getProjectionByCode(String code) {
194        Projection proj = projectionsByCode_cache.get(code);
195        if (proj != null) return proj;
196        ProjectionChoice pc = allProjectionChoicesByCode.get(code);
197        if (pc != null) {
198            Collection<String> pref = pc.getPreferencesFromCode(code);
199            pc.setPreferences(pref);
200            try {
201                proj = pc.getProjection();
202            } catch (Exception e) {
203                String cause = e.getMessage();
204                Main.warn("Unable to get projection "+code+" with "+pc + (cause != null ? ". "+cause : ""));
205            }
206        }
207        if (proj == null) {
208            Pair<String, String> pair = inits.get(code);
209            if (pair == null) return null;
210            String name = pair.a;
211            String init = pair.b;
212            proj = new CustomProjection(name, code, init, null);
213        }
214        projectionsByCode_cache.put(code, proj);
215        return proj;
216    }
217
218    public static Collection<String> getAllProjectionCodes() {
219        return Collections.unmodifiableCollection(allCodes);
220    }
221
222    private static String listKeys(Map<String, ?> map) {
223        List<String> keys = new ArrayList<>(map.keySet());
224        Collections.sort(keys);
225        return Utils.join(", ", keys);
226    }
227
228    /**
229     * Replies the list of projections as string (comma separated).
230     * @return the list of projections as string (comma separated)
231     * @since 8533
232     */
233    public static String listProjs() {
234        return listKeys(projs);
235    }
236
237    /**
238     * Replies the list of ellipsoids as string (comma separated).
239     * @return the list of ellipsoids as string (comma separated)
240     * @since 8533
241     */
242    public static String listEllipsoids() {
243        return listKeys(ellipsoids);
244    }
245
246    /**
247     * Replies the list of datums as string (comma separated).
248     * @return the list of datums as string (comma separated)
249     * @since 8533
250     */
251    public static String listDatums() {
252        return listKeys(datums);
253    }
254
255    /**
256     * Replies the list of nadgrids as string (comma separated).
257     * @return the list of nadgrids as string (comma separated)
258     * @since 8533
259     */
260    public static String listNadgrids() {
261        return listKeys(nadgrids);
262    }
263}