source: josm/trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java @ 12620

Last change on this file since 12620 was 12620, checked in by Don-vip, 5 weeks ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

  • Property svn:eol-style set to native
File size: 36.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.projection;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.util.ArrayList;
7import java.util.Arrays;
8import java.util.EnumMap;
9import java.util.HashMap;
10import java.util.List;
11import java.util.Map;
12import java.util.Optional;
13import java.util.concurrent.ConcurrentHashMap;
14import java.util.regex.Matcher;
15import java.util.regex.Pattern;
16
17import org.openstreetmap.josm.data.Bounds;
18import org.openstreetmap.josm.data.ProjectionBounds;
19import org.openstreetmap.josm.data.coor.EastNorth;
20import org.openstreetmap.josm.data.coor.LatLon;
21import org.openstreetmap.josm.data.projection.datum.CentricDatum;
22import org.openstreetmap.josm.data.projection.datum.Datum;
23import org.openstreetmap.josm.data.projection.datum.NTV2Datum;
24import org.openstreetmap.josm.data.projection.datum.NullDatum;
25import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum;
26import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum;
27import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
28import org.openstreetmap.josm.data.projection.proj.ICentralMeridianProvider;
29import org.openstreetmap.josm.data.projection.proj.IScaleFactorProvider;
30import org.openstreetmap.josm.data.projection.proj.Mercator;
31import org.openstreetmap.josm.data.projection.proj.Proj;
32import org.openstreetmap.josm.data.projection.proj.ProjParameters;
33import org.openstreetmap.josm.tools.JosmRuntimeException;
34import org.openstreetmap.josm.tools.Logging;
35import org.openstreetmap.josm.tools.Utils;
36import org.openstreetmap.josm.tools.bugreport.BugReport;
37
38/**
39 * Custom projection.
40 *
41 * Inspired by PROJ.4 and Proj4J.
42 * @since 5072
43 */
44public class CustomProjection extends AbstractProjection {
45
46    /*
47     * Equation for METER_PER_UNIT_DEGREE taken from:
48     * https://github.com/openlayers/ol3/blob/master/src/ol/proj/epsg4326projection.js#L58
49     * Value for Radius taken form:
50     * https://github.com/openlayers/ol3/blob/master/src/ol/sphere/wgs84sphere.js#L11
51     */
52    private static final double METER_PER_UNIT_DEGREE = 2 * Math.PI * 6378137.0 / 360;
53    private static final Map<String, Double> UNITS_TO_METERS = getUnitsToMeters();
54    private static final Map<String, Double> PRIME_MERIDANS = getPrimeMeridians();
55
56    /**
57     * pref String that defines the projection
58     *
59     * null means fall back mode (Mercator)
60     */
61    protected String pref;
62    protected String name;
63    protected String code;
64    protected String cacheDir;
65    protected Bounds bounds;
66    private double metersPerUnitWMTS;
67    private String axis = "enu"; // default axis orientation is East, North, Up
68
69    private static final List<String> LON_LAT_VALUES = Arrays.asList("longlat", "latlon", "latlong");
70
71    /**
72     * Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>.
73     * @since 7370 (public)
74     */
75    public enum Param {
76
77        /** False easting */
78        x_0("x_0", true),
79        /** False northing */
80        y_0("y_0", true),
81        /** Central meridian */
82        lon_0("lon_0", true),
83        /** Prime meridian */
84        pm("pm", true),
85        /** Scaling factor */
86        k_0("k_0", true),
87        /** Ellipsoid name (see {@code proj -le}) */
88        ellps("ellps", true),
89        /** Semimajor radius of the ellipsoid axis */
90        a("a", true),
91        /** Eccentricity of the ellipsoid squared */
92        es("es", true),
93        /** Reciprocal of the ellipsoid flattening term (e.g. 298) */
94        rf("rf", true),
95        /** Flattening of the ellipsoid = 1-sqrt(1-e^2) */
96        f("f", true),
97        /** Semiminor radius of the ellipsoid axis */
98        b("b", true),
99        /** Datum name (see {@code proj -ld}) */
100        datum("datum", true),
101        /** 3 or 7 term datum transform parameters */
102        towgs84("towgs84", true),
103        /** Filename of NTv2 grid file to use for datum transforms */
104        nadgrids("nadgrids", true),
105        /** Projection name (see {@code proj -l}) */
106        proj("proj", true),
107        /** Latitude of origin */
108        lat_0("lat_0", true),
109        /** Latitude of first standard parallel */
110        lat_1("lat_1", true),
111        /** Latitude of second standard parallel */
112        lat_2("lat_2", true),
113        /** Latitude of true scale (Polar Stereographic) */
114        lat_ts("lat_ts", true),
115        /** longitude of the center of the projection (Oblique Mercator) */
116        lonc("lonc", true),
117        /** azimuth (true) of the center line passing through the center of the
118         * projection (Oblique Mercator) */
119        alpha("alpha", true),
120        /** rectified bearing of the center line (Oblique Mercator) */
121        gamma("gamma", true),
122        /** select "Hotine" variant of Oblique Mercator */
123        no_off("no_off", false),
124        /** legacy alias for no_off */
125        no_uoff("no_uoff", false),
126        /** longitude of first point (Oblique Mercator) */
127        lon_1("lon_1", true),
128        /** longitude of second point (Oblique Mercator) */
129        lon_2("lon_2", true),
130        /** the exact proj.4 string will be preserved in the WKT representation */
131        wktext("wktext", false),  // ignored
132        /** meters, US survey feet, etc. */
133        units("units", true),
134        /** Don't use the /usr/share/proj/proj_def.dat defaults file */
135        no_defs("no_defs", false),
136        init("init", true),
137        /** crs units to meter multiplier */
138        to_meter("to_meter", true),
139        /** definition of axis for projection */
140        axis("axis", true),
141        /** UTM zone */
142        zone("zone", true),
143        /** indicate southern hemisphere for UTM */
144        south("south", false),
145        /** vertical units - ignore, as we don't use height information */
146        vunits("vunits", true),
147        // JOSM extensions, not present in PROJ.4
148        wmssrs("wmssrs", true),
149        bounds("bounds", true);
150
151        /** Parameter key */
152        public final String key;
153        /** {@code true} if the parameter has a value */
154        public final boolean hasValue;
155
156        /** Map of all parameters by key */
157        static final Map<String, Param> paramsByKey = new ConcurrentHashMap<>();
158        static {
159            for (Param p : Param.values()) {
160                paramsByKey.put(p.key, p);
161            }
162            // alias
163            paramsByKey.put("k", Param.k_0);
164        }
165
166        Param(String key, boolean hasValue) {
167            this.key = key;
168            this.hasValue = hasValue;
169        }
170    }
171
172    enum Polarity {
173        NORTH(LatLon.NORTH_POLE),
174        SOUTH(LatLon.SOUTH_POLE);
175
176        private final LatLon latlon;
177
178        Polarity(LatLon latlon) {
179            this.latlon = latlon;
180        }
181
182        LatLon getLatLon() {
183            return latlon;
184        }
185    }
186
187    private EnumMap<Polarity, EastNorth> polesEN;
188
189    /**
190     * Constructs a new empty {@code CustomProjection}.
191     */
192    public CustomProjection() {
193        // contents can be set later with update()
194    }
195
196    /**
197     * Constructs a new {@code CustomProjection} with given parameters.
198     * @param pref String containing projection parameters
199     * (ex: "+proj=tmerc +lon_0=-3 +k_0=0.9996 +x_0=500000 +ellps=WGS84 +datum=WGS84 +bounds=-8,-5,2,85")
200     */
201    public CustomProjection(String pref) {
202        this(null, null, pref);
203    }
204
205    /**
206     * Constructs a new {@code CustomProjection} with given name, code and parameters.
207     *
208     * @param name describe projection in one or two words
209     * @param code unique code for this projection - may be null
210     * @param pref the string that defines the custom projection
211     * @param cacheDir cache directory name
212     * @deprecated unused - remove in 2017-09
213     */
214    @Deprecated
215    public CustomProjection(String name, String code, String pref, String cacheDir) {
216        this(name, code, pref);
217    }
218
219    /**
220     * Constructs a new {@code CustomProjection} with given name, code and parameters.
221     *
222     * @param name describe projection in one or two words
223     * @param code unique code for this projection - may be null
224     * @param pref the string that defines the custom projection
225     */
226    public CustomProjection(String name, String code, String pref) {
227        this.name = name;
228        this.code = code;
229        this.pref = pref;
230        try {
231            update(pref);
232        } catch (ProjectionConfigurationException ex) {
233            Logging.trace(ex);
234            try {
235                update(null);
236            } catch (ProjectionConfigurationException ex1) {
237                throw BugReport.intercept(ex1).put("name", name).put("code", code).put("pref", pref);
238            }
239        }
240    }
241
242    /**
243     * Updates this {@code CustomProjection} with given parameters.
244     * @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90")
245     * @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly
246     */
247    public final void update(String pref) throws ProjectionConfigurationException {
248        this.pref = pref;
249        if (pref == null) {
250            ellps = Ellipsoid.WGS84;
251            datum = WGS84Datum.INSTANCE;
252            proj = new Mercator();
253            bounds = new Bounds(
254                    -85.05112877980659, -180.0,
255                    85.05112877980659, 180.0, true);
256        } else {
257            Map<String, String> parameters = parseParameterList(pref, false);
258            parameters = resolveInits(parameters, false);
259            ellps = parseEllipsoid(parameters);
260            datum = parseDatum(parameters, ellps);
261            if (ellps == null) {
262                ellps = datum.getEllipsoid();
263            }
264            proj = parseProjection(parameters, ellps);
265            // "utm" is a shortcut for a set of parameters
266            if ("utm".equals(parameters.get(Param.proj.key))) {
267                Integer zone;
268                try {
269                    zone = Integer.valueOf(Optional.ofNullable(parameters.get(Param.zone.key)).orElseThrow(
270                            () -> new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter."))));
271                } catch (NumberFormatException e) {
272                    zone = null;
273                }
274                if (zone == null || zone < 1 || zone > 60)
275                    throw new ProjectionConfigurationException(tr("Expected integer value in range 1-60 for ''+zone=...'' parameter."));
276                this.lon0 = 6d * zone - 183d;
277                this.k0 = 0.9996;
278                this.x0 = 500_000;
279                this.y0 = parameters.containsKey(Param.south.key) ? 10_000_000 : 0;
280            }
281            String s = parameters.get(Param.x_0.key);
282            if (s != null) {
283                this.x0 = parseDouble(s, Param.x_0.key);
284            }
285            s = parameters.get(Param.y_0.key);
286            if (s != null) {
287                this.y0 = parseDouble(s, Param.y_0.key);
288            }
289            s = parameters.get(Param.lon_0.key);
290            if (s != null) {
291                this.lon0 = parseAngle(s, Param.lon_0.key);
292            }
293            if (proj instanceof ICentralMeridianProvider) {
294                this.lon0 = ((ICentralMeridianProvider) proj).getCentralMeridian();
295            }
296            s = parameters.get(Param.pm.key);
297            if (s != null) {
298                if (PRIME_MERIDANS.containsKey(s)) {
299                    this.pm = PRIME_MERIDANS.get(s);
300                } else {
301                    this.pm = parseAngle(s, Param.pm.key);
302                }
303            }
304            s = parameters.get(Param.k_0.key);
305            if (s != null) {
306                this.k0 = parseDouble(s, Param.k_0.key);
307            }
308            if (proj instanceof IScaleFactorProvider) {
309                this.k0 *= ((IScaleFactorProvider) proj).getScaleFactor();
310            }
311            s = parameters.get(Param.bounds.key);
312            if (s != null) {
313                this.bounds = parseBounds(s);
314            }
315            s = parameters.get(Param.wmssrs.key);
316            if (s != null) {
317                this.code = s;
318            }
319            boolean defaultUnits = true;
320            s = parameters.get(Param.units.key);
321            if (s != null) {
322                s = Utils.strip(s, "\"");
323                if (UNITS_TO_METERS.containsKey(s)) {
324                    this.toMeter = UNITS_TO_METERS.get(s);
325                    this.metersPerUnitWMTS = this.toMeter;
326                    defaultUnits = false;
327                } else {
328                    throw new ProjectionConfigurationException(tr("No unit found for: {0}", s));
329                }
330            }
331            s = parameters.get(Param.to_meter.key);
332            if (s != null) {
333                this.toMeter = parseDouble(s, Param.to_meter.key);
334                this.metersPerUnitWMTS = this.toMeter;
335                defaultUnits = false;
336            }
337            if (defaultUnits) {
338                this.toMeter = 1;
339                this.metersPerUnitWMTS = proj.isGeographic() ? METER_PER_UNIT_DEGREE : 1;
340            }
341            s = parameters.get(Param.axis.key);
342            if (s != null) {
343                this.axis = s;
344            }
345        }
346    }
347
348    /**
349     * Parse a parameter list to key=value pairs.
350     *
351     * @param pref the parameter list
352     * @param ignoreUnknownParameter true, if unknown parameter should not raise exception
353     * @return parameters map
354     * @throws ProjectionConfigurationException in case of invalid parameter
355     */
356    public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException {
357        Map<String, String> parameters = new HashMap<>();
358        String trimmedPref = pref.trim();
359        if (trimmedPref.isEmpty()) {
360            return parameters;
361        }
362
363        Pattern keyPattern = Pattern.compile("\\+(?<key>[a-zA-Z0-9_]+)(=(?<value>.*))?");
364        String[] parts = Utils.WHITE_SPACES_PATTERN.split(trimmedPref);
365        for (String part : parts) {
366            Matcher m = keyPattern.matcher(part);
367            if (m.matches()) {
368                String key = m.group("key");
369                String value = m.group("value");
370                // some aliases
371                if (key.equals(Param.proj.key) && LON_LAT_VALUES.contains(value)) {
372                    value = "lonlat";
373                }
374                Param param = Param.paramsByKey.get(key);
375                if (param == null) {
376                    if (!ignoreUnknownParameter)
377                        throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
378                } else {
379                    if (param.hasValue && value == null)
380                        throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
381                    if (!param.hasValue && value != null)
382                        throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
383                    key = param.key; // To be really sure, we might have an alias.
384                }
385                parameters.put(key, value);
386            } else if (!part.startsWith("+")) {
387                throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
388            } else {
389                throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
390            }
391        }
392        return parameters;
393    }
394
395    /**
396     * Recursive resolution of +init includes.
397     *
398     * @param parameters parameters map
399     * @param ignoreUnknownParameter true, if unknown parameter should not raise exception
400     * @return parameters map with +init includes resolved
401     * @throws ProjectionConfigurationException in case of invalid parameter
402     */
403    public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter)
404            throws ProjectionConfigurationException {
405        // recursive resolution of +init includes
406        String initKey = parameters.get(Param.init.key);
407        if (initKey != null) {
408            Map<String, String> initp;
409            try {
410                initp = parseParameterList(Optional.ofNullable(Projections.getInit(initKey)).orElseThrow(
411                        () -> new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey))),
412                        ignoreUnknownParameter);
413                initp = resolveInits(initp, ignoreUnknownParameter);
414            } catch (ProjectionConfigurationException ex) {
415                throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex);
416            }
417            initp.putAll(parameters);
418            return initp;
419        }
420        return parameters;
421    }
422
423    /**
424     * Gets the ellipsoid
425     * @param parameters The parameters to get the value from
426     * @return The Ellipsoid as specified with the parameters
427     * @throws ProjectionConfigurationException in case of invalid parameters
428     */
429    public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
430        String code = parameters.get(Param.ellps.key);
431        if (code != null) {
432            return Optional.ofNullable(Projections.getEllipsoid(code)).orElseThrow(
433                () -> new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code)));
434        }
435        String s = parameters.get(Param.a.key);
436        if (s != null) {
437            double a = parseDouble(s, Param.a.key);
438            if (parameters.get(Param.es.key) != null) {
439                double es = parseDouble(parameters, Param.es.key);
440                return Ellipsoid.createAes(a, es);
441            }
442            if (parameters.get(Param.rf.key) != null) {
443                double rf = parseDouble(parameters, Param.rf.key);
444                return Ellipsoid.createArf(a, rf);
445            }
446            if (parameters.get(Param.f.key) != null) {
447                double f = parseDouble(parameters, Param.f.key);
448                return Ellipsoid.createAf(a, f);
449            }
450            if (parameters.get(Param.b.key) != null) {
451                double b = parseDouble(parameters, Param.b.key);
452                return Ellipsoid.createAb(a, b);
453            }
454        }
455        if (parameters.containsKey(Param.a.key) ||
456                parameters.containsKey(Param.es.key) ||
457                parameters.containsKey(Param.rf.key) ||
458                parameters.containsKey(Param.f.key) ||
459                parameters.containsKey(Param.b.key))
460            throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
461        return null;
462    }
463
464    /**
465     * Gets the datum
466     * @param parameters The parameters to get the value from
467     * @param ellps The ellisoid that was previously computed
468     * @return The Datum as specified with the parameters
469     * @throws ProjectionConfigurationException in case of invalid parameters
470     */
471    public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
472        String datumId = parameters.get(Param.datum.key);
473        if (datumId != null) {
474            return Optional.ofNullable(Projections.getDatum(datumId)).orElseThrow(
475                    () -> new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId)));
476        }
477        if (ellps == null) {
478            if (parameters.containsKey(Param.no_defs.key))
479                throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
480            // nothing specified, use WGS84 as default
481            ellps = Ellipsoid.WGS84;
482        }
483
484        String nadgridsId = parameters.get(Param.nadgrids.key);
485        if (nadgridsId != null) {
486            if (nadgridsId.startsWith("@")) {
487                nadgridsId = nadgridsId.substring(1);
488            }
489            if ("null".equals(nadgridsId))
490                return new NullDatum(null, ellps);
491            final String fNadgridsId = nadgridsId;
492            return new NTV2Datum(fNadgridsId, null, ellps, Optional.ofNullable(Projections.getNTV2Grid(fNadgridsId)).orElseThrow(
493                    () -> new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", fNadgridsId))));
494        }
495
496        String towgs84 = parameters.get(Param.towgs84.key);
497        if (towgs84 != null)
498            return parseToWGS84(towgs84, ellps);
499
500        return new NullDatum(null, ellps);
501    }
502
503    /**
504     * Parse {@code towgs84} parameter.
505     * @param paramList List of parameter arguments (expected: 3 or 7)
506     * @param ellps ellipsoid
507     * @return parsed datum ({@link ThreeParameterDatum} or {@link SevenParameterDatum})
508     * @throws ProjectionConfigurationException if the arguments cannot be parsed
509     */
510    public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
511        String[] numStr = paramList.split(",");
512
513        if (numStr.length != 3 && numStr.length != 7)
514            throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
515        List<Double> towgs84Param = new ArrayList<>();
516        for (String str : numStr) {
517            try {
518                towgs84Param.add(Double.valueOf(str));
519            } catch (NumberFormatException e) {
520                throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e);
521            }
522        }
523        boolean isCentric = true;
524        for (Double param : towgs84Param) {
525            if (param != 0) {
526                isCentric = false;
527                break;
528            }
529        }
530        if (isCentric)
531            return new CentricDatum(null, null, ellps);
532        boolean is3Param = true;
533        for (int i = 3; i < towgs84Param.size(); i++) {
534            if (towgs84Param.get(i) != 0) {
535                is3Param = false;
536                break;
537            }
538        }
539        if (is3Param)
540            return new ThreeParameterDatum(null, null, ellps,
541                    towgs84Param.get(0),
542                    towgs84Param.get(1),
543                    towgs84Param.get(2));
544        else
545            return new SevenParameterDatum(null, null, ellps,
546                    towgs84Param.get(0),
547                    towgs84Param.get(1),
548                    towgs84Param.get(2),
549                    towgs84Param.get(3),
550                    towgs84Param.get(4),
551                    towgs84Param.get(5),
552                    towgs84Param.get(6));
553    }
554
555    /**
556     * Gets a projection using the given ellipsoid
557     * @param parameters Additional parameters
558     * @param ellps The {@link Ellipsoid}
559     * @return The projection
560     * @throws ProjectionConfigurationException in case of invalid parameters
561     */
562    public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
563        String id = parameters.get(Param.proj.key);
564        if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
565
566        // "utm" is not a real projection, but a shortcut for a set of parameters
567        if ("utm".equals(id)) {
568            id = "tmerc";
569        }
570        Proj proj = Projections.getBaseProjection(id);
571        if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id));
572
573        ProjParameters projParams = new ProjParameters();
574
575        projParams.ellps = ellps;
576
577        String s;
578        s = parameters.get(Param.lat_0.key);
579        if (s != null) {
580            projParams.lat0 = parseAngle(s, Param.lat_0.key);
581        }
582        s = parameters.get(Param.lat_1.key);
583        if (s != null) {
584            projParams.lat1 = parseAngle(s, Param.lat_1.key);
585        }
586        s = parameters.get(Param.lat_2.key);
587        if (s != null) {
588            projParams.lat2 = parseAngle(s, Param.lat_2.key);
589        }
590        s = parameters.get(Param.lat_ts.key);
591        if (s != null) {
592            projParams.lat_ts = parseAngle(s, Param.lat_ts.key);
593        }
594        s = parameters.get(Param.lonc.key);
595        if (s != null) {
596            projParams.lonc = parseAngle(s, Param.lonc.key);
597        }
598        s = parameters.get(Param.alpha.key);
599        if (s != null) {
600            projParams.alpha = parseAngle(s, Param.alpha.key);
601        }
602        s = parameters.get(Param.gamma.key);
603        if (s != null) {
604            projParams.gamma = parseAngle(s, Param.gamma.key);
605        }
606        s = parameters.get(Param.lon_1.key);
607        if (s != null) {
608            projParams.lon1 = parseAngle(s, Param.lon_1.key);
609        }
610        s = parameters.get(Param.lon_2.key);
611        if (s != null) {
612            projParams.lon2 = parseAngle(s, Param.lon_2.key);
613        }
614        if (parameters.containsKey(Param.no_off.key) || parameters.containsKey(Param.no_uoff.key)) {
615            projParams.no_off = Boolean.TRUE;
616        }
617        proj.initialize(projParams);
618        return proj;
619    }
620
621    /**
622     * Converts a string to a bounds object
623     * @param boundsStr The string as comma separated list of angles.
624     * @return The bounds.
625     * @throws ProjectionConfigurationException in case of invalid parameter
626     * @see CustomProjection#parseAngle(String, String)
627     */
628    public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
629        String[] numStr = boundsStr.split(",");
630        if (numStr.length != 4)
631            throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
632        return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
633                parseAngle(numStr[0], "minlon (+bounds)"),
634                parseAngle(numStr[3], "maxlat (+bounds)"),
635                parseAngle(numStr[2], "maxlon (+bounds)"), false);
636    }
637
638    public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
639        if (!parameters.containsKey(parameterName))
640            throw new ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName));
641        return parseDouble(Optional.ofNullable(parameters.get(parameterName)).orElseThrow(
642                () -> new ProjectionConfigurationException(tr("Expected number argument for parameter ''{0}''", parameterName))),
643                parameterName);
644    }
645
646    public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
647        try {
648            return Double.parseDouble(doubleStr);
649        } catch (NumberFormatException e) {
650            throw new ProjectionConfigurationException(
651                    tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e);
652        }
653    }
654
655    /**
656     * Convert an angle string to a double value
657     * @param angleStr The string. e.g. -1.1 or 50d 10' 3"
658     * @param parameterName Only for error message.
659     * @return The angle value, in degrees.
660     * @throws ProjectionConfigurationException in case of invalid parameter
661     */
662    public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
663        final String floatPattern = "(\\d+(\\.\\d*)?)";
664        // pattern does all error handling.
665        Matcher in = Pattern.compile("^(?<neg1>-)?"
666                + "(?=\\d)(?:(?<single>" + floatPattern + ")|"
667                + "((?<degree>" + floatPattern + ")d)?"
668                + "((?<minutes>" + floatPattern + ")\')?"
669                + "((?<seconds>" + floatPattern + ")\")?)"
670                + "(?:[NE]|(?<neg2>[SW]))?$").matcher(angleStr);
671
672        if (!in.find()) {
673            throw new ProjectionConfigurationException(
674                    tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
675        }
676
677        double value = 0;
678        if (in.group("single") != null) {
679            value += Double.parseDouble(in.group("single"));
680        }
681        if (in.group("degree") != null) {
682            value += Double.parseDouble(in.group("degree"));
683        }
684        if (in.group("minutes") != null) {
685            value += Double.parseDouble(in.group("minutes")) / 60;
686        }
687        if (in.group("seconds") != null) {
688            value += Double.parseDouble(in.group("seconds")) / 3600;
689        }
690
691        if (in.group("neg1") != null ^ in.group("neg2") != null) {
692            value = -value;
693        }
694        return value;
695    }
696
697    @Override
698    public Integer getEpsgCode() {
699        if (code != null && code.startsWith("EPSG:")) {
700            try {
701                return Integer.valueOf(code.substring(5));
702            } catch (NumberFormatException e) {
703                Logging.warn(e);
704            }
705        }
706        return null;
707    }
708
709    @Override
710    public String toCode() {
711        if (code != null) {
712            return code;
713        } else if (pref != null) {
714            return "proj:" + pref;
715        } else {
716            return "proj:ERROR";
717        }
718    }
719
720    /**
721     * {@inheritDoc}
722     * @deprecated unused - remove in 2017-09
723     */
724    @Override
725    @Deprecated
726    public String getCacheDirectoryName() {
727        if (cacheDir != null) {
728            return cacheDir;
729        } else {
730            return "proj-" + Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
731        }
732    }
733
734    @Override
735    public Bounds getWorldBoundsLatLon() {
736        if (bounds == null) {
737            Bounds ab = proj.getAlgorithmBounds();
738            if (ab != null) {
739                double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180);
740                double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180);
741                bounds = new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false);
742            } else {
743                bounds = new Bounds(
744                    new LatLon(-90.0, -180.0),
745                    new LatLon(90.0, 180.0));
746            }
747        }
748        return bounds;
749    }
750
751    @Override
752    public String toString() {
753        return name != null ? name : tr("Custom Projection");
754    }
755
756    /**
757     * Factor to convert units of east/north coordinates to meters.
758     *
759     * When east/north coordinates are in degrees (geographic CRS), the scale
760     * at the equator is taken, i.e. 360 degrees corresponds to the length of
761     * the equator in meters.
762     *
763     * @return factor to convert units to meter
764     */
765    @Override
766    public double getMetersPerUnit() {
767        return metersPerUnitWMTS;
768    }
769
770    @Override
771    public boolean switchXY() {
772        // TODO: support for other axis orientation such as West South, and Up Down
773        return this.axis.startsWith("ne");
774    }
775
776    private static Map<String, Double> getUnitsToMeters() {
777        Map<String, Double> ret = new ConcurrentHashMap<>();
778        ret.put("km", 1000d);
779        ret.put("m", 1d);
780        ret.put("dm", 1d/10);
781        ret.put("cm", 1d/100);
782        ret.put("mm", 1d/1000);
783        ret.put("kmi", 1852.0);
784        ret.put("in", 0.0254);
785        ret.put("ft", 0.3048);
786        ret.put("yd", 0.9144);
787        ret.put("mi", 1609.344);
788        ret.put("fathom", 1.8288);
789        ret.put("chain", 20.1168);
790        ret.put("link", 0.201168);
791        ret.put("us-in", 1d/39.37);
792        ret.put("us-ft", 0.304800609601219);
793        ret.put("us-yd", 0.914401828803658);
794        ret.put("us-ch", 20.11684023368047);
795        ret.put("us-mi", 1609.347218694437);
796        ret.put("ind-yd", 0.91439523);
797        ret.put("ind-ft", 0.30479841);
798        ret.put("ind-ch", 20.11669506);
799        ret.put("degree", METER_PER_UNIT_DEGREE);
800        return ret;
801    }
802
803    private static Map<String, Double> getPrimeMeridians() {
804        Map<String, Double> ret = new ConcurrentHashMap<>();
805        try {
806            ret.put("greenwich", 0.0);
807            ret.put("lisbon", parseAngle("9d07'54.862\"W", null));
808            ret.put("paris", parseAngle("2d20'14.025\"E", null));
809            ret.put("bogota", parseAngle("74d04'51.3\"W", null));
810            ret.put("madrid", parseAngle("3d41'16.58\"W", null));
811            ret.put("rome", parseAngle("12d27'8.4\"E", null));
812            ret.put("bern", parseAngle("7d26'22.5\"E", null));
813            ret.put("jakarta", parseAngle("106d48'27.79\"E", null));
814            ret.put("ferro", parseAngle("17d40'W", null));
815            ret.put("brussels", parseAngle("4d22'4.71\"E", null));
816            ret.put("stockholm", parseAngle("18d3'29.8\"E", null));
817            ret.put("athens", parseAngle("23d42'58.815\"E", null));
818            ret.put("oslo", parseAngle("10d43'22.5\"E", null));
819        } catch (ProjectionConfigurationException ex) {
820            throw new IllegalStateException(ex);
821        }
822        return ret;
823    }
824
825    private static EastNorth getPointAlong(int i, int n, ProjectionBounds r) {
826        double dEast = (r.maxEast - r.minEast) / n;
827        double dNorth = (r.maxNorth - r.minNorth) / n;
828        if (i < n) {
829            return new EastNorth(r.minEast + i * dEast, r.minNorth);
830        } else if (i < 2*n) {
831            i -= n;
832            return new EastNorth(r.maxEast, r.minNorth + i * dNorth);
833        } else if (i < 3*n) {
834            i -= 2*n;
835            return new EastNorth(r.maxEast - i * dEast, r.maxNorth);
836        } else if (i < 4*n) {
837            i -= 3*n;
838            return new EastNorth(r.minEast, r.maxNorth - i * dNorth);
839        } else {
840            throw new AssertionError();
841        }
842    }
843
844    private EastNorth getPole(Polarity whichPole) {
845        if (polesEN == null) {
846            polesEN = new EnumMap<>(Polarity.class);
847            for (Polarity p : Polarity.values()) {
848                polesEN.put(p, null);
849                LatLon ll = p.getLatLon();
850                try {
851                    EastNorth enPole = latlon2eastNorth(ll);
852                    if (enPole.isValid()) {
853                        // project back and check if the result is somewhat reasonable
854                        LatLon llBack = eastNorth2latlon(enPole);
855                        if (llBack.isValid() && ll.greatCircleDistance(llBack) < 1000) {
856                            polesEN.put(p, enPole);
857                        }
858                    }
859                } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
860                    Logging.error(e);
861                }
862            }
863        }
864        return polesEN.get(whichPole);
865    }
866
867    @Override
868    public Bounds getLatLonBoundsBox(ProjectionBounds r) {
869        final int n = 10;
870        Bounds result = new Bounds(eastNorth2latlon(r.getMin()));
871        result.extend(eastNorth2latlon(r.getMax()));
872        LatLon llPrev = null;
873        for (int i = 0; i < 4*n; i++) {
874            LatLon llNow = eastNorth2latlon(getPointAlong(i, n, r));
875            result.extend(llNow);
876            // check if segment crosses 180th meridian and if so, make sure
877            // to extend bounds to +/-180 degrees longitude
878            if (llPrev != null) {
879                double lon1 = llPrev.lon();
880                double lon2 = llNow.lon();
881                if (90 < lon1 && lon1 < 180 && -180 < lon2 && lon2 < -90) {
882                    result.extend(new LatLon(llPrev.lat(), 180));
883                    result.extend(new LatLon(llNow.lat(), -180));
884                }
885                if (90 < lon2 && lon2 < 180 && -180 < lon1 && lon1 < -90) {
886                    result.extend(new LatLon(llNow.lat(), 180));
887                    result.extend(new LatLon(llPrev.lat(), -180));
888                }
889            }
890            llPrev = llNow;
891        }
892        // if the box contains one of the poles, the above method did not get
893        // correct min/max latitude value
894        for (Polarity p : Polarity.values()) {
895            EastNorth pole = getPole(p);
896            if (pole != null && r.contains(pole)) {
897                result.extend(p.getLatLon());
898            }
899        }
900        return result;
901    }
902
903    @Override
904    public ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection) {
905        final int n = 8;
906        ProjectionBounds result = null;
907        for (int i = 0; i < 4*n; i++) {
908            EastNorth en = latlon2eastNorth(boxProjection.eastNorth2latlon(getPointAlong(i, n, box)));
909            if (result == null) {
910                result = new ProjectionBounds(en);
911            } else {
912                result.extend(en);
913            }
914        }
915        return result;
916    }
917}
Note: See TracBrowser for help on using the repository browser.