// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.projection;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.projection.datum.CentricDatum;
import org.openstreetmap.josm.data.projection.datum.Datum;
import org.openstreetmap.josm.data.projection.datum.NTV2Datum;
import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
import org.openstreetmap.josm.data.projection.datum.NullDatum;
import org.openstreetmap.josm.data.projection.datum.SevenParameterDatum;
import org.openstreetmap.josm.data.projection.datum.ThreeParameterDatum;
import org.openstreetmap.josm.data.projection.datum.WGS84Datum;
import org.openstreetmap.josm.data.projection.proj.Mercator;
import org.openstreetmap.josm.data.projection.proj.Proj;
import org.openstreetmap.josm.data.projection.proj.ProjParameters;
import org.openstreetmap.josm.tools.Utils;
/**
* Custom projection.
*
* Inspired by PROJ.4 and Proj4J.
* @since 5072
*/
public class CustomProjection extends AbstractProjection {
/**
* pref String that defines the projection
*
* null means fall back mode (Mercator)
*/
protected String pref;
protected String name;
protected String code;
protected String cacheDir;
protected Bounds bounds;
/**
* Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>.
* @since 7370 (public)
*/
public static enum Param {
/** False easting */
x_0("x_0", true),
/** False northing */
y_0("y_0", true),
/** Central meridian */
lon_0("lon_0", true),
/** Scaling factor */
k_0("k_0", true),
/** Ellipsoid name (see {@code proj -le}) */
ellps("ellps", true),
/** Semimajor radius of the ellipsoid axis */
a("a", true),
/** Eccentricity of the ellipsoid squared */
es("es", true),
/** Reciprocal of the ellipsoid flattening term (e.g. 298) */
rf("rf", true),
/** Flattening of the ellipsoid = 1-sqrt(1-e^2) */
f("f", true),
/** Semiminor radius of the ellipsoid axis */
b("b", true),
/** Datum name (see {@code proj -ld}) */
datum("datum", true),
/** 3 or 7 term datum transform parameters */
towgs84("towgs84", true),
/** Filename of NTv2 grid file to use for datum transforms */
nadgrids("nadgrids", true),
/** Projection name (see {@code proj -l}) */
proj("proj", true),
/** Latitude of origin */
lat_0("lat_0", true),
/** Latitude of first standard parallel */
lat_1("lat_1", true),
/** Latitude of second standard parallel */
lat_2("lat_2", true),
/** the exact proj.4 string will be preserved in the WKT representation */
wktext("wktext", false), // ignored
/** meters, US survey feet, etc. */
units("units", true), // ignored
/** Don't use the /usr/share/proj/proj_def.dat defaults file */
no_defs("no_defs", false),
init("init", true),
// JOSM extensions, not present in PROJ.4
wmssrs("wmssrs", true),
bounds("bounds", true);
/** Parameter key */
public final String key;
/** {@code true} if the parameter has a value */
public final boolean hasValue;
/** Map of all parameters by key */
public static final Map<String, Param> paramsByKey = new HashMap<>();
static {
for (Param p : Param.values()) {
paramsByKey.put(p.key, p);
}
}
Param(String key, boolean hasValue) {
this.key = key;
this.hasValue = hasValue;
}
}
/**
* Constructs a new empty {@code CustomProjection}.
*/
public CustomProjection() {
}
/**
* Constructs a new {@code CustomProjection} with given parameters.
* @param pref String containing projection parameters (ex: "+proj=tmerc +lon_0=-3 +k_0=0.9996 +x_0=500000 +ellps=WGS84 +datum=WGS84 +bounds=-8,-5,2,85")
*/
public CustomProjection(String pref) {
this(null, null, pref, null);
}
/**
* Constructs a new {@code CustomProjection} with given name, code and parameters.
*
* @param name describe projection in one or two words
* @param code unique code for this projection - may be null
* @param pref the string that defines the custom projection
* @param cacheDir cache directory name
*/
public CustomProjection(String name, String code, String pref, String cacheDir) {
this.name = name;
this.code = code;
this.pref = pref;
this.cacheDir = cacheDir;
try {
update(pref);
} catch (ProjectionConfigurationException ex) {
try {
update(null);
} catch (ProjectionConfigurationException ex1) {
throw new RuntimeException(ex1);
}
}
}
/**
* Updates this {@code CustomProjection} with given parameters.
* @param pref String containing projection parameters (ex: "+proj=lonlat +ellps=WGS84 +datum=WGS84 +bounds=-180,-90,180,90")
* @throws ProjectionConfigurationException if {@code pref} cannot be parsed properly
*/
public final void update(String pref) throws ProjectionConfigurationException {
this.pref = pref;
if (pref == null) {
ellps = Ellipsoid.WGS84;
datum = WGS84Datum.INSTANCE;
proj = new Mercator();
bounds = new Bounds(
-85.05112877980659, -180.0,
85.05112877980659, 180.0, true);
} else {
Map<String, String> parameters = parseParameterList(pref);
ellps = parseEllipsoid(parameters);
datum = parseDatum(parameters, ellps);
proj = parseProjection(parameters, ellps);
String s = parameters.get(Param.x_0.key);
if (s != null) {
this.x_0 = parseDouble(s, Param.x_0.key);
}
s = parameters.get(Param.y_0.key);
if (s != null) {
this.y_0 = parseDouble(s, Param.y_0.key);
}
s = parameters.get(Param.lon_0.key);
if (s != null) {
this.lon_0 = parseAngle(s, Param.lon_0.key);
}
s = parameters.get(Param.k_0.key);
if (s != null) {
this.k_0 = parseDouble(s, Param.k_0.key);
}
s = parameters.get(Param.bounds.key);
if (s != null) {
this.bounds = parseBounds(s);
}
s = parameters.get(Param.wmssrs.key);
if (s != null) {
this.code = s;
}
}
}
private Map<String, String> parseParameterList(String pref) throws ProjectionConfigurationException {
Map<String, String> parameters = new HashMap<>();
String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim());
if (pref.trim().isEmpty()) {
parts = new String[0];
}
for (String part : parts) {
if (part.isEmpty() || part.charAt(0) != '+')
throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
Matcher m = Pattern.compile("\\+([a-zA-Z0-9_]+)(=(.*))?").matcher(part);
if (m.matches()) {
String key = m.group(1);
// alias
if ("k".equals(key)) {
key = Param.k_0.key;
}
String value = null;
if (m.groupCount() >= 3) {
value = m.group(3);
// some aliases
if (key.equals(Param.proj.key)) {
if ("longlat".equals(value) || "latlon".equals(value) || "latlong".equals(value)) {
value = "lonlat";
}
}
}
if (!Param.paramsByKey.containsKey(key))
throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
if (Param.paramsByKey.get(key).hasValue && value == null)
throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
if (!Param.paramsByKey.get(key).hasValue && value != null)
throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
parameters.put(key, value);
} else
throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
}
// recursive resolution of +init includes
String initKey = parameters.get(Param.init.key);
if (initKey != null) {
String init = Projections.getInit(initKey);
if (init == null)
throw new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey));
Map<String, String> initp = null;
try {
initp = parseParameterList(init);
} catch (ProjectionConfigurationException ex) {
throw new ProjectionConfigurationException(tr(initKey+": "+ex.getMessage()), ex);
}
for (Map.Entry<String, String> e : parameters.entrySet()) {
initp.put(e.getKey(), e.getValue());
}
return initp;
}
return parameters;
}
public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
String code = parameters.get(Param.ellps.key);
if (code != null) {
Ellipsoid ellipsoid = Projections.getEllipsoid(code);
if (ellipsoid == null) {
throw new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code));
} else {
return ellipsoid;
}
}
String s = parameters.get(Param.a.key);
if (s != null) {
double a = parseDouble(s, Param.a.key);
if (parameters.get(Param.es.key) != null) {
double es = parseDouble(parameters, Param.es.key);
return Ellipsoid.create_a_es(a, es);
}
if (parameters.get(Param.rf.key) != null) {
double rf = parseDouble(parameters, Param.rf.key);
return Ellipsoid.create_a_rf(a, rf);
}
if (parameters.get(Param.f.key) != null) {
double f = parseDouble(parameters, Param.f.key);
return Ellipsoid.create_a_f(a, f);
}
if (parameters.get(Param.b.key) != null) {
double b = parseDouble(parameters, Param.b.key);
return Ellipsoid.create_a_b(a, b);
}
}
if (parameters.containsKey(Param.a.key) ||
parameters.containsKey(Param.es.key) ||
parameters.containsKey(Param.rf.key) ||
parameters.containsKey(Param.f.key) ||
parameters.containsKey(Param.b.key))
throw new ProjectionConfigurationException(tr("Combination of ellipsoid parameters is not supported."));
if (parameters.containsKey(Param.no_defs.key))
throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
// nothing specified, use WGS84 as default
return Ellipsoid.WGS84;
}
public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
String nadgridsId = parameters.get(Param.nadgrids.key);
if (nadgridsId != null) {
if (nadgridsId.startsWith("@")) {
nadgridsId = nadgridsId.substring(1);
}
if ("null".equals(nadgridsId))
return new NullDatum(null, ellps);
NTV2GridShiftFileWrapper nadgrids = Projections.getNTV2Grid(nadgridsId);
if (nadgrids == null)
throw new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", nadgridsId));
return new NTV2Datum(nadgridsId, null, ellps, nadgrids);
}
String towgs84 = parameters.get(Param.towgs84.key);
if (towgs84 != null)
return parseToWGS84(towgs84, ellps);
String datumId = parameters.get(Param.datum.key);
if (datumId != null) {
Datum datum = Projections.getDatum(datumId);
if (datum == null) throw new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId));
return datum;
}
if (parameters.containsKey(Param.no_defs.key))
throw new ProjectionConfigurationException(tr("Datum required (+datum=*, +towgs84=* or +nadgrids=*)"));
return new CentricDatum(null, null, ellps);
}
public Datum parseToWGS84(String paramList, Ellipsoid ellps) throws ProjectionConfigurationException {
String[] numStr = paramList.split(",");
if (numStr.length != 3 && numStr.length != 7)
throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''towgs84'' (must be 3 or 7)"));
List<Double> towgs84Param = new ArrayList<>();
for (String str : numStr) {
try {
towgs84Param.add(Double.parseDouble(str));
} catch (NumberFormatException e) {
throw new ProjectionConfigurationException(tr("Unable to parse value of parameter ''towgs84'' (''{0}'')", str), e);
}
}
boolean isCentric = true;
for (Double param : towgs84Param) {
if (param != 0.0) {
isCentric = false;
break;
}
}
if (isCentric)
return new CentricDatum(null, null, ellps);
boolean is3Param = true;
for (int i = 3; i<towgs84Param.size(); i++) {
if (towgs84Param.get(i) != 0.0) {
is3Param = false;
break;
}
}
if (is3Param)
return new ThreeParameterDatum(null, null, ellps,
towgs84Param.get(0),
towgs84Param.get(1),
towgs84Param.get(2));
else
return new SevenParameterDatum(null, null, ellps,
towgs84Param.get(0),
towgs84Param.get(1),
towgs84Param.get(2),
towgs84Param.get(3),
towgs84Param.get(4),
towgs84Param.get(5),
towgs84Param.get(6));
}
public Proj parseProjection(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
String id = parameters.get(Param.proj.key);
if (id == null) throw new ProjectionConfigurationException(tr("Projection required (+proj=*)"));
Proj proj = Projections.getBaseProjection(id);
if (proj == null) throw new ProjectionConfigurationException(tr("Unknown projection identifier: ''{0}''", id));
ProjParameters projParams = new ProjParameters();
projParams.ellps = ellps;
String s;
s = parameters.get(Param.lat_0.key);
if (s != null) {
projParams.lat_0 = parseAngle(s, Param.lat_0.key);
}
s = parameters.get(Param.lat_1.key);
if (s != null) {
projParams.lat_1 = parseAngle(s, Param.lat_1.key);
}
s = parameters.get(Param.lat_2.key);
if (s != null) {
projParams.lat_2 = parseAngle(s, Param.lat_2.key);
}
proj.initialize(projParams);
return proj;
}
public static Bounds parseBounds(String boundsStr) throws ProjectionConfigurationException {
String[] numStr = boundsStr.split(",");
if (numStr.length != 4)
throw new ProjectionConfigurationException(tr("Unexpected number of arguments for parameter ''+bounds'' (must be 4)"));
return new Bounds(parseAngle(numStr[1], "minlat (+bounds)"),
parseAngle(numStr[0], "minlon (+bounds)"),
parseAngle(numStr[3], "maxlat (+bounds)"),
parseAngle(numStr[2], "maxlon (+bounds)"), false);
}
public static double parseDouble(Map<String, String> parameters, String parameterName) throws ProjectionConfigurationException {
if (!parameters.containsKey(parameterName))
throw new IllegalArgumentException(tr("Unknown parameter ''{0}''", parameterName));
String doubleStr = parameters.get(parameterName);
if (doubleStr == null)
throw new ProjectionConfigurationException(
tr("Expected number argument for parameter ''{0}''", parameterName));
return parseDouble(doubleStr, parameterName);
}
public static double parseDouble(String doubleStr, String parameterName) throws ProjectionConfigurationException {
try {
return Double.parseDouble(doubleStr);
} catch (NumberFormatException e) {
throw new ProjectionConfigurationException(
tr("Unable to parse value ''{1}'' of parameter ''{0}'' as number.", parameterName, doubleStr), e);
}
}
public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
String s = angleStr;
double value = 0;
boolean neg = false;
Matcher m = Pattern.compile("^-").matcher(s);
if (m.find()) {
neg = true;
s = s.substring(m.end());
}
final String FLOAT = "(\\d+(\\.\\d*)?)";
boolean dms = false;
double deg = 0.0, min = 0.0, sec = 0.0;
// degrees
m = Pattern.compile("^"+FLOAT+"d").matcher(s);
if (m.find()) {
s = s.substring(m.end());
deg = Double.parseDouble(m.group(1));
dms = true;
}
// minutes
m = Pattern.compile("^"+FLOAT+"'").matcher(s);
if (m.find()) {
s = s.substring(m.end());
min = Double.parseDouble(m.group(1));
dms = true;
}
// seconds
m = Pattern.compile("^"+FLOAT+"\"").matcher(s);
if (m.find()) {
s = s.substring(m.end());
sec = Double.parseDouble(m.group(1));
dms = true;
}
// plain number (in degrees)
if (dms) {
value = deg + (min/60.0) + (sec/3600.0);
} else {
m = Pattern.compile("^"+FLOAT).matcher(s);
if (m.find()) {
s = s.substring(m.end());
value += Double.parseDouble(m.group(1));
}
}
m = Pattern.compile("^(N|E)", Pattern.CASE_INSENSITIVE).matcher(s);
if (m.find()) {
s = s.substring(m.end());
} else {
m = Pattern.compile("^(S|W)", Pattern.CASE_INSENSITIVE).matcher(s);
if (m.find()) {
s = s.substring(m.end());
neg = !neg;
}
}
if (neg) {
value = -value;
}
if (!s.isEmpty()) {
throw new ProjectionConfigurationException(
tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
}
return value;
}
@Override
public Integer getEpsgCode() {
if (code != null && code.startsWith("EPSG:")) {
try {
return Integer.parseInt(code.substring(5));
} catch (NumberFormatException e) {
Main.warn(e);
}
}
return null;
}
@Override
public String toCode() {
return code != null ? code : "proj:" + (pref == null ? "ERROR" : pref);
}
@Override
public String getCacheDirectoryName() {
return cacheDir != null ? cacheDir : "proj-"+Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
}
@Override
public Bounds getWorldBoundsLatLon() {
if (bounds != null) return bounds;
return new Bounds(
new LatLon(-90.0, -180.0),
new LatLon(90.0, 180.0));
}
@Override
public String toString() {
return name != null ? name : tr("Custom Projection");
}
}