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