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