// 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.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
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.ProjectionBounds;
import org.openstreetmap.josm.data.coor.EastNorth;
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.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.ICentralMeridianProvider;
import org.openstreetmap.josm.data.projection.proj.IScaleFactorProvider;
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.JosmRuntimeException;
import org.openstreetmap.josm.tools.Utils;
import org.openstreetmap.josm.tools.bugreport.BugReport;
/**
* Custom projection.
*
* Inspired by PROJ.4 and Proj4J.
* @since 5072
*/
public class CustomProjection extends AbstractProjection {
/*
* Equation for METER_PER_UNIT_DEGREE taken from:
* https://github.com/openlayers/ol3/blob/master/src/ol/proj/epsg4326projection.js#L58
* Value for Radius taken form:
* https://github.com/openlayers/ol3/blob/master/src/ol/sphere/wgs84sphere.js#L11
*/
private static final double METER_PER_UNIT_DEGREE = 2 * Math.PI * 6378137.0 / 360;
private static final Map<String, Double> UNITS_TO_METERS = getUnitsToMeters();
private static final Map<String, Double> PRIME_MERIDANS = getPrimeMeridians();
/**
* 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;
private double metersPerUnitWMTS;
private String axis = "enu"; // default axis orientation is East, North, Up
private static final List<String> LON_LAT_VALUES = Arrays.asList("longlat", "latlon", "latlong");
/**
* Proj4-like projection parameters. See <a href="https://trac.osgeo.org/proj/wiki/GenParms">reference</a>.
* @since 7370 (public)
*/
public enum Param {
/** False easting */
x_0("x_0", true),
/** False northing */
y_0("y_0", true),
/** Central meridian */
lon_0("lon_0", true),
/** Prime meridian */
pm("pm", 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),
/** Latitude of true scale (Polar Stereographic) */
lat_ts("lat_ts", true),
/** longitude of the center of the projection (Oblique Mercator) */
lonc("lonc", true),
/** azimuth (true) of the center line passing through the center of the
* projection (Oblique Mercator) */
alpha("alpha", true),
/** rectified bearing of the center line (Oblique Mercator) */
gamma("gamma", true),
/** select "Hotine" variant of Oblique Mercator */
no_off("no_off", false),
/** legacy alias for no_off */
no_uoff("no_uoff", false),
/** longitude of first point (Oblique Mercator) */
lon_1("lon_1", true),
/** longitude of second point (Oblique Mercator) */
lon_2("lon_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),
/** Don't use the /usr/share/proj/proj_def.dat defaults file */
no_defs("no_defs", false),
init("init", true),
/** crs units to meter multiplier */
to_meter("to_meter", true),
/** definition of axis for projection */
axis("axis", true),
/** UTM zone */
zone("zone", true),
/** indicate southern hemisphere for UTM */
south("south", false),
/** vertical units - ignore, as we don't use height information */
vunits("vunits", 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 */
static final Map<String, Param> paramsByKey = new ConcurrentHashMap<>();
static {
for (Param p : Param.values()) {
paramsByKey.put(p.key, p);
}
// alias
paramsByKey.put("k", Param.k_0);
}
Param(String key, boolean hasValue) {
this.key = key;
this.hasValue = hasValue;
}
}
private enum Polarity {
NORTH(LatLon.NORTH_POLE),
SOUTH(LatLon.SOUTH_POLE);
private final LatLon latlon;
Polarity(LatLon latlon) {
this.latlon = latlon;
}
private LatLon getLatLon() {
return latlon;
}
}
private EnumMap<Polarity, EastNorth> polesEN;
/**
* Constructs a new empty {@code CustomProjection}.
*/
public CustomProjection() {
// contents can be set later with update()
}
/**
* 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) {
Main.trace(ex);
try {
update(null);
} catch (ProjectionConfigurationException ex1) {
throw BugReport.intercept(ex1).put("name", name).put("code", code).put("pref", pref);
}
}
}
/**
* 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, false);
parameters = resolveInits(parameters, false);
ellps = parseEllipsoid(parameters);
datum = parseDatum(parameters, ellps);
if (ellps == null) {
ellps = datum.getEllipsoid();
}
proj = parseProjection(parameters, ellps);
// "utm" is a shortcut for a set of parameters
if ("utm".equals(parameters.get(Param.proj.key))) {
Integer zone;
try {
zone = Integer.valueOf(Optional.ofNullable(parameters.get(Param.zone.key)).orElseThrow(
() -> new ProjectionConfigurationException(tr("UTM projection (''+proj=utm'') requires ''+zone=...'' parameter."))));
} catch (NumberFormatException e) {
zone = null;
}
if (zone == null || zone < 1 || zone > 60)
throw new ProjectionConfigurationException(tr("Expected integer value in range 1-60 for ''+zone=...'' parameter."));
this.lon0 = 6d * zone - 183d;
this.k0 = 0.9996;
this.x0 = 500_000;
this.y0 = parameters.containsKey(Param.south.key) ? 10_000_000 : 0;
}
String s = parameters.get(Param.x_0.key);
if (s != null) {
this.x0 = parseDouble(s, Param.x_0.key);
}
s = parameters.get(Param.y_0.key);
if (s != null) {
this.y0 = parseDouble(s, Param.y_0.key);
}
s = parameters.get(Param.lon_0.key);
if (s != null) {
this.lon0 = parseAngle(s, Param.lon_0.key);
}
if (proj instanceof ICentralMeridianProvider) {
this.lon0 = ((ICentralMeridianProvider) proj).getCentralMeridian();
}
s = parameters.get(Param.pm.key);
if (s != null) {
if (PRIME_MERIDANS.containsKey(s)) {
this.pm = PRIME_MERIDANS.get(s);
} else {
this.pm = parseAngle(s, Param.pm.key);
}
}
s = parameters.get(Param.k_0.key);
if (s != null) {
this.k0 = parseDouble(s, Param.k_0.key);
}
if (proj instanceof IScaleFactorProvider) {
this.k0 *= ((IScaleFactorProvider) proj).getScaleFactor();
}
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;
}
boolean defaultUnits = true;
s = parameters.get(Param.units.key);
if (s != null) {
s = Utils.strip(s, "\"");
if (UNITS_TO_METERS.containsKey(s)) {
this.toMeter = UNITS_TO_METERS.get(s);
this.metersPerUnitWMTS = this.toMeter;
defaultUnits = false;
} else {
throw new ProjectionConfigurationException(tr("No unit found for: {0}", s));
}
}
s = parameters.get(Param.to_meter.key);
if (s != null) {
this.toMeter = parseDouble(s, Param.to_meter.key);
this.metersPerUnitWMTS = this.toMeter;
defaultUnits = false;
}
if (defaultUnits) {
this.toMeter = 1;
this.metersPerUnitWMTS = proj.isGeographic() ? METER_PER_UNIT_DEGREE : 1;
}
s = parameters.get(Param.axis.key);
if (s != null) {
this.axis = s;
}
}
}
/**
* Parse a parameter list to key=value pairs.
*
* @param pref the parameter list
* @param ignoreUnknownParameter true, if unknown parameter should not raise exception
* @return parameters map
* @throws ProjectionConfigurationException in case of invalid parameter
*/
public static Map<String, String> parseParameterList(String pref, boolean ignoreUnknownParameter) throws ProjectionConfigurationException {
Map<String, String> parameters = new HashMap<>();
if (pref.trim().isEmpty()) {
return parameters;
}
Pattern keyPattern = Pattern.compile("\\+(?<key>[a-zA-Z0-9_]+)(=(?<value>.*))?");
String[] parts = Utils.WHITE_SPACES_PATTERN.split(pref.trim());
for (String part : parts) {
Matcher m = keyPattern.matcher(part);
if (m.matches()) {
String key = m.group("key");
String value = m.group("value");
// some aliases
if (key.equals(Param.proj.key) && LON_LAT_VALUES.contains(value)) {
value = "lonlat";
}
Param param = Param.paramsByKey.get(key);
if (param == null) {
if (!ignoreUnknownParameter)
throw new ProjectionConfigurationException(tr("Unknown parameter: ''{0}''.", key));
} else {
if (param.hasValue && value == null)
throw new ProjectionConfigurationException(tr("Value expected for parameter ''{0}''.", key));
if (!param.hasValue && value != null)
throw new ProjectionConfigurationException(tr("No value expected for parameter ''{0}''.", key));
key = param.key; // To be really sure, we might have an alias.
}
parameters.put(key, value);
} else if (!part.startsWith("+")) {
throw new ProjectionConfigurationException(tr("Parameter must begin with a ''+'' character (found ''{0}'')", part));
} else {
throw new ProjectionConfigurationException(tr("Unexpected parameter format (''{0}'')", part));
}
}
return parameters;
}
/**
* Recursive resolution of +init includes.
*
* @param parameters parameters map
* @param ignoreUnknownParameter true, if unknown parameter should not raise exception
* @return parameters map with +init includes resolved
* @throws ProjectionConfigurationException in case of invalid parameter
*/
public static Map<String, String> resolveInits(Map<String, String> parameters, boolean ignoreUnknownParameter)
throws ProjectionConfigurationException {
// recursive resolution of +init includes
String initKey = parameters.get(Param.init.key);
if (initKey != null) {
Map<String, String> initp;
try {
initp = parseParameterList(Optional.ofNullable(Projections.getInit(initKey)).orElseThrow(
() -> new ProjectionConfigurationException(tr("Value ''{0}'' for option +init not supported.", initKey))),
ignoreUnknownParameter);
initp = resolveInits(initp, ignoreUnknownParameter);
} catch (ProjectionConfigurationException ex) {
throw new ProjectionConfigurationException(initKey+": "+ex.getMessage(), ex);
}
initp.putAll(parameters);
return initp;
}
return parameters;
}
/**
* Gets the ellipsoid
* @param parameters The parameters to get the value from
* @return The Ellipsoid as specified with the parameters
* @throws ProjectionConfigurationException in case of invalid parameters
*/
public Ellipsoid parseEllipsoid(Map<String, String> parameters) throws ProjectionConfigurationException {
String code = parameters.get(Param.ellps.key);
if (code != null) {
return Optional.ofNullable(Projections.getEllipsoid(code)).orElseThrow(
() -> new ProjectionConfigurationException(tr("Ellipsoid ''{0}'' not supported.", code)));
}
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.createAes(a, es);
}
if (parameters.get(Param.rf.key) != null) {
double rf = parseDouble(parameters, Param.rf.key);
return Ellipsoid.createArf(a, rf);
}
if (parameters.get(Param.f.key) != null) {
double f = parseDouble(parameters, Param.f.key);
return Ellipsoid.createAf(a, f);
}
if (parameters.get(Param.b.key) != null) {
double b = parseDouble(parameters, Param.b.key);
return Ellipsoid.createAb(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."));
return null;
}
/**
* Gets the datum
* @param parameters The parameters to get the value from
* @param ellps The ellisoid that was previously computed
* @return The Datum as specified with the parameters
* @throws ProjectionConfigurationException in case of invalid parameters
*/
public Datum parseDatum(Map<String, String> parameters, Ellipsoid ellps) throws ProjectionConfigurationException {
String datumId = parameters.get(Param.datum.key);
if (datumId != null) {
return Optional.ofNullable(Projections.getDatum(datumId)).orElseThrow(
() -> new ProjectionConfigurationException(tr("Unknown datum identifier: ''{0}''", datumId)));
}
if (ellps == null) {
if (parameters.containsKey(Param.no_defs.key))
throw new ProjectionConfigurationException(tr("Ellipsoid required (+ellps=* or +a=*, +b=*)"));
// nothing specified, use WGS84 as default
ellps = Ellipsoid.WGS84;
}
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);
final String fNadgridsId = nadgridsId;
return new NTV2Datum(fNadgridsId, null, ellps, Optional.ofNullable(Projections.getNTV2Grid(fNadgridsId)).orElseThrow(
() -> new ProjectionConfigurationException(tr("Grid shift file ''{0}'' for option +nadgrids not supported.", fNadgridsId))));
}
String towgs84 = parameters.get(Param.towgs84.key);
if (towgs84 != null)
return parseToWGS84(towgs84, ellps);
return new NullDatum(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.valueOf(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) {
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) {
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));
}
/**
* Gets a projection using the given ellipsoid
* @param parameters Additional parameters
* @param ellps The {@link Ellipsoid}
* @return The projection
* @throws ProjectionConfigurationException in case of invalid parameters
*/
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=*)"));
// "utm" is not a real projection, but a shortcut for a set of parameters
if ("utm".equals(id)) {
id = "tmerc";
}
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.lat0 = parseAngle(s, Param.lat_0.key);
}
s = parameters.get(Param.lat_1.key);
if (s != null) {
projParams.lat1 = parseAngle(s, Param.lat_1.key);
}
s = parameters.get(Param.lat_2.key);
if (s != null) {
projParams.lat2 = parseAngle(s, Param.lat_2.key);
}
s = parameters.get(Param.lat_ts.key);
if (s != null) {
projParams.lat_ts = parseAngle(s, Param.lat_ts.key);
}
s = parameters.get(Param.lonc.key);
if (s != null) {
projParams.lonc = parseAngle(s, Param.lonc.key);
}
s = parameters.get(Param.alpha.key);
if (s != null) {
projParams.alpha = parseAngle(s, Param.alpha.key);
}
s = parameters.get(Param.gamma.key);
if (s != null) {
projParams.gamma = parseAngle(s, Param.gamma.key);
}
s = parameters.get(Param.lon_1.key);
if (s != null) {
projParams.lon1 = parseAngle(s, Param.lon_1.key);
}
s = parameters.get(Param.lon_2.key);
if (s != null) {
projParams.lon2 = parseAngle(s, Param.lon_2.key);
}
if (parameters.containsKey(Param.no_off.key) || parameters.containsKey(Param.no_uoff.key)) {
projParams.no_off = Boolean.TRUE;
}
proj.initialize(projParams);
return proj;
}
/**
* Converts a string to a bounds object
* @param boundsStr The string as comma separated list of angles.
* @return The bounds.
* @throws ProjectionConfigurationException in case of invalid parameter
* @see CustomProjection#parseAngle(String, String)
*/
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 ProjectionConfigurationException(tr("Unknown parameter ''{0}''", parameterName));
return parseDouble(Optional.ofNullable(parameters.get(parameterName)).orElseThrow(
() -> new ProjectionConfigurationException(tr("Expected number argument for parameter ''{0}''", parameterName))),
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);
}
}
/**
* Convert an angle string to a double value
* @param angleStr The string. e.g. -1.1 or 50d 10' 3"
* @param parameterName Only for error message.
* @return The angle value, in degrees.
* @throws ProjectionConfigurationException in case of invalid parameter
*/
public static double parseAngle(String angleStr, String parameterName) throws ProjectionConfigurationException {
final String floatPattern = "(\\d+(\\.\\d*)?)";
// pattern does all error handling.
Matcher in = Pattern.compile("^(?<neg1>-)?"
+ "(?=\\d)(?:(?<single>" + floatPattern + ")|"
+ "((?<degree>" + floatPattern + ")d)?"
+ "((?<minutes>" + floatPattern + ")\')?"
+ "((?<seconds>" + floatPattern + ")\")?)"
+ "(?:[NE]|(?<neg2>[SW]))?$").matcher(angleStr);
if (!in.find()) {
throw new ProjectionConfigurationException(
tr("Unable to parse value ''{1}'' of parameter ''{0}'' as coordinate value.", parameterName, angleStr));
}
double value = 0;
if (in.group("single") != null) {
value += Double.parseDouble(in.group("single"));
}
if (in.group("degree") != null) {
value += Double.parseDouble(in.group("degree"));
}
if (in.group("minutes") != null) {
value += Double.parseDouble(in.group("minutes")) / 60;
}
if (in.group("seconds") != null) {
value += Double.parseDouble(in.group("seconds")) / 3600;
}
if (in.group("neg1") != null ^ in.group("neg2") != null) {
value = -value;
}
return value;
}
@Override
public Integer getEpsgCode() {
if (code != null && code.startsWith("EPSG:")) {
try {
return Integer.valueOf(code.substring(5));
} catch (NumberFormatException e) {
Main.warn(e);
}
}
return null;
}
@Override
public String toCode() {
if (code != null) {
return code;
} else if (pref != null) {
return "proj:" + pref;
} else {
return "proj:ERROR";
}
}
@Override
public String getCacheDirectoryName() {
if (cacheDir != null) {
return cacheDir;
} else {
return "proj-" + Utils.md5Hex(pref == null ? "" : pref).substring(0, 4);
}
}
@Override
public Bounds getWorldBoundsLatLon() {
if (bounds == null) {
Bounds ab = proj.getAlgorithmBounds();
if (ab != null) {
double minlon = Math.max(ab.getMinLon() + lon0 + pm, -180);
double maxlon = Math.min(ab.getMaxLon() + lon0 + pm, 180);
bounds = new Bounds(ab.getMinLat(), minlon, ab.getMaxLat(), maxlon, false);
} else {
bounds = new Bounds(
new LatLon(-90.0, -180.0),
new LatLon(90.0, 180.0));
}
}
return bounds;
}
@Override
public String toString() {
return name != null ? name : tr("Custom Projection");
}
/**
* Factor to convert units of east/north coordinates to meters.
*
* When east/north coordinates are in degrees (geographic CRS), the scale
* at the equator is taken, i.e. 360 degrees corresponds to the length of
* the equator in meters.
*
* @return factor to convert units to meter
*/
@Override
public double getMetersPerUnit() {
return metersPerUnitWMTS;
}
@Override
public boolean switchXY() {
// TODO: support for other axis orientation such as West South, and Up Down
return this.axis.startsWith("ne");
}
private static Map<String, Double> getUnitsToMeters() {
Map<String, Double> ret = new ConcurrentHashMap<>();
ret.put("km", 1000d);
ret.put("m", 1d);
ret.put("dm", 1d/10);
ret.put("cm", 1d/100);
ret.put("mm", 1d/1000);
ret.put("kmi", 1852.0);
ret.put("in", 0.0254);
ret.put("ft", 0.3048);
ret.put("yd", 0.9144);
ret.put("mi", 1609.344);
ret.put("fathom", 1.8288);
ret.put("chain", 20.1168);
ret.put("link", 0.201168);
ret.put("us-in", 1d/39.37);
ret.put("us-ft", 0.304800609601219);
ret.put("us-yd", 0.914401828803658);
ret.put("us-ch", 20.11684023368047);
ret.put("us-mi", 1609.347218694437);
ret.put("ind-yd", 0.91439523);
ret.put("ind-ft", 0.30479841);
ret.put("ind-ch", 20.11669506);
ret.put("degree", METER_PER_UNIT_DEGREE);
return ret;
}
private static Map<String, Double> getPrimeMeridians() {
Map<String, Double> ret = new ConcurrentHashMap<>();
try {
ret.put("greenwich", 0.0);
ret.put("lisbon", parseAngle("9d07'54.862\"W", null));
ret.put("paris", parseAngle("2d20'14.025\"E", null));
ret.put("bogota", parseAngle("74d04'51.3\"W", null));
ret.put("madrid", parseAngle("3d41'16.58\"W", null));
ret.put("rome", parseAngle("12d27'8.4\"E", null));
ret.put("bern", parseAngle("7d26'22.5\"E", null));
ret.put("jakarta", parseAngle("106d48'27.79\"E", null));
ret.put("ferro", parseAngle("17d40'W", null));
ret.put("brussels", parseAngle("4d22'4.71\"E", null));
ret.put("stockholm", parseAngle("18d3'29.8\"E", null));
ret.put("athens", parseAngle("23d42'58.815\"E", null));
ret.put("oslo", parseAngle("10d43'22.5\"E", null));
} catch (ProjectionConfigurationException ex) {
throw new IllegalStateException(ex);
}
return ret;
}
private static EastNorth getPointAlong(int i, int n, ProjectionBounds r) {
double dEast = (r.maxEast - r.minEast) / n;
double dNorth = (r.maxNorth - r.minNorth) / n;
if (i < n) {
return new EastNorth(r.minEast + i * dEast, r.minNorth);
} else if (i < 2*n) {
i -= n;
return new EastNorth(r.maxEast, r.minNorth + i * dNorth);
} else if (i < 3*n) {
i -= 2*n;
return new EastNorth(r.maxEast - i * dEast, r.maxNorth);
} else if (i < 4*n) {
i -= 3*n;
return new EastNorth(r.minEast, r.maxNorth - i * dNorth);
} else {
throw new AssertionError();
}
}
private EastNorth getPole(Polarity whichPole) {
if (polesEN == null) {
polesEN = new EnumMap<>(Polarity.class);
for (Polarity p : Polarity.values()) {
polesEN.put(p, null);
LatLon ll = p.getLatLon();
try {
EastNorth enPole = latlon2eastNorth(ll);
if (enPole.isValid()) {
// project back and check if the result is somewhat reasonable
LatLon llBack = eastNorth2latlon(enPole);
if (llBack.isValid() && ll.greatCircleDistance(llBack) < 1000) {
polesEN.put(p, enPole);
}
}
} catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
Main.error(e);
}
}
}
return polesEN.get(whichPole);
}
@Override
public Bounds getLatLonBoundsBox(ProjectionBounds r) {
final int n = 10;
Bounds result = new Bounds(eastNorth2latlon(r.getMin()));
result.extend(eastNorth2latlon(r.getMax()));
LatLon llPrev = null;
for (int i = 0; i < 4*n; i++) {
LatLon llNow = eastNorth2latlon(getPointAlong(i, n, r));
result.extend(llNow);
// check if segment crosses 180th meridian and if so, make sure
// to extend bounds to +/-180 degrees longitude
if (llPrev != null) {
double lon1 = llPrev.lon();
double lon2 = llNow.lon();
if (90 < lon1 && lon1 < 180 && -180 < lon2 && lon2 < -90) {
result.extend(new LatLon(llPrev.lat(), 180));
result.extend(new LatLon(llNow.lat(), -180));
}
if (90 < lon2 && lon2 < 180 && -180 < lon1 && lon1 < -90) {
result.extend(new LatLon(llNow.lat(), 180));
result.extend(new LatLon(llPrev.lat(), -180));
}
}
llPrev = llNow;
}
// if the box contains one of the poles, the above method did not get
// correct min/max latitude value
for (Polarity p : Polarity.values()) {
EastNorth pole = getPole(p);
if (pole != null && r.contains(pole)) {
result.extend(p.getLatLon());
}
}
return result;
}
@Override
public ProjectionBounds getEastNorthBoundsBox(ProjectionBounds box, Projection boxProjection) {
final int n = 8;
ProjectionBounds result = null;
for (int i = 0; i < 4*n; i++) {
EastNorth en = latlon2eastNorth(boxProjection.eastNorth2latlon(getPointAlong(i, n, box)));
if (result == null) {
result = new ProjectionBounds(en);
} else {
result.extend(en);
}
}
return result;
}
}