// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.projection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.DoubleUnaryOperator;
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.Datum;
import org.openstreetmap.josm.data.projection.proj.Proj;
import org.openstreetmap.josm.tools.Utils;
/**
* Implementation of the Projection interface that represents a coordinate reference system and delegates
* the real projection and datum conversion to other classes.
*
* It handles false easting and northing, central meridian and general scale factor before calling the
* delegate projection.
*
* Forwards lat/lon values to the real projection in units of radians.
*
* The fields are named after Proj.4 parameters.
*
* Subclasses of AbstractProjection must set ellps and proj to a non-null value.
* In addition, either datum or nadgrid has to be initialized to some value.
*/
public abstract class AbstractProjection implements Projection {
protected Ellipsoid ellps;
protected Datum datum;
protected Proj proj;
protected double x0; /* false easting (in meters) */
protected double y0; /* false northing (in meters) */
protected double lon0; /* central meridian */
protected double pm; /* prime meridian */
protected double k0 = 1.0; /* general scale factor */
protected double toMeter = 1.0; /* switch from meters to east/north coordinate units */
private volatile ProjectionBounds projectionBoundsBox;
/**
* Get the base ellipsoid that this projection uses.
* @return The {@link Ellipsoid}
*/
public final Ellipsoid getEllipsoid() {
return ellps;
}
/**
* Gets the datum this projection is based on.
* @return The datum
*/
public final Datum getDatum() {
return datum;
}
/**
* Replies the projection (in the narrow sense)
* @return The projection object
*/
public final Proj getProj() {
return proj;
}
/**
* Gets an east offset that gets applied when converting the coordinate
* @return The offset to apply in meter
*/
public final double getFalseEasting() {
return x0;
}
/**
* Gets an north offset that gets applied when converting the coordinate
* @return The offset to apply in meter
*/
public final double getFalseNorthing() {
return y0;
}
/**
* Gets the meridian that this projection is centered on.
* @return The longitude of the meridian.
*/
public final double getCentralMeridian() {
return lon0;
}
public final double getScaleFactor() {
return k0;
}
/**
* Get the factor that converts meters to intended units of east/north coordinates.
*
* For projected coordinate systems, the semi-major axis of the ellipsoid is
* always given in meters, which means the preliminary projection result will
* be in meters as well. This factor is used to convert to the intended units
* of east/north coordinates (e.g. feet in the US).
*
* For geographic coordinate systems, the preliminary "projection" result will
* be in degrees, so there is no reason to convert anything and this factor
* will by 1 by default.
*
* @return factor that converts meters to intended units of east/north coordinates
*/
public final double getToMeter() {
return toMeter;
}
@Override
public EastNorth latlon2eastNorth(LatLon ll) {
ll = datum.fromWGS84(ll);
double[] en = proj.project(Math.toRadians(ll.lat()), Math.toRadians(LatLon.normalizeLon(ll.lon() - lon0 - pm)));
return new EastNorth((ellps.a * k0 * en[0] + x0) / toMeter, (ellps.a * k0 * en[1] + y0) / toMeter);
}
@Override
public LatLon eastNorth2latlon(EastNorth en) {
return eastNorth2latlon(en, LatLon::normalizeLon);
}
@Override
public LatLon eastNorth2latlonClamped(EastNorth en) {
LatLon ll = eastNorth2latlon(en, lon -> Utils.clamp(lon, -180, 180));
Bounds bounds = getWorldBoundsLatLon();
return new LatLon(Utils.clamp(ll.lat(), bounds.getMinLat(), bounds.getMaxLat()),
Utils.clamp(ll.lon(), bounds.getMinLon(), bounds.getMaxLon()));
}
private LatLon eastNorth2latlon(EastNorth en, DoubleUnaryOperator normalizeLon) {
double[] latlonRad = proj.invproject((en.east() * toMeter - x0) / ellps.a / k0, (en.north() * toMeter - y0) / ellps.a / k0);
double lon = Math.toDegrees(latlonRad[1]) + lon0 + pm;
LatLon ll = new LatLon(Math.toDegrees(latlonRad[0]), normalizeLon.applyAsDouble(lon));
return datum.toWGS84(ll);
}
@Override
public Map<ProjectionBounds, Projecting> getProjectingsForArea(ProjectionBounds area) {
if (proj.lonIsLinearToEast()) {
//FIXME: Respect datum?
// wrap the wrold around
Bounds bounds = getWorldBoundsLatLon();
double minEast = latlon2eastNorth(bounds.getMin()).east();
double maxEast = latlon2eastNorth(bounds.getMax()).east();
double dEast = maxEast - minEast;
if ((area.minEast < minEast || area.maxEast > maxEast) && dEast > 0) {
// We could handle the dEast < 0 case but we don't need it atm.
int minChunk = (int) Math.floor((area.minEast - minEast) / dEast);
int maxChunk = (int) Math.floor((area.maxEast - minEast) / dEast);
HashMap<ProjectionBounds, Projecting> ret = new HashMap<>();
for (int chunk = minChunk; chunk <= maxChunk; chunk++) {
ret.put(new ProjectionBounds(Math.max(area.minEast, minEast + chunk * dEast), area.minNorth,
Math.min(area.maxEast, maxEast + chunk * dEast), area.maxNorth),
new ShiftedProjecting(this, new EastNorth(-chunk * dEast, 0)));
}
return ret;
}
}
return Collections.singletonMap(area, this);
}
@Override
public double getDefaultZoomInPPD() {
// this will set the map scaler to about 1000 m
return 10;
}
/**
* @return The EPSG Code of this CRS, null if it doesn't have one.
*/
public abstract Integer getEpsgCode();
/**
* Default implementation of toCode().
* Should be overridden, if there is no EPSG code for this CRS.
*/
@Override
public String toCode() {
return "EPSG:" + getEpsgCode();
}
protected static final double convertMinuteSecond(double minute, double second) {
return (minute/60.0) + (second/3600.0);
}
protected static final double convertDegreeMinuteSecond(double degree, double minute, double second) {
return degree + (minute/60.0) + (second/3600.0);
}
@Override
public final ProjectionBounds getWorldBoundsBoxEastNorth() {
ProjectionBounds result = projectionBoundsBox;
if (result == null) {
synchronized (this) {
result = projectionBoundsBox;
if (result == null) {
Bounds b = getWorldBoundsLatLon();
// add 4 corners
result = new ProjectionBounds(latlon2eastNorth(b.getMin()));
result.extend(latlon2eastNorth(b.getMax()));
result.extend(latlon2eastNorth(new LatLon(b.getMinLat(), b.getMaxLon())));
result.extend(latlon2eastNorth(new LatLon(b.getMaxLat(), b.getMinLon())));
// and trace along the outline
double dLon = (b.getMaxLon() - b.getMinLon()) / 1000;
double dLat = (b.getMaxLat() - b.getMinLat()) / 1000;
for (double lon = b.getMinLon(); lon < b.getMaxLon(); lon += dLon) {
result.extend(latlon2eastNorth(new LatLon(b.getMinLat(), lon)));
result.extend(latlon2eastNorth(new LatLon(b.getMaxLat(), lon)));
}
for (double lat = b.getMinLat(); lat < b.getMaxLat(); lat += dLat) {
result.extend(latlon2eastNorth(new LatLon(lat, b.getMinLon())));
result.extend(latlon2eastNorth(new LatLon(lat, b.getMaxLon())));
}
projectionBoundsBox = result;
}
}
}
return projectionBoundsBox;
}
@Override
public Projection getBaseProjection() {
return this;
}
}