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

Last change on this file since 12294 was 12294, checked in by bastiK, 7 years ago

fix

  • 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.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.JosmRuntimeException;
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 public CustomProjection(String name, String code, String pref, String cacheDir) {
215 this(name, code, pref);
216 }
217
218 /**
219 * Constructs a new {@code CustomProjection} with given name, code and parameters.
220 *
221 * @param name describe projection in one or two words
222 * @param code unique code for this projection - may be null
223 * @param pref the string that defines the custom projection
224 */
225 public CustomProjection(String name, String code, String pref) {
226 this.name = name;
227 this.code = code;
228 this.pref = pref;
229 try {
230 update(pref);
231 } catch (ProjectionConfigurationException ex) {
232 Main.trace(ex);
233 try {
234 update(null);
235 } catch (ProjectionConfigurationException ex1) {
236 throw BugReport.intercept(ex1).put("name", name).put("code", code).put("pref", pref);
237 }
238 }
239 }
240
241 /**
242 * Updates this {@code CustomProjection} with given parameters.
243 * @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90")
244 * @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly
245 */
246 public final void update(String pref) throws ProjectionConfigurationException {
247 this.pref = pref;
248 if (pref == null) {
249 ellps = Ellipsoid.WGS84;
250 datum = WGS84Datum.INSTANCE;
251 proj = new Mercator();
252 bounds = new Bounds(
253 -85.05112877980659, -180.0,
254 85.05112877980659, 180.0, true);
255 } else {
256 Map<String, String> parameters = parseParameterList(pref, false);
257 parameters = resolveInits(parameters, false);
258 ellps = parseEllipsoid(parameters);
259 datum = parseDatum(parameters, ellps);
260 if (ellps == null) {
261 ellps = datum.getEllipsoid();
262 }
263 proj = parseProjection(parameters, ellps);
264 // "utm" is a shortcut for a set of parameters
265 if ("utm".equals(parameters.get(Param.proj.key))) {
266 Integer zone;
267 try {
268 zone = Integer.valueOf(Optional.ofNullable(parameters.get(Param.zone.key)).orElseThrow(
269 () -> new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter."))));
270 } catch (NumberFormatException e) {
271 zone = null;
272 }
273 if (zone == null || zone < 1 || zone > 60)
274 throw new ProjectionConfigurationException(tr("Expected integer value in range 1-60 for ''+zone=...'' parameter."));
275 this.lon0 = 6d * zone - 183d;
276 this.k0 = 0.9996;
277 this.x0 = 500_000;
278 this.y0 = parameters.containsKey(Param.south.key) ? 10_000_000 : 0;
279 }
280 String s = parameters.get(Param.x_0.key);
281 if (s != null) {
282 this.x0 = parseDouble(s, Param.x_0.key);
283 }
284 s = parameters.get(Param.y_0.key);
285 if (s != null) {
286 this.y0 = parseDouble(s, Param.y_0.key);
287 }
288 s = parameters.get(Param.lon_0.key);
289 if (s != null) {
290 this.lon0 = parseAngle(s, Param.lon_0.key);
291 }
292 if (proj instanceof ICentralMeridianProvider) {
293 this.lon0 = ((ICentralMeridianProvider) proj).getCentralMeridian();
294 }
295 s = parameters.get(Param.pm.key);
296 if (s != null) {
297 if (PRIME_MERIDANS.containsKey(s)) {
298 this.pm = PRIME_MERIDANS.get(s);
299 } else {
300 this.pm = parseAngle(s, Param.pm.key);
301 }
302 }
303 s = parameters.get(Param.k_0.key);
304 if (s != null) {
305 this.k0 = parseDouble(s, Param.k_0.key);
306 }
307 if (proj instanceof IScaleFactorProvider) {
308 this.k0 *= ((IScaleFactorProvider) proj).getScaleFactor();
309 }
310 s = parameters.get(Param.bounds.key);
311 if (s != null) {
312 this.bounds = parseBounds(s);
313 }
314 s = parameters.get(Param.wmssrs.key);
315 if (s != null) {
316 this.code = s;
317 }
318 boolean defaultUnits = true;
319 s = parameters.get(Param.units.key);
320 if (s != null) {
321 s = Utils.strip(s, "\"");
322 if (UNITS_TO_METERS.containsKey(s)) {
323 this.toMeter = UNITS_TO_METERS.get(s);
324 this.metersPerUnitWMTS = this.toMeter;
325 defaultUnits = false;
326 } else {
327 throw new ProjectionConfigurationException(tr("No unit found for: {0}", s));
328 }
329 }
330 s = parameters.get(Param.to_meter.key);
331 if (s != null) {
332 this.toMeter = parseDouble(s, Param.to_meter.key);
333 this.metersPerUnitWMTS = this.toMeter;
334 defaultUnits = false;
335 }
336 if (defaultUnits) {
337 this.toMeter = 1;
338 this.metersPerUnitWMTS = proj.isGeographic() ? METER_PER_UNIT_DEGREE : 1;
339 }
340 s = parameters.get(Param.axis.key);
341 if (s != null) {
342 this.axis = s;
343 }
344 }
345 }
346
347 /**
348 * Parse a parameter list to key=value pairs.
349 *
350 * @param pref the parameter list
351 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception
352 * @return parameters map
353 * @throws ProjectionConfigurationException in case of invalid parameter
354 */
355 public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException {
356 Map<String, String> parameters = new HashMap<>();
357 String trimmedPref = pref.trim();
358 if (trimmedPref.isEmpty()) {
359 return parameters;
360 }
361
362 Pattern keyPattern = Pattern.compile("\\+(?<key>[a-zA-Z0-9_]+)(=(?<value>.*))?");
363 String[] parts = Utils.WHITE_SPACES_PATTERN.split(trimmedPref);
364 for (String part : parts) {
365 Matcher m = keyPattern.matcher(part);
366 if (m.matches()) {
367 String key = m.group("key");
368 String value = m.group("value");
369 // some aliases
370 if (key.equals(Param.proj.key) && LON_LAT_VALUES.contains(value)) {
371 value = "lonlat";
372 }
373 Param param = Param.paramsByKey.get(key);
374 if (param == null) {
375 if (!ignoreUnknownParameter)
376 throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
377 } else {
378 if (param.hasValue && value == null)
379 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
380 if (!param.hasValue && value != null)
381 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
382 key = param.key; // To be really sure, we might have an alias.
383 }
384 parameters.put(key, value);
385 } else if (!part.startsWith("+")) {
386 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
387 } else {
388 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
389 }
390 }
391 return parameters;
392 }
393
394 /**
395 * Recursive resolution of +init includes.
396 *
397 * @param parameters parameters map
398 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception
399 * @return parameters map with +init includes resolved
400 * @throws ProjectionConfigurationException in case of invalid parameter
401 */
402 public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter)
403 throws ProjectionConfigurationException {
404 // recursive resolution of +init includes
405 String initKey = parameters.get(Param.init.key);
406 if (initKey != null) {
407 Map<String, String> initp;
408 try {
409 initp = parseParameterList(Optional.ofNullable(Projections.getInit(initKey)).orElseThrow(
410 () -> new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey))),
411 ignoreUnknownParameter);
412 initp = resolveInits(initp, ignoreUnknownParameter);
413 } catch (ProjectionConfigurationException ex) {
414 throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex);
415 }
416 initp.putAll(parameters);
417 return initp;
418 }
419 return parameters;
420 }
421
422 /**
423 * Gets the ellipsoid
424 * @param parameters The parameters to get the value from
425 * @return The Ellipsoid as specified with the parameters
426 * @throws ProjectionConfigurationException in case of invalid parameters
427 */
428 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
429 String code = parameters.get(Param.ellps.key);
430 if (code != null) {
431 return Optional.ofNullable(Projections.getEllipsoid(code)).orElseThrow(
432 () -> new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code)));
433 }
434 String s = parameters.get(Param.a.key);
435 if (s != null) {
436 double a = parseDouble(s, Param.a.key);
437 if (parameters.get(Param.es.key) != null) {
438 double es = parseDouble(parameters, Param.es.key);
439 return Ellipsoid.createAes(a, es);
440 }
441 if (parameters.get(Param.rf.key) != null) {
442 double rf = parseDouble(parameters, Param.rf.key);
443 return Ellipsoid.createArf(a, rf);
444 }
445 if (parameters.get(Param.f.key) != null) {
446 double f = parseDouble(parameters, Param.f.key);
447 return Ellipsoid.createAf(a, f);
448 }
449 if (parameters.get(Param.b.key) != null) {
450 double b = parseDouble(parameters, Param.b.key);
451 return Ellipsoid.createAb(a, b);
452 }
453 }
454 if (parameters.containsKey(Param.a.key) ||
455 parameters.containsKey(Param.es.key) ||
456 parameters.containsKey(Param.rf.key) ||
457 parameters.containsKey(Param.f.key) ||
458 parameters.containsKey(Param.b.key))
459 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
460 return null;
461 }
462
463 /**
464 * Gets the datum
465 * @param parameters The parameters to get the value from
466 * @param ellps The ellisoid that was previously computed
467 * @return The Datum as specified with the parameters
468 * @throws ProjectionConfigurationException in case of invalid parameters
469 */
470 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
471 String datumId = parameters.get(Param.datum.key);
472 if (datumId != null) {
473 return Optional.ofNullable(Projections.getDatum(datumId)).orElseThrow(
474 () -> new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId)));
475 }
476 if (ellps == null) {
477 if (parameters.containsKey(Param.no_defs.key))
478 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
479 // nothing specified, use WGS84 as default
480 ellps = Ellipsoid.WGS84;
481 }
482
483 String nadgridsId = parameters.get(Param.nadgrids.key);
484 if (nadgridsId != null) {
485 if (nadgridsId.startsWith("@")) {
486 nadgridsId = nadgridsId.substring(1);
487 }
488 if ("null".equals(nadgridsId))
489 return new NullDatum(null, ellps);
490 final String fNadgridsId = nadgridsId;
491 return new NTV2Datum(fNadgridsId, null, ellps, Optional.ofNullable(Projections.getNTV2Grid(fNadgridsId)).orElseThrow(
492 () -> new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", fNadgridsId))));
493 }
494
495 String towgs84 = parameters.get(Param.towgs84.key);
496 if (towgs84 != null)
497 return parseToWGS84(towgs84, ellps);
498
499 return new NullDatum(null, ellps);
500 }
501
502 /**
503 * Parse {@code towgs84} parameter.
504 * @param paramList List of parameter arguments (expected: 3 or 7)
505 * @param ellps ellipsoid
506 * @return parsed datum ({@link ThreeParameterDatum} or {@link SevenParameterDatum})
507 * @throws ProjectionConfigurationException if the arguments cannot be parsed
508 */
509 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
510 String[] numStr = paramList.split(",");
511
512 if (numStr.length != 3 && numStr.length != 7)
513 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
514 List<Double> towgs84Param = new ArrayList<>();
515 for (String str : numStr) {
516 try {
517 towgs84Param.add(Double.valueOf(str));
518 } catch (NumberFormatException e) {
519 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e);
520 }
521 }
522 boolean isCentric = true;
523 for (Double param : towgs84Param) {
524 if (param != 0) {
525 isCentric = false;
526 break;
527 }
528 }
529 if (isCentric)
530 return new CentricDatum(null, null, ellps);
531 boolean is3Param = true;
532 for (int i = 3; i < towgs84Param.size(); i++) {
533 if (towgs84Param.get(i) != 0) {
534 is3Param = false;
535 break;
536 }
537 }
538 if (is3Param)
539 return new ThreeParameterDatum(null, null, ellps,
540 towgs84Param.get(0),
541 towgs84Param.get(1),
542 towgs84Param.get(2));
543 else
544 return new SevenParameterDatum(null, null, ellps,
545 towgs84Param.get(0),
546 towgs84Param.get(1),
547 towgs84Param.get(2),
548 towgs84Param.get(3),
549 towgs84Param.get(4),
550 towgs84Param.get(5),
551 towgs84Param.get(6));
552 }
553
554 /**
555 * Gets a projection using the given ellipsoid
556 * @param parameters Additional parameters
557 * @param ellps The {@link Ellipsoid}
558 * @return The projection
559 * @throws ProjectionConfigurationException in case of invalid parameters
560 */
561 public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
562 String id = parameters.get(Param.proj.key);
563 if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
564
565 // "utm" is not a real projection, but a shortcut for a set of parameters
566 if ("utm".equals(id)) {
567 id = "tmerc";
568 }
569 Proj proj = Projections.getBaseProjection(id);
570 if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id));
571
572 ProjParameters projParams = new ProjParameters();
573
574 projParams.ellps = ellps;
575
576 String s;
577 s = parameters.get(Param.lat_0.key);
578 if (s != null) {
579 projParams.lat0 = parseAngle(s, Param.lat_0.key);
580 }
581 s = parameters.get(Param.lat_1.key);
582 if (s != null) {
583 projParams.lat1 = parseAngle(s, Param.lat_1.key);
584 }
585 s = parameters.get(Param.lat_2.key);
586 if (s != null) {
587 projParams.lat2 = parseAngle(s, Param.lat_2.key);
588 }
589 s = parameters.get(Param.lat_ts.key);
590 if (s != null) {
591 projParams.lat_ts = parseAngle(s, Param.lat_ts.key);
592 }
593 s = parameters.get(Param.lonc.key);
594 if (s != null) {
595 projParams.lonc = parseAngle(s, Param.lonc.key);
596 }
597 s = parameters.get(Param.alpha.key);
598 if (s != null) {
599 projParams.alpha = parseAngle(s, Param.alpha.key);
600 }
601 s = parameters.get(Param.gamma.key);
602 if (s != null) {
603 projParams.gamma = parseAngle(s, Param.gamma.key);
604 }
605 s = parameters.get(Param.lon_1.key);
606 if (s != null) {
607 projParams.lon1 = parseAngle(s, Param.lon_1.key);
608 }
609 s = parameters.get(Param.lon_2.key);
610 if (s != null) {
611 projParams.lon2 = parseAngle(s, Param.lon_2.key);
612 }
613 if (parameters.containsKey(Param.no_off.key) || parameters.containsKey(Param.no_uoff.key)) {
614 projParams.no_off = Boolean.TRUE;
615 }
616 proj.initialize(projParams);
617 return proj;
618 }
619
620 /**
621 * Converts a string to a bounds object
622 * @param boundsStr The string as comma separated list of angles.
623 * @return The bounds.
624 * @throws ProjectionConfigurationException in case of invalid parameter
625 * @see CustomProjection#parseAngle(String, String)
626 */
627 public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
628 String[] numStr = boundsStr.split(",");
629 if (numStr.length != 4)
630 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
631 return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
632 parseAngle(numStr[0], "minlon (+bounds)"),
633 parseAngle(numStr[3], "maxlat (+bounds)"),
634 parseAngle(numStr[2], "maxlon (+bounds)"), false);
635 }
636
637 public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
638 if (!parameters.containsKey(parameterName))
639 throw new ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName));
640 return parseDouble(Optional.ofNullable(parameters.get(parameterName)).orElseThrow(
641 () -> new ProjectionConfigurationException(tr("Expected number argument for parameter ''{0}''", parameterName))),
642 parameterName);
643 }
644
645 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
646 try {
647 return Double.parseDouble(doubleStr);
648 } catch (NumberFormatException e) {
649 throw new ProjectionConfigurationException(
650 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e);
651 }
652 }
653
654 /**
655 * Convert an angle string to a double value
656 * @param angleStr The string. e.g. -1.1 or 50d 10' 3"
657 * @param parameterName Only for error message.
658 * @return The angle value, in degrees.
659 * @throws ProjectionConfigurationException in case of invalid parameter
660 */
661 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
662 final String floatPattern = "(\\d+(\\.\\d*)?)";
663 // pattern does all error handling.
664 Matcher in = Pattern.compile("^(?<neg1>-)?"
665 + "(?=\\d)(?:(?<single>" + floatPattern + ")|"
666 + "((?<degree>" + floatPattern + ")d)?"
667 + "((?<minutes>" + floatPattern + ")\')?"
668 + "((?<seconds>" + floatPattern + ")\")?)"
669 + "(?:[NE]|(?<neg2>[SW]))?$").matcher(angleStr);
670
671 if (!in.find()) {
672 throw new ProjectionConfigurationException(
673 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
674 }
675
676 double value = 0;
677 if (in.group("single") != null) {
678 value += Double.parseDouble(in.group("single"));
679 }
680 if (in.group("degree") != null) {
681 value += Double.parseDouble(in.group("degree"));
682 }
683 if (in.group("minutes") != null) {
684 value += Double.parseDouble(in.group("minutes")) / 60;
685 }
686 if (in.group("seconds") != null) {
687 value += Double.parseDouble(in.group("seconds")) / 3600;
688 }
689
690 if (in.group("neg1") != null ^ in.group("neg2") != null) {
691 value = -value;
692 }
693 return value;
694 }
695
696 @Override
697 public Integer getEpsgCode() {
698 if (code != null && code.startsWith("EPSG:")) {
699 try {
700 return Integer.valueOf(code.substring(5));
701 } catch (NumberFormatException e) {
702 Main.warn(e);
703 }
704 }
705 return null;
706 }
707
708 @Override
709 public String toCode() {
710 if (code != null) {
711 return code;
712 } else if (pref != null) {
713 return "proj:" + pref;
714 } else {
715 return "proj:ERROR";
716 }
717 }
718
719 /**
720 * {@inheritDoc}
721 * @deprecated unused - remove in 2017-09
722 */
723 @Override
724 @Deprecated
725 public String getCacheDirectoryName() {
726 if (cacheDir != null) {
727 return cacheDir;
728 } else {
729 return "proj-" + Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
730 }
731 }
732
733 @Override
734 public Bounds getWorldBoundsLatLon() {
735 if (bounds == null) {
736 Bounds ab = proj.getAlgorithmBounds();
737 if (ab != null) {
738 double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180);
739 double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180);
740 bounds = new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false);
741 } else {
742 bounds = new Bounds(
743 new LatLon(-90.0, -180.0),
744 new LatLon(90.0, 180.0));
745 }
746 }
747 return bounds;
748 }
749
750 @Override
751 public String toString() {
752 return name != null ? name : tr("Custom Projection");
753 }
754
755 /**
756 * Factor to convert units of east/north coordinates to meters.
757 *
758 * When east/north coordinates are in degrees (geographic CRS), the scale
759 * at the equator is taken, i.e. 360 degrees corresponds to the length of
760 * the equator in meters.
761 *
762 * @return factor to convert units to meter
763 */
764 @Override
765 public double getMetersPerUnit() {
766 return metersPerUnitWMTS;
767 }
768
769 @Override
770 public boolean switchXY() {
771 // TODO: support for other axis orientation such as West South, and Up Down
772 return this.axis.startsWith("ne");
773 }
774
775 private static Map<String, Double> getUnitsToMeters() {
776 Map<String, Double> ret = new ConcurrentHashMap<>();
777 ret.put("km", 1000d);
778 ret.put("m", 1d);
779 ret.put("dm", 1d/10);
780 ret.put("cm", 1d/100);
781 ret.put("mm", 1d/1000);
782 ret.put("kmi", 1852.0);
783 ret.put("in", 0.0254);
784 ret.put("ft", 0.3048);
785 ret.put("yd", 0.9144);
786 ret.put("mi", 1609.344);
787 ret.put("fathom", 1.8288);
788 ret.put("chain", 20.1168);
789 ret.put("link", 0.201168);
790 ret.put("us-in", 1d/39.37);
791 ret.put("us-ft", 0.304800609601219);
792 ret.put("us-yd", 0.914401828803658);
793 ret.put("us-ch", 20.11684023368047);
794 ret.put("us-mi", 1609.347218694437);
795 ret.put("ind-yd", 0.91439523);
796 ret.put("ind-ft", 0.30479841);
797 ret.put("ind-ch", 20.11669506);
798 ret.put("degree", METER_PER_UNIT_DEGREE);
799 return ret;
800 }
801
802 private static Map<String, Double> getPrimeMeridians() {
803 Map<String, Double> ret = new ConcurrentHashMap<>();
804 try {
805 ret.put("greenwich", 0.0);
806 ret.put("lisbon", parseAngle("9d07'54.862\"W", null));
807 ret.put("paris", parseAngle("2d20'14.025\"E", null));
808 ret.put("bogota", parseAngle("74d04'51.3\"W", null));
809 ret.put("madrid", parseAngle("3d41'16.58\"W", null));
810 ret.put("rome", parseAngle("12d27'8.4\"E", null));
811 ret.put("bern", parseAngle("7d26'22.5\"E", null));
812 ret.put("jakarta", parseAngle("106d48'27.79\"E", null));
813 ret.put("ferro", parseAngle("17d40'W", null));
814 ret.put("brussels", parseAngle("4d22'4.71\"E", null));
815 ret.put("stockholm", parseAngle("18d3'29.8\"E", null));
816 ret.put("athens", parseAngle("23d42'58.815\"E", null));
817 ret.put("oslo", parseAngle("10d43'22.5\"E", null));
818 } catch (ProjectionConfigurationException ex) {
819 throw new IllegalStateException(ex);
820 }
821 return ret;
822 }
823
824 private static EastNorth getPointAlong(int i, int n, ProjectionBounds r) {
825 double dEast = (r.maxEast - r.minEast) / n;
826 double dNorth = (r.maxNorth - r.minNorth) / n;
827 if (i < n) {
828 return new EastNorth(r.minEast + i * dEast, r.minNorth);
829 } else if (i < 2*n) {
830 i -= n;
831 return new EastNorth(r.maxEast, r.minNorth + i * dNorth);
832 } else if (i < 3*n) {
833 i -= 2*n;
834 return new EastNorth(r.maxEast - i * dEast, r.maxNorth);
835 } else if (i < 4*n) {
836 i -= 3*n;
837 return new EastNorth(r.minEast, r.maxNorth - i * dNorth);
838 } else {
839 throw new AssertionError();
840 }
841 }
842
843 private EastNorth getPole(Polarity whichPole) {
844 if (polesEN == null) {
845 polesEN = new EnumMap<>(Polarity.class);
846 for (Polarity p : Polarity.values()) {
847 polesEN.put(p, null);
848 LatLon ll = p.getLatLon();
849 try {
850 EastNorth enPole = latlon2eastNorth(ll);
851 if (enPole.isValid()) {
852 // project back and check if the result is somewhat reasonable
853 LatLon llBack = eastNorth2latlon(enPole);
854 if (llBack.isValid() && ll.greatCircleDistance(llBack) < 1000) {
855 polesEN.put(p, enPole);
856 }
857 }
858 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
859 Main.error(e);
860 }
861 }
862 }
863 return polesEN.get(whichPole);
864 }
865
866 @Override
867 public Bounds getLatLonBoundsBox(ProjectionBounds r) {
868 final int n = 10;
869 Bounds result = new Bounds(eastNorth2latlon(r.getMin()));
870 result.extend(eastNorth2latlon(r.getMax()));
871 LatLon llPrev = null;
872 for (int i = 0; i < 4*n; i++) {
873 LatLon llNow = eastNorth2latlon(getPointAlong(i, n, r));
874 result.extend(llNow);
875 // check if segment crosses 180th meridian and if so, make sure
876 // to extend bounds to +/-180 degrees longitude
877 if (llPrev != null) {
878 double lon1 = llPrev.lon();
879 double lon2 = llNow.lon();
880 if (90 < lon1 && lon1 < 180 && -180 < lon2 && lon2 < -90) {
881 result.extend(new LatLon(llPrev.lat(), 180));
882 result.extend(new LatLon(llNow.lat(), -180));
883 }
884 if (90 < lon2 && lon2 < 180 && -180 < lon1 && lon1 < -90) {
885 result.extend(new LatLon(llNow.lat(), 180));
886 result.extend(new LatLon(llPrev.lat(), -180));
887 }
888 }
889 llPrev = llNow;
890 }
891 // if the box contains one of the poles, the above method did not get
892 // correct min/max latitude value
893 for (Polarity p : Polarity.values()) {
894 EastNorth pole = getPole(p);
895 if (pole != null && r.contains(pole)) {
896 result.extend(p.getLatLon());
897 }
898 }
899 return result;
900 }
901
902 @Override
903 public ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection) {
904 final int n = 8;
905 ProjectionBounds result = null;
906 for (int i = 0; i < 4*n; i++) {
907 EastNorth en = latlon2eastNorth(boxProjection.eastNorth2latlon(getPointAlong(i, n, box)));
908 if (result == null) {
909 result = new ProjectionBounds(en);
910 } else {
911 result.extend(en);
912 }
913 }
914 return result;
915 }
916}
Note: See TracBrowser for help on using the repository browser.