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