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

Last change on this file since 16643 was 16643, checked in by simon04, 4 years ago

see #19334 - https://errorprone.info/bugpattern/StringSplitter

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