001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.projection; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.util.ArrayList; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Map; 010import java.util.regex.Matcher; 011import java.util.regex.Pattern; 012 013import org.openstreetmap.josm.Main; 014import org.openstreetmap.josm.data.Bounds; 015import org.openstreetmap.josm.data.coor.LatLon; 016import org.openstreetmap.josm.data.projection.datum.CentricDatum; 017import org.openstreetmap.josm.data.projection.datum.Datum; 018import org.openstreetmap.josm.data.projection.datum.NTV2Datum; 019import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper; 020import org.openstreetmap.josm.data.projection.datum.NullDatum; 021import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum; 022import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum; 023import org.openstreetmap.josm.data.projection.datum.WGS84Datum; 024import org.openstreetmap.josm.data.projection.proj.Mercator; 025import org.openstreetmap.josm.data.projection.proj.Proj; 026import org.openstreetmap.josm.data.projection.proj.ProjParameters; 027import org.openstreetmap.josm.tools.Utils; 028 029/** 030 * Custom projection 031 * 032 * Inspired by PROJ.4 and Proj4J. 033 */ 034public class CustomProjection extends AbstractProjection { 035 036 /** 037 * pref String that defines the projection 038 * 039 * null means fall back mode (Mercator) 040 */ 041 protected String pref; 042 protected String name; 043 protected String code; 044 protected String cacheDir; 045 protected Bounds bounds; 046 047 protected static enum Param { 048 049 x_0("x_0", true), 050 y_0("y_0", true), 051 lon_0("lon_0", true), 052 k_0("k_0", true), 053 ellps("ellps", true), 054 a("a", true), 055 es("es", true), 056 rf("rf", true), 057 f("f", true), 058 b("b", true), 059 datum("datum", true), 060 towgs84("towgs84", true), 061 nadgrids("nadgrids", true), 062 proj("proj", true), 063 lat_0("lat_0", true), 064 lat_1("lat_1", true), 065 lat_2("lat_2", true), 066 wktext("wktext", false), // ignored 067 units("units", true), // ignored 068 no_defs("no_defs", false), 069 init("init", true), 070 // JOSM extension, not present in PROJ.4 071 bounds("bounds", true); 072 073 public String key; 074 public boolean hasValue; 075 076 public final static Map<String, Param> paramsByKey = new HashMap<String, Param>(); 077 static { 078 for (Param p : Param.values()) { 079 paramsByKey.put(p.key, p); 080 } 081 } 082 083 Param(String key, boolean hasValue) { 084 this.key = key; 085 this.hasValue = hasValue; 086 } 087 } 088 089 public CustomProjection() { 090 } 091 092 public CustomProjection(String pref) { 093 this(null, null, pref, null); 094 } 095 096 /** 097 * Constructor. 098 * 099 * @param name describe projection in one or two words 100 * @param code unique code for this projection - may be null 101 * @param pref the string that defines the custom projection 102 * @param cacheDir cache directory name 103 */ 104 public CustomProjection(String name, String code, String pref, String cacheDir) { 105 this.name = name; 106 this.code = code; 107 this.pref = pref; 108 this.cacheDir = cacheDir; 109 try { 110 update(pref); 111 } catch (ProjectionConfigurationException ex) { 112 try { 113 update(null); 114 } catch (ProjectionConfigurationException ex1) { 115 throw new RuntimeException(); 116 } 117 } 118 } 119 120 public void update(String pref) throws ProjectionConfigurationException { 121 this.pref = pref; 122 if (pref == null) { 123 ellps = Ellipsoid.WGS84; 124 datum = WGS84Datum.INSTANCE; 125 proj = new Mercator(); 126 bounds = new Bounds( 127 -85.05112877980659, -180.0, 128 85.05112877980659, 180.0, true); 129 } else { 130 Map<String, String> parameters = parseParameterList(pref); 131 ellps = parseEllipsoid(parameters); 132 datum = parseDatum(parameters, ellps); 133 proj = parseProjection(parameters, ellps); 134 String s = parameters.get(Param.x_0.key); 135 if (s != null) { 136 this.x_0 = parseDouble(s, Param.x_0.key); 137 } 138 s = parameters.get(Param.y_0.key); 139 if (s != null) { 140 this.y_0 = parseDouble(s, Param.y_0.key); 141 } 142 s = parameters.get(Param.lon_0.key); 143 if (s != null) { 144 this.lon_0 = parseAngle(s, Param.lon_0.key); 145 } 146 s = parameters.get(Param.k_0.key); 147 if (s != null) { 148 this.k_0 = parseDouble(s, Param.k_0.key); 149 } 150 s = parameters.get(Param.bounds.key); 151 if (s != null) { 152 this.bounds = parseBounds(s); 153 } 154 } 155 } 156 157 private Map<String, String> parseParameterList(String pref) throws ProjectionConfigurationException { 158 Map<String, String> parameters = new HashMap<String, String>(); 159 String[] parts = pref.trim().split("\\s+"); 160 if (pref.trim().isEmpty()) { 161 parts = new String[0]; 162 } 163 for (String part : parts) { 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 +nadgrids=*)")); 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 (String str : numStr) { 291 try { 292 towgs84Param.add(Double.parseDouble(str)); 293 } catch (NumberFormatException e) { 294 throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str)); 295 } 296 } 297 boolean isCentric = true; 298 for (Double param : towgs84Param) { 299 if (param != 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 = 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 if (!parameters.containsKey(parameterName)) 369 throw new IllegalArgumentException(tr("Unknown parameter ''{0}''", parameterName)); 370 String doubleStr = parameters.get(parameterName); 371 if (doubleStr == null) 372 throw new ProjectionConfigurationException( 373 tr("Expected number argument for parameter ''{0}''", parameterName)); 374 return parseDouble(doubleStr, parameterName); 375 } 376 377 public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException { 378 try { 379 return Double.parseDouble(doubleStr); 380 } catch (NumberFormatException e) { 381 throw new ProjectionConfigurationException( 382 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr)); 383 } 384 } 385 386 public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException { 387 String s = angleStr; 388 double value = 0; 389 boolean neg = false; 390 Matcher m = Pattern.compile("^-").matcher(s); 391 if (m.find()) { 392 neg = true; 393 s = s.substring(m.end()); 394 } 395 final String FLOAT = "(\\d+(\\.\\d*)?)"; 396 boolean dms = false; 397 double deg = 0.0, min = 0.0, sec = 0.0; 398 // degrees 399 m = Pattern.compile("^"+FLOAT+"d").matcher(s); 400 if (m.find()) { 401 s = s.substring(m.end()); 402 deg = Double.parseDouble(m.group(1)); 403 dms = true; 404 } 405 // minutes 406 m = Pattern.compile("^"+FLOAT+"'").matcher(s); 407 if (m.find()) { 408 s = s.substring(m.end()); 409 min = Double.parseDouble(m.group(1)); 410 dms = true; 411 } 412 // seconds 413 m = Pattern.compile("^"+FLOAT+"\"").matcher(s); 414 if (m.find()) { 415 s = s.substring(m.end()); 416 sec = Double.parseDouble(m.group(1)); 417 dms = true; 418 } 419 // plain number (in degrees) 420 if (dms) { 421 value = deg + (min/60.0) + (sec/3600.0); 422 } else { 423 m = Pattern.compile("^"+FLOAT).matcher(s); 424 if (m.find()) { 425 s = s.substring(m.end()); 426 value += Double.parseDouble(m.group(1)); 427 } 428 } 429 m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s); 430 if (m.find()) { 431 s = s.substring(m.end()); 432 } else { 433 m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s); 434 if (m.find()) { 435 s = s.substring(m.end()); 436 neg = !neg; 437 } 438 } 439 if (neg) { 440 value = -value; 441 } 442 if (!s.isEmpty()) { 443 throw new ProjectionConfigurationException( 444 tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr)); 445 } 446 return value; 447 } 448 449 @Override 450 public Integer getEpsgCode() { 451 if (code != null && code.startsWith("EPSG:")) { 452 try { 453 return Integer.parseInt(code.substring(5)); 454 } catch (NumberFormatException e) { 455 Main.warn(e); 456 } 457 } 458 return null; 459 } 460 461 @Override 462 public String toCode() { 463 return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref); 464 } 465 466 @Override 467 public String getCacheDirectoryName() { 468 return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4); 469 } 470 471 @Override 472 public Bounds getWorldBoundsLatLon() { 473 if (bounds != null) return bounds; 474 return new Bounds( 475 new LatLon(-90.0, -180.0), 476 new LatLon(90.0, 180.0)); 477 } 478 479 @Override 480 public String toString() { 481 return name != null ? name : tr("Custom Projection"); 482 } 483}