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

Last change on this file since 11710 was 11553, checked in by Don-vip, 7 years ago

refactor handling of null values - use Java 8 Optional where possible

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