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

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

see #15310 - remove most of deprecated APIs

  • Property svn:eol-style set to native
File size: 34.6 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 */
213 public CustomProjection(String name, String code, String pref) {
214 this.name = name;
215 this.code = code;
216 this.pref = pref;
217 try {
218 update(pref);
219 } catch (ProjectionConfigurationException ex) {
220 Logging.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 String trimmedPref = pref.trim();
346 if (trimmedPref.isEmpty()) {
347 return parameters;
348 }
349
350 Pattern keyPattern = Pattern.compile("\\+(?<key>[a-zA-Z0-9_]+)(=(?<value>.*))?");
351 String[] parts = Utils.WHITE_SPACES_PATTERN.split(trimmedPref);
352 for (String part : parts) {
353 Matcher m = keyPattern.matcher(part);
354 if (m.matches()) {
355 String key = m.group("key");
356 String value = m.group("value");
357 // some aliases
358 if (key.equals(Param.proj.key) && LON_LAT_VALUES.contains(value)) {
359 value = "lonlat";
360 }
361 Param param = Param.paramsByKey.get(key);
362 if (param == null) {
363 if (!ignoreUnknownParameter)
364 throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
365 } else {
366 if (param.hasValue && value == null)
367 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
368 if (!param.hasValue && value != null)
369 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
370 key = param.key; // To be really sure, we might have an alias.
371 }
372 parameters.put(key, value);
373 } else if (!part.startsWith("+")) {
374 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
375 } else {
376 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
377 }
378 }
379 return parameters;
380 }
381
382 /**
383 * Recursive resolution of +init includes.
384 *
385 * @param parameters parameters map
386 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception
387 * @return parameters map with +init includes resolved
388 * @throws ProjectionConfigurationException in case of invalid parameter
389 */
390 public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter)
391 throws ProjectionConfigurationException {
392 // recursive resolution of +init includes
393 String initKey = parameters.get(Param.init.key);
394 if (initKey != null) {
395 Map<String, String> initp;
396 try {
397 initp = parseParameterList(Optional.ofNullable(Projections.getInit(initKey)).orElseThrow(
398 () -> new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey))),
399 ignoreUnknownParameter);
400 initp = resolveInits(initp, ignoreUnknownParameter);
401 } catch (ProjectionConfigurationException ex) {
402 throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex);
403 }
404 initp.putAll(parameters);
405 return initp;
406 }
407 return parameters;
408 }
409
410 /**
411 * Gets the ellipsoid
412 * @param parameters The parameters to get the value from
413 * @return The Ellipsoid as specified with the parameters
414 * @throws ProjectionConfigurationException in case of invalid parameters
415 */
416 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
417 String code = parameters.get(Param.ellps.key);
418 if (code != null) {
419 return Optional.ofNullable(Projections.getEllipsoid(code)).orElseThrow(
420 () -> new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code)));
421 }
422 String s = parameters.get(Param.a.key);
423 if (s != null) {
424 double a = parseDouble(s, Param.a.key);
425 if (parameters.get(Param.es.key) != null) {
426 double es = parseDouble(parameters, Param.es.key);
427 return Ellipsoid.createAes(a, es);
428 }
429 if (parameters.get(Param.rf.key) != null) {
430 double rf = parseDouble(parameters, Param.rf.key);
431 return Ellipsoid.createArf(a, rf);
432 }
433 if (parameters.get(Param.f.key) != null) {
434 double f = parseDouble(parameters, Param.f.key);
435 return Ellipsoid.createAf(a, f);
436 }
437 if (parameters.get(Param.b.key) != null) {
438 double b = parseDouble(parameters, Param.b.key);
439 return Ellipsoid.createAb(a, b);
440 }
441 }
442 if (parameters.containsKey(Param.a.key) ||
443 parameters.containsKey(Param.es.key) ||
444 parameters.containsKey(Param.rf.key) ||
445 parameters.containsKey(Param.f.key) ||
446 parameters.containsKey(Param.b.key))
447 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
448 return null;
449 }
450
451 /**
452 * Gets the datum
453 * @param parameters The parameters to get the value from
454 * @param ellps The ellisoid that was previously computed
455 * @return The Datum as specified with the parameters
456 * @throws ProjectionConfigurationException in case of invalid parameters
457 */
458 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
459 String datumId = parameters.get(Param.datum.key);
460 if (datumId != null) {
461 return Optional.ofNullable(Projections.getDatum(datumId)).orElseThrow(
462 () -> new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId)));
463 }
464 if (ellps == null) {
465 if (parameters.containsKey(Param.no_defs.key))
466 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
467 // nothing specified, use WGS84 as default
468 ellps = Ellipsoid.WGS84;
469 }
470
471 String nadgridsId = parameters.get(Param.nadgrids.key);
472 if (nadgridsId != null) {
473 if (nadgridsId.startsWith("@")) {
474 nadgridsId = nadgridsId.substring(1);
475 }
476 if ("null".equals(nadgridsId))
477 return new NullDatum(null, ellps);
478 final String fNadgridsId = nadgridsId;
479 return new NTV2Datum(fNadgridsId, null, ellps, Optional.ofNullable(Projections.getNTV2Grid(fNadgridsId)).orElseThrow(
480 () -> new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", fNadgridsId))));
481 }
482
483 String towgs84 = parameters.get(Param.towgs84.key);
484 if (towgs84 != null)
485 return parseToWGS84(towgs84, ellps);
486
487 return new NullDatum(null, ellps);
488 }
489
490 /**
491 * Parse {@code towgs84} parameter.
492 * @param paramList List of parameter arguments (expected: 3 or 7)
493 * @param ellps ellipsoid
494 * @return parsed datum ({@link ThreeParameterDatum} or {@link SevenParameterDatum})
495 * @throws ProjectionConfigurationException if the arguments cannot be parsed
496 */
497 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
498 String[] numStr = paramList.split(",");
499
500 if (numStr.length != 3 && numStr.length != 7)
501 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
502 List<Double> towgs84Param = new ArrayList<>();
503 for (String str : numStr) {
504 try {
505 towgs84Param.add(Double.valueOf(str));
506 } catch (NumberFormatException e) {
507 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e);
508 }
509 }
510 boolean isCentric = true;
511 for (Double param : towgs84Param) {
512 if (param != 0) {
513 isCentric = false;
514 break;
515 }
516 }
517 if (isCentric)
518 return new CentricDatum(null, null, ellps);
519 boolean is3Param = true;
520 for (int i = 3; i < towgs84Param.size(); i++) {
521 if (towgs84Param.get(i) != 0) {
522 is3Param = false;
523 break;
524 }
525 }
526 if (is3Param)
527 return new ThreeParameterDatum(null, null, ellps,
528 towgs84Param.get(0),
529 towgs84Param.get(1),
530 towgs84Param.get(2));
531 else
532 return new SevenParameterDatum(null, null, ellps,
533 towgs84Param.get(0),
534 towgs84Param.get(1),
535 towgs84Param.get(2),
536 towgs84Param.get(3),
537 towgs84Param.get(4),
538 towgs84Param.get(5),
539 towgs84Param.get(6));
540 }
541
542 /**
543 * Gets a projection using the given ellipsoid
544 * @param parameters Additional parameters
545 * @param ellps The {@link Ellipsoid}
546 * @return The projection
547 * @throws ProjectionConfigurationException in case of invalid parameters
548 */
549 public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
550 String id = parameters.get(Param.proj.key);
551 if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
552
553 // "utm" is not a real projection, but a shortcut for a set of parameters
554 if ("utm".equals(id)) {
555 id = "tmerc";
556 }
557 Proj proj = Projections.getBaseProjection(id);
558 if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id));
559
560 ProjParameters projParams = new ProjParameters();
561
562 projParams.ellps = ellps;
563
564 String s;
565 s = parameters.get(Param.lat_0.key);
566 if (s != null) {
567 projParams.lat0 = parseAngle(s, Param.lat_0.key);
568 }
569 s = parameters.get(Param.lat_1.key);
570 if (s != null) {
571 projParams.lat1 = parseAngle(s, Param.lat_1.key);
572 }
573 s = parameters.get(Param.lat_2.key);
574 if (s != null) {
575 projParams.lat2 = parseAngle(s, Param.lat_2.key);
576 }
577 s = parameters.get(Param.lat_ts.key);
578 if (s != null) {
579 projParams.lat_ts = parseAngle(s, Param.lat_ts.key);
580 }
581 s = parameters.get(Param.lonc.key);
582 if (s != null) {
583 projParams.lonc = parseAngle(s, Param.lonc.key);
584 }
585 s = parameters.get(Param.alpha.key);
586 if (s != null) {
587 projParams.alpha = parseAngle(s, Param.alpha.key);
588 }
589 s = parameters.get(Param.gamma.key);
590 if (s != null) {
591 projParams.gamma = parseAngle(s, Param.gamma.key);
592 }
593 s = parameters.get(Param.lon_1.key);
594 if (s != null) {
595 projParams.lon1 = parseAngle(s, Param.lon_1.key);
596 }
597 s = parameters.get(Param.lon_2.key);
598 if (s != null) {
599 projParams.lon2 = parseAngle(s, Param.lon_2.key);
600 }
601 if (parameters.containsKey(Param.no_off.key) || parameters.containsKey(Param.no_uoff.key)) {
602 projParams.no_off = Boolean.TRUE;
603 }
604 proj.initialize(projParams);
605 return proj;
606 }
607
608 /**
609 * Converts a string to a bounds object
610 * @param boundsStr The string as comma separated list of angles.
611 * @return The bounds.
612 * @throws ProjectionConfigurationException in case of invalid parameter
613 * @see CustomProjection#parseAngle(String, String)
614 */
615 public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
616 String[] numStr = boundsStr.split(",");
617 if (numStr.length != 4)
618 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
619 return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
620 parseAngle(numStr[0], "minlon (+bounds)"),
621 parseAngle(numStr[3], "maxlat (+bounds)"),
622 parseAngle(numStr[2], "maxlon (+bounds)"), false);
623 }
624
625 public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
626 if (!parameters.containsKey(parameterName))
627 throw new ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName));
628 return parseDouble(Optional.ofNullable(parameters.get(parameterName)).orElseThrow(
629 () -> new ProjectionConfigurationException(tr("Expected number argument for parameter ''{0}''", parameterName))),
630 parameterName);
631 }
632
633 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
634 try {
635 return Double.parseDouble(doubleStr);
636 } catch (NumberFormatException e) {
637 throw new ProjectionConfigurationException(
638 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e);
639 }
640 }
641
642 /**
643 * Convert an angle string to a double value
644 * @param angleStr The string. e.g. -1.1 or 50d10'3"
645 * @param parameterName Only for error message.
646 * @return The angle value, in degrees.
647 * @throws ProjectionConfigurationException in case of invalid parameter
648 */
649 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
650 try {
651 return LatLonParser.parseCoordinate(angleStr);
652 } catch (IllegalArgumentException e) {
653 throw new ProjectionConfigurationException(
654 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr), e);
655 }
656 }
657
658 @Override
659 public Integer getEpsgCode() {
660 if (code != null && code.startsWith("EPSG:")) {
661 try {
662 return Integer.valueOf(code.substring(5));
663 } catch (NumberFormatException e) {
664 Logging.warn(e);
665 }
666 }
667 return null;
668 }
669
670 @Override
671 public String toCode() {
672 if (code != null) {
673 return code;
674 } else if (pref != null) {
675 return "proj:" + pref;
676 } else {
677 return "proj:ERROR";
678 }
679 }
680
681 @Override
682 public Bounds getWorldBoundsLatLon() {
683 if (bounds == null) {
684 Bounds ab = proj.getAlgorithmBounds();
685 if (ab != null) {
686 double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180);
687 double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180);
688 bounds = new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false);
689 } else {
690 bounds = new Bounds(
691 new LatLon(-90.0, -180.0),
692 new LatLon(90.0, 180.0));
693 }
694 }
695 return bounds;
696 }
697
698 @Override
699 public String toString() {
700 return name != null ? name : tr("Custom Projection");
701 }
702
703 /**
704 * Factor to convert units of east/north coordinates to meters.
705 *
706 * When east/north coordinates are in degrees (geographic CRS), the scale
707 * at the equator is taken, i.e. 360 degrees corresponds to the length of
708 * the equator in meters.
709 *
710 * @return factor to convert units to meter
711 */
712 @Override
713 public double getMetersPerUnit() {
714 return metersPerUnitWMTS;
715 }
716
717 @Override
718 public boolean switchXY() {
719 // TODO: support for other axis orientation such as West South, and Up Down
720 return this.axis.startsWith("ne");
721 }
722
723 private static Map<String, Double> getUnitsToMeters() {
724 Map<String, Double> ret = new ConcurrentHashMap<>();
725 ret.put("km", 1000d);
726 ret.put("m", 1d);
727 ret.put("dm", 1d/10);
728 ret.put("cm", 1d/100);
729 ret.put("mm", 1d/1000);
730 ret.put("kmi", 1852.0);
731 ret.put("in", 0.0254);
732 ret.put("ft", 0.3048);
733 ret.put("yd", 0.9144);
734 ret.put("mi", 1609.344);
735 ret.put("fathom", 1.8288);
736 ret.put("chain", 20.1168);
737 ret.put("link", 0.201168);
738 ret.put("us-in", 1d/39.37);
739 ret.put("us-ft", 0.304800609601219);
740 ret.put("us-yd", 0.914401828803658);
741 ret.put("us-ch", 20.11684023368047);
742 ret.put("us-mi", 1609.347218694437);
743 ret.put("ind-yd", 0.91439523);
744 ret.put("ind-ft", 0.30479841);
745 ret.put("ind-ch", 20.11669506);
746 ret.put("degree", METER_PER_UNIT_DEGREE);
747 return ret;
748 }
749
750 private static Map<String, Double> getPrimeMeridians() {
751 Map<String, Double> ret = new ConcurrentHashMap<>();
752 try {
753 ret.put("greenwich", 0.0);
754 ret.put("lisbon", parseAngle("9d07'54.862\"W", null));
755 ret.put("paris", parseAngle("2d20'14.025\"E", null));
756 ret.put("bogota", parseAngle("74d04'51.3\"W", null));
757 ret.put("madrid", parseAngle("3d41'16.58\"W", null));
758 ret.put("rome", parseAngle("12d27'8.4\"E", null));
759 ret.put("bern", parseAngle("7d26'22.5\"E", null));
760 ret.put("jakarta", parseAngle("106d48'27.79\"E", null));
761 ret.put("ferro", parseAngle("17d40'W", null));
762 ret.put("brussels", parseAngle("4d22'4.71\"E", null));
763 ret.put("stockholm", parseAngle("18d3'29.8\"E", null));
764 ret.put("athens", parseAngle("23d42'58.815\"E", null));
765 ret.put("oslo", parseAngle("10d43'22.5\"E", null));
766 } catch (ProjectionConfigurationException ex) {
767 throw new IllegalStateException(ex);
768 }
769 return ret;
770 }
771
772 private static EastNorth getPointAlong(int i, int n, ProjectionBounds r) {
773 double dEast = (r.maxEast - r.minEast) / n;
774 double dNorth = (r.maxNorth - r.minNorth) / n;
775 if (i < n) {
776 return new EastNorth(r.minEast + i * dEast, r.minNorth);
777 } else if (i < 2*n) {
778 i -= n;
779 return new EastNorth(r.maxEast, r.minNorth + i * dNorth);
780 } else if (i < 3*n) {
781 i -= 2*n;
782 return new EastNorth(r.maxEast - i * dEast, r.maxNorth);
783 } else if (i < 4*n) {
784 i -= 3*n;
785 return new EastNorth(r.minEast, r.maxNorth - i * dNorth);
786 } else {
787 throw new AssertionError();
788 }
789 }
790
791 private EastNorth getPole(Polarity whichPole) {
792 if (polesEN == null) {
793 polesEN = new EnumMap<>(Polarity.class);
794 for (Polarity p : Polarity.values()) {
795 polesEN.put(p, null);
796 LatLon ll = p.getLatLon();
797 try {
798 EastNorth enPole = latlon2eastNorth(ll);
799 if (enPole.isValid()) {
800 // project back and check if the result is somewhat reasonable
801 LatLon llBack = eastNorth2latlon(enPole);
802 if (llBack.isValid() && ll.greatCircleDistance(llBack) < 1000) {
803 polesEN.put(p, enPole);
804 }
805 }
806 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
807 Logging.error(e);
808 }
809 }
810 }
811 return polesEN.get(whichPole);
812 }
813
814 @Override
815 public Bounds getLatLonBoundsBox(ProjectionBounds r) {
816 final int n = 10;
817 Bounds result = new Bounds(eastNorth2latlon(r.getMin()));
818 result.extend(eastNorth2latlon(r.getMax()));
819 LatLon llPrev = null;
820 for (int i = 0; i < 4*n; i++) {
821 LatLon llNow = eastNorth2latlon(getPointAlong(i, n, r));
822 result.extend(llNow);
823 // check if segment crosses 180th meridian and if so, make sure
824 // to extend bounds to +/-180 degrees longitude
825 if (llPrev != null) {
826 double lon1 = llPrev.lon();
827 double lon2 = llNow.lon();
828 if (90 < lon1 && lon1 < 180 && -180 < lon2 && lon2 < -90) {
829 result.extend(new LatLon(llPrev.lat(), 180));
830 result.extend(new LatLon(llNow.lat(), -180));
831 }
832 if (90 < lon2 && lon2 < 180 && -180 < lon1 && lon1 < -90) {
833 result.extend(new LatLon(llNow.lat(), 180));
834 result.extend(new LatLon(llPrev.lat(), -180));
835 }
836 }
837 llPrev = llNow;
838 }
839 // if the box contains one of the poles, the above method did not get
840 // correct min/max latitude value
841 for (Polarity p : Polarity.values()) {
842 EastNorth pole = getPole(p);
843 if (pole != null && r.contains(pole)) {
844 result.extend(p.getLatLon());
845 }
846 }
847 return result;
848 }
849
850 @Override
851 public ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection) {
852 final int n = 8;
853 ProjectionBounds result = null;
854 for (int i = 0; i < 4*n; i++) {
855 EastNorth en = latlon2eastNorth(boxProjection.eastNorth2latlon(getPointAlong(i, n, box)));
856 if (result == null) {
857 result = new ProjectionBounds(en);
858 } else {
859 result.extend(en);
860 }
861 }
862 return result;
863 }
864
865 /**
866 * Return true, if a geographic coordinate reference system is represented.
867 *
868 * I.e. if it returns latitude/longitude values rather than Cartesian
869 * east/north coordinates on a flat surface.
870 * @return true, if it is geographic
871 * @since 12792
872 */
873 public boolean isGeographic() {
874 return proj.isGeographic();
875 }
876
877}
Note: See TracBrowser for help on using the repository browser.