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

Last change on this file since 12620 was 12620, checked in by Don-vip, 7 years ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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