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.EnumMap; 008import java.util.HashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.concurrent.ConcurrentHashMap; 012import java.util.regex.Matcher; 013import java.util.regex.Pattern; 014 015import org.openstreetmap.josm.Main; 016import org.openstreetmap.josm.data.Bounds; 017import org.openstreetmap.josm.data.ProjectionBounds; 018import org.openstreetmap.josm.data.coor.EastNorth; 019import org.openstreetmap.josm.data.coor.LatLon; 020import org.openstreetmap.josm.data.projection.datum.CentricDatum; 021import org.openstreetmap.josm.data.projection.datum.Datum; 022import org.openstreetmap.josm.data.projection.datum.NTV2Datum; 023import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper; 024import org.openstreetmap.josm.data.projection.datum.NullDatum; 025import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum; 026import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum; 027import org.openstreetmap.josm.data.projection.datum.WGS84Datum; 028import org.openstreetmap.josm.data.projection.proj.ICentralMeridianProvider; 029import org.openstreetmap.josm.data.projection.proj.IScaleFactorProvider; 030import org.openstreetmap.josm.data.projection.proj.Mercator; 031import org.openstreetmap.josm.data.projection.proj.Proj; 032import org.openstreetmap.josm.data.projection.proj.ProjParameters; 033import org.openstreetmap.josm.tools.Utils; 034 035/** 036 * Custom projection. 037 * 038 * Inspired by PROJ.4 and Proj4J. 039 * @since 5072 040 */ 041public class CustomProjection extends AbstractProjection { 042 043 /* 044 * Equation for METER_PER_UNIT_DEGREE taken from: 045 * https://github.com/openlayers/ol3/blob/master/src/ol/proj/epsg4326projection.js#L58 046 * Value for Radius taken form: 047 * https://github.com/openlayers/ol3/blob/master/src/ol/sphere/wgs84sphere.js#L11 048 */ 049 private static final double METER_PER_UNIT_DEGREE = 2 * Math.PI * 6378137.0 / 360; 050 private static final Map<String, Double> UNITS_TO_METERS = getUnitsToMeters(); 051 private static final Map<String, Double> PRIME_MERIDANS = getPrimeMeridians(); 052 053 /** 054 * pref String that defines the projection 055 * 056 * null means fall back mode (Mercator) 057 */ 058 protected String pref; 059 protected String name; 060 protected String code; 061 protected String cacheDir; 062 protected Bounds bounds; 063 private double metersPerUnitWMTS; 064 private String axis = "enu"; // default axis orientation is East, North, Up 065 066 /** 067 * Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>. 068 * @since 7370 (public) 069 */ 070 public enum Param { 071 072 /** False easting */ 073 x_0("x_0", true), 074 /** False northing */ 075 y_0("y_0", true), 076 /** Central meridian */ 077 lon_0("lon_0", true), 078 /** Prime meridian */ 079 pm("pm", true), 080 /** Scaling factor */ 081 k_0("k_0", true), 082 /** Ellipsoid name (see {@code proj -le}) */ 083 ellps("ellps", true), 084 /** Semimajor radius of the ellipsoid axis */ 085 a("a", true), 086 /** Eccentricity of the ellipsoid squared */ 087 es("es", true), 088 /** Reciprocal of the ellipsoid flattening term (e.g. 298) */ 089 rf("rf", true), 090 /** Flattening of the ellipsoid = 1-sqrt(1-e^2) */ 091 f("f", true), 092 /** Semiminor radius of the ellipsoid axis */ 093 b("b", true), 094 /** Datum name (see {@code proj -ld}) */ 095 datum("datum", true), 096 /** 3 or 7 term datum transform parameters */ 097 towgs84("towgs84", true), 098 /** Filename of NTv2 grid file to use for datum transforms */ 099 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 Main.trace(ex); 208 try { 209 update(null); 210 } catch (ProjectionConfigurationException ex1) { 211 throw new RuntimeException(ex1); 212 } 213 } 214 } 215 216 /** 217 * Updates this {@code CustomProjection} with given parameters. 218 * @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90") 219 * @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly 220 */ 221 public final void update(String pref) throws ProjectionConfigurationException { 222 this.pref = pref; 223 if (pref == null) { 224 ellps = Ellipsoid.WGS84; 225 datum = WGS84Datum.INSTANCE; 226 proj = new Mercator(); 227 bounds = new Bounds( 228 -85.05112877980659, -180.0, 229 85.05112877980659, 180.0, true); 230 } else { 231 Map<String, String> parameters = parseParameterList(pref, false); 232 parameters = resolveInits(parameters, false); 233 ellps = parseEllipsoid(parameters); 234 datum = parseDatum(parameters, ellps); 235 if (ellps == null) { 236 ellps = datum.getEllipsoid(); 237 } 238 proj = parseProjection(parameters, ellps); 239 // "utm" is a shortcut for a set of parameters 240 if ("utm".equals(parameters.get(Param.proj.key))) { 241 String zoneStr = parameters.get(Param.zone.key); 242 Integer zone; 243 if (zoneStr == null) 244 throw new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter.")); 245 try { 246 zone = Integer.valueOf(zoneStr); 247 } catch (NumberFormatException e) { 248 zone = null; 249 } 250 if (zone == null || zone < 1 || zone > 60) 251 throw new ProjectionConfigurationException(tr("Expected integer value in range 1-60 for ''+zone=...'' parameter.")); 252 this.lon0 = 6d * zone - 183d; 253 this.k0 = 0.9996; 254 this.x0 = 500000; 255 this.y0 = parameters.containsKey(Param.south.key) ? 10000000 : 0; 256 } 257 String s = parameters.get(Param.x_0.key); 258 if (s != null) { 259 this.x0 = parseDouble(s, Param.x_0.key); 260 } 261 s = parameters.get(Param.y_0.key); 262 if (s != null) { 263 this.y0 = parseDouble(s, Param.y_0.key); 264 } 265 s = parameters.get(Param.lon_0.key); 266 if (s != null) { 267 this.lon0 = parseAngle(s, Param.lon_0.key); 268 } 269 if (proj instanceof ICentralMeridianProvider) { 270 this.lon0 = ((ICentralMeridianProvider) proj).getCentralMeridian(); 271 } 272 s = parameters.get(Param.pm.key); 273 if (s != null) { 274 if (PRIME_MERIDANS.containsKey(s)) { 275 this.pm = PRIME_MERIDANS.get(s); 276 } else { 277 this.pm = parseAngle(s, Param.pm.key); 278 } 279 } 280 s = parameters.get(Param.k_0.key); 281 if (s != null) { 282 this.k0 = parseDouble(s, Param.k_0.key); 283 } 284 if (proj instanceof IScaleFactorProvider) { 285 this.k0 *= ((IScaleFactorProvider) proj).getScaleFactor(); 286 } 287 s = parameters.get(Param.bounds.key); 288 if (s != null) { 289 this.bounds = parseBounds(s); 290 } 291 s = parameters.get(Param.wmssrs.key); 292 if (s != null) { 293 this.code = s; 294 } 295 boolean defaultUnits = true; 296 s = parameters.get(Param.units.key); 297 if (s != null) { 298 s = Utils.strip(s, "\""); 299 if (UNITS_TO_METERS.containsKey(s)) { 300 this.toMeter = UNITS_TO_METERS.get(s); 301 this.metersPerUnitWMTS = this.toMeter; 302 defaultUnits = false; 303 } else { 304 throw new ProjectionConfigurationException(tr("No unit found for: {0}", s)); 305 } 306 } 307 s = parameters.get(Param.to_meter.key); 308 if (s != null) { 309 this.toMeter = parseDouble(s, Param.to_meter.key); 310 this.metersPerUnitWMTS = this.toMeter; 311 defaultUnits = false; 312 } 313 if (defaultUnits) { 314 this.toMeter = 1; 315 this.metersPerUnitWMTS = proj.isGeographic() ? METER_PER_UNIT_DEGREE : 1; 316 } 317 s = parameters.get(Param.axis.key); 318 if (s != null) { 319 this.axis = s; 320 } 321 } 322 } 323 324 /** 325 * Parse a parameter list to key=value pairs. 326 * 327 * @param pref the parameter list 328 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception 329 * @return parameters map 330 * @throws ProjectionConfigurationException in case of invalid parameter 331 */ 332 public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException { 333 Map<String, String> parameters = new HashMap<>(); 334 String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim()); 335 if (pref.trim().isEmpty()) { 336 parts = new String[0]; 337 } 338 for (String part : parts) { 339 if (part.isEmpty() || part.charAt(0) != '+') 340 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part)); 341 Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part); 342 if (m.matches()) { 343 String key = m.group(1); 344 // alias 345 if ("k".equals(key)) { 346 key = Param.k_0.key; 347 } 348 String value = null; 349 if (m.groupCount() >= 3) { 350 value = m.group(3); 351 // some aliases 352 if (key.equals(Param.proj.key)) { 353 if ("longlat".equals(value) || "latlon".equals(value) || "latlong".equals(value)) { 354 value = "lonlat"; 355 } 356 } 357 } 358 if (!Param.paramsByKey.containsKey(key)) { 359 if (!ignoreUnknownParameter) 360 throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key)); 361 } else { 362 if (Param.paramsByKey.get(key).hasValue && value == null) 363 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key)); 364 if (!Param.paramsByKey.get(key).hasValue && value != null) 365 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key)); 366 } 367 parameters.put(key, value); 368 } else 369 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part)); 370 } 371 return parameters; 372 } 373 374 /** 375 * Recursive resolution of +init includes. 376 * 377 * @param parameters parameters map 378 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception 379 * @return parameters map with +init includes resolved 380 * @throws ProjectionConfigurationException in case of invalid parameter 381 */ 382 public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter) 383 throws ProjectionConfigurationException { 384 // recursive resolution of +init includes 385 String initKey = parameters.get(Param.init.key); 386 if (initKey != null) { 387 String init = Projections.getInit(initKey); 388 if (init == null) 389 throw new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey)); 390 Map<String, String> initp; 391 try { 392 initp = parseParameterList(init, ignoreUnknownParameter); 393 initp = resolveInits(initp, ignoreUnknownParameter); 394 } catch (ProjectionConfigurationException ex) { 395 throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex); 396 } 397 initp.putAll(parameters); 398 return initp; 399 } 400 return parameters; 401 } 402 403 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException { 404 String code = parameters.get(Param.ellps.key); 405 if (code != null) { 406 Ellipsoid ellipsoid = Projections.getEllipsoid(code); 407 if (ellipsoid == null) { 408 throw new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code)); 409 } else { 410 return ellipsoid; 411 } 412 } 413 String s = parameters.get(Param.a.key); 414 if (s != null) { 415 double a = parseDouble(s, Param.a.key); 416 if (parameters.get(Param.es.key) != null) { 417 double es = parseDouble(parameters, Param.es.key); 418 return Ellipsoid.create_a_es(a, es); 419 } 420 if (parameters.get(Param.rf.key) != null) { 421 double rf = parseDouble(parameters, Param.rf.key); 422 return Ellipsoid.create_a_rf(a, rf); 423 } 424 if (parameters.get(Param.f.key) != null) { 425 double f = parseDouble(parameters, Param.f.key); 426 return Ellipsoid.create_a_f(a, f); 427 } 428 if (parameters.get(Param.b.key) != null) { 429 double b = parseDouble(parameters, Param.b.key); 430 return Ellipsoid.create_a_b(a, b); 431 } 432 } 433 if (parameters.containsKey(Param.a.key) || 434 parameters.containsKey(Param.es.key) || 435 parameters.containsKey(Param.rf.key) || 436 parameters.containsKey(Param.f.key) || 437 parameters.containsKey(Param.b.key)) 438 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported.")); 439 return null; 440 } 441 442 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException { 443 String datumId = parameters.get(Param.datum.key); 444 if (datumId != null) { 445 Datum datum = Projections.getDatum(datumId); 446 if (datum == null) throw new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId)); 447 return datum; 448 } 449 if (ellps == null) { 450 if (parameters.containsKey(Param.no_defs.key)) 451 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)")); 452 // nothing specified, use WGS84 as default 453 ellps = Ellipsoid.WGS84; 454 } 455 456 String nadgridsId = parameters.get(Param.nadgrids.key); 457 if (nadgridsId != null) { 458 if (nadgridsId.startsWith("@")) { 459 nadgridsId = nadgridsId.substring(1); 460 } 461 if ("null".equals(nadgridsId)) 462 return new NullDatum(null, ellps); 463 NTV2GridShiftFileWrapper nadgrids = Projections.getNTV2Grid(nadgridsId); 464 if (nadgrids == null) 465 throw new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", nadgridsId)); 466 return new NTV2Datum(nadgridsId, null, ellps, nadgrids); 467 } 468 469 String towgs84 = parameters.get(Param.towgs84.key); 470 if (towgs84 != null) 471 return parseToWGS84(towgs84, ellps); 472 473 return new NullDatum(null, ellps); 474 } 475 476 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException { 477 String[] numStr = paramList.split(","); 478 479 if (numStr.length != 3 && numStr.length != 7) 480 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)")); 481 List<Double> towgs84Param = new ArrayList<>(); 482 for (String str : numStr) { 483 try { 484 towgs84Param.add(Double.valueOf(str)); 485 } catch (NumberFormatException e) { 486 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e); 487 } 488 } 489 boolean isCentric = true; 490 for (Double param : towgs84Param) { 491 if (param != 0) { 492 isCentric = false; 493 break; 494 } 495 } 496 if (isCentric) 497 return new CentricDatum(null, null, ellps); 498 boolean is3Param = true; 499 for (int i = 3; i < towgs84Param.size(); i++) { 500 if (towgs84Param.get(i) != 0) { 501 is3Param = false; 502 break; 503 } 504 } 505 if (is3Param) 506 return new ThreeParameterDatum(null, null, ellps, 507 towgs84Param.get(0), 508 towgs84Param.get(1), 509 towgs84Param.get(2)); 510 else 511 return new SevenParameterDatum(null, null, ellps, 512 towgs84Param.get(0), 513 towgs84Param.get(1), 514 towgs84Param.get(2), 515 towgs84Param.get(3), 516 towgs84Param.get(4), 517 towgs84Param.get(5), 518 towgs84Param.get(6)); 519 } 520 521 public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException { 522 String id = parameters.get(Param.proj.key); 523 if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)")); 524 525 // "utm" is not a real projection, but a shortcut for a set of parameters 526 if ("utm".equals(id)) { 527 id = "tmerc"; 528 } 529 Proj proj = Projections.getBaseProjection(id); 530 if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id)); 531 532 ProjParameters projParams = new ProjParameters(); 533 534 projParams.ellps = ellps; 535 536 String s; 537 s = parameters.get(Param.lat_0.key); 538 if (s != null) { 539 projParams.lat0 = parseAngle(s, Param.lat_0.key); 540 } 541 s = parameters.get(Param.lat_1.key); 542 if (s != null) { 543 projParams.lat1 = parseAngle(s, Param.lat_1.key); 544 } 545 s = parameters.get(Param.lat_2.key); 546 if (s != null) { 547 projParams.lat2 = parseAngle(s, Param.lat_2.key); 548 } 549 s = parameters.get(Param.lat_ts.key); 550 if (s != null) { 551 projParams.lat_ts = parseAngle(s, Param.lat_ts.key); 552 } 553 s = parameters.get(Param.lonc.key); 554 if (s != null) { 555 projParams.lonc = parseAngle(s, Param.lonc.key); 556 } 557 s = parameters.get(Param.alpha.key); 558 if (s != null) { 559 projParams.alpha = parseAngle(s, Param.alpha.key); 560 } 561 s = parameters.get(Param.gamma.key); 562 if (s != null) { 563 projParams.gamma = parseAngle(s, Param.gamma.key); 564 } 565 s = parameters.get(Param.lon_1.key); 566 if (s != null) { 567 projParams.lon1 = parseAngle(s, Param.lon_1.key); 568 } 569 s = parameters.get(Param.lon_2.key); 570 if (s != null) { 571 projParams.lon2 = parseAngle(s, Param.lon_2.key); 572 } 573 if (parameters.containsKey(Param.no_off.key) || parameters.containsKey(Param.no_uoff.key)) { 574 projParams.no_off = Boolean.TRUE; 575 } 576 proj.initialize(projParams); 577 return proj; 578 } 579 580 public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException { 581 String[] numStr = boundsStr.split(","); 582 if (numStr.length != 4) 583 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)")); 584 return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"), 585 parseAngle(numStr[0], "minlon (+bounds)"), 586 parseAngle(numStr[3], "maxlat (+bounds)"), 587 parseAngle(numStr[2], "maxlon (+bounds)"), false); 588 } 589 590 public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException { 591 if (!parameters.containsKey(parameterName)) 592 throw new ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName)); 593 String doubleStr = parameters.get(parameterName); 594 if (doubleStr == null) 595 throw new ProjectionConfigurationException( 596 tr("Expected number argument for parameter ''{0}''", parameterName)); 597 return parseDouble(doubleStr, parameterName); 598 } 599 600 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException { 601 try { 602 return Double.parseDouble(doubleStr); 603 } catch (NumberFormatException e) { 604 throw new ProjectionConfigurationException( 605 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e); 606 } 607 } 608 609 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException { 610 String s = angleStr; 611 double value = 0; 612 boolean neg = false; 613 Matcher m = Pattern.compile("^-").matcher(s); 614 if (m.find()) { 615 neg = true; 616 s = s.substring(m.end()); 617 } 618 final String floatPattern = "(\\d+(\\.\\d*)?)"; 619 boolean dms = false; 620 double deg = 0.0, min = 0.0, sec = 0.0; 621 // degrees 622 m = Pattern.compile("^"+floatPattern+"d").matcher(s); 623 if (m.find()) { 624 s = s.substring(m.end()); 625 deg = Double.parseDouble(m.group(1)); 626 dms = true; 627 } 628 // minutes 629 m = Pattern.compile("^"+floatPattern+"'").matcher(s); 630 if (m.find()) { 631 s = s.substring(m.end()); 632 min = Double.parseDouble(m.group(1)); 633 dms = true; 634 } 635 // seconds 636 m = Pattern.compile("^"+floatPattern+"\"").matcher(s); 637 if (m.find()) { 638 s = s.substring(m.end()); 639 sec = Double.parseDouble(m.group(1)); 640 dms = true; 641 } 642 // plain number (in degrees) 643 if (dms) { 644 value = deg + (min/60.0) + (sec/3600.0); 645 } else { 646 m = Pattern.compile("^"+floatPattern).matcher(s); 647 if (m.find()) { 648 s = s.substring(m.end()); 649 value += Double.parseDouble(m.group(1)); 650 } 651 } 652 m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s); 653 if (m.find()) { 654 s = s.substring(m.end()); 655 } else { 656 m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s); 657 if (m.find()) { 658 s = s.substring(m.end()); 659 neg = !neg; 660 } 661 } 662 if (neg) { 663 value = -value; 664 } 665 if (!s.isEmpty()) { 666 throw new ProjectionConfigurationException( 667 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr)); 668 } 669 return value; 670 } 671 672 @Override 673 public Integer getEpsgCode() { 674 if (code != null && code.startsWith("EPSG:")) { 675 try { 676 return Integer.valueOf(code.substring(5)); 677 } catch (NumberFormatException e) { 678 Main.warn(e); 679 } 680 } 681 return null; 682 } 683 684 @Override 685 public String toCode() { 686 return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref); 687 } 688 689 @Override 690 public String getCacheDirectoryName() { 691 return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4); 692 } 693 694 @Override 695 public Bounds getWorldBoundsLatLon() { 696 if (bounds != null) return bounds; 697 Bounds ab = proj.getAlgorithmBounds(); 698 if (ab != null) { 699 double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180); 700 double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180); 701 return new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false); 702 } else { 703 return new Bounds( 704 new LatLon(-90.0, -180.0), 705 new LatLon(90.0, 180.0)); 706 } 707 } 708 709 @Override 710 public String toString() { 711 return name != null ? name : tr("Custom Projection"); 712 } 713 714 /** 715 * Factor to convert units of east/north coordinates to meters. 716 * 717 * When east/north coordinates are in degrees (geographic CRS), the scale 718 * at the equator is taken, i.e. 360 degrees corresponds to the length of 719 * the equator in meters. 720 * 721 * @return factor to convert units to meter 722 */ 723 @Override 724 public double getMetersPerUnit() { 725 return metersPerUnitWMTS; 726 } 727 728 @Override 729 public boolean switchXY() { 730 // TODO: support for other axis orientation such as West South, and Up Down 731 return this.axis.startsWith("ne"); 732 } 733 734 private static Map<String, Double> getUnitsToMeters() { 735 Map<String, Double> ret = new ConcurrentHashMap<>(); 736 ret.put("km", 1000d); 737 ret.put("m", 1d); 738 ret.put("dm", 1d/10); 739 ret.put("cm", 1d/100); 740 ret.put("mm", 1d/1000); 741 ret.put("kmi", 1852.0); 742 ret.put("in", 0.0254); 743 ret.put("ft", 0.3048); 744 ret.put("yd", 0.9144); 745 ret.put("mi", 1609.344); 746 ret.put("fathom", 1.8288); 747 ret.put("chain", 20.1168); 748 ret.put("link", 0.201168); 749 ret.put("us-in", 1d/39.37); 750 ret.put("us-ft", 0.304800609601219); 751 ret.put("us-yd", 0.914401828803658); 752 ret.put("us-ch", 20.11684023368047); 753 ret.put("us-mi", 1609.347218694437); 754 ret.put("ind-yd", 0.91439523); 755 ret.put("ind-ft", 0.30479841); 756 ret.put("ind-ch", 20.11669506); 757 ret.put("degree", METER_PER_UNIT_DEGREE); 758 return ret; 759 } 760 761 private static Map<String, Double> getPrimeMeridians() { 762 Map<String, Double> ret = new ConcurrentHashMap<>(); 763 try { 764 ret.put("greenwich", 0.0); 765 ret.put("lisbon", parseAngle("9d07'54.862\"W", null)); 766 ret.put("paris", parseAngle("2d20'14.025\"E", null)); 767 ret.put("bogota", parseAngle("74d04'51.3\"W", null)); 768 ret.put("madrid", parseAngle("3d41'16.58\"W", null)); 769 ret.put("rome", parseAngle("12d27'8.4\"E", null)); 770 ret.put("bern", parseAngle("7d26'22.5\"E", null)); 771 ret.put("jakarta", parseAngle("106d48'27.79\"E", null)); 772 ret.put("ferro", parseAngle("17d40'W", null)); 773 ret.put("brussels", parseAngle("4d22'4.71\"E", null)); 774 ret.put("stockholm", parseAngle("18d3'29.8\"E", null)); 775 ret.put("athens", parseAngle("23d42'58.815\"E", null)); 776 ret.put("oslo", parseAngle("10d43'22.5\"E", null)); 777 } catch (ProjectionConfigurationException ex) { 778 throw new IllegalStateException(ex); 779 } 780 return ret; 781 } 782 783 private static EastNorth getPointAlong(int i, int n, ProjectionBounds r) { 784 double dEast = (r.maxEast - r.minEast) / n; 785 double dNorth = (r.maxNorth - r.minNorth) / n; 786 if (i < n) { 787 return new EastNorth(r.minEast + i * dEast, r.minNorth); 788 } else if (i < 2*n) { 789 i -= n; 790 return new EastNorth(r.maxEast, r.minNorth + i * dNorth); 791 } else if (i < 3*n) { 792 i -= 2*n; 793 return new EastNorth(r.maxEast - i * dEast, r.maxNorth); 794 } else if (i < 4*n) { 795 i -= 3*n; 796 return new EastNorth(r.minEast, r.maxNorth - i * dNorth); 797 } else { 798 throw new AssertionError(); 799 } 800 } 801 802 private EastNorth getPole(Polarity whichPole) { 803 if (polesEN == null) { 804 polesEN = new EnumMap<>(Polarity.class); 805 for (Polarity p : Polarity.values()) { 806 polesEN.put(p, null); 807 LatLon ll = polesLL.get(p); 808 try { 809 EastNorth enPole = latlon2eastNorth(ll); 810 if (enPole.isValid()) { 811 // project back and check if the result is somewhat reasonable 812 LatLon llBack = eastNorth2latlon(enPole); 813 if (llBack.isValid() && ll.greatCircleDistance(llBack) < 1000) { 814 polesEN.put(p, enPole); 815 } 816 } 817 } catch (RuntimeException e) { 818 Main.error(e); 819 } 820 } 821 } 822 return polesEN.get(whichPole); 823 } 824 825 @Override 826 public Bounds getLatLonBoundsBox(ProjectionBounds r) { 827 final int n = 10; 828 Bounds result = new Bounds(eastNorth2latlon(r.getMin())); 829 result.extend(eastNorth2latlon(r.getMax())); 830 LatLon llPrev = null; 831 for (int i = 0; i < 4*n; i++) { 832 LatLon llNow = eastNorth2latlon(getPointAlong(i, n, r)); 833 result.extend(llNow); 834 // check if segment crosses 180th meridian and if so, make sure 835 // to extend bounds to +/-180 degrees longitude 836 if (llPrev != null) { 837 double lon1 = llPrev.lon(); 838 double lon2 = llNow.lon(); 839 if (90 < lon1 && lon1 < 180 && -180 < lon2 && lon2 < -90) { 840 result.extend(new LatLon(llPrev.lat(), 180)); 841 result.extend(new LatLon(llNow.lat(), -180)); 842 } 843 if (90 < lon2 && lon2 < 180 && -180 < lon1 && lon1 < -90) { 844 result.extend(new LatLon(llNow.lat(), 180)); 845 result.extend(new LatLon(llPrev.lat(), -180)); 846 } 847 } 848 llPrev = llNow; 849 } 850 // if the box contains one of the poles, the above method did not get 851 // correct min/max latitude value 852 for (Polarity p : Polarity.values()) { 853 EastNorth pole = getPole(p); 854 if (pole != null && r.contains(pole)) { 855 result.extend(polesLL.get(p)); 856 } 857 } 858 return result; 859 } 860}