001 // License: GPL. For details, see LICENSE file.
002 package org.openstreetmap.josm.data.projection;
003
004 import static org.openstreetmap.josm.tools.I18n.tr;
005
006 import java.util.ArrayList;
007 import java.util.HashMap;
008 import java.util.List;
009 import java.util.Map;
010 import java.util.regex.Matcher;
011 import java.util.regex.Pattern;
012
013 import org.openstreetmap.josm.data.Bounds;
014 import org.openstreetmap.josm.data.coor.LatLon;
015 import org.openstreetmap.josm.data.projection.datum.CentricDatum;
016 import org.openstreetmap.josm.data.projection.datum.Datum;
017 import org.openstreetmap.josm.data.projection.datum.NTV2Datum;
018 import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
019 import org.openstreetmap.josm.data.projection.datum.NullDatum;
020 import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum;
021 import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum;
022 import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
023 import org.openstreetmap.josm.data.projection.proj.Mercator;
024 import org.openstreetmap.josm.data.projection.proj.Proj;
025 import org.openstreetmap.josm.data.projection.proj.ProjParameters;
026 import org.openstreetmap.josm.tools.Utils;
027
028 /**
029 * Custom projection
030 *
031 * Inspired by PROJ.4 and Proj4J.
032 */
033 public class CustomProjection extends AbstractProjection {
034
035 /**
036 * pref String that defines the projection
037 *
038 * null means fall back mode (Mercator)
039 */
040 protected String pref;
041 protected String name;
042 protected String code;
043 protected String cacheDir;
044 protected Bounds bounds;
045
046 protected static enum Param {
047
048 x_0("x_0", true),
049 y_0("y_0", true),
050 lon_0("lon_0", true),
051 k_0("k_0", true),
052 ellps("ellps", true),
053 a("a", true),
054 es("es", true),
055 rf("rf", true),
056 f("f", true),
057 b("b", true),
058 datum("datum", true),
059 towgs84("towgs84", true),
060 nadgrids("nadgrids", true),
061 proj("proj", true),
062 lat_0("lat_0", true),
063 lat_1("lat_1", true),
064 lat_2("lat_2", true),
065 wktext("wktext", false), // ignored
066 units("units", true), // ignored
067 no_defs("no_defs", false),
068 init("init", true),
069 // JOSM extension, not present in PROJ.4
070 bounds("bounds", true);
071
072 public String key;
073 public boolean hasValue;
074
075 public final static Map<String, Param> paramsByKey = new HashMap<String, Param>();
076 static {
077 for (Param p : Param.values()) {
078 paramsByKey.put(p.key, p);
079 }
080 }
081
082 Param(String key, boolean hasValue) {
083 this.key = key;
084 this.hasValue = hasValue;
085 }
086 }
087
088 public CustomProjection() {
089 }
090
091 public CustomProjection(String pref) {
092 this(null, null, pref, null);
093 }
094
095 /**
096 * Constructor.
097 *
098 * @param name describe projection in one or two words
099 * @param code unique code for this projection - may be null
100 * @param pref the string that defines the custom projection
101 * @param cacheDir cache directory name
102 */
103 public CustomProjection(String name, String code, String pref, String cacheDir) {
104 this.name = name;
105 this.code = code;
106 this.pref = pref;
107 this.cacheDir = cacheDir;
108 try {
109 update(pref);
110 } catch (ProjectionConfigurationException ex) {
111 try {
112 update(null);
113 } catch (ProjectionConfigurationException ex1) {
114 throw new RuntimeException();
115 }
116 }
117 }
118
119 public void update(String pref) throws ProjectionConfigurationException {
120 this.pref = pref;
121 if (pref == null) {
122 ellps = Ellipsoid.WGS84;
123 datum = WGS84Datum.INSTANCE;
124 proj = new Mercator();
125 bounds = new Bounds(
126 new LatLon(-85.05112877980659, -180.0),
127 new LatLon(85.05112877980659, 180.0), true);
128 } else {
129 Map<String, String> parameters = parseParameterList(pref);
130 ellps = parseEllipsoid(parameters);
131 datum = parseDatum(parameters, ellps);
132 proj = parseProjection(parameters, ellps);
133 String s = parameters.get(Param.x_0.key);
134 if (s != null) {
135 this.x_0 = parseDouble(s, Param.x_0.key);
136 }
137 s = parameters.get(Param.y_0.key);
138 if (s != null) {
139 this.y_0 = parseDouble(s, Param.y_0.key);
140 }
141 s = parameters.get(Param.lon_0.key);
142 if (s != null) {
143 this.lon_0 = parseAngle(s, Param.lon_0.key);
144 }
145 s = parameters.get(Param.k_0.key);
146 if (s != null) {
147 this.k_0 = parseDouble(s, Param.k_0.key);
148 }
149 s = parameters.get(Param.bounds.key);
150 if (s != null) {
151 this.bounds = parseBounds(s);
152 }
153 }
154 }
155
156 private Map<String, String> parseParameterList(String pref) throws ProjectionConfigurationException {
157 Map<String, String> parameters = new HashMap<String, String>();
158 String[] parts = pref.trim().split("\\s+");
159 if (pref.trim().isEmpty()) {
160 parts = new String[0];
161 }
162 for (int i = 0; i < parts.length; i++) {
163 String part = parts[i];
164 if (part.isEmpty() || part.charAt(0) != '+')
165 throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
166 Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part);
167 if (m.matches()) {
168 String key = m.group(1);
169 // alias
170 if (key.equals("k")) {
171 key = Param.k_0.key;
172 }
173 String value = null;
174 if (m.groupCount() >= 3) {
175 value = m.group(3);
176 // same aliases
177 if (key.equals(Param.proj.key)) {
178 if (value.equals("longlat") || value.equals("latlon") || value.equals("latlong")) {
179 value = "lonlat";
180 }
181 }
182 }
183 if (!Param.paramsByKey.containsKey(key))
184 throw new ProjectionConfigurationException(tr("Unkown parameter: ''{0}''.", key));
185 if (Param.paramsByKey.get(key).hasValue && value == null)
186 throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
187 if (!Param.paramsByKey.get(key).hasValue && value != null)
188 throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
189 parameters.put(key, value);
190 } else
191 throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
192 }
193 // recursive resolution of +init includes
194 String initKey = parameters.get(Param.init.key);
195 if (initKey != null) {
196 String init = Projections.getInit(initKey);
197 if (init == null)
198 throw new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey));
199 Map<String, String> initp = null;
200 try {
201 initp = parseParameterList(init);
202 } catch (ProjectionConfigurationException ex) {
203 throw new ProjectionConfigurationException(tr(initKey+": "+ex.getMessage()));
204 }
205 for (Map.Entry<String, String> e : parameters.entrySet()) {
206 initp.put(e.getKey(), e.getValue());
207 }
208 return initp;
209 }
210 return parameters;
211 }
212
213 public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
214 String code = parameters.get(Param.ellps.key);
215 if (code != null) {
216 Ellipsoid ellipsoid = Projections.getEllipsoid(code);
217 if (ellipsoid == null) {
218 throw new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code));
219 } else {
220 return ellipsoid;
221 }
222 }
223 String s = parameters.get(Param.a.key);
224 if (s != null) {
225 double a = parseDouble(s, Param.a.key);
226 if (parameters.get(Param.es.key) != null) {
227 double es = parseDouble(parameters, Param.es.key);
228 return Ellipsoid.create_a_es(a, es);
229 }
230 if (parameters.get(Param.rf.key) != null) {
231 double rf = parseDouble(parameters, Param.rf.key);
232 return Ellipsoid.create_a_rf(a, rf);
233 }
234 if (parameters.get(Param.f.key) != null) {
235 double f = parseDouble(parameters, Param.f.key);
236 return Ellipsoid.create_a_f(a, f);
237 }
238 if (parameters.get(Param.b.key) != null) {
239 double b = parseDouble(parameters, Param.b.key);
240 return Ellipsoid.create_a_b(a, b);
241 }
242 }
243 if (parameters.containsKey(Param.a.key) ||
244 parameters.containsKey(Param.es.key) ||
245 parameters.containsKey(Param.rf.key) ||
246 parameters.containsKey(Param.f.key) ||
247 parameters.containsKey(Param.b.key))
248 throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
249 if (parameters.containsKey(Param.no_defs.key))
250 throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
251 // nothing specified, use WGS84 as default
252 return Ellipsoid.WGS84;
253 }
254
255 public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
256 String nadgridsId = parameters.get(Param.nadgrids.key);
257 if (nadgridsId != null) {
258 if (nadgridsId.startsWith("@")) {
259 nadgridsId = nadgridsId.substring(1);
260 }
261 if (nadgridsId.equals("null"))
262 return new NullDatum(null, ellps);
263 NTV2GridShiftFileWrapper nadgrids = Projections.getNTV2Grid(nadgridsId);
264 if (nadgrids == null)
265 throw new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", nadgridsId));
266 return new NTV2Datum(nadgridsId, null, ellps, nadgrids);
267 }
268
269 String towgs84 = parameters.get(Param.towgs84.key);
270 if (towgs84 != null)
271 return parseToWGS84(towgs84, ellps);
272
273 String datumId = parameters.get(Param.datum.key);
274 if (datumId != null) {
275 Datum datum = Projections.getDatum(datumId);
276 if (datum == null) throw new ProjectionConfigurationException(tr("Unkown datum identifier: ''{0}''", datumId));
277 return datum;
278 }
279 if (parameters.containsKey(Param.no_defs.key))
280 throw new ProjectionConfigurationException(tr("Datum required (+datum=*, +towgs84=* or +nadgirds=*)"));
281 return new CentricDatum(null, null, ellps);
282 }
283
284 public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
285 String[] numStr = paramList.split(",");
286
287 if (numStr.length != 3 && numStr.length != 7)
288 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
289 List<Double> towgs84Param = new ArrayList<Double>();
290 for (int i = 0; i < numStr.length; i++) {
291 try {
292 towgs84Param.add(Double.parseDouble(numStr[i]));
293 } catch (NumberFormatException e) {
294 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", numStr[i]));
295 }
296 }
297 boolean isCentric = true;
298 for (int i = 0; i<towgs84Param.size(); i++) {
299 if (towgs84Param.get(i) != 0.0) {
300 isCentric = false;
301 break;
302 }
303 }
304 if (isCentric)
305 return new CentricDatum(null, null, ellps);
306 boolean is3Param = true;
307 for (int i = 3; i<towgs84Param.size(); i++) {
308 if (towgs84Param.get(i) != 0.0) {
309 is3Param = false;
310 break;
311 }
312 }
313 if (is3Param)
314 return new ThreeParameterDatum(null, null, ellps,
315 towgs84Param.get(0),
316 towgs84Param.get(1),
317 towgs84Param.get(2));
318 else
319 return new SevenParameterDatum(null, null, ellps,
320 towgs84Param.get(0),
321 towgs84Param.get(1),
322 towgs84Param.get(2),
323 towgs84Param.get(3),
324 towgs84Param.get(4),
325 towgs84Param.get(5),
326 towgs84Param.get(6));
327 }
328
329 public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
330 String id = (String) parameters.get(Param.proj.key);
331 if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
332
333 Proj proj = Projections.getBaseProjection(id);
334 if (proj == null) throw new ProjectionConfigurationException(tr("Unkown projection identifier: ''{0}''", id));
335
336 ProjParameters projParams = new ProjParameters();
337
338 projParams.ellps = ellps;
339
340 String s;
341 s = parameters.get(Param.lat_0.key);
342 if (s != null) {
343 projParams.lat_0 = parseAngle(s, Param.lat_0.key);
344 }
345 s = parameters.get(Param.lat_1.key);
346 if (s != null) {
347 projParams.lat_1 = parseAngle(s, Param.lat_1.key);
348 }
349 s = parameters.get(Param.lat_2.key);
350 if (s != null) {
351 projParams.lat_2 = parseAngle(s, Param.lat_2.key);
352 }
353 proj.initialize(projParams);
354 return proj;
355 }
356
357 public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
358 String[] numStr = boundsStr.split(",");
359 if (numStr.length != 4)
360 throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
361 return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
362 parseAngle(numStr[0], "minlon (+bounds)"),
363 parseAngle(numStr[3], "maxlat (+bounds)"),
364 parseAngle(numStr[2], "maxlon (+bounds)"), false);
365 }
366
367 public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
368 String doubleStr = parameters.get(parameterName);
369 if (doubleStr == null && parameters.containsKey(parameterName))
370 throw new ProjectionConfigurationException(
371 tr("Expected number argument for parameter ''{0}''", parameterName));
372 return parseDouble(doubleStr, parameterName);
373 }
374
375 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
376 try {
377 return Double.parseDouble(doubleStr);
378 } catch (NumberFormatException e) {
379 throw new ProjectionConfigurationException(
380 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr));
381 }
382 }
383
384 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
385 String s = angleStr;
386 double value = 0;
387 boolean neg = false;
388 Matcher m = Pattern.compile("^-").matcher(s);
389 if (m.find()) {
390 neg = true;
391 s = s.substring(m.end());
392 }
393 final String FLOAT = "(\\d+(\\.\\d*)?)";
394 boolean dms = false;
395 double deg = 0.0, min = 0.0, sec = 0.0;
396 // degrees
397 m = Pattern.compile("^"+FLOAT+"d").matcher(s);
398 if (m.find()) {
399 s = s.substring(m.end());
400 deg = Double.parseDouble(m.group(1));
401 dms = true;
402 }
403 // minutes
404 m = Pattern.compile("^"+FLOAT+"'").matcher(s);
405 if (m.find()) {
406 s = s.substring(m.end());
407 min = Double.parseDouble(m.group(1));
408 dms = true;
409 }
410 // seconds
411 m = Pattern.compile("^"+FLOAT+"\"").matcher(s);
412 if (m.find()) {
413 s = s.substring(m.end());
414 sec = Double.parseDouble(m.group(1));
415 dms = true;
416 }
417 // plain number (in degrees)
418 if (dms) {
419 value = deg + (min/60.0) + (sec/3600.0);
420 } else {
421 m = Pattern.compile("^"+FLOAT).matcher(s);
422 if (m.find()) {
423 s = s.substring(m.end());
424 value += Double.parseDouble(m.group(1));
425 }
426 }
427 m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s);
428 if (m.find()) {
429 s = s.substring(m.end());
430 } else {
431 m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s);
432 if (m.find()) {
433 s = s.substring(m.end());
434 neg = !neg;
435 }
436 }
437 if (neg) {
438 value = -value;
439 }
440 if (!s.isEmpty()) {
441 throw new ProjectionConfigurationException(
442 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
443 }
444 return value;
445 }
446
447 @Override
448 public Integer getEpsgCode() {
449 if (code != null && code.startsWith("EPSG:")) {
450 try {
451 return Integer.parseInt(code.substring(5));
452 } catch (NumberFormatException e) {}
453 }
454 return null;
455 }
456
457 @Override
458 public String toCode() {
459 return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref);
460 }
461
462 @Override
463 public String getCacheDirectoryName() {
464 return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
465 }
466
467 @Override
468 public Bounds getWorldBoundsLatLon() {
469 if (bounds != null) return bounds;
470 return new Bounds(
471 new LatLon(-90.0, -180.0),
472 new LatLon(90.0, 180.0));
473 }
474
475 @Override
476 public String toString() {
477 return name != null ? name : tr("Custom Projection");
478 }
479 }