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

Last change on this file since 8568 was 8568, checked in by wiktorn, 9 years ago

Basic WMTS support.

  • added information about units and to_meter to EPSG projection definitions (needed for WMTS)
  • added WMTSTileSource and WMTSLayer classes
  • a bit of cleanup of AbstractTileSourceLayer and align so it will work properly with WMTS tile definitions
  • added Imagery Preferences panel for WMTS and icon for button
  • added removal of wms: / tms: / wmts: prefix, if user will paste them into the field
  • CachedFile - added possibility to send custom headers with request
  • added support for unit and to_meter in CustomProjection
  • AbstractTMSTileSource cleanups (change of Coordinate to ICoordinate)
  • moved JCSCachedTileLoaderJob.read() to Utils

Addresses: #10623

Tested with Polish WMTS service proivders, Walonnie needs still some debugging, as it is not working right now.

  • Property svn:eol-style set to native
File size: 22.1 KB
Line 
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;
7import java.util.HashMap;
8import java.util.List;
9import java.util.Map;
10import java.util.concurrent.ConcurrentHashMap;
11import java.util.regex.Matcher;
12import java.util.regex.Pattern;
13
14import org.openstreetmap.josm.Main;
15import org.openstreetmap.josm.data.Bounds;
16import org.openstreetmap.josm.data.coor.LatLon;
17import org.openstreetmap.josm.data.projection.datum.CentricDatum;
18import org.openstreetmap.josm.data.projection.datum.Datum;
19import org.openstreetmap.josm.data.projection.datum.NTV2Datum;
20import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
21import org.openstreetmap.josm.data.projection.datum.NullDatum;
22import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum;
23import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum;
24import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
25import org.openstreetmap.josm.data.projection.proj.Mercator;
26import org.openstreetmap.josm.data.projection.proj.Proj;
27import org.openstreetmap.josm.data.projection.proj.ProjParameters;
28import org.openstreetmap.josm.tools.Utils;
29
30/**
31 * Custom projection.
32 *
33 * Inspired by PROJ.4 and Proj4J.
34 * @since 5072
35 */
36public class CustomProjection extends AbstractProjection {
37
38 private final static Map<String, Double> UNITS_TO_METERS = getUnitsToMeters();
39 private final static double METER_PER_UNIT_DEGREE = 2 * Math.PI * 6370997 / 360;
40
41 /**
42 * pref String that defines the projection
43 *
44 * null means fall back mode (Mercator)
45 */
46 protected String pref;
47 protected String name;
48 protected String code;
49 protected String cacheDir;
50 protected Bounds bounds;
51 private double metersPerUnit = METER_PER_UNIT_DEGREE; // default to degrees
52
53 /**
54 * Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>.
55 * @since 7370 (public)
56 */
57 public static enum Param {
58
59 /** False easting */
60 x_0("x_0", true),
61 /** False northing */
62 y_0("y_0", true),
63 /** Central meridian */
64 lon_0("lon_0", true),
65 /** Scaling factor */
66 k_0("k_0", true),
67 /** Ellipsoid name (see {@code proj -le}) */
68 ellps("ellps", true),
69 /** Semimajor radius of the ellipsoid axis */
70 a("a", true),
71 /** Eccentricity of the ellipsoid squared */
72 es("es", true),
73 /** Reciprocal of the ellipsoid flattening term (e.g. 298) */
74 rf("rf", true),
75 /** Flattening of the ellipsoid = 1-sqrt(1-e^2) */
76 f("f", true),
77 /** Semiminor radius of the ellipsoid axis */
78 b("b", true),
79 /** Datum name (see {@code proj -ld}) */
80 datum("datum", true),
81 /** 3 or 7 term datum transform parameters */
82 towgs84("towgs84", true),
83 /** Filename of NTv2 grid file to use for datum transforms */
84 nadgrids("nadgrids", true),
85 /** Projection name (see {@code proj -l}) */
86 proj("proj", true),
87 /** Latitude of origin */
88 lat_0("lat_0", true),
89 /** Latitude of first standard parallel */
90 lat_1("lat_1", true),
91 /** Latitude of second standard parallel */
92 lat_2("lat_2", true),
93 /** the exact proj.4 string will be preserved in the WKT representation */
94 wktext("wktext", false), // ignored
95 /** meters, US survey feet, etc. */
96 units("units", true),
97 /** Don't use the /usr/share/proj/proj_def.dat defaults file */
98 no_defs("no_defs", false),
99 init("init", true),
100 to_meter("to_meter", true),
101 // JOSM extensions, not present in PROJ.4
102 wmssrs("wmssrs", true),
103 bounds("bounds", true);
104
105 /** Parameter key */
106 public final String key;
107 /** {@code true} if the parameter has a value */
108 public final boolean hasValue;
109
110 /** Map of all parameters by key */
111 static final Map<String, Param> paramsByKey = new ConcurrentHashMap<>();
112 static {
113 for (Param p : Param.values()) {
114 paramsByKey.put(p.key, p);
115 }
116 }
117
118 Param(String key, boolean hasValue) {
119 this.key = key;
120 this.hasValue = hasValue;
121 }
122 }
123
124 /**
125 * Constructs a new empty {@code CustomProjection}.
126 */
127 public CustomProjection() {
128 // contents can be set later with update()
129 }
130
131 /**
132 * Constructs a new {@code CustomProjection} with given parameters.
133 * @param pref String containing projection parameters
134 * (ex: "+proj=tmerc +lon_0=-3 +k_0=0.9996 +x_0=500000 +ellps=WGS84 +datum=WGS84 +bounds=-8,-5,2,85")
135 */
136 public CustomProjection(String pref) {
137 this(null, null, pref, null);
138 }
139
140 /**
141 * Constructs a new {@code CustomProjection} with given name, code and parameters.
142 *
143 * @param name describe projection in one or two words
144 * @param code unique code for this projection - may be null
145 * @param pref the string that defines the custom projection
146 * @param cacheDir cache directory name
147 */
148 public CustomProjection(String name, String code, String pref, String cacheDir) {
149 this.name = name;
150 this.code = code;
151 this.pref = pref;
152 this.cacheDir = cacheDir;
153 try {
154 update(pref);
155 } catch (ProjectionConfigurationException ex) {
156 try {
157 update(null);
158 } catch (ProjectionConfigurationException ex1) {
159 throw new RuntimeException(ex1);
160 }
161 }
162 }
163
164 /**
165 * Updates this {@code CustomProjection} with given parameters.
166 * @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90")
167 * @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly
168 */
169 public final void update(String pref) throws ProjectionConfigurationException {
170 this.pref = pref;
171 if (pref == null) {
172 ellps = Ellipsoid.WGS84;
173 datum = WGS84Datum.INSTANCE;
174 proj = new Mercator();
175 bounds = new Bounds(
176 -85.05112877980659, -180.0,
177 85.05112877980659, 180.0, true);
178 } else {
179 Map<String, String> parameters = parseParameterList(pref);
180 ellps = parseEllipsoid(parameters);
181 datum = parseDatum(parameters, ellps);
182 proj = parseProjection(parameters, ellps);
183 String s = parameters.get(Param.x_0.key);
184 if (s != null) {
185 this.x0 = parseDouble(s, Param.x_0.key);
186 }
187 s = parameters.get(Param.y_0.key);
188 if (s != null) {
189 this.y0 = parseDouble(s, Param.y_0.key);
190 }
191 s = parameters.get(Param.lon_0.key);
192 if (s != null) {
193 this.lon0 = parseAngle(s, Param.lon_0.key);
194 }
195 s = parameters.get(Param.k_0.key);
196 if (s != null) {
197 this.k0 = parseDouble(s, Param.k_0.key);
198 }
199 s = parameters.get(Param.bounds.key);
200 if (s != null) {
201 this.bounds = parseBounds(s);
202 }
203 s = parameters.get(Param.wmssrs.key);
204 if (s != null) {
205 this.code = s;
206 }
207 s = parameters.get(Param.units.key);
208 if (s != null) {
209 this.metersPerUnit = UNITS_TO_METERS.get(s);
210 }
211 s = parameters.get(Param.to_meter.key);
212 if (s != null) {
213 this.metersPerUnit = parseDouble(s, Param.to_meter.key);
214 }
215 }
216 }
217
218 private Map<String, String> parseParameterList(String pref) throws ProjectionConfigurationException {
219 Map<String, String> parameters = new HashMap<>();
220 String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim());
221 if (pref.trim().isEmpty()) {
222 parts = new String[0];
223 }
224 for (String part : parts) {
225 if (part.isEmpty() || part.charAt(0) != '+')
226 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
227 Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part);
228 if (m.matches()) {
229 String key = m.group(1);
230 // alias
231 if ("k".equals(key)) {
232 key = Param.k_0.key;
233 }
234 String value = null;
235 if (m.groupCount() >= 3) {
236 value = m.group(3);
237 // some aliases
238 if (key.equals(Param.proj.key)) {
239 if ("longlat".equals(value) || "latlon".equals(value) || "latlong".equals(value)) {
240 value = "lonlat";
241 }
242 }
243 }
244 if (!Param.paramsByKey.containsKey(key))
245 throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
246 if (Param.paramsByKey.get(key).hasValue && value == null)
247 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
248 if (!Param.paramsByKey.get(key).hasValue && value != null)
249 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
250 parameters.put(key, value);
251 } else
252 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
253 }
254 // recursive resolution of +init includes
255 String initKey = parameters.get(Param.init.key);
256 if (initKey != null) {
257 String init = Projections.getInit(initKey);
258 if (init == null)
259 throw new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey));
260 Map<String, String> initp = null;
261 try {
262 initp = parseParameterList(init);
263 } catch (ProjectionConfigurationException ex) {
264 throw new ProjectionConfigurationException(tr(initKey+": "+ex.getMessage()), ex);
265 }
266 for (Map.Entry<String, String> e : parameters.entrySet()) {
267 initp.put(e.getKey(), e.getValue());
268 }
269 return initp;
270 }
271 return parameters;
272 }
273
274 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
275 String code = parameters.get(Param.ellps.key);
276 if (code != null) {
277 Ellipsoid ellipsoid = Projections.getEllipsoid(code);
278 if (ellipsoid == null) {
279 throw new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code));
280 } else {
281 return ellipsoid;
282 }
283 }
284 String s = parameters.get(Param.a.key);
285 if (s != null) {
286 double a = parseDouble(s, Param.a.key);
287 if (parameters.get(Param.es.key) != null) {
288 double es = parseDouble(parameters, Param.es.key);
289 return Ellipsoid.create_a_es(a, es);
290 }
291 if (parameters.get(Param.rf.key) != null) {
292 double rf = parseDouble(parameters, Param.rf.key);
293 return Ellipsoid.create_a_rf(a, rf);
294 }
295 if (parameters.get(Param.f.key) != null) {
296 double f = parseDouble(parameters, Param.f.key);
297 return Ellipsoid.create_a_f(a, f);
298 }
299 if (parameters.get(Param.b.key) != null) {
300 double b = parseDouble(parameters, Param.b.key);
301 return Ellipsoid.create_a_b(a, b);
302 }
303 }
304 if (parameters.containsKey(Param.a.key) ||
305 parameters.containsKey(Param.es.key) ||
306 parameters.containsKey(Param.rf.key) ||
307 parameters.containsKey(Param.f.key) ||
308 parameters.containsKey(Param.b.key))
309 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
310 if (parameters.containsKey(Param.no_defs.key))
311 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
312 // nothing specified, use WGS84 as default
313 return Ellipsoid.WGS84;
314 }
315
316 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
317 String nadgridsId = parameters.get(Param.nadgrids.key);
318 if (nadgridsId != null) {
319 if (nadgridsId.startsWith("@")) {
320 nadgridsId = nadgridsId.substring(1);
321 }
322 if ("null".equals(nadgridsId))
323 return new NullDatum(null, ellps);
324 NTV2GridShiftFileWrapper nadgrids = Projections.getNTV2Grid(nadgridsId);
325 if (nadgrids == null)
326 throw new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", nadgridsId));
327 return new NTV2Datum(nadgridsId, null, ellps, nadgrids);
328 }
329
330 String towgs84 = parameters.get(Param.towgs84.key);
331 if (towgs84 != null)
332 return parseToWGS84(towgs84, ellps);
333
334 String datumId = parameters.get(Param.datum.key);
335 if (datumId != null) {
336 Datum datum = Projections.getDatum(datumId);
337 if (datum == null) throw new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId));
338 return datum;
339 }
340 if (parameters.containsKey(Param.no_defs.key))
341 throw new ProjectionConfigurationException(tr("Datum required (+datum=*, +towgs84=* or +nadgrids=*)"));
342 return new CentricDatum(null, null, ellps);
343 }
344
345 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
346 String[] numStr = paramList.split(",");
347
348 if (numStr.length != 3 && numStr.length != 7)
349 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
350 List<Double> towgs84Param = new ArrayList<>();
351 for (String str : numStr) {
352 try {
353 towgs84Param.add(Double.valueOf(str));
354 } catch (NumberFormatException e) {
355 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e);
356 }
357 }
358 boolean isCentric = true;
359 for (Double param : towgs84Param) {
360 if (param != 0) {
361 isCentric = false;
362 break;
363 }
364 }
365 if (isCentric)
366 return new CentricDatum(null, null, ellps);
367 boolean is3Param = true;
368 for (int i = 3; i < towgs84Param.size(); i++) {
369 if (towgs84Param.get(i) != 0) {
370 is3Param = false;
371 break;
372 }
373 }
374 if (is3Param)
375 return new ThreeParameterDatum(null, null, ellps,
376 towgs84Param.get(0),
377 towgs84Param.get(1),
378 towgs84Param.get(2));
379 else
380 return new SevenParameterDatum(null, null, ellps,
381 towgs84Param.get(0),
382 towgs84Param.get(1),
383 towgs84Param.get(2),
384 towgs84Param.get(3),
385 towgs84Param.get(4),
386 towgs84Param.get(5),
387 towgs84Param.get(6));
388 }
389
390 public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
391 String id = parameters.get(Param.proj.key);
392 if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
393
394 Proj proj = Projections.getBaseProjection(id);
395 if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id));
396
397 ProjParameters projParams = new ProjParameters();
398
399 projParams.ellps = ellps;
400
401 String s;
402 s = parameters.get(Param.lat_0.key);
403 if (s != null) {
404 projParams.lat0 = parseAngle(s, Param.lat_0.key);
405 }
406 s = parameters.get(Param.lat_1.key);
407 if (s != null) {
408 projParams.lat1 = parseAngle(s, Param.lat_1.key);
409 }
410 s = parameters.get(Param.lat_2.key);
411 if (s != null) {
412 projParams.lat2 = parseAngle(s, Param.lat_2.key);
413 }
414 proj.initialize(projParams);
415 return proj;
416 }
417
418 public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
419 String[] numStr = boundsStr.split(",");
420 if (numStr.length != 4)
421 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
422 return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
423 parseAngle(numStr[0], "minlon (+bounds)"),
424 parseAngle(numStr[3], "maxlat (+bounds)"),
425 parseAngle(numStr[2], "maxlon (+bounds)"), false);
426 }
427
428 public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
429 if (!parameters.containsKey(parameterName))
430 throw new IllegalArgumentException(tr("Unknown parameter ''{0}''", parameterName));
431 String doubleStr = parameters.get(parameterName);
432 if (doubleStr == null)
433 throw new ProjectionConfigurationException(
434 tr("Expected number argument for parameter ''{0}''", parameterName));
435 return parseDouble(doubleStr, parameterName);
436 }
437
438 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
439 try {
440 return Double.parseDouble(doubleStr);
441 } catch (NumberFormatException e) {
442 throw new ProjectionConfigurationException(
443 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e);
444 }
445 }
446
447 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
448 String s = angleStr;
449 double value = 0;
450 boolean neg = false;
451 Matcher m = Pattern.compile("^-").matcher(s);
452 if (m.find()) {
453 neg = true;
454 s = s.substring(m.end());
455 }
456 final String FLOAT = "(\\d+(\\.\\d*)?)";
457 boolean dms = false;
458 double deg = 0.0, min = 0.0, sec = 0.0;
459 // degrees
460 m = Pattern.compile("^"+FLOAT+"d").matcher(s);
461 if (m.find()) {
462 s = s.substring(m.end());
463 deg = Double.parseDouble(m.group(1));
464 dms = true;
465 }
466 // minutes
467 m = Pattern.compile("^"+FLOAT+"'").matcher(s);
468 if (m.find()) {
469 s = s.substring(m.end());
470 min = Double.parseDouble(m.group(1));
471 dms = true;
472 }
473 // seconds
474 m = Pattern.compile("^"+FLOAT+"\"").matcher(s);
475 if (m.find()) {
476 s = s.substring(m.end());
477 sec = Double.parseDouble(m.group(1));
478 dms = true;
479 }
480 // plain number (in degrees)
481 if (dms) {
482 value = deg + (min/60.0) + (sec/3600.0);
483 } else {
484 m = Pattern.compile("^"+FLOAT).matcher(s);
485 if (m.find()) {
486 s = s.substring(m.end());
487 value += Double.parseDouble(m.group(1));
488 }
489 }
490 m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s);
491 if (m.find()) {
492 s = s.substring(m.end());
493 } else {
494 m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s);
495 if (m.find()) {
496 s = s.substring(m.end());
497 neg = !neg;
498 }
499 }
500 if (neg) {
501 value = -value;
502 }
503 if (!s.isEmpty()) {
504 throw new ProjectionConfigurationException(
505 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
506 }
507 return value;
508 }
509
510 @Override
511 public Integer getEpsgCode() {
512 if (code != null && code.startsWith("EPSG:")) {
513 try {
514 return Integer.valueOf(code.substring(5));
515 } catch (NumberFormatException e) {
516 Main.warn(e);
517 }
518 }
519 return null;
520 }
521
522 @Override
523 public String toCode() {
524 return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref);
525 }
526
527 @Override
528 public String getCacheDirectoryName() {
529 return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
530 }
531
532 @Override
533 public Bounds getWorldBoundsLatLon() {
534 if (bounds != null) return bounds;
535 return new Bounds(
536 new LatLon(-90.0, -180.0),
537 new LatLon(90.0, 180.0));
538 }
539
540 @Override
541 public String toString() {
542 return name != null ? name : tr("Custom Projection");
543 }
544
545 @Override
546 public double getMetersPerUnit() {
547 return metersPerUnit;
548 }
549
550 private static Map<String, Double> getUnitsToMeters() {
551 Map<String, Double> ret = new ConcurrentHashMap<>();
552 ret.put("km", 1000d);
553 ret.put("m", 1d);
554 ret.put("dm", 1d/10);
555 ret.put("cm", 1d/100);
556 ret.put("mm", 1d/1000);
557 ret.put("kmi", 1852.0);
558 ret.put("in", 0.0254);
559 ret.put("ft", 0.3048);
560 ret.put("yd", 0.9144);
561 ret.put("mi", 1609.344);
562 ret.put("fathom", 1.8288);
563 ret.put("chain", 20.1168);
564 ret.put("link", 0.201168);
565 ret.put("us-in", 1d/39.37);
566 ret.put("us-ft", 0.304800609601219);
567 ret.put("us-yd", 0.914401828803658);
568 ret.put("us-ch", 20.11684023368047);
569 ret.put("us-mi", 1609.347218694437);
570 ret.put("ind-yd", 0.91439523);
571 ret.put("ind-ft", 0.30479841);
572 ret.put("ind-ch", 20.11669506);
573 ret.put("degree", METER_PER_UNIT_DEGREE);
574 return ret;
575 }
576}
Note: See TracBrowser for help on using the repository browser.