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; 016import java.util.stream.IntStream; 017 018import org.openstreetmap.josm.data.Bounds; 019import org.openstreetmap.josm.data.ProjectionBounds; 020import org.openstreetmap.josm.data.coor.EastNorth; 021import org.openstreetmap.josm.data.coor.LatLon; 022import org.openstreetmap.josm.data.coor.conversion.LatLonParser; 023import org.openstreetmap.josm.data.projection.datum.CentricDatum; 024import org.openstreetmap.josm.data.projection.datum.Datum; 025import org.openstreetmap.josm.data.projection.datum.NTV2Datum; 026import org.openstreetmap.josm.data.projection.datum.NullDatum; 027import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum; 028import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum; 029import org.openstreetmap.josm.data.projection.datum.WGS84Datum; 030import org.openstreetmap.josm.data.projection.proj.ICentralMeridianProvider; 031import org.openstreetmap.josm.data.projection.proj.IScaleFactorProvider; 032import org.openstreetmap.josm.data.projection.proj.Mercator; 033import org.openstreetmap.josm.data.projection.proj.Proj; 034import org.openstreetmap.josm.data.projection.proj.ProjParameters; 035import org.openstreetmap.josm.tools.JosmRuntimeException; 036import org.openstreetmap.josm.tools.Logging; 037import org.openstreetmap.josm.tools.Utils; 038import org.openstreetmap.josm.tools.bugreport.BugReport; 039 040/** 041 * Custom projection. 042 * 043 * Inspired by PROJ.4 and Proj4J. 044 * @since 5072 045 */ 046public class CustomProjection extends AbstractProjection { 047 048 /* 049 * Equation for METER_PER_UNIT_DEGREE taken from: 050 * https://github.com/openlayers/ol3/blob/master/src/ol/proj/epsg4326projection.js#L58 051 * Value for Radius taken form: 052 * https://github.com/openlayers/ol3/blob/master/src/ol/sphere/wgs84sphere.js#L11 053 */ 054 private static final double METER_PER_UNIT_DEGREE = 2 * Math.PI * 6378137.0 / 360; 055 private static final Map<String, Double> UNITS_TO_METERS = getUnitsToMeters(); 056 private static final Map<String, Double> PRIME_MERIDANS = getPrimeMeridians(); 057 058 /** 059 * pref String that defines the projection 060 * 061 * null means fall back mode (Mercator) 062 */ 063 protected String pref; 064 protected String name; 065 protected String code; 066 protected Bounds bounds; 067 private double metersPerUnitWMTS; 068 /** 069 * Starting in PROJ 4.8.0, the {@code +axis} argument can be used to control the axis orientation of the coordinate system. 070 * The default orientation is "easting, northing, up" but directions can be flipped, or axes flipped using 071 * combinations of the axes in the {@code +axis} switch. The values are: {@code e} (Easting), {@code w} (Westing), 072 * {@code n} (Northing), {@code s} (Southing), {@code u} (Up), {@code d} (Down); 073 * Examples: {@code +axis=enu} (the default easting, northing, elevation), {@code +axis=neu} (northing, easting, up; 074 * useful for "lat/long" geographic coordinates, or south orientated transverse mercator), {@code +axis=wnu} 075 * (westing, northing, up - some planetary coordinate systems have "west positive" coordinate systems)<p> 076 * See <a href="https://proj4.org/usage/projections.html#axis-orientation">proj4.org</a> 077 */ 078 private String axis = "enu"; // default axis orientation is East, North, Up 079 080 private static final List<String> LON_LAT_VALUES = Arrays.asList("longlat", "latlon", "latlong"); 081 082 /** 083 * Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>. 084 * @since 7370 (public) 085 */ 086 public enum Param { 087 088 /** False easting */ 089 x_0("x_0", true), 090 /** False northing */ 091 y_0("y_0", true), 092 /** Central meridian */ 093 lon_0("lon_0", true), 094 /** Prime meridian */ 095 pm("pm", true), 096 /** Scaling factor */ 097 k_0("k_0", true), 098 /** Ellipsoid name (see {@code proj -le}) */ 099 ellps("ellps", true), 100 /** Semimajor radius of the ellipsoid axis */ 101 a("a", true), 102 /** Eccentricity of the ellipsoid squared */ 103 es("es", true), 104 /** Reciprocal of the ellipsoid flattening term (e.g. 298) */ 105 rf("rf", true), 106 /** Flattening of the ellipsoid = 1-sqrt(1-e^2) */ 107 f("f", true), 108 /** Semiminor radius of the ellipsoid axis */ 109 b("b", true), 110 /** Datum name (see {@code proj -ld}) */ 111 datum("datum", true), 112 /** 3 or 7 term datum transform parameters */ 113 towgs84("towgs84", true), 114 /** Filename of NTv2 grid file to use for datum transforms */ 115 nadgrids("nadgrids", true), 116 /** Projection name (see {@code proj -l}) */ 117 proj("proj", true), 118 /** Latitude of origin */ 119 lat_0("lat_0", true), 120 /** Latitude of first standard parallel */ 121 lat_1("lat_1", true), 122 /** Latitude of second standard parallel */ 123 lat_2("lat_2", true), 124 /** Latitude of true scale (Polar Stereographic) */ 125 lat_ts("lat_ts", true), 126 /** longitude of the center of the projection (Oblique Mercator) */ 127 lonc("lonc", true), 128 /** azimuth (true) of the center line passing through the center of the 129 * projection (Oblique Mercator) */ 130 alpha("alpha", true), 131 /** rectified bearing of the center line (Oblique Mercator) */ 132 gamma("gamma", true), 133 /** select "Hotine" variant of Oblique Mercator */ 134 no_off("no_off", false), 135 /** legacy alias for no_off */ 136 no_uoff("no_uoff", false), 137 /** longitude of first point (Oblique Mercator) */ 138 lon_1("lon_1", true), 139 /** longitude of second point (Oblique Mercator) */ 140 lon_2("lon_2", true), 141 /** the exact proj.4 string will be preserved in the WKT representation */ 142 wktext("wktext", false), // ignored 143 /** meters, US survey feet, etc. */ 144 units("units", true), 145 /** Don't use the /usr/share/proj/proj_def.dat defaults file */ 146 no_defs("no_defs", false), 147 init("init", true), 148 /** crs units to meter multiplier */ 149 to_meter("to_meter", true), 150 /** definition of axis for projection */ 151 axis("axis", true), 152 /** UTM zone */ 153 zone("zone", true), 154 /** indicate southern hemisphere for UTM */ 155 south("south", false), 156 /** vertical units - ignore, as we don't use height information */ 157 vunits("vunits", true), 158 // JOSM extensions, not present in PROJ.4 159 wmssrs("wmssrs", true), 160 bounds("bounds", true); 161 162 /** Parameter key */ 163 public final String key; 164 /** {@code true} if the parameter has a value */ 165 public final boolean hasValue; 166 167 /** Map of all parameters by key */ 168 static final Map<String, Param> paramsByKey = new ConcurrentHashMap<>(); 169 static { 170 for (Param p : Param.values()) { 171 paramsByKey.put(p.key, p); 172 } 173 // alias 174 paramsByKey.put("k", Param.k_0); 175 } 176 177 Param(String key, boolean hasValue) { 178 this.key = key; 179 this.hasValue = hasValue; 180 } 181 } 182 183 enum Polarity { 184 NORTH(LatLon.NORTH_POLE), 185 SOUTH(LatLon.SOUTH_POLE); 186 187 @SuppressWarnings("ImmutableEnumChecker") 188 private final LatLon latlon; 189 190 Polarity(LatLon latlon) { 191 this.latlon = latlon; 192 } 193 194 LatLon getLatLon() { 195 return latlon; 196 } 197 } 198 199 private EnumMap<Polarity, EastNorth> polesEN; 200 201 /** 202 * Constructs a new empty {@code CustomProjection}. 203 */ 204 public CustomProjection() { 205 // contents can be set later with update() 206 } 207 208 /** 209 * Constructs a new {@code CustomProjection} with given parameters. 210 * @param pref String containing projection parameters 211 * (ex: "+proj=tmerc +lon_0=-3 +k_0=0.9996 +x_0=500000 +ellps=WGS84 +datum=WGS84 +bounds=-8,-5,2,85") 212 */ 213 public CustomProjection(String pref) { 214 this(null, null, pref); 215 } 216 217 /** 218 * Constructs a new {@code CustomProjection} with given name, code and parameters. 219 * 220 * @param name describe projection in one or two words 221 * @param code unique code for this projection - may be null 222 * @param pref the string that defines the custom projection 223 */ 224 public CustomProjection(String name, String code, String pref) { 225 this.name = name; 226 this.code = code; 227 this.pref = pref; 228 try { 229 update(pref); 230 } catch (ProjectionConfigurationException ex) { 231 Logging.trace(ex); 232 try { 233 update(null); 234 } catch (ProjectionConfigurationException ex1) { 235 throw BugReport.intercept(ex1).put("name", name).put("code", code).put("pref", pref); 236 } 237 } 238 } 239 240 /** 241 * Updates this {@code CustomProjection} with given parameters. 242 * @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90") 243 * @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly 244 */ 245 public final void update(String pref) throws ProjectionConfigurationException { 246 this.pref = pref; 247 if (pref == null) { 248 ellps = Ellipsoid.WGS84; 249 datum = WGS84Datum.INSTANCE; 250 proj = new Mercator(); 251 bounds = new Bounds( 252 -85.05112877980659, -180.0, 253 85.05112877980659, 180.0, true); 254 } else { 255 Map<String, String> parameters = parseParameterList(pref, false); 256 parameters = resolveInits(parameters, false); 257 ellps = parseEllipsoid(parameters); 258 datum = parseDatum(parameters, ellps); 259 if (ellps == null) { 260 ellps = datum.getEllipsoid(); 261 } 262 proj = parseProjection(parameters, ellps); 263 // "utm" is a shortcut for a set of parameters 264 if ("utm".equals(parameters.get(Param.proj.key))) { 265 Integer zone; 266 try { 267 zone = Integer.valueOf(Optional.ofNullable(parameters.get(Param.zone.key)).orElseThrow( 268 () -> new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter.")))); 269 } catch (NumberFormatException e) { 270 zone = null; 271 } 272 if (zone == null || zone < 1 || zone > 60) 273 throw new ProjectionConfigurationException(tr("Expected integer value in range 1-60 for ''+zone=...'' parameter.")); 274 this.lon0 = 6d * zone - 183d; 275 this.k0 = 0.9996; 276 this.x0 = 500_000; 277 this.y0 = parameters.containsKey(Param.south.key) ? 10_000_000 : 0; 278 } 279 String s = parameters.get(Param.x_0.key); 280 if (s != null) { 281 this.x0 = parseDouble(s, Param.x_0.key); 282 } 283 s = parameters.get(Param.y_0.key); 284 if (s != null) { 285 this.y0 = parseDouble(s, Param.y_0.key); 286 } 287 s = parameters.get(Param.lon_0.key); 288 if (s != null) { 289 this.lon0 = parseAngle(s, Param.lon_0.key); 290 } 291 if (proj instanceof ICentralMeridianProvider) { 292 this.lon0 = ((ICentralMeridianProvider) proj).getCentralMeridian(); 293 } 294 s = parameters.get(Param.pm.key); 295 if (s != null) { 296 if (PRIME_MERIDANS.containsKey(s)) { 297 this.pm = PRIME_MERIDANS.get(s); 298 } else { 299 this.pm = parseAngle(s, Param.pm.key); 300 } 301 } 302 s = parameters.get(Param.k_0.key); 303 if (s != null) { 304 this.k0 = parseDouble(s, Param.k_0.key); 305 } 306 if (proj instanceof IScaleFactorProvider) { 307 this.k0 *= ((IScaleFactorProvider) proj).getScaleFactor(); 308 } 309 s = parameters.get(Param.bounds.key); 310 this.bounds = s != null ? parseBounds(s) : null; 311 s = parameters.get(Param.wmssrs.key); 312 if (s != null) { 313 this.code = s; 314 } 315 boolean defaultUnits = true; 316 s = parameters.get(Param.units.key); 317 if (s != null) { 318 s = Utils.strip(s, "\""); 319 if (UNITS_TO_METERS.containsKey(s)) { 320 this.toMeter = UNITS_TO_METERS.get(s); 321 this.metersPerUnitWMTS = this.toMeter; 322 defaultUnits = false; 323 } else { 324 throw new ProjectionConfigurationException(tr("No unit found for: {0}", s)); 325 } 326 } 327 s = parameters.get(Param.to_meter.key); 328 if (s != null) { 329 this.toMeter = parseDouble(s, Param.to_meter.key); 330 this.metersPerUnitWMTS = this.toMeter; 331 defaultUnits = false; 332 } 333 if (defaultUnits) { 334 this.toMeter = 1; 335 this.metersPerUnitWMTS = proj.isGeographic() ? METER_PER_UNIT_DEGREE : 1; 336 } 337 s = parameters.get(Param.axis.key); 338 if (s != null) { 339 this.axis = s; 340 } 341 } 342 } 343 344 /** 345 * Parse a parameter list to key=value pairs. 346 * 347 * @param pref the parameter list 348 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception 349 * @return parameters map 350 * @throws ProjectionConfigurationException in case of invalid parameter 351 */ 352 public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException { 353 Map<String, String> parameters = new HashMap<>(); 354 String trimmedPref = pref.trim(); 355 if (trimmedPref.isEmpty()) { 356 return parameters; 357 } 358 359 Pattern keyPattern = Pattern.compile("\\+(?<key>[a-zA-Z0-9_]+)(=(?<value>.*))?"); 360 String[] parts = Utils.WHITE_SPACES_PATTERN.split(trimmedPref, -1); 361 for (String part : parts) { 362 Matcher m = keyPattern.matcher(part); 363 if (m.matches()) { 364 String key = m.group("key"); 365 String value = m.group("value"); 366 // some aliases 367 if (key.equals(Param.proj.key) && LON_LAT_VALUES.contains(value)) { 368 value = "lonlat"; 369 } 370 Param param = Param.paramsByKey.get(key); 371 if (param == null) { 372 if (!ignoreUnknownParameter) 373 throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key)); 374 } else { 375 if (param.hasValue && value == null) 376 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key)); 377 if (!param.hasValue && value != null) 378 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key)); 379 key = param.key; // To be really sure, we might have an alias. 380 } 381 parameters.put(key, value); 382 } else if (!part.startsWith("+")) { 383 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part)); 384 } else { 385 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part)); 386 } 387 } 388 return parameters; 389 } 390 391 /** 392 * Recursive resolution of +init includes. 393 * 394 * @param parameters parameters map 395 * @param ignoreUnknownParameter true, if unknown parameter should not raise exception 396 * @return parameters map with +init includes resolved 397 * @throws ProjectionConfigurationException in case of invalid parameter 398 */ 399 public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter) 400 throws ProjectionConfigurationException { 401 // recursive resolution of +init includes 402 String initKey = parameters.get(Param.init.key); 403 if (initKey != null) { 404 Map<String, String> initp; 405 try { 406 initp = parseParameterList(Optional.ofNullable(Projections.getInit(initKey)).orElseThrow( 407 () -> new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey))), 408 ignoreUnknownParameter); 409 initp = resolveInits(initp, ignoreUnknownParameter); 410 } catch (ProjectionConfigurationException ex) { 411 throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex); 412 } 413 initp.putAll(parameters); 414 return initp; 415 } 416 return parameters; 417 } 418 419 /** 420 * Gets the ellipsoid 421 * @param parameters The parameters to get the value from 422 * @return The Ellipsoid as specified with the parameters 423 * @throws ProjectionConfigurationException in case of invalid parameters 424 */ 425 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException { 426 String code = parameters.get(Param.ellps.key); 427 if (code != null) { 428 return Optional.ofNullable(Projections.getEllipsoid(code)).orElseThrow( 429 () -> new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code))); 430 } 431 String s = parameters.get(Param.a.key); 432 if (s != null) { 433 double a = parseDouble(s, Param.a.key); 434 if (parameters.get(Param.es.key) != null) { 435 double es = parseDouble(parameters, Param.es.key); 436 return Ellipsoid.createAes(a, es); 437 } 438 if (parameters.get(Param.rf.key) != null) { 439 double rf = parseDouble(parameters, Param.rf.key); 440 return Ellipsoid.createArf(a, rf); 441 } 442 if (parameters.get(Param.f.key) != null) { 443 double f = parseDouble(parameters, Param.f.key); 444 return Ellipsoid.createAf(a, f); 445 } 446 if (parameters.get(Param.b.key) != null) { 447 double b = parseDouble(parameters, Param.b.key); 448 return Ellipsoid.createAb(a, b); 449 } 450 } 451 if (parameters.containsKey(Param.a.key) || 452 parameters.containsKey(Param.es.key) || 453 parameters.containsKey(Param.rf.key) || 454 parameters.containsKey(Param.f.key) || 455 parameters.containsKey(Param.b.key)) 456 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported.")); 457 return null; 458 } 459 460 /** 461 * Gets the datum 462 * @param parameters The parameters to get the value from 463 * @param ellps The ellisoid that was previously computed 464 * @return The Datum as specified with the parameters 465 * @throws ProjectionConfigurationException in case of invalid parameters 466 */ 467 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException { 468 Datum result = null; 469 String datumId = parameters.get(Param.datum.key); 470 if (datumId != null) { 471 result = Optional.ofNullable(Projections.getDatum(datumId)).orElseThrow( 472 () -> new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId))); 473 } 474 if (ellps == null) { 475 if (result == null && parameters.containsKey(Param.no_defs.key)) 476 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)")); 477 // nothing specified, use WGS84 as default 478 ellps = result != null ? result.getEllipsoid() : Ellipsoid.WGS84; 479 } 480 481 String nadgridsId = parameters.get(Param.nadgrids.key); 482 if (nadgridsId != null) { 483 if (nadgridsId.startsWith("@")) { 484 nadgridsId = nadgridsId.substring(1); 485 } 486 if ("null".equals(nadgridsId)) 487 return new NullDatum(null, ellps); 488 final String fNadgridsId = nadgridsId; 489 return new NTV2Datum(fNadgridsId, null, ellps, Optional.ofNullable(Projections.getNTV2Grid(fNadgridsId)).orElseThrow( 490 () -> new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", fNadgridsId)))); 491 } 492 493 String towgs84 = parameters.get(Param.towgs84.key); 494 if (towgs84 != null) { 495 Datum towgs84Datum = parseToWGS84(towgs84, ellps); 496 if (result == null || towgs84Datum instanceof ThreeParameterDatum || towgs84Datum instanceof SevenParameterDatum) { 497 // +datum has priority over +towgs84=0,0,0[,0,0,0,0] 498 return towgs84Datum; 499 } 500 } 501 502 return result != null ? result : new NullDatum(null, ellps); 503 } 504 505 /** 506 * Parse {@code towgs84} parameter. 507 * @param paramList List of parameter arguments (expected: 3 or 7) 508 * @param ellps ellipsoid 509 * @return parsed datum ({@link ThreeParameterDatum} or {@link SevenParameterDatum}) 510 * @throws ProjectionConfigurationException if the arguments cannot be parsed 511 */ 512 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException { 513 String[] numStr = paramList.split(",", -1); 514 515 if (numStr.length != 3 && numStr.length != 7) 516 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)")); 517 List<Double> towgs84Param = new ArrayList<>(); 518 for (String str : numStr) { 519 try { 520 towgs84Param.add(Double.valueOf(str)); 521 } catch (NumberFormatException e) { 522 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e); 523 } 524 } 525 boolean isCentric = towgs84Param.stream().noneMatch(param -> param != 0); 526 if (isCentric) 527 return Ellipsoid.WGS84.equals(ellps) ? WGS84Datum.INSTANCE : new CentricDatum(null, null, ellps); 528 boolean is3Param = IntStream.range(3, towgs84Param.size()).noneMatch(i -> towgs84Param.get(i) != 0); 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(",", -1); 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 // +axis=neu 728 return this.axis.startsWith("ne"); 729 } 730 731 private static Map<String, Double> getUnitsToMeters() { 732 Map<String, Double> ret = new ConcurrentHashMap<>(); 733 ret.put("km", 1000d); 734 ret.put("m", 1d); 735 ret.put("dm", 1d/10); 736 ret.put("cm", 1d/100); 737 ret.put("mm", 1d/1000); 738 ret.put("kmi", 1852.0); 739 ret.put("in", 0.0254); 740 ret.put("ft", 0.3048); 741 ret.put("yd", 0.9144); 742 ret.put("mi", 1609.344); 743 ret.put("fathom", 1.8288); 744 ret.put("chain", 20.1168); 745 ret.put("link", 0.201168); 746 ret.put("us-in", 1d/39.37); 747 ret.put("us-ft", 0.304800609601219); 748 ret.put("us-yd", 0.914401828803658); 749 ret.put("us-ch", 20.11684023368047); 750 ret.put("us-mi", 1609.347218694437); 751 ret.put("ind-yd", 0.91439523); 752 ret.put("ind-ft", 0.30479841); 753 ret.put("ind-ch", 20.11669506); 754 ret.put("degree", METER_PER_UNIT_DEGREE); 755 return ret; 756 } 757 758 private static Map<String, Double> getPrimeMeridians() { 759 Map<String, Double> ret = new ConcurrentHashMap<>(); 760 try { 761 ret.put("greenwich", 0.0); 762 ret.put("lisbon", parseAngle("9d07'54.862\"W", null)); 763 ret.put("paris", parseAngle("2d20'14.025\"E", null)); 764 ret.put("bogota", parseAngle("74d04'51.3\"W", null)); 765 ret.put("madrid", parseAngle("3d41'16.58\"W", null)); 766 ret.put("rome", parseAngle("12d27'8.4\"E", null)); 767 ret.put("bern", parseAngle("7d26'22.5\"E", null)); 768 ret.put("jakarta", parseAngle("106d48'27.79\"E", null)); 769 ret.put("ferro", parseAngle("17d40'W", null)); 770 ret.put("brussels", parseAngle("4d22'4.71\"E", null)); 771 ret.put("stockholm", parseAngle("18d3'29.8\"E", null)); 772 ret.put("athens", parseAngle("23d42'58.815\"E", null)); 773 ret.put("oslo", parseAngle("10d43'22.5\"E", null)); 774 } catch (ProjectionConfigurationException ex) { 775 throw new IllegalStateException(ex); 776 } 777 return ret; 778 } 779 780 private static EastNorth getPointAlong(int i, int n, ProjectionBounds r) { 781 double dEast = (r.maxEast - r.minEast) / n; 782 double dNorth = (r.maxNorth - r.minNorth) / n; 783 if (i < n) { 784 return new EastNorth(r.minEast + i * dEast, r.minNorth); 785 } else if (i < 2*n) { 786 i -= n; 787 return new EastNorth(r.maxEast, r.minNorth + i * dNorth); 788 } else if (i < 3*n) { 789 i -= 2*n; 790 return new EastNorth(r.maxEast - i * dEast, r.maxNorth); 791 } else if (i < 4*n) { 792 i -= 3*n; 793 return new EastNorth(r.minEast, r.maxNorth - i * dNorth); 794 } else { 795 throw new AssertionError(); 796 } 797 } 798 799 private EastNorth getPole(Polarity whichPole) { 800 if (polesEN == null) { 801 polesEN = new EnumMap<>(Polarity.class); 802 for (Polarity p : Polarity.values()) { 803 polesEN.put(p, null); 804 LatLon ll = p.getLatLon(); 805 try { 806 EastNorth enPole = latlon2eastNorth(ll); 807 if (enPole.isValid()) { 808 // project back and check if the result is somewhat reasonable 809 LatLon llBack = eastNorth2latlon(enPole); 810 if (llBack.isValid() && ll.greatCircleDistance(llBack) < 1000) { 811 polesEN.put(p, enPole); 812 } 813 } 814 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) { 815 Logging.error(e); 816 } 817 } 818 } 819 return polesEN.get(whichPole); 820 } 821 822 @Override 823 public Bounds getLatLonBoundsBox(ProjectionBounds r) { 824 final int n = 10; 825 Bounds result = new Bounds(eastNorth2latlon(r.getMin())); 826 result.extend(eastNorth2latlon(r.getMax())); 827 LatLon llPrev = null; 828 for (int i = 0; i < 4*n; i++) { 829 LatLon llNow = eastNorth2latlon(getPointAlong(i, n, r)); 830 result.extend(llNow); 831 // check if segment crosses 180th meridian and if so, make sure 832 // to extend bounds to +/-180 degrees longitude 833 if (llPrev != null) { 834 double lon1 = llPrev.lon(); 835 double lon2 = llNow.lon(); 836 if (90 < lon1 && lon1 < 180 && -180 < lon2 && lon2 < -90) { 837 result.extend(new LatLon(llPrev.lat(), 180)); 838 result.extend(new LatLon(llNow.lat(), -180)); 839 } 840 if (90 < lon2 && lon2 < 180 && -180 < lon1 && lon1 < -90) { 841 result.extend(new LatLon(llNow.lat(), 180)); 842 result.extend(new LatLon(llPrev.lat(), -180)); 843 } 844 } 845 llPrev = llNow; 846 } 847 // if the box contains one of the poles, the above method did not get 848 // correct min/max latitude value 849 for (Polarity p : Polarity.values()) { 850 EastNorth pole = getPole(p); 851 if (pole != null && r.contains(pole)) { 852 result.extend(p.getLatLon()); 853 } 854 } 855 return result; 856 } 857 858 @Override 859 public ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection) { 860 final int n = 8; 861 ProjectionBounds result = null; 862 for (int i = 0; i < 4*n; i++) { 863 EastNorth en = latlon2eastNorth(boxProjection.eastNorth2latlon(getPointAlong(i, n, box))); 864 if (result == null) { 865 result = new ProjectionBounds(en); 866 } else { 867 result.extend(en); 868 } 869 } 870 return result; 871 } 872 873 /** 874 * Return true, if a geographic coordinate reference system is represented. 875 * 876 * I.e. if it returns latitude/longitude values rather than Cartesian 877 * east/north coordinates on a flat surface. 878 * @return true, if it is geographic 879 * @since 12792 880 */ 881 public boolean isGeographic() { 882 return proj.isGeographic(); 883 } 884 885}