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