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

Last change on this file since 9573 was 9565, checked in by bastiK, 8 years ago

add 2 standard parallel & non-spherical variants for Mercator projection (see #12186)
(imports pieces of code from the Geotools project)

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