source: josm/trunk/src/org/openstreetmap/josm/data/projection/CustomProjection.java@ 12792

Last change on this file since 12792 was 12792, checked in by bastiK, 7 years ago

closes #15273, see #15229, see #15182 - add command line interface module for projections

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