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

Last change on this file since 9992 was 9790, checked in by bastiK, 8 years ago

use intended units for east/north coordinates (see #12186)

When east/north coordinates are not in meter, but
feet, ... convert them to correct units.
Currently they are always stored in meters or degrees.
This makes no difference to JOSM internally, but affects
services like WMS/WMTS.
Only relevant for projections with +units=... or +to_meter=...
parameter set to non-default value.

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