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