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 this.bounds = s != null ? parseBounds(s) : null; 299 s = parameters.get(Param.wmssrs.key); 300 if (s != null) { 301 this.code = s; 302 } 303 boolean defaultUnits = true; 304 s = parameters.get(Param.units.key); 305 if (s != null) { 306 s = Utils.strip(s, "\""); 307 if (UNITS_TO_METERS.containsKey(s)) { 308 this.toMeter = UNITS_TO_METERS.get(s); 309 this.metersPerUnitWMTS = this.toMeter; 310 defaultUnits = false; 311 } else { 312 throw new ProjectionConfigurationException(tr("No unit found for: {0}", s)); 313 } 314 } 315 s = parameters.get(Param.to_meter.key); 316 if (s != null) { 317 this.toMeter = parseDouble(s, Param.to_meter.key); 318 this.metersPerUnitWMTS = this.toMeter; 319 defaultUnits = false; 320 } 321 if (defaultUnits) { 322 this.toMeter = 1; 323 this.metersPerUnitWMTS = proj.isGeographic() ? METER_PER_UNIT_DEGREE : 1; 324 } 325 s = parameters.get(Param.axis.key); 326 if (s != null) { 327 this.axis = s; 328 } 329 } 330 } 331 332 /** 333 * Parse a parameter list to key=value pairs. 334 * 335 * @param pref the parameter list 336 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception 337 * @return parameters map 338 * @throws ProjectionConfigurationException in case of invalid parameter 339 */ 340 public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException { 341 Map<String, String> parameters = new HashMap<>(); 342 String trimmedPref = pref.trim(); 343 if (trimmedPref.isEmpty()) { 344 return parameters; 345 } 346 347 Pattern keyPattern = Pattern.compile("\\+(?<key>[a-zA-Z0-9_]+)(=(?<value>.*))?"); 348 String[] parts = Utils.WHITE_SPACES_PATTERN.split(trimmedPref); 349 for (String part : parts) { 350 Matcher m = keyPattern.matcher(part); 351 if (m.matches()) { 352 String key = m.group("key"); 353 String value = m.group("value"); 354 // some aliases 355 if (key.equals(Param.proj.key) && LON_LAT_VALUES.contains(value)) { 356 value = "lonlat"; 357 } 358 Param param = Param.paramsByKey.get(key); 359 if (param == null) { 360 if (!ignoreUnknownParameter) 361 throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key)); 362 } else { 363 if (param.hasValue && value == null) 364 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key)); 365 if (!param.hasValue && value != null) 366 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key)); 367 key = param.key; // To be really sure, we might have an alias. 368 } 369 parameters.put(key, value); 370 } else if (!part.startsWith("+")) { 371 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part)); 372 } else { 373 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part)); 374 } 375 } 376 return parameters; 377 } 378 379 /** 380 * Recursive resolution of +init includes. 381 * 382 * @param parameters parameters map 383 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception 384 * @return parameters map with +init includes resolved 385 * @throws ProjectionConfigurationException in case of invalid parameter 386 */ 387 public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter) 388 throws ProjectionConfigurationException { 389 // recursive resolution of +init includes 390 String initKey = parameters.get(Param.init.key); 391 if (initKey != null) { 392 Map<String, String> initp; 393 try { 394 initp = parseParameterList(Optional.ofNullable(Projections.getInit(initKey)).orElseThrow( 395 () -> new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey))), 396 ignoreUnknownParameter); 397 initp = resolveInits(initp, ignoreUnknownParameter); 398 } catch (ProjectionConfigurationException ex) { 399 throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex); 400 } 401 initp.putAll(parameters); 402 return initp; 403 } 404 return parameters; 405 } 406 407 /** 408 * Gets the ellipsoid 409 * @param parameters The parameters to get the value from 410 * @return The Ellipsoid as specified with the parameters 411 * @throws ProjectionConfigurationException in case of invalid parameters 412 */ 413 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException { 414 String code = parameters.get(Param.ellps.key); 415 if (code != null) { 416 return Optional.ofNullable(Projections.getEllipsoid(code)).orElseThrow( 417 () -> new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code))); 418 } 419 String s = parameters.get(Param.a.key); 420 if (s != null) { 421 double a = parseDouble(s, Param.a.key); 422 if (parameters.get(Param.es.key) != null) { 423 double es = parseDouble(parameters, Param.es.key); 424 return Ellipsoid.createAes(a, es); 425 } 426 if (parameters.get(Param.rf.key) != null) { 427 double rf = parseDouble(parameters, Param.rf.key); 428 return Ellipsoid.createArf(a, rf); 429 } 430 if (parameters.get(Param.f.key) != null) { 431 double f = parseDouble(parameters, Param.f.key); 432 return Ellipsoid.createAf(a, f); 433 } 434 if (parameters.get(Param.b.key) != null) { 435 double b = parseDouble(parameters, Param.b.key); 436 return Ellipsoid.createAb(a, b); 437 } 438 } 439 if (parameters.containsKey(Param.a.key) || 440 parameters.containsKey(Param.es.key) || 441 parameters.containsKey(Param.rf.key) || 442 parameters.containsKey(Param.f.key) || 443 parameters.containsKey(Param.b.key)) 444 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported.")); 445 return null; 446 } 447 448 /** 449 * Gets the datum 450 * @param parameters The parameters to get the value from 451 * @param ellps The ellisoid that was previously computed 452 * @return The Datum as specified with the parameters 453 * @throws ProjectionConfigurationException in case of invalid parameters 454 */ 455 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException { 456 Datum result = null; 457 String datumId = parameters.get(Param.datum.key); 458 if (datumId != null) { 459 result = Optional.ofNullable(Projections.getDatum(datumId)).orElseThrow( 460 () -> new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId))); 461 } 462 if (ellps == null) { 463 if (result == null && parameters.containsKey(Param.no_defs.key)) 464 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)")); 465 // nothing specified, use WGS84 as default 466 ellps = result != null ? result.getEllipsoid() : Ellipsoid.WGS84; 467 } 468 469 String nadgridsId = parameters.get(Param.nadgrids.key); 470 if (nadgridsId != null) { 471 if (nadgridsId.startsWith("@")) { 472 nadgridsId = nadgridsId.substring(1); 473 } 474 if ("null".equals(nadgridsId)) 475 return new NullDatum(null, ellps); 476 final String fNadgridsId = nadgridsId; 477 return new NTV2Datum(fNadgridsId, null, ellps, Optional.ofNullable(Projections.getNTV2Grid(fNadgridsId)).orElseThrow( 478 () -> new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", fNadgridsId)))); 479 } 480 481 String towgs84 = parameters.get(Param.towgs84.key); 482 if (towgs84 != null) { 483 Datum towgs84Datum = parseToWGS84(towgs84, ellps); 484 if (result == null || towgs84Datum instanceof ThreeParameterDatum || towgs84Datum instanceof SevenParameterDatum) { 485 // +datum has priority over +towgs84=0,0,0[,0,0,0,0] 486 return towgs84Datum; 487 } 488 } 489 490 return result != null ? result : new NullDatum(null, ellps); 491 } 492 493 /** 494 * Parse {@code towgs84} parameter. 495 * @param paramList List of parameter arguments (expected: 3 or 7) 496 * @param ellps ellipsoid 497 * @return parsed datum ({@link ThreeParameterDatum} or {@link SevenParameterDatum}) 498 * @throws ProjectionConfigurationException if the arguments cannot be parsed 499 */ 500 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException { 501 String[] numStr = paramList.split(","); 502 503 if (numStr.length != 3 && numStr.length != 7) 504 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)")); 505 List<Double> towgs84Param = new ArrayList<>(); 506 for (String str : numStr) { 507 try { 508 towgs84Param.add(Double.valueOf(str)); 509 } catch (NumberFormatException e) { 510 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e); 511 } 512 } 513 boolean isCentric = true; 514 for (Double param : towgs84Param) { 515 if (param != 0) { 516 isCentric = false; 517 break; 518 } 519 } 520 if (isCentric) 521 return Ellipsoid.WGS84.equals(ellps) ? WGS84Datum.INSTANCE : new CentricDatum(null, null, ellps); 522 boolean is3Param = true; 523 for (int i = 3; i < towgs84Param.size(); i++) { 524 if (towgs84Param.get(i) != 0) { 525 is3Param = false; 526 break; 527 } 528 } 529 if (is3Param) 530 return new ThreeParameterDatum(null, null, ellps, 531 towgs84Param.get(0), 532 towgs84Param.get(1), 533 towgs84Param.get(2)); 534 else 535 return new SevenParameterDatum(null, null, ellps, 536 towgs84Param.get(0), 537 towgs84Param.get(1), 538 towgs84Param.get(2), 539 towgs84Param.get(3), 540 towgs84Param.get(4), 541 towgs84Param.get(5), 542 towgs84Param.get(6)); 543 } 544 545 /** 546 * Gets a projection using the given ellipsoid 547 * @param parameters Additional parameters 548 * @param ellps The {@link Ellipsoid} 549 * @return The projection 550 * @throws ProjectionConfigurationException in case of invalid parameters 551 */ 552 public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException { 553 String id = parameters.get(Param.proj.key); 554 if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)")); 555 556 // "utm" is not a real projection, but a shortcut for a set of parameters 557 if ("utm".equals(id)) { 558 id = "tmerc"; 559 } 560 Proj proj = Projections.getBaseProjection(id); 561 if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id)); 562 563 ProjParameters projParams = new ProjParameters(); 564 565 projParams.ellps = ellps; 566 567 String s; 568 s = parameters.get(Param.lat_0.key); 569 if (s != null) { 570 projParams.lat0 = parseAngle(s, Param.lat_0.key); 571 } 572 s = parameters.get(Param.lat_1.key); 573 if (s != null) { 574 projParams.lat1 = parseAngle(s, Param.lat_1.key); 575 } 576 s = parameters.get(Param.lat_2.key); 577 if (s != null) { 578 projParams.lat2 = parseAngle(s, Param.lat_2.key); 579 } 580 s = parameters.get(Param.lat_ts.key); 581 if (s != null) { 582 projParams.lat_ts = parseAngle(s, Param.lat_ts.key); 583 } 584 s = parameters.get(Param.lonc.key); 585 if (s != null) { 586 projParams.lonc = parseAngle(s, Param.lonc.key); 587 } 588 s = parameters.get(Param.alpha.key); 589 if (s != null) { 590 projParams.alpha = parseAngle(s, Param.alpha.key); 591 } 592 s = parameters.get(Param.gamma.key); 593 if (s != null) { 594 projParams.gamma = parseAngle(s, Param.gamma.key); 595 } 596 s = parameters.get(Param.lon_0.key); 597 if (s != null) { 598 projParams.lon0 = parseAngle(s, Param.lon_0.key); 599 } 600 s = parameters.get(Param.lon_1.key); 601 if (s != null) { 602 projParams.lon1 = parseAngle(s, Param.lon_1.key); 603 } 604 s = parameters.get(Param.lon_2.key); 605 if (s != null) { 606 projParams.lon2 = parseAngle(s, Param.lon_2.key); 607 } 608 if (parameters.containsKey(Param.no_off.key) || parameters.containsKey(Param.no_uoff.key)) { 609 projParams.no_off = Boolean.TRUE; 610 } 611 proj.initialize(projParams); 612 return proj; 613 } 614 615 /** 616 * Converts a string to a bounds object 617 * @param boundsStr The string as comma separated list of angles. 618 * @return The bounds. 619 * @throws ProjectionConfigurationException in case of invalid parameter 620 * @see CustomProjection#parseAngle(String, String) 621 */ 622 public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException { 623 String[] numStr = boundsStr.split(","); 624 if (numStr.length != 4) 625 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)")); 626 return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"), 627 parseAngle(numStr[0], "minlon (+bounds)"), 628 parseAngle(numStr[3], "maxlat (+bounds)"), 629 parseAngle(numStr[2], "maxlon (+bounds)"), false); 630 } 631 632 public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException { 633 if (!parameters.containsKey(parameterName)) 634 throw new ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName)); 635 return parseDouble(Optional.ofNullable(parameters.get(parameterName)).orElseThrow( 636 () -> new ProjectionConfigurationException(tr("Expected number argument for parameter ''{0}''", parameterName))), 637 parameterName); 638 } 639 640 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException { 641 try { 642 return Double.parseDouble(doubleStr); 643 } catch (NumberFormatException e) { 644 throw new ProjectionConfigurationException( 645 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e); 646 } 647 } 648 649 /** 650 * Convert an angle string to a double value 651 * @param angleStr The string. e.g. -1.1 or 50d10'3" 652 * @param parameterName Only for error message. 653 * @return The angle value, in degrees. 654 * @throws ProjectionConfigurationException in case of invalid parameter 655 */ 656 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException { 657 try { 658 return LatLonParser.parseCoordinate(angleStr); 659 } catch (IllegalArgumentException e) { 660 throw new ProjectionConfigurationException( 661 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr), e); 662 } 663 } 664 665 @Override 666 public Integer getEpsgCode() { 667 if (code != null && code.startsWith("EPSG:")) { 668 try { 669 return Integer.valueOf(code.substring(5)); 670 } catch (NumberFormatException e) { 671 Logging.warn(e); 672 } 673 } 674 return null; 675 } 676 677 @Override 678 public String toCode() { 679 if (code != null) { 680 return code; 681 } else if (pref != null) { 682 return "proj:" + pref; 683 } else { 684 return "proj:ERROR"; 685 } 686 } 687 688 @Override 689 public Bounds getWorldBoundsLatLon() { 690 if (bounds == null) { 691 Bounds ab = proj.getAlgorithmBounds(); 692 if (ab != null) { 693 double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180); 694 double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180); 695 bounds = new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false); 696 } else { 697 bounds = new Bounds( 698 new LatLon(-90.0, -180.0), 699 new LatLon(90.0, 180.0)); 700 } 701 } 702 return bounds; 703 } 704 705 @Override 706 public String toString() { 707 return name != null ? name : tr("Custom Projection"); 708 } 709 710 /** 711 * Factor to convert units of east/north coordinates to meters. 712 * 713 * When east/north coordinates are in degrees (geographic CRS), the scale 714 * at the equator is taken, i.e. 360 degrees corresponds to the length of 715 * the equator in meters. 716 * 717 * @return factor to convert units to meter 718 */ 719 @Override 720 public double getMetersPerUnit() { 721 return metersPerUnitWMTS; 722 } 723 724 @Override 725 public boolean switchXY() { 726 // TODO: support for other axis orientation such as West South, and Up Down 727 return this.axis.startsWith("ne"); 728 } 729 730 private static Map<String, Double> getUnitsToMeters() { 731 Map<String, Double> ret = new ConcurrentHashMap<>(); 732 ret.put("km", 1000d); 733 ret.put("m", 1d); 734 ret.put("dm", 1d/10); 735 ret.put("cm", 1d/100); 736 ret.put("mm", 1d/1000); 737 ret.put("kmi", 1852.0); 738 ret.put("in", 0.0254); 739 ret.put("ft", 0.3048); 740 ret.put("yd", 0.9144); 741 ret.put("mi", 1609.344); 742 ret.put("fathom", 1.8288); 743 ret.put("chain", 20.1168); 744 ret.put("link", 0.201168); 745 ret.put("us-in", 1d/39.37); 746 ret.put("us-ft", 0.304800609601219); 747 ret.put("us-yd", 0.914401828803658); 748 ret.put("us-ch", 20.11684023368047); 749 ret.put("us-mi", 1609.347218694437); 750 ret.put("ind-yd", 0.91439523); 751 ret.put("ind-ft", 0.30479841); 752 ret.put("ind-ch", 20.11669506); 753 ret.put("degree", METER_PER_UNIT_DEGREE); 754 return ret; 755 } 756 757 private static Map<String, Double> getPrimeMeridians() { 758 Map<String, Double> ret = new ConcurrentHashMap<>(); 759 try { 760 ret.put("greenwich", 0.0); 761 ret.put("lisbon", parseAngle("9d07'54.862\"W", null)); 762 ret.put("paris", parseAngle("2d20'14.025\"E", null)); 763 ret.put("bogota", parseAngle("74d04'51.3\"W", null)); 764 ret.put("madrid", parseAngle("3d41'16.58\"W", null)); 765 ret.put("rome", parseAngle("12d27'8.4\"E", null)); 766 ret.put("bern", parseAngle("7d26'22.5\"E", null)); 767 ret.put("jakarta", parseAngle("106d48'27.79\"E", null)); 768 ret.put("ferro", parseAngle("17d40'W", null)); 769 ret.put("brussels", parseAngle("4d22'4.71\"E", null)); 770 ret.put("stockholm", parseAngle("18d3'29.8\"E", null)); 771 ret.put("athens", parseAngle("23d42'58.815\"E", null)); 772 ret.put("oslo", parseAngle("10d43'22.5\"E", null)); 773 } catch (ProjectionConfigurationException ex) { 774 throw new IllegalStateException(ex); 775 } 776 return ret; 777 } 778 779 private static EastNorth getPointAlong(int i, int n, ProjectionBounds r) { 780 double dEast = (r.maxEast - r.minEast) / n; 781 double dNorth = (r.maxNorth - r.minNorth) / n; 782 if (i < n) { 783 return new EastNorth(r.minEast + i * dEast, r.minNorth); 784 } else if (i < 2*n) { 785 i -= n; 786 return new EastNorth(r.maxEast, r.minNorth + i * dNorth); 787 } else if (i < 3*n) { 788 i -= 2*n; 789 return new EastNorth(r.maxEast - i * dEast, r.maxNorth); 790 } else if (i < 4*n) { 791 i -= 3*n; 792 return new EastNorth(r.minEast, r.maxNorth - i * dNorth); 793 } else { 794 throw new AssertionError(); 795 } 796 } 797 798 private EastNorth getPole(Polarity whichPole) { 799 if (polesEN == null) { 800 polesEN = new EnumMap<>(Polarity.class); 801 for (Polarity p : Polarity.values()) { 802 polesEN.put(p, null); 803 LatLon ll = p.getLatLon(); 804 try { 805 EastNorth enPole = latlon2eastNorth(ll); 806 if (enPole.isValid()) { 807 // project back and check if the result is somewhat reasonable 808 LatLon llBack = eastNorth2latlon(enPole); 809 if (llBack.isValid() && ll.greatCircleDistance(llBack) < 1000) { 810 polesEN.put(p, enPole); 811 } 812 } 813 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 814 Logging.error(e); 815 } 816 } 817 } 818 return polesEN.get(whichPole); 819 } 820 821 @Override 822 public Bounds getLatLonBoundsBox(ProjectionBounds r) { 823 final int n = 10; 824 Bounds result = new Bounds(eastNorth2latlon(r.getMin())); 825 result.extend(eastNorth2latlon(r.getMax())); 826 LatLon llPrev = null; 827 for (int i = 0; i < 4*n; i++) { 828 LatLon llNow = eastNorth2latlon(getPointAlong(i, n, r)); 829 result.extend(llNow); 830 // check if segment crosses 180th meridian and if so, make sure 831 // to extend bounds to +/-180 degrees longitude 832 if (llPrev != null) { 833 double lon1 = llPrev.lon(); 834 double lon2 = llNow.lon(); 835 if (90 < lon1 && lon1 < 180 && -180 < lon2 && lon2 < -90) { 836 result.extend(new LatLon(llPrev.lat(), 180)); 837 result.extend(new LatLon(llNow.lat(), -180)); 838 } 839 if (90 < lon2 && lon2 < 180 && -180 < lon1 && lon1 < -90) { 840 result.extend(new LatLon(llNow.lat(), 180)); 841 result.extend(new LatLon(llPrev.lat(), -180)); 842 } 843 } 844 llPrev = llNow; 845 } 846 // if the box contains one of the poles, the above method did not get 847 // correct min/max latitude value 848 for (Polarity p : Polarity.values()) { 849 EastNorth pole = getPole(p); 850 if (pole != null && r.contains(pole)) { 851 result.extend(p.getLatLon()); 852 } 853 } 854 return result; 855 } 856 857 @Override 858 public ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection) { 859 final int n = 8; 860 ProjectionBounds result = null; 861 for (int i = 0; i < 4*n; i++) { 862 EastNorth en = latlon2eastNorth(boxProjection.eastNorth2latlon(getPointAlong(i, n, box))); 863 if (result == null) { 864 result = new ProjectionBounds(en); 865 } else { 866 result.extend(en); 867 } 868 } 869 return result; 870 } 871 872 /** 873 * Return true, if a geographic coordinate reference system is represented. 874 * 875 * I.e. if it returns latitude/longitude values rather than Cartesian 876 * east/north coordinates on a flat surface. 877 * @return true, if it is geographic 878 * @since 12792 879 */ 880 public boolean isGeographic() { 881 return proj.isGeographic(); 882 } 883 884}