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 }