001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.projection; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.ArrayList; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Map; 010import java.util.concurrent.ConcurrentHashMap; 011import java.util.regex.Matcher; 012import java.util.regex.Pattern; 013 014import org.openstreetmap.josm.Main; 015import org.openstreetmap.josm.data.Bounds; 016import org.openstreetmap.josm.data.coor.LatLon; 017import org.openstreetmap.josm.data.projection.datum.CentricDatum; 018import org.openstreetmap.josm.data.projection.datum.Datum; 019import org.openstreetmap.josm.data.projection.datum.NTV2Datum; 020import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper; 021import org.openstreetmap.josm.data.projection.datum.NullDatum; 022import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum; 023import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum; 024import org.openstreetmap.josm.data.projection.datum.WGS84Datum; 025import org.openstreetmap.josm.data.projection.proj.Mercator; 026import org.openstreetmap.josm.data.projection.proj.Proj; 027import org.openstreetmap.josm.data.projection.proj.ProjParameters; 028import org.openstreetmap.josm.tools.Utils; 029 030/** 031 * Custom projection. 032 * 033 * Inspired by PROJ.4 and Proj4J. 034 * @since 5072 035 */ 036public class CustomProjection extends AbstractProjection { 037 038 private static final double METER_PER_UNIT_DEGREE = 2 * Math.PI * 6370997 / 360; 039 private static final Map<String, Double> UNITS_TO_METERS = getUnitsToMeters(); 040 private static final Map<String, Double> PRIME_MERIDANS = getPrimeMeridians(); 041 042 /** 043 * pref String that defines the projection 044 * 045 * null means fall back mode (Mercator) 046 */ 047 protected String pref; 048 protected String name; 049 protected String code; 050 protected String cacheDir; 051 protected Bounds bounds; 052 private double metersPerUnit = METER_PER_UNIT_DEGREE; // default to degrees 053 private String axis = "enu"; // default axis orientation is East, North, Up 054 055 /** 056 * Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>. 057 * @since 7370 (public) 058 */ 059 public enum Param { 060 061 /** False easting */ 062 x_0("x_0", true), 063 /** False northing */ 064 y_0("y_0", true), 065 /** Central meridian */ 066 lon_0("lon_0", true), 067 /** Prime meridian */ 068 pm("pm", true), 069 /** Scaling factor */ 070 k_0("k_0", true), 071 /** Ellipsoid name (see {@code proj -le}) */ 072 ellps("ellps", true), 073 /** Semimajor radius of the ellipsoid axis */ 074 a("a", true), 075 /** Eccentricity of the ellipsoid squared */ 076 es("es", true), 077 /** Reciprocal of the ellipsoid flattening term (e.g. 298) */ 078 rf("rf", true), 079 /** Flattening of the ellipsoid = 1-sqrt(1-e^2) */ 080 f("f", true), 081 /** Semiminor radius of the ellipsoid axis */ 082 b("b", true), 083 /** Datum name (see {@code proj -ld}) */ 084 datum("datum", true), 085 /** 3 or 7 term datum transform parameters */ 086 towgs84("towgs84", true), 087 /** Filename of NTv2 grid file to use for datum transforms */ 088 nadgrids("nadgrids", true), 089 /** Projection name (see {@code proj -l}) */ 090 proj("proj", true), 091 /** Latitude of origin */ 092 lat_0("lat_0", true), 093 /** Latitude of first standard parallel */ 094 lat_1("lat_1", true), 095 /** Latitude of second standard parallel */ 096 lat_2("lat_2", true), 097 /** the exact proj.4 string will be preserved in the WKT representation */ 098 wktext("wktext", false), // ignored 099 /** meters, US survey feet, etc. */ 100 units("units", true), 101 /** Don't use the /usr/share/proj/proj_def.dat defaults file */ 102 no_defs("no_defs", false), 103 init("init", true), 104 /** crs units to meter multiplier */ 105 to_meter("to_meter", true), 106 /** definition of axis for projection */ 107 axis("axis", true), 108 /** UTM zone */ 109 zone("zone", true), 110 /** indicate southern hemisphere for UTM */ 111 south("south", false), 112 /** vertical units - ignore, as we don't use height information */ 113 vunits("vunits", true), 114 // JOSM extensions, not present in PROJ.4 115 wmssrs("wmssrs", true), 116 bounds("bounds", true); 117 118 /** Parameter key */ 119 public final String key; 120 /** {@code true} if the parameter has a value */ 121 public final boolean hasValue; 122 123 /** Map of all parameters by key */ 124 static final Map<String, Param> paramsByKey = new ConcurrentHashMap<>(); 125 static { 126 for (Param p : Param.values()) { 127 paramsByKey.put(p.key, p); 128 } 129 } 130 131 Param(String key, boolean hasValue) { 132 this.key = key; 133 this.hasValue = hasValue; 134 } 135 } 136 137 /** 138 * Constructs a new empty {@code CustomProjection}. 139 */ 140 public CustomProjection() { 141 // contents can be set later with update() 142 } 143 144 /** 145 * Constructs a new {@code CustomProjection} with given parameters. 146 * @param pref String containing projection parameters 147 * (ex: "+proj=tmerc +lon_0=-3 +k_0=0.9996 +x_0=500000 +ellps=WGS84 +datum=WGS84 +bounds=-8,-5,2,85") 148 */ 149 public CustomProjection(String pref) { 150 this(null, null, pref, null); 151 } 152 153 /** 154 * Constructs a new {@code CustomProjection} with given name, code and parameters. 155 * 156 * @param name describe projection in one or two words 157 * @param code unique code for this projection - may be null 158 * @param pref the string that defines the custom projection 159 * @param cacheDir cache directory name 160 */ 161 public CustomProjection(String name, String code, String pref, String cacheDir) { 162 this.name = name; 163 this.code = code; 164 this.pref = pref; 165 this.cacheDir = cacheDir; 166 try { 167 update(pref); 168 } catch (ProjectionConfigurationException ex) { 169 try { 170 update(null); 171 } catch (ProjectionConfigurationException ex1) { 172 throw new RuntimeException(ex1); 173 } 174 } 175 } 176 177 /** 178 * Updates this {@code CustomProjection} with given parameters. 179 * @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90") 180 * @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly 181 */ 182 public final void update(String pref) throws ProjectionConfigurationException { 183 this.pref = pref; 184 if (pref == null) { 185 ellps = Ellipsoid.WGS84; 186 datum = WGS84Datum.INSTANCE; 187 proj = new Mercator(); 188 bounds = new Bounds( 189 -85.05112877980659, -180.0, 190 85.05112877980659, 180.0, true); 191 } else { 192 Map<String, String> parameters = parseParameterList(pref, false); 193 parameters = resolveInits(parameters, false); 194 ellps = parseEllipsoid(parameters); 195 datum = parseDatum(parameters, ellps); 196 if (ellps == null) { 197 ellps = datum.getEllipsoid(); 198 } 199 proj = parseProjection(parameters, ellps); 200 // "utm" is a shortcut for a set of parameters 201 if ("utm".equals(parameters.get(Param.proj.key))) { 202 String zoneStr = parameters.get(Param.zone.key); 203 Integer zone; 204 if (zoneStr == null) 205 throw new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter.")); 206 try { 207 zone = Integer.valueOf(zoneStr); 208 } catch (NumberFormatException e) { 209 zone = null; 210 } 211 if (zone == null || zone < 1 || zone > 60) 212 throw new ProjectionConfigurationException(tr("Expected integer value in range 1-60 for ''+zone=...'' parameter.")); 213 this.lon0 = 6 * zone - 183; 214 this.k0 = 0.9996; 215 this.x0 = 500000; 216 this.y0 = parameters.containsKey(Param.south.key) ? 10000000 : 0; 217 } 218 String s = parameters.get(Param.x_0.key); 219 if (s != null) { 220 this.x0 = parseDouble(s, Param.x_0.key); 221 } 222 s = parameters.get(Param.y_0.key); 223 if (s != null) { 224 this.y0 = parseDouble(s, Param.y_0.key); 225 } 226 s = parameters.get(Param.lon_0.key); 227 if (s != null) { 228 this.lon0 = parseAngle(s, Param.lon_0.key); 229 } 230 s = parameters.get(Param.pm.key); 231 if (s != null) { 232 if (PRIME_MERIDANS.containsKey(s)) { 233 this.pm = PRIME_MERIDANS.get(s); 234 } else { 235 this.pm = parseAngle(s, Param.pm.key); 236 } 237 } 238 s = parameters.get(Param.k_0.key); 239 if (s != null) { 240 this.k0 = parseDouble(s, Param.k_0.key); 241 } 242 s = parameters.get(Param.bounds.key); 243 if (s != null) { 244 this.bounds = parseBounds(s); 245 } 246 s = parameters.get(Param.wmssrs.key); 247 if (s != null) { 248 this.code = s; 249 } 250 s = parameters.get(Param.units.key); 251 if (s != null) { 252 s = Utils.strip(s, "\""); 253 if (UNITS_TO_METERS.containsKey(s)) { 254 this.metersPerUnit = UNITS_TO_METERS.get(s); 255 } else { 256 Main.warn("No metersPerUnit found for: " + s); 257 } 258 } 259 s = parameters.get(Param.to_meter.key); 260 if (s != null) { 261 this.metersPerUnit = parseDouble(s, Param.to_meter.key); 262 } 263 s = parameters.get(Param.axis.key); 264 if (s != null) { 265 this.axis = s; 266 } 267 } 268 } 269 270 /** 271 * Parse a parameter list to key=value pairs. 272 * 273 * @param pref the parameter list 274 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception 275 * @return parameters map 276 * @throws ProjectionConfigurationException in case of invalid parameter 277 */ 278 public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException { 279 Map<String, String> parameters = new HashMap<>(); 280 String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim()); 281 if (pref.trim().isEmpty()) { 282 parts = new String[0]; 283 } 284 for (String part : parts) { 285 if (part.isEmpty() || part.charAt(0) != '+') 286 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part)); 287 Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part); 288 if (m.matches()) { 289 String key = m.group(1); 290 // alias 291 if ("k".equals(key)) { 292 key = Param.k_0.key; 293 } 294 String value = null; 295 if (m.groupCount() >= 3) { 296 value = m.group(3); 297 // some aliases 298 if (key.equals(Param.proj.key)) { 299 if ("longlat".equals(value) || "latlon".equals(value) || "latlong".equals(value)) { 300 value = "lonlat"; 301 } 302 } 303 } 304 if (!Param.paramsByKey.containsKey(key)) { 305 if (!ignoreUnknownParameter) 306 throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key)); 307 } else { 308 if (Param.paramsByKey.get(key).hasValue && value == null) 309 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key)); 310 if (!Param.paramsByKey.get(key).hasValue && value != null) 311 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key)); 312 } 313 parameters.put(key, value); 314 } else 315 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part)); 316 } 317 return parameters; 318 } 319 320 /** 321 * Recursive resolution of +init includes. 322 * 323 * @param parameters parameters map 324 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception 325 * @return parameters map with +init includes resolved 326 * @throws ProjectionConfigurationException in case of invalid parameter 327 */ 328 public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter) 329 throws ProjectionConfigurationException { 330 // recursive resolution of +init includes 331 String initKey = parameters.get(Param.init.key); 332 if (initKey != null) { 333 String init = Projections.getInit(initKey); 334 if (init == null) 335 throw new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey)); 336 Map<String, String> initp; 337 try { 338 initp = parseParameterList(init, ignoreUnknownParameter); 339 initp = resolveInits(initp, ignoreUnknownParameter); 340 } catch (ProjectionConfigurationException ex) { 341 throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex); 342 } 343 initp.putAll(parameters); 344 return initp; 345 } 346 return parameters; 347 } 348 349 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException { 350 String code = parameters.get(Param.ellps.key); 351 if (code != null) { 352 Ellipsoid ellipsoid = Projections.getEllipsoid(code); 353 if (ellipsoid == null) { 354 throw new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code)); 355 } else { 356 return ellipsoid; 357 } 358 } 359 String s = parameters.get(Param.a.key); 360 if (s != null) { 361 double a = parseDouble(s, Param.a.key); 362 if (parameters.get(Param.es.key) != null) { 363 double es = parseDouble(parameters, Param.es.key); 364 return Ellipsoid.create_a_es(a, es); 365 } 366 if (parameters.get(Param.rf.key) != null) { 367 double rf = parseDouble(parameters, Param.rf.key); 368 return Ellipsoid.create_a_rf(a, rf); 369 } 370 if (parameters.get(Param.f.key) != null) { 371 double f = parseDouble(parameters, Param.f.key); 372 return Ellipsoid.create_a_f(a, f); 373 } 374 if (parameters.get(Param.b.key) != null) { 375 double b = parseDouble(parameters, Param.b.key); 376 return Ellipsoid.create_a_b(a, b); 377 } 378 } 379 if (parameters.containsKey(Param.a.key) || 380 parameters.containsKey(Param.es.key) || 381 parameters.containsKey(Param.rf.key) || 382 parameters.containsKey(Param.f.key) || 383 parameters.containsKey(Param.b.key)) 384 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported.")); 385 return null; 386 } 387 388 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException { 389 String datumId = parameters.get(Param.datum.key); 390 if (datumId != null) { 391 Datum datum = Projections.getDatum(datumId); 392 if (datum == null) throw new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId)); 393 return datum; 394 } 395 if (ellps == null) { 396 if (parameters.containsKey(Param.no_defs.key)) 397 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)")); 398 // nothing specified, use WGS84 as default 399 ellps = Ellipsoid.WGS84; 400 } 401 402 String nadgridsId = parameters.get(Param.nadgrids.key); 403 if (nadgridsId != null) { 404 if (nadgridsId.startsWith("@")) { 405 nadgridsId = nadgridsId.substring(1); 406 } 407 if ("null".equals(nadgridsId)) 408 return new NullDatum(null, ellps); 409 NTV2GridShiftFileWrapper nadgrids = Projections.getNTV2Grid(nadgridsId); 410 if (nadgrids == null) 411 throw new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", nadgridsId)); 412 return new NTV2Datum(nadgridsId, null, ellps, nadgrids); 413 } 414 415 String towgs84 = parameters.get(Param.towgs84.key); 416 if (towgs84 != null) 417 return parseToWGS84(towgs84, ellps); 418 419 return new NullDatum(null, ellps); 420 } 421 422 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException { 423 String[] numStr = paramList.split(","); 424 425 if (numStr.length != 3 && numStr.length != 7) 426 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)")); 427 List<Double> towgs84Param = new ArrayList<>(); 428 for (String str : numStr) { 429 try { 430 towgs84Param.add(Double.valueOf(str)); 431 } catch (NumberFormatException e) { 432 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e); 433 } 434 } 435 boolean isCentric = true; 436 for (Double param : towgs84Param) { 437 if (param != 0) { 438 isCentric = false; 439 break; 440 } 441 } 442 if (isCentric) 443 return new CentricDatum(null, null, ellps); 444 boolean is3Param = true; 445 for (int i = 3; i < towgs84Param.size(); i++) { 446 if (towgs84Param.get(i) != 0) { 447 is3Param = false; 448 break; 449 } 450 } 451 if (is3Param) 452 return new ThreeParameterDatum(null, null, ellps, 453 towgs84Param.get(0), 454 towgs84Param.get(1), 455 towgs84Param.get(2)); 456 else 457 return new SevenParameterDatum(null, null, ellps, 458 towgs84Param.get(0), 459 towgs84Param.get(1), 460 towgs84Param.get(2), 461 towgs84Param.get(3), 462 towgs84Param.get(4), 463 towgs84Param.get(5), 464 towgs84Param.get(6)); 465 } 466 467 public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException { 468 String id = parameters.get(Param.proj.key); 469 if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)")); 470 471 // "utm" is not a real projection, but a shortcut for a set of parameters 472 if ("utm".equals(id)) { 473 id = "tmerc"; 474 } 475 Proj proj = Projections.getBaseProjection(id); 476 if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id)); 477 478 ProjParameters projParams = new ProjParameters(); 479 480 projParams.ellps = ellps; 481 482 String s; 483 s = parameters.get(Param.lat_0.key); 484 if (s != null) { 485 projParams.lat0 = parseAngle(s, Param.lat_0.key); 486 } 487 s = parameters.get(Param.lat_1.key); 488 if (s != null) { 489 projParams.lat1 = parseAngle(s, Param.lat_1.key); 490 } 491 s = parameters.get(Param.lat_2.key); 492 if (s != null) { 493 projParams.lat2 = parseAngle(s, Param.lat_2.key); 494 } 495 proj.initialize(projParams); 496 return proj; 497 } 498 499 public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException { 500 String[] numStr = boundsStr.split(","); 501 if (numStr.length != 4) 502 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)")); 503 return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"), 504 parseAngle(numStr[0], "minlon (+bounds)"), 505 parseAngle(numStr[3], "maxlat (+bounds)"), 506 parseAngle(numStr[2], "maxlon (+bounds)"), false); 507 } 508 509 public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException { 510 if (!parameters.containsKey(parameterName)) 511 throw new ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName)); 512 String doubleStr = parameters.get(parameterName); 513 if (doubleStr == null) 514 throw new ProjectionConfigurationException( 515 tr("Expected number argument for parameter ''{0}''", parameterName)); 516 return parseDouble(doubleStr, parameterName); 517 } 518 519 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException { 520 try { 521 return Double.parseDouble(doubleStr); 522 } catch (NumberFormatException e) { 523 throw new ProjectionConfigurationException( 524 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e); 525 } 526 } 527 528 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException { 529 String s = angleStr; 530 double value = 0; 531 boolean neg = false; 532 Matcher m = Pattern.compile("^-").matcher(s); 533 if (m.find()) { 534 neg = true; 535 s = s.substring(m.end()); 536 } 537 final String FLOAT = "(\\d+(\\.\\d*)?)"; 538 boolean dms = false; 539 double deg = 0.0, min = 0.0, sec = 0.0; 540 // degrees 541 m = Pattern.compile("^"+FLOAT+"d").matcher(s); 542 if (m.find()) { 543 s = s.substring(m.end()); 544 deg = Double.parseDouble(m.group(1)); 545 dms = true; 546 } 547 // minutes 548 m = Pattern.compile("^"+FLOAT+"'").matcher(s); 549 if (m.find()) { 550 s = s.substring(m.end()); 551 min = Double.parseDouble(m.group(1)); 552 dms = true; 553 } 554 // seconds 555 m = Pattern.compile("^"+FLOAT+"\"").matcher(s); 556 if (m.find()) { 557 s = s.substring(m.end()); 558 sec = Double.parseDouble(m.group(1)); 559 dms = true; 560 } 561 // plain number (in degrees) 562 if (dms) { 563 value = deg + (min/60.0) + (sec/3600.0); 564 } else { 565 m = Pattern.compile("^"+FLOAT).matcher(s); 566 if (m.find()) { 567 s = s.substring(m.end()); 568 value += Double.parseDouble(m.group(1)); 569 } 570 } 571 m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s); 572 if (m.find()) { 573 s = s.substring(m.end()); 574 } else { 575 m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s); 576 if (m.find()) { 577 s = s.substring(m.end()); 578 neg = !neg; 579 } 580 } 581 if (neg) { 582 value = -value; 583 } 584 if (!s.isEmpty()) { 585 throw new ProjectionConfigurationException( 586 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr)); 587 } 588 return value; 589 } 590 591 @Override 592 public Integer getEpsgCode() { 593 if (code != null && code.startsWith("EPSG:")) { 594 try { 595 return Integer.valueOf(code.substring(5)); 596 } catch (NumberFormatException e) { 597 Main.warn(e); 598 } 599 } 600 return null; 601 } 602 603 @Override 604 public String toCode() { 605 return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref); 606 } 607 608 @Override 609 public String getCacheDirectoryName() { 610 return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4); 611 } 612 613 @Override 614 public Bounds getWorldBoundsLatLon() { 615 if (bounds != null) return bounds; 616 Bounds ab = proj.getAlgorithmBounds(); 617 if (ab != null) { 618 double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180); 619 double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180); 620 return new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false); 621 } else { 622 return new Bounds( 623 new LatLon(-90.0, -180.0), 624 new LatLon(90.0, 180.0)); 625 } 626 } 627 628 @Override 629 public String toString() { 630 return name != null ? name : tr("Custom Projection"); 631 } 632 633 @Override 634 public double getMetersPerUnit() { 635 return metersPerUnit; 636 } 637 638 @Override 639 public boolean switchXY() { 640 // TODO: support for other axis orientation such as West South, and Up Down 641 return this.axis.startsWith("ne"); 642 } 643 644 private static Map<String, Double> getUnitsToMeters() { 645 Map<String, Double> ret = new ConcurrentHashMap<>(); 646 ret.put("km", 1000d); 647 ret.put("m", 1d); 648 ret.put("dm", 1d/10); 649 ret.put("cm", 1d/100); 650 ret.put("mm", 1d/1000); 651 ret.put("kmi", 1852.0); 652 ret.put("in", 0.0254); 653 ret.put("ft", 0.3048); 654 ret.put("yd", 0.9144); 655 ret.put("mi", 1609.344); 656 ret.put("fathom", 1.8288); 657 ret.put("chain", 20.1168); 658 ret.put("link", 0.201168); 659 ret.put("us-in", 1d/39.37); 660 ret.put("us-ft", 0.304800609601219); 661 ret.put("us-yd", 0.914401828803658); 662 ret.put("us-ch", 20.11684023368047); 663 ret.put("us-mi", 1609.347218694437); 664 ret.put("ind-yd", 0.91439523); 665 ret.put("ind-ft", 0.30479841); 666 ret.put("ind-ch", 20.11669506); 667 ret.put("degree", METER_PER_UNIT_DEGREE); 668 return ret; 669 } 670 671 private static Map<String, Double> getPrimeMeridians() { 672 Map<String, Double> ret = new ConcurrentHashMap<>(); 673 try { 674 ret.put("greenwich", 0.0); 675 ret.put("lisbon", parseAngle("9d07'54.862\"W", null)); 676 ret.put("paris", parseAngle("2d20'14.025\"E", null)); 677 ret.put("bogota", parseAngle("74d04'51.3\"W", null)); 678 ret.put("madrid", parseAngle("3d41'16.58\"W", null)); 679 ret.put("rome", parseAngle("12d27'8.4\"E", null)); 680 ret.put("bern", parseAngle("7d26'22.5\"E", null)); 681 ret.put("jakarta", parseAngle("106d48'27.79\"E", null)); 682 ret.put("ferro", parseAngle("17d40'W", null)); 683 ret.put("brussels", parseAngle("4d22'4.71\"E", null)); 684 ret.put("stockholm", parseAngle("18d3'29.8\"E", null)); 685 ret.put("athens", parseAngle("23d42'58.815\"E", null)); 686 ret.put("oslo", parseAngle("10d43'22.5\"E", null)); 687 } catch (ProjectionConfigurationException ex) { 688 throw new RuntimeException(); 689 } 690 return ret; 691 } 692}