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

Last change on this file since 12792 was 12792, checked in by bastiK, 3 months ago

closes #15273, see #15229, see #15182 - add command line interface module for projections

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