001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.projection;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.List;
009import java.util.Map;
010import java.util.concurrent.ConcurrentHashMap;
011import java.util.regex.Matcher;
012import java.util.regex.Pattern;
013
014import org.openstreetmap.josm.Main;
015import org.openstreetmap.josm.data.Bounds;
016import org.openstreetmap.josm.data.coor.LatLon;
017import org.openstreetmap.josm.data.projection.datum.CentricDatum;
018import org.openstreetmap.josm.data.projection.datum.Datum;
019import org.openstreetmap.josm.data.projection.datum.NTV2Datum;
020import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
021import org.openstreetmap.josm.data.projection.datum.NullDatum;
022import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum;
023import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum;
024import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
025import org.openstreetmap.josm.data.projection.proj.Mercator;
026import org.openstreetmap.josm.data.projection.proj.Proj;
027import org.openstreetmap.josm.data.projection.proj.ProjParameters;
028import org.openstreetmap.josm.tools.Utils;
029
030/**
031 * Custom projection.
032 *
033 * Inspired by PROJ.4 and Proj4J.
034 * @since 5072
035 */
036public class CustomProjection extends AbstractProjection {
037
038    private static final double METER_PER_UNIT_DEGREE = 2 * Math.PI * 6370997 / 360;
039    private static final Map<String, Double> UNITS_TO_METERS = getUnitsToMeters();
040    private static final Map<String, Double> PRIME_MERIDANS = getPrimeMeridians();
041
042    /**
043     * pref String that defines the projection
044     *
045     * null means fall back mode (Mercator)
046     */
047    protected String pref;
048    protected String name;
049    protected String code;
050    protected String cacheDir;
051    protected Bounds bounds;
052    private double metersPerUnit = METER_PER_UNIT_DEGREE; // default to degrees
053    private String axis = "enu"; // default axis orientation is East, North, Up
054
055    /**
056     * Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>.
057     * @since 7370 (public)
058     */
059    public enum Param {
060
061        /** False easting */
062        x_0("x_0", true),
063        /** False northing */
064        y_0("y_0", true),
065        /** Central meridian */
066        lon_0("lon_0", true),
067        /** Prime meridian */
068        pm("pm", true),
069        /** Scaling factor */
070        k_0("k_0", true),
071        /** Ellipsoid name (see {@code proj -le}) */
072        ellps("ellps", true),
073        /** Semimajor radius of the ellipsoid axis */
074        a("a", true),
075        /** Eccentricity of the ellipsoid squared */
076        es("es", true),
077        /** Reciprocal of the ellipsoid flattening term (e.g. 298) */
078        rf("rf", true),
079        /** Flattening of the ellipsoid = 1-sqrt(1-e^2) */
080        f("f", true),
081        /** Semiminor radius of the ellipsoid axis */
082        b("b", true),
083        /** Datum name (see {@code proj -ld}) */
084        datum("datum", true),
085        /** 3 or 7 term datum transform parameters */
086        towgs84("towgs84", true),
087        /** Filename of NTv2 grid file to use for datum transforms */
088        nadgrids("nadgrids", true),
089        /** Projection name (see {@code proj -l}) */
090        proj("proj", true),
091        /** Latitude of origin */
092        lat_0("lat_0", true),
093        /** Latitude of first standard parallel */
094        lat_1("lat_1", true),
095        /** Latitude of second standard parallel */
096        lat_2("lat_2", true),
097        /** the exact proj.4 string will be preserved in the WKT representation */
098        wktext("wktext", false),  // ignored
099        /** meters, US survey feet, etc. */
100        units("units", true),
101        /** Don't use the /usr/share/proj/proj_def.dat defaults file */
102        no_defs("no_defs", false),
103        init("init", true),
104        /** crs units to meter multiplier */
105        to_meter("to_meter", true),
106        /** definition of axis for projection */
107        axis("axis", true),
108        /** UTM zone */
109        zone("zone", true),
110        /** indicate southern hemisphere for UTM */
111        south("south", false),
112        /** vertical units - ignore, as we don't use height information */
113        vunits("vunits", true),
114        // JOSM extensions, not present in PROJ.4
115        wmssrs("wmssrs", true),
116        bounds("bounds", true);
117
118        /** Parameter key */
119        public final String key;
120        /** {@code true} if the parameter has a value */
121        public final boolean hasValue;
122
123        /** Map of all parameters by key */
124        static final Map<String, Param> paramsByKey = new ConcurrentHashMap<>();
125        static {
126            for (Param p : Param.values()) {
127                paramsByKey.put(p.key, p);
128            }
129        }
130
131        Param(String key, boolean hasValue) {
132            this.key = key;
133            this.hasValue = hasValue;
134        }
135    }
136
137    /**
138     * Constructs a new empty {@code CustomProjection}.
139     */
140    public CustomProjection() {
141        // contents can be set later with update()
142    }
143
144    /**
145     * Constructs a new {@code CustomProjection} with given parameters.
146     * @param pref String containing projection parameters
147     * (ex: "+proj=tmerc +lon_0=-3 +k_0=0.9996 +x_0=500000 +ellps=WGS84 +datum=WGS84 +bounds=-8,-5,2,85")
148     */
149    public CustomProjection(String pref) {
150        this(null, null, pref, null);
151    }
152
153    /**
154     * Constructs a new {@code CustomProjection} with given name, code and parameters.
155     *
156     * @param name describe projection in one or two words
157     * @param code unique code for this projection - may be null
158     * @param pref the string that defines the custom projection
159     * @param cacheDir cache directory name
160     */
161    public CustomProjection(String name, String code, String pref, String cacheDir) {
162        this.name = name;
163        this.code = code;
164        this.pref = pref;
165        this.cacheDir = cacheDir;
166        try {
167            update(pref);
168        } catch (ProjectionConfigurationException ex) {
169            try {
170                update(null);
171            } catch (ProjectionConfigurationException ex1) {
172                throw new RuntimeException(ex1);
173            }
174        }
175    }
176
177    /**
178     * Updates this {@code CustomProjection} with given parameters.
179     * @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90")
180     * @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly
181     */
182    public final void update(String pref) throws ProjectionConfigurationException {
183        this.pref = pref;
184        if (pref == null) {
185            ellps = Ellipsoid.WGS84;
186            datum = WGS84Datum.INSTANCE;
187            proj = new Mercator();
188            bounds = new Bounds(
189                    -85.05112877980659, -180.0,
190                    85.05112877980659, 180.0, true);
191        } else {
192            Map<String, String> parameters = parseParameterList(pref, false);
193            parameters = resolveInits(parameters, false);
194            ellps = parseEllipsoid(parameters);
195            datum = parseDatum(parameters, ellps);
196            if (ellps == null) {
197                ellps = datum.getEllipsoid();
198            }
199            proj = parseProjection(parameters, ellps);
200            // "utm" is a shortcut for a set of parameters
201            if ("utm".equals(parameters.get(Param.proj.key))) {
202                String zoneStr = parameters.get(Param.zone.key);
203                Integer zone;
204                if (zoneStr == null)
205                    throw new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter."));
206                try {
207                    zone = Integer.valueOf(zoneStr);
208                } catch (NumberFormatException e) {
209                    zone = null;
210                }
211                if (zone == null || zone < 1 || zone > 60)
212                    throw new ProjectionConfigurationException(tr("Expected integer value in range 1-60 for ''+zone=...'' parameter."));
213                this.lon0 = 6 * zone - 183;
214                this.k0 = 0.9996;
215                this.x0 = 500000;
216                this.y0 = parameters.containsKey(Param.south.key) ? 10000000 : 0;
217            }
218            String s = parameters.get(Param.x_0.key);
219            if (s != null) {
220                this.x0 = parseDouble(s, Param.x_0.key);
221            }
222            s = parameters.get(Param.y_0.key);
223            if (s != null) {
224                this.y0 = parseDouble(s, Param.y_0.key);
225            }
226            s = parameters.get(Param.lon_0.key);
227            if (s != null) {
228                this.lon0 = parseAngle(s, Param.lon_0.key);
229            }
230            s = parameters.get(Param.pm.key);
231            if (s != null) {
232                if (PRIME_MERIDANS.containsKey(s)) {
233                    this.pm = PRIME_MERIDANS.get(s);
234                } else {
235                    this.pm = parseAngle(s, Param.pm.key);
236                }
237            }
238            s = parameters.get(Param.k_0.key);
239            if (s != null) {
240                this.k0 = parseDouble(s, Param.k_0.key);
241            }
242            s = parameters.get(Param.bounds.key);
243            if (s != null) {
244                this.bounds = parseBounds(s);
245            }
246            s = parameters.get(Param.wmssrs.key);
247            if (s != null) {
248                this.code = s;
249            }
250            s = parameters.get(Param.units.key);
251            if (s != null) {
252                s = Utils.strip(s, "\"");
253                if (UNITS_TO_METERS.containsKey(s)) {
254                    this.metersPerUnit = UNITS_TO_METERS.get(s);
255                } else {
256                    Main.warn("No metersPerUnit found for: " + s);
257                }
258            }
259            s = parameters.get(Param.to_meter.key);
260            if (s != null) {
261                this.metersPerUnit = parseDouble(s, Param.to_meter.key);
262            }
263            s = parameters.get(Param.axis.key);
264            if (s != null) {
265                this.axis  = s;
266            }
267        }
268    }
269
270    /**
271     * Parse a parameter list to key=value pairs.
272     *
273     * @param pref the parameter list
274     * @param ignoreUnknownParameter true, if unknown parameter should not raise exception
275     * @return parameters map
276     * @throws ProjectionConfigurationException in case of invalid parameter
277     */
278    public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException {
279        Map<String, String> parameters = new HashMap<>();
280        String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim());
281        if (pref.trim().isEmpty()) {
282            parts = new String[0];
283        }
284        for (String part : parts) {
285            if (part.isEmpty() || part.charAt(0) != '+')
286                throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
287            Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part);
288            if (m.matches()) {
289                String key = m.group(1);
290                // alias
291                if ("k".equals(key)) {
292                    key = Param.k_0.key;
293                }
294                String value = null;
295                if (m.groupCount() >= 3) {
296                    value = m.group(3);
297                    // some aliases
298                    if (key.equals(Param.proj.key)) {
299                        if ("longlat".equals(value) || "latlon".equals(value) || "latlong".equals(value)) {
300                            value = "lonlat";
301                        }
302                    }
303                }
304                if (!Param.paramsByKey.containsKey(key)) {
305                    if (!ignoreUnknownParameter)
306                        throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
307                } else {
308                    if (Param.paramsByKey.get(key).hasValue && value == null)
309                        throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
310                    if (!Param.paramsByKey.get(key).hasValue && value != null)
311                        throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
312                }
313                parameters.put(key, value);
314            } else
315                throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
316        }
317        return parameters;
318    }
319
320    /**
321     * Recursive resolution of +init includes.
322     *
323     * @param parameters parameters map
324     * @param ignoreUnknownParameter true, if unknown parameter should not raise exception
325     * @return parameters map with +init includes resolved
326     * @throws ProjectionConfigurationException in case of invalid parameter
327     */
328    public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter)
329            throws ProjectionConfigurationException {
330        // recursive resolution of +init includes
331        String initKey = parameters.get(Param.init.key);
332        if (initKey != null) {
333            String init = Projections.getInit(initKey);
334            if (init == null)
335                throw new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey));
336            Map<String, String> initp;
337            try {
338                initp = parseParameterList(init, ignoreUnknownParameter);
339                initp = resolveInits(initp, ignoreUnknownParameter);
340            } catch (ProjectionConfigurationException ex) {
341                throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex);
342            }
343            initp.putAll(parameters);
344            return initp;
345        }
346        return parameters;
347    }
348
349    public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
350        String code = parameters.get(Param.ellps.key);
351        if (code != null) {
352            Ellipsoid ellipsoid = Projections.getEllipsoid(code);
353            if (ellipsoid == null) {
354                throw new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code));
355            } else {
356                return ellipsoid;
357            }
358        }
359        String s = parameters.get(Param.a.key);
360        if (s != null) {
361            double a = parseDouble(s, Param.a.key);
362            if (parameters.get(Param.es.key) != null) {
363                double es = parseDouble(parameters, Param.es.key);
364                return Ellipsoid.create_a_es(a, es);
365            }
366            if (parameters.get(Param.rf.key) != null) {
367                double rf = parseDouble(parameters, Param.rf.key);
368                return Ellipsoid.create_a_rf(a, rf);
369            }
370            if (parameters.get(Param.f.key) != null) {
371                double f = parseDouble(parameters, Param.f.key);
372                return Ellipsoid.create_a_f(a, f);
373            }
374            if (parameters.get(Param.b.key) != null) {
375                double b = parseDouble(parameters, Param.b.key);
376                return Ellipsoid.create_a_b(a, b);
377            }
378        }
379        if (parameters.containsKey(Param.a.key) ||
380                parameters.containsKey(Param.es.key) ||
381                parameters.containsKey(Param.rf.key) ||
382                parameters.containsKey(Param.f.key) ||
383                parameters.containsKey(Param.b.key))
384            throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
385        return null;
386    }
387
388    public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
389        String datumId = parameters.get(Param.datum.key);
390        if (datumId != null) {
391            Datum datum = Projections.getDatum(datumId);
392            if (datum == null) throw new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId));
393            return datum;
394        }
395        if (ellps == null) {
396            if (parameters.containsKey(Param.no_defs.key))
397                throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
398            // nothing specified, use WGS84 as default
399            ellps = Ellipsoid.WGS84;
400        }
401
402        String nadgridsId = parameters.get(Param.nadgrids.key);
403        if (nadgridsId != null) {
404            if (nadgridsId.startsWith("@")) {
405                nadgridsId = nadgridsId.substring(1);
406            }
407            if ("null".equals(nadgridsId))
408                return new NullDatum(null, ellps);
409            NTV2GridShiftFileWrapper nadgrids = Projections.getNTV2Grid(nadgridsId);
410            if (nadgrids == null)
411                throw new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", nadgridsId));
412            return new NTV2Datum(nadgridsId, null, ellps, nadgrids);
413        }
414
415        String towgs84 = parameters.get(Param.towgs84.key);
416        if (towgs84 != null)
417            return parseToWGS84(towgs84, ellps);
418
419        return new NullDatum(null, ellps);
420    }
421
422    public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
423        String[] numStr = paramList.split(",");
424
425        if (numStr.length != 3 && numStr.length != 7)
426            throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
427        List<Double> towgs84Param = new ArrayList<>();
428        for (String str : numStr) {
429            try {
430                towgs84Param.add(Double.valueOf(str));
431            } catch (NumberFormatException e) {
432                throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e);
433            }
434        }
435        boolean isCentric = true;
436        for (Double param : towgs84Param) {
437            if (param != 0) {
438                isCentric = false;
439                break;
440            }
441        }
442        if (isCentric)
443            return new CentricDatum(null, null, ellps);
444        boolean is3Param = true;
445        for (int i = 3; i < towgs84Param.size(); i++) {
446            if (towgs84Param.get(i) != 0) {
447                is3Param = false;
448                break;
449            }
450        }
451        if (is3Param)
452            return new ThreeParameterDatum(null, null, ellps,
453                    towgs84Param.get(0),
454                    towgs84Param.get(1),
455                    towgs84Param.get(2));
456        else
457            return new SevenParameterDatum(null, null, ellps,
458                    towgs84Param.get(0),
459                    towgs84Param.get(1),
460                    towgs84Param.get(2),
461                    towgs84Param.get(3),
462                    towgs84Param.get(4),
463                    towgs84Param.get(5),
464                    towgs84Param.get(6));
465    }
466
467    public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
468        String id = parameters.get(Param.proj.key);
469        if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
470
471        // "utm" is not a real projection, but a shortcut for a set of parameters
472        if ("utm".equals(id)) {
473            id = "tmerc";
474        }
475        Proj proj =  Projections.getBaseProjection(id);
476        if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id));
477
478        ProjParameters projParams = new ProjParameters();
479
480        projParams.ellps = ellps;
481
482        String s;
483        s = parameters.get(Param.lat_0.key);
484        if (s != null) {
485            projParams.lat0 = parseAngle(s, Param.lat_0.key);
486        }
487        s = parameters.get(Param.lat_1.key);
488        if (s != null) {
489            projParams.lat1 = parseAngle(s, Param.lat_1.key);
490        }
491        s = parameters.get(Param.lat_2.key);
492        if (s != null) {
493            projParams.lat2 = parseAngle(s, Param.lat_2.key);
494        }
495        proj.initialize(projParams);
496        return proj;
497    }
498
499    public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
500        String[] numStr = boundsStr.split(",");
501        if (numStr.length != 4)
502            throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
503        return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
504                parseAngle(numStr[0], "minlon (+bounds)"),
505                parseAngle(numStr[3], "maxlat (+bounds)"),
506                parseAngle(numStr[2], "maxlon (+bounds)"), false);
507    }
508
509    public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
510        if (!parameters.containsKey(parameterName))
511            throw new ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName));
512        String doubleStr = parameters.get(parameterName);
513        if (doubleStr == null)
514            throw new ProjectionConfigurationException(
515                    tr("Expected number argument for parameter ''{0}''", parameterName));
516        return parseDouble(doubleStr, parameterName);
517    }
518
519    public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
520        try {
521            return Double.parseDouble(doubleStr);
522        } catch (NumberFormatException e) {
523            throw new ProjectionConfigurationException(
524                    tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e);
525        }
526    }
527
528    public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
529        String s = angleStr;
530        double value = 0;
531        boolean neg = false;
532        Matcher m = Pattern.compile("^-").matcher(s);
533        if (m.find()) {
534            neg = true;
535            s = s.substring(m.end());
536        }
537        final String FLOAT = "(\\d+(\\.\\d*)?)";
538        boolean dms = false;
539        double deg = 0.0, min = 0.0, sec = 0.0;
540        // degrees
541        m = Pattern.compile("^"+FLOAT+"d").matcher(s);
542        if (m.find()) {
543            s = s.substring(m.end());
544            deg = Double.parseDouble(m.group(1));
545            dms = true;
546        }
547        // minutes
548        m = Pattern.compile("^"+FLOAT+"'").matcher(s);
549        if (m.find()) {
550            s = s.substring(m.end());
551            min = Double.parseDouble(m.group(1));
552            dms = true;
553        }
554        // seconds
555        m = Pattern.compile("^"+FLOAT+"\"").matcher(s);
556        if (m.find()) {
557            s = s.substring(m.end());
558            sec = Double.parseDouble(m.group(1));
559            dms = true;
560        }
561        // plain number (in degrees)
562        if (dms) {
563            value = deg + (min/60.0) + (sec/3600.0);
564        } else {
565            m = Pattern.compile("^"+FLOAT).matcher(s);
566            if (m.find()) {
567                s = s.substring(m.end());
568                value += Double.parseDouble(m.group(1));
569            }
570        }
571        m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s);
572        if (m.find()) {
573            s = s.substring(m.end());
574        } else {
575            m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s);
576            if (m.find()) {
577                s = s.substring(m.end());
578                neg = !neg;
579            }
580        }
581        if (neg) {
582            value = -value;
583        }
584        if (!s.isEmpty()) {
585            throw new ProjectionConfigurationException(
586                    tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
587        }
588        return value;
589    }
590
591    @Override
592    public Integer getEpsgCode() {
593        if (code != null && code.startsWith("EPSG:")) {
594            try {
595                return Integer.valueOf(code.substring(5));
596            } catch (NumberFormatException e) {
597                Main.warn(e);
598            }
599        }
600        return null;
601    }
602
603    @Override
604    public String toCode() {
605        return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref);
606    }
607
608    @Override
609    public String getCacheDirectoryName() {
610        return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
611    }
612
613    @Override
614    public Bounds getWorldBoundsLatLon() {
615        if (bounds != null) return bounds;
616        Bounds ab = proj.getAlgorithmBounds();
617        if (ab != null) {
618            double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180);
619            double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180);
620            return new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false);
621        } else {
622            return new Bounds(
623                new LatLon(-90.0, -180.0),
624                new LatLon(90.0, 180.0));
625        }
626    }
627
628    @Override
629    public String toString() {
630        return name != null ? name : tr("Custom Projection");
631    }
632
633    @Override
634    public double getMetersPerUnit() {
635        return metersPerUnit;
636    }
637
638    @Override
639    public boolean switchXY() {
640        // TODO: support for other axis orientation such as West South, and Up Down
641        return this.axis.startsWith("ne");
642    }
643
644    private static Map<String, Double> getUnitsToMeters() {
645        Map<String, Double> ret = new ConcurrentHashMap<>();
646        ret.put("km", 1000d);
647        ret.put("m", 1d);
648        ret.put("dm", 1d/10);
649        ret.put("cm", 1d/100);
650        ret.put("mm", 1d/1000);
651        ret.put("kmi", 1852.0);
652        ret.put("in", 0.0254);
653        ret.put("ft", 0.3048);
654        ret.put("yd", 0.9144);
655        ret.put("mi", 1609.344);
656        ret.put("fathom", 1.8288);
657        ret.put("chain", 20.1168);
658        ret.put("link", 0.201168);
659        ret.put("us-in", 1d/39.37);
660        ret.put("us-ft", 0.304800609601219);
661        ret.put("us-yd", 0.914401828803658);
662        ret.put("us-ch", 20.11684023368047);
663        ret.put("us-mi", 1609.347218694437);
664        ret.put("ind-yd", 0.91439523);
665        ret.put("ind-ft", 0.30479841);
666        ret.put("ind-ch", 20.11669506);
667        ret.put("degree", METER_PER_UNIT_DEGREE);
668        return ret;
669    }
670
671    private static Map<String, Double> getPrimeMeridians() {
672        Map<String, Double> ret = new ConcurrentHashMap<>();
673        try {
674            ret.put("greenwich", 0.0);
675            ret.put("lisbon", parseAngle("9d07'54.862\"W", null));
676            ret.put("paris", parseAngle("2d20'14.025\"E", null));
677            ret.put("bogota", parseAngle("74d04'51.3\"W", null));
678            ret.put("madrid", parseAngle("3d41'16.58\"W", null));
679            ret.put("rome", parseAngle("12d27'8.4\"E", null));
680            ret.put("bern", parseAngle("7d26'22.5\"E", null));
681            ret.put("jakarta", parseAngle("106d48'27.79\"E", null));
682            ret.put("ferro", parseAngle("17d40'W", null));
683            ret.put("brussels", parseAngle("4d22'4.71\"E", null));
684            ret.put("stockholm", parseAngle("18d3'29.8\"E", null));
685            ret.put("athens", parseAngle("23d42'58.815\"E", null));
686            ret.put("oslo", parseAngle("10d43'22.5\"E", null));
687        } catch (ProjectionConfigurationException ex) {
688            throw new RuntimeException();
689        }
690        return ret;
691    }
692}