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