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

Last change on this file since 15456 was 14058, checked in by Don-vip, 6 years ago

fix javadoc warning

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