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