/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* This package contains formulas from the PROJ package of USGS.
* USGS's work is fully acknowledged here. This derived work has
* been relicensed under LGPL with Frank Warmerdam's permission.
*/
package org.geotools.referencing.operation.projection;
import java.awt.geom.Point2D;
import java.util.Collection;
import javax.measure.unit.NonSI;
import org.opengis.util.InternationalString;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.InvalidParameterValueException;
import org.opengis.referencing.operation.CylindricalProjection;
import org.opengis.referencing.operation.MathTransform;
import org.geotools.measure.Angle;
import org.geotools.measure.Latitude;
import org.geotools.metadata.iso.citation.Citations;
import org.geotools.referencing.NamedIdentifier;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import static java.lang.Math.*;
/**
* Oblique Mercator Projection. A conformal, oblique, cylindrical projection with the cylinder
* touching the ellipsoid (or sphere) along a great circle path (the central line). The
* {@linkplain Mercator} and {@linkplain TransverseMercator Transverse Mercator} projections can
* be thought of as special cases of the oblique mercator, where the central line is along the
* equator or a meridian, respectively. The Oblique Mercator projection has been used in
* Switzerland, Hungary, Madagascar, Malaysia, Borneo and the panhandle of Alaska.
*
* The Oblique Mercator projection uses a (U,V) coordinate system, with the
* U axis along the central line. During the forward projection, coordinates from the
* ellipsoid are projected conformally to a sphere of constant total curvature, called the
* "aposphere", before being projected onto the plane. The projection coordinates are further
* convented to a (X,Y) coordinate system by rotating the calculated
* (u,v) coordinates to give output (x,y) coordinates.
* The rotation value is usually the same as the projection azimuth (the angle, east of north, of
* the central line), but some cases allow a separate rotation parameter.
*
* There are two forms of the oblique mercator, differing in the origin of their grid coordinates.
* The {@linkplain HotineObliqueMercator Hotine Oblique Mercator} (EPSG code 9812) has grid
* coordinates start at the intersection of the central line and the equator of the aposphere.
* The {@linkplain ObliqueMercator Oblique Mercator} (EPSG code 9815) is the same, except the
* grid coordinates begin at the central point (where the latitude of center and central line
* intersect). ESRI separates these two case by appending {@code "Natural_Origin"} (for the
* {@code "Hotine_Oblique_Mercator"}) and {@code "Center"} (for the {@code "Oblique_Mercator"})
* to the projection names.
*
* Two different methods are used to specify the central line for the oblique mercator:
* 1) a central point and an azimuth, east of north, describing the central line and
* 2) two points on the central line. The EPSG does not use the two point method,
* while ESRI separates the two cases by putting {@code "Azimuth"} and {@code "Two_Point"}
* in their projection names. Both cases use the point where the {@code "latitude_of_center"}
* parameter crosses the central line as the projection's central point.
* The {@linkplain #centralMeridian central meridian} is not a projection parameter,
* and is instead calculated as the intersection between the central line and the
* equator of the aposphere.
*
* For the azimuth method, the central latitude cannot be ±90.0 degrees
* and the central line cannot be at a maximum or minimum latitude at the central point.
* In the two point method, the latitude of the first and second points cannot be
* equal. Also, the latitude of the first point and central point cannot be
* ±90.0 degrees. Furthermore, the latitude of the first point cannot be 0.0 and
* the latitude of the second point cannot be -90.0 degrees. A change of
* 10-7 radians can allow calculation at these special cases. Snyder's restriction
* of the central latitude being 0.0 has been removed, since the equations appear
* to work correctly in this case.
*
* Azimuth values of 0.0 and ±90.0 degrees are allowed (and used in Hungary
* and Switzerland), though these cases would usually use a Mercator or
* Transverse Mercator projection instead. Azimuth values > 90 degrees cause
* errors in the equations.
*
* The oblique mercator is also called the "Rectified Skew Orthomorphic" (RSO). It appears
* is that the only difference from the oblique mercator is that the RSO allows the rotation
* from the (U,V) to (X,Y) coordinate system to
* be different from the azimuth. This separate parameter is called
* {@code "rectified_grid_angle"} (or {@code "XY_Plane_Rotation"} by ESRI) and is also
* included in the EPSG's parameters for the Oblique Mercator and Hotine Oblique Mercator.
* The rotation parameter is optional in all the non-two point projections and will be
* set to the azimuth if not specified.
*
* Projection cases and aliases implemented by the {@link ObliqueMercator} are:
*
* - {@code Oblique_Mercator} (EPSG code 9815)
* grid coordinates begin at the central point,
* has {@code "rectified_grid_angle"} parameter.
* - {@code Hotine_Oblique_Mercator_Azimuth_Center} (ESRI)
* grid coordinates begin at the central point.
* - {@code Rectified_Skew_Orthomorphic_Center} (ESRI)
* grid coordinates begin at the central point,
* has {@code "rectified_grid_angle"} parameter.
*
* - {@code Hotine_Oblique_Mercator} (EPSG code 9812)
* grid coordinates begin at the interseciton of the central line and aposphere equator,
* has {@code "rectified_grid_angle"} parameter.
* - {@code Hotine_Oblique_Mercator_Azimuth_Natural_Origin} (ESRI)
* grid coordinates begin at the interseciton of the central line and aposphere equator.
* - {@code Rectified_Skew_Orthomorphic_Natural_Origin} (ESRI)
* grid coordinates begin at the interseciton of the central line and aposphere equator,
* has {@code "rectified_grid_angle"} parameter.
*
* - {@code Hotine_Oblique_Mercator_Two_Point_Center} (ESRI)
* grid coordinates begin at the central point.
* - {@code Hotine_Oblique_Mercator_Two_Point_Natural_Origin} (ESRI)
* grid coordinates begin at the interseciton of the central line and aposphere equator.
*
*
* References:
*
* - {@code libproj4} is available at
* libproj4 Miscellanea
* Relevent files are: {@code PJ_omerc.c}, {@code pj_tsfn.c},
* {@code pj_fwd.c}, {@code pj_inv.c} and {@code lib_proj.h}
* - John P. Snyder (Map Projections - A Working Manual,
* U.S. Geological Survey Professional Paper 1395, 1987)
* - "Coordinate Conversions and Transformations including Formulas",
* EPSG Guidence Note Number 7 part 2, Version 24.
* - Gerald Evenden, 2004,
* Documentation of revised Oblique Mercator
*
*
* @see Oblique Mercator projection on MathWorld
* @see "hotine_oblique_mercator" on RemoteSensing.org
* @see "oblique_mercator" on RemoteSensing.org
*
* @since 2.1
*
* @source $URL$
* @version $Id$
* @author Gerald I. Evenden (for original code in Proj4)
* @author Rueben Schulz
*/
public class ObliqueMercator extends MapProjection {
/**
* For compatibility with different versions during deserialization.
*/
private static final long serialVersionUID = 5382294977124711214L;
/**
* Maximum difference allowed when comparing real numbers.
*/
private static final double EPSILON = 1E-6;
/**
* Maximum difference allowed when comparing latitudes.
*/
private static final double EPSILON_LATITUDE = 1E-10;
//////
////// Map projection parameters. The following are NOT used by the transformation
////// methods, but are stored in order to restitute them in WKT formatting. They
////// are made visible ('protected' access) for documentation purpose and because
////// they are user-supplied parameters, not derived coefficients.
//////
/**
* Latitude of the projection centre. This is similar to the
* {@link #latitudeOfOrigin}, but the latitude of origin is the
* Earth equator on aposphere for the oblique mercator.
*/
protected final double latitudeOfCentre;
/**
* Longitude of the projection centre. This is NOT equal
* to the {@link #centralMeridian}, which is the meridian where the
* central line intersects the Earth equator on aposphere.
*
* This parameter applies to the "azimuth" case only.
* It is set to {@link Double#NaN NaN} for the "two points" case.
*/
protected final double longitudeOfCentre;
/**
* The azimuth of the central line passing throught the centre of the projection, in radians.
*/
protected final double azimuth;
/**
* The rectified bearing of the central line, in radians. This is equals to the
* {@linkplain #azimuth} if the {@link Provider#RECTIFIED_GRID_ANGLE "rectified_grid_angle"}
* parameter value is not set.
*/
protected final double rectifiedGridAngle;
// The next parameters still private for now because I'm not
// sure if they should appear in some 'TwoPoint' subclass...
/**
* The latitude of the 1st point used to specify the central line, in radians.
*
* This parameter applies to the "two points" case only.
* It is set to {@link Double#NaN NaN} for the "azimuth" case.
*/
private final double latitudeOf1stPoint;
/**
* The longitude of the 1st point used to specify the central line, in radians.
*
* This parameter applies to the "two points" case only.
* It is set to {@link Double#NaN NaN} for the "azimuth" case.
*/
private final double longitudeOf1stPoint;
/**
* The latitude of the 2nd point used to specify the central line, in radians.
*
* This parameter applies to the "two points" case only.
* It is set to {@link Double#NaN NaN} for the "azimuth" case.
*/
private final double latitudeOf2ndPoint;
/**
* The longitude of the 2nd point used to specify the central line, in radians.
*
* This parameter applies to the "two points" case only.
* It is set to {@link Double#NaN NaN} for the "azimuth" case.
*/
private final double longitudeOf2ndPoint;
//////
////// Map projection coefficients computed from the above parameters.
////// They are the fields used for coordinate transformations.
//////
/**
* Constants used in the transformation.
*/
private final double B, A, E;
/**
* Convenience values equal to {@link #A} / {@link #B},
* {@link #A}×{@link #B}, and {@link #B} / {@link #A}.
*/
private final double ArB, AB, BrA;
/**
* v values when the input latitude is a pole.
*/
private final double v_pole_n, v_pole_s;
/**
* Sine and Cosine values for gamma0 (the angle between the meridian
* and central line at the intersection between the central line and
* the Earth equator on aposphere).
*/
private final double singamma0, cosgamma0;
/**
* Sine and Cosine values for the rotation between (U,V) and
* (X,Y) coordinate systems
*/
private final double sinrot, cosrot;
/**
* u value (in (U,V) coordinate system) of the central point. Used in
* the oblique mercator case. The v value of the central point is 0.0.
*/
private final double u_c;
/**
* {@code true} if using two points on the central line to specify the azimuth.
*/
final boolean twoPoint;
/**
* Constructs a new map projection from the supplied parameters.
*
* @param parameters The parameter values in standard units.
* @throws ParameterNotFoundException if a mandatory parameter is missing.
*
* @since 2.4
*
* @todo Current implementation assumes a "azimuth" case. We may try to detect the
* "two points" case in a future version if needed.
*/
protected ObliqueMercator(final ParameterValueGroup parameters)
throws ParameterNotFoundException
{
this(parameters, Provider.PARAMETERS.descriptors(), false, false);
}
/**
* Constructs a new map projection from the supplied parameters.
*
* @param parameters The parameter values in standard units.
* @param expected The expected parameter descriptors.
* @param twoPoint {@code true} for the "two points" case, or {@code false} for the
* "azimuth" case. The former is used by ESRI but not EPSG.
* @param hotine {@code true} only if invoked by the {@link HotineObliqueMercator}
* constructor.
* @throws ParameterNotFoundException if a mandatory parameter is missing.
*/
ObliqueMercator(final ParameterValueGroup parameters,
final Collection expected,
final boolean twoPoint, final boolean hotine)
throws ParameterNotFoundException
{
// Fetch parameters
super(parameters, expected);
this.twoPoint = twoPoint;
/*
* Sets the 'centralMeridian' and 'latitudeOfOrigin' fields to NaN for safety
* (they are not 'ObliqueMercator' parameters, so the super-class initialized
* them to 0.0 by default). The 'centralMeridian' value will be computed later.
*/
centralMeridian = Double.NaN;
latitudeOfOrigin = Double.NaN;
latitudeOfCentre = doubleValue(expected, Provider.LATITUDE_OF_CENTRE, parameters);
/*
* Checks that 'latitudeOfCentre' is not +- 90 degrees.
* Not checking if 'latitudeOfCentere' is 0, since equations behave correctly.
*/
ensureLatitudeInRange(Provider.LATITUDE_OF_CENTRE, latitudeOfCentre, false);
/*
* Computes common constants now. In a previous version of 'ObliqueMercator', those
* constants were computed a little bit later. We compute them now in order to have
* a single 'if (twoPoint) ... else' statement, which help us to keep every fields
* final and catch some potential errors at compile-time (e.g. unitialized fields).
*/
final double com = sqrt(1.0 - excentricitySquared);
final double sinphi0 = sin(latitudeOfCentre);
final double cosphi0 = cos(latitudeOfCentre);
double temp = cosphi0 * cosphi0;
B = sqrt(1.0 + excentricitySquared * (temp * temp) / (1.0 - excentricitySquared));
final double con = 1.0 - excentricitySquared * sinphi0 * sinphi0;
A = B * com / con;
final double D = B * com / (cosphi0 * sqrt(con));
double F = D * D - 1.0;
if (F < 0.0) {
F = 0.0;
} else {
F = sqrt(F);
if (latitudeOfCentre < 0.0) { // Taking sign of 'latitudeOfCentre'
F = -F;
}
}
F = F += D;
E = F * pow(tsfn(latitudeOfCentre, sinphi0), B);
/*
* Computes the constants that depend on the "twoPoint" vs "azimuth" case. In the
* two points case, we compute them from (LAT_OF_1ST_POINT, LONG_OF_1ST_POINT) and
* (LAT_OF_2ND_POINT, LONG_OF_2ND_POINT). For the "azimuth" case, we compute them
* from LONGITUDE_OF_CENTRE and AZIMUTH.
*/
final double gamma0;
if (twoPoint) {
longitudeOfCentre = Double.NaN; // This is for the "azimuth" case only.
latitudeOf1stPoint = doubleValue(expected, Provider_TwoPoint.LAT_OF_1ST_POINT, parameters);
// Checks that latOf1stPoint is not +-90 degrees
ensureLatitudeInRange(Provider_TwoPoint.LAT_OF_1ST_POINT, latitudeOf1stPoint, false);
longitudeOf1stPoint = doubleValue(expected, Provider_TwoPoint.LONG_OF_1ST_POINT, parameters);
ensureLongitudeInRange(Provider_TwoPoint.LONG_OF_1ST_POINT, longitudeOf1stPoint, true);
latitudeOf2ndPoint = doubleValue(expected, Provider_TwoPoint.LAT_OF_2ND_POINT, parameters);
ensureLatitudeInRange(Provider_TwoPoint.LAT_OF_2ND_POINT, latitudeOf2ndPoint, true);
double longitudeOf2ndPoint; // Will be assigned to the field later.
longitudeOf2ndPoint = doubleValue(expected, Provider_TwoPoint.LONG_OF_2ND_POINT, parameters);
ensureLongitudeInRange(Provider_TwoPoint.LONG_OF_2ND_POINT, longitudeOf2ndPoint, true);
/*
* Ensures that (phi1 != phi2), (phi1 != 0°) and (phi2 != -90°),
* as specified in class javadoc.
*/
ParameterDescriptor desc = null;
Object value = null;
if (abs(latitudeOf1stPoint - latitudeOf2ndPoint) < EPSILON_LATITUDE) {
desc = Provider_TwoPoint.LAT_OF_1ST_POINT;
value = Provider_TwoPoint.LAT_OF_2ND_POINT.getName().getCode();
// Exception will be thrown below.
}
if (abs(latitudeOf1stPoint) < EPSILON_LATITUDE) {
desc = Provider_TwoPoint.LAT_OF_1ST_POINT;
value = new Latitude(latitudeOf1stPoint);
// Exception will be thrown below.
}
if (abs(latitudeOf2ndPoint + PI/2.0) < EPSILON_LATITUDE) {
desc = Provider_TwoPoint.LAT_OF_2ND_POINT;
value = new Latitude(latitudeOf2ndPoint);
// Exception will be thrown below.
}
if (desc != null) {
final String name = desc.getName().getCode();
throw new InvalidParameterValueException(Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2, name, value), name, value);
}
/*
* The coefficients for the "two points" case.
*/
final double H = pow(tsfn(latitudeOf1stPoint, sin(latitudeOf1stPoint)), B);
final double L = pow(tsfn(latitudeOf2ndPoint, sin(latitudeOf2ndPoint)), B);
final double Fp = E / H;
final double P = (L - H) / (L + H);
double J = E * E;
J = (J - L * H) / (J + L * H);
double diff = longitudeOf1stPoint - longitudeOf2ndPoint;
if (diff < -PI) {
longitudeOf2ndPoint -= 2.0 * PI;
} else if (diff > PI) {
longitudeOf2ndPoint += 2.0 * PI;
}
this.longitudeOf2ndPoint = longitudeOf2ndPoint;
centralMeridian = rollLongitude(0.5 * (longitudeOf1stPoint + longitudeOf2ndPoint) -
atan(J * tan(0.5 * B * (longitudeOf1stPoint - longitudeOf2ndPoint)) / P) / B);
gamma0 = atan(2.0 * sin(B * rollLongitude(longitudeOf1stPoint - centralMeridian)) /
(Fp - 1.0 / Fp));
azimuth = asin(D * sin(gamma0));
rectifiedGridAngle = azimuth;
} else {
/*
* Computes coefficients for the "azimuth" case. Set the 1st and 2nd points
* to (NaN,NaN) since they are specific to the "two points" case. They are
* involved in WKT formatting only, not in transformation calculation.
*/
latitudeOf1stPoint = Double.NaN;
longitudeOf1stPoint = Double.NaN;
latitudeOf2ndPoint = Double.NaN;
longitudeOf2ndPoint = Double.NaN;
longitudeOfCentre = doubleValue(expected, Provider.LONGITUDE_OF_CENTRE, parameters);
ensureLongitudeInRange(Provider.LONGITUDE_OF_CENTRE, longitudeOfCentre, true);
azimuth = doubleValue(expected, Provider.AZIMUTH, parameters);
// Already checked for +-360 deg. above.
if ((azimuth > -1.5*PI && azimuth < -0.5*PI) ||
(azimuth > 0.5*PI && azimuth < 1.5*PI))
{
final String name = Provider.AZIMUTH.getName().getCode();
final Angle value = new Angle(toDegrees(azimuth));
throw new InvalidParameterValueException(Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2, name, value), name, value);
}
temp = doubleValue(expected, Provider.RECTIFIED_GRID_ANGLE, parameters);
if (Double.isNaN(temp)) {
temp = azimuth;
}
rectifiedGridAngle = temp;
gamma0 = asin(sin(azimuth) / D);
// Check for asin(+-1.00000001)
temp = 0.5 * (F - 1.0 / F) * tan(gamma0);
if (abs(temp) > 1.0) {
if (abs(abs(temp) - 1.0) > EPSILON) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.TOLERANCE_ERROR));
}
temp = (temp > 0) ? 1.0 : -1.0;
}
centralMeridian = longitudeOfCentre - asin(temp) / B;
}
/*
* More coefficients common to all kind of oblique mercator.
*/
singamma0 = sin(gamma0);
cosgamma0 = cos(gamma0);
sinrot = sin(rectifiedGridAngle);
cosrot = cos(rectifiedGridAngle);
ArB = A / B;
AB = A * B;
BrA = B / A;
v_pole_n = ArB * log(tan(0.5 * (PI/2.0 - gamma0)));
v_pole_s = ArB * log(tan(0.5 * (PI/2.0 + gamma0)));
if (hotine) {
u_c = 0.0;
} else {
if (abs(abs(azimuth) - PI/2.0) < EPSILON_LATITUDE) {
// LongitudeOfCentre = NaN in twoPoint, but azimuth cannot be 90 here (lat1 != lat2)
u_c = A * (longitudeOfCentre - centralMeridian);
} else {
double u_c = abs(ArB * atan2(sqrt(D * D - 1.0), cos(azimuth)));
if (latitudeOfCentre < 0.0) {
u_c = -u_c;
}
this.u_c = u_c;
}
}
}
/**
* {@inheritDoc}
*/
public ParameterDescriptorGroup getParameterDescriptors() {
return (twoPoint) ? Provider_TwoPoint.PARAMETERS : Provider.PARAMETERS;
}
/**
* {@inheritDoc}
*/
@Override
public ParameterValueGroup getParameterValues() {
final ParameterValueGroup values = super.getParameterValues();
final Collection expected = getParameterDescriptors().descriptors();
// Note: we don't need a "if (twoPoint) ... else" statement since
// the "set" method will actually set the value only if applicable.
set(expected, Provider.LATITUDE_OF_CENTRE, values, latitudeOfCentre);
set(expected, Provider.LONGITUDE_OF_CENTRE, values, longitudeOfCentre);
set(expected, Provider.AZIMUTH, values, azimuth);
set(expected, Provider.RECTIFIED_GRID_ANGLE, values, rectifiedGridAngle);
set(expected, Provider_TwoPoint.LAT_OF_1ST_POINT, values, latitudeOf1stPoint);
set(expected, Provider_TwoPoint.LONG_OF_1ST_POINT, values, longitudeOf1stPoint);
set(expected, Provider_TwoPoint.LAT_OF_2ND_POINT, values, latitudeOf2ndPoint);
set(expected, Provider_TwoPoint.LONG_OF_2ND_POINT, values, longitudeOf2ndPoint);
return values;
}
/**
* {@inheritDoc}
*
* @param x The longitude of the coordinate, in radians.
* @param y The latitude of the coordinate, in radians.
*/
protected Point2D transformNormalized(double x, double y, Point2D ptDst)
throws ProjectionException
{
double u, v;
if (abs(abs(y) - PI/2.0) > EPSILON) {
double Q = E / pow(tsfn(y, sin(y)), B);
double temp = 1.0 / Q;
double S = 0.5 * (Q - temp);
double V = sin(B * x);
double U = (S * singamma0 - V * cosgamma0) / (0.5 * (Q + temp));
if (abs(abs(U) - 1.0) < EPSILON) {
throw new ProjectionException(ErrorKeys.INFINITE_VALUE_$1, "v");
}
v = 0.5 * ArB * log((1.0 - U) / (1.0 + U));
temp = cos(B * x);
if (abs(temp) < EPSILON_LATITUDE) {
u = AB * x;
} else {
u = ArB * atan2((S * cosgamma0 + V * singamma0), temp);
}
} else {
v = y > 0 ? v_pole_n : v_pole_s;
u = ArB * y;
}
u -= u_c;
x = v * cosrot + u * sinrot;
y = u * cosrot - v * sinrot;
if (ptDst != null) {
ptDst.setLocation(x,y);
return ptDst;
}
return new Point2D.Double(x,y);
}
/**
* {@inheritDoc}
*/
protected Point2D inverseTransformNormalized(double x, double y, Point2D ptDst)
throws ProjectionException
{
double v = x * cosrot - y * sinrot;
double u = y * cosrot + x * sinrot + u_c;
double Qp = exp(-BrA * v);
double temp = 1.0 / Qp;
double Sp = 0.5 * (Qp - temp);
double Vp = sin(BrA * u);
double Up = (Vp * cosgamma0 + Sp * singamma0) / (0.5 * (Qp + temp));
if (abs(abs(Up) - 1.0) < EPSILON) {
x = 0.0;
y = Up < 0.0 ? -PI / 2.0 : PI / 2.0;
} else {
y = pow(E / sqrt((1. + Up) / (1. - Up)), 1.0 / B); //calculate t
y = cphi2(y);
x = -atan2((Sp * cosgamma0 - Vp * singamma0), cos(BrA * u)) / B;
}
if (ptDst != null) {
ptDst.setLocation(x,y);
return ptDst;
}
return new Point2D.Double(x,y);
}
/**
* Maximal error (in metres) tolerated for assertion, if enabled.
*
* @param longitude The longitude in decimal degrees.
* @param latitude The latitude in decimal degrees.
* @return The tolerance level for assertions, in meters.
*/
@Override
protected double getToleranceForAssertions(final double longitude, final double latitude) {
if (abs(longitude - centralMeridian)/2 + abs(latitude - latitudeOfCentre) > 10) {
// When far from the valid area, use a larger tolerance.
return 1;
}
return super.getToleranceForAssertions(longitude, latitude);
}
/**
* Returns a hash value for this projection.
*/
@Override
public int hashCode() {
long code = Double.doubleToLongBits(latitudeOfCentre);
code = code*37 + Double.doubleToLongBits(longitudeOfCentre);
code = code*37 + Double.doubleToLongBits(azimuth);
code = code*37 + Double.doubleToLongBits(rectifiedGridAngle);
code = code*37 + Double.doubleToLongBits(latitudeOf1stPoint);
code = code*37 + Double.doubleToLongBits(latitudeOf2ndPoint);
return ((int)code ^ (int)(code >>> 32)) + 37*super.hashCode();
}
/**
* Compares the specified object with this map projection for equality.
*/
@Override
public boolean equals(final Object object) {
if (object == this) {
// Slight optimization
return true;
}
if (super.equals(object)) {
final ObliqueMercator that = (ObliqueMercator) object;
return this.twoPoint == that.twoPoint &&
equals(this.latitudeOfCentre , that.latitudeOfCentre ) &&
equals(this.longitudeOfCentre , that.longitudeOfCentre ) &&
equals(this.azimuth , that.azimuth ) &&
equals(this.rectifiedGridAngle , that.rectifiedGridAngle ) &&
equals(this.latitudeOf1stPoint , that.latitudeOf1stPoint ) &&
equals(this.longitudeOf1stPoint, that.longitudeOf1stPoint) &&
equals(this.latitudeOf2ndPoint , that.latitudeOf2ndPoint ) &&
equals(this.longitudeOf2ndPoint, that.longitudeOf2ndPoint) &&
equals(this.u_c, that.u_c);
// Note: "u_c" is a derived parameter, so in theory we don't need to compare it.
// However we still compare it as a safety, because it takes a different
// value in the "hotine" case.
}
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// PROVIDERS ////////
//////// ////////
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
/**
* The {@linkplain org.geotools.referencing.operation.MathTransformProvider math transform
* provider} for an {@linkplain ObliqueMercator Oblique Mercator} projection (EPSG code 9815).
*
* @since 2.1
* @version $Id$
* @author Rueben Schulz
*
* @see org.geotools.referencing.operation.DefaultMathTransformFactory
*/
public static class Provider extends AbstractProvider {
/**
* For compatibility with different versions during deserialization.
*/
private static final long serialVersionUID = 201776686002266891L;
/**
* The operation parameter descriptor for the {@link #latitudeOfCentre latitudeOfCentre}
* parameter value. Valid values range is from -90 to 90°. Default value is 0.
*/
public static final ParameterDescriptor LATITUDE_OF_CENTRE = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "latitude_of_center"),
new NamedIdentifier(Citations.EPSG, "Latitude of projection centre"),
new NamedIdentifier(Citations.ESRI, "Latitude_Of_Center"),
new NamedIdentifier(Citations.GEOTIFF, "CenterLat")
},
0, -90, 90, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@link #longitudeOfCentre longitudeOfCentre}
* parameter value. Valid values range is from -180 to 180°. Default value is 0.
*/
public static final ParameterDescriptor LONGITUDE_OF_CENTRE = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "longitude_of_center"),
new NamedIdentifier(Citations.EPSG, "Longitude of projection centre"),
new NamedIdentifier(Citations.ESRI, "Longitude_Of_Center"),
new NamedIdentifier(Citations.GEOTIFF, "CenterLong")
},
0, -180, 180, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@link #azimuth azimuth}
* parameter value. Valid values range is from -360 to -270, -90 to 90,
* and 270 to 360 degrees. Default value is 0.
*/
public static final ParameterDescriptor AZIMUTH = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "azimuth"),
new NamedIdentifier(Citations.ESRI, "Azimuth"),
new NamedIdentifier(Citations.EPSG, "Azimuth of initial line"),
new NamedIdentifier(Citations.GEOTIFF, "AzimuthAngle")
},
0, -360, 360, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@link #rectifiedGridAngle
* rectifiedGridAngle} parameter value. It is an optional parameter with
* valid values ranging from -360 to 360°. Default value is {@link #azimuth azimuth}.
*/
public static final ParameterDescriptor RECTIFIED_GRID_ANGLE = createOptionalDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "rectified_grid_angle"),
new NamedIdentifier(Citations.EPSG, "Angle from Rectified to Skew Grid"),
new NamedIdentifier(Citations.ESRI, "XY_Plane_Rotation"),
new NamedIdentifier(Citations.GEOTIFF, "RectifiedGridAngle")
},
-360, 360, NonSI.DEGREE_ANGLE);
/**
* The localized name for "Oblique Mercator".
*/
static final InternationalString NAME =
Vocabulary.formatInternational(VocabularyKeys.OBLIQUE_MERCATOR_PROJECTION);
/**
* The parameters group.
*/
static final ParameterDescriptorGroup PARAMETERS = createDescriptorGroup(new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "Oblique_Mercator"),
new NamedIdentifier(Citations.EPSG, "Oblique Mercator"),
new NamedIdentifier(Citations.EPSG, "9815"),
new NamedIdentifier(Citations.GEOTIFF, "CT_ObliqueMercator"),
new NamedIdentifier(Citations.ESRI, "Hotine_Oblique_Mercator_Azimuth_Center"),
new NamedIdentifier(Citations.ESRI, "Rectified_Skew_Orthomorphic_Center"),
new NamedIdentifier(Citations.GEOTOOLS, NAME)
}, new ParameterDescriptor[] {
SEMI_MAJOR, SEMI_MINOR,
LONGITUDE_OF_CENTRE, LATITUDE_OF_CENTRE,
AZIMUTH, RECTIFIED_GRID_ANGLE,
SCALE_FACTOR,
FALSE_EASTING, FALSE_NORTHING
});
/**
* Constructs a new provider.
*/
public Provider() {
super(PARAMETERS);
}
/**
* Constructs a new provider.
*
* @param params A description of parameters.
*/
protected Provider(final ParameterDescriptorGroup params) {
super(params);
}
/**
* Returns the operation type for this map projection.
*/
@Override
public Class getOperationType() {
return CylindricalProjection.class;
}
/**
* Creates a transform from the specified group of parameter values.
*
* @param parameters The group of parameter values.
* @return The created math transform.
* @throws ParameterNotFoundException if a required parameter was not found.
*/
protected MathTransform createMathTransform(final ParameterValueGroup parameters)
throws ParameterNotFoundException
{
final Collection descriptors = PARAMETERS.descriptors();
return new ObliqueMercator(parameters, descriptors, false, false);
}
}
/**
* The {@linkplain org.geotools.referencing.operation.MathTransformProvider math transform
* provider} for a {@linkplain ObliqueMercator Oblique Mercator} projection, specified
* with two points on the central line (instead of a central point and azimuth).
*
* @since 2.1
* @version $Id$
* @author Rueben Schulz
*
* @see org.geotools.referencing.operation.DefaultMathTransformFactory
*/
public static class Provider_TwoPoint extends Provider {
/**
* For compatibility with different versions during deserialization.
*/
private static final long serialVersionUID = 7124258885016543889L;
/**
* The operation parameter descriptor for the {@code latitudeOf1stPoint}
* parameter value. Valid values range is from -90 to 90°. Default value is 0.
*/
public static final ParameterDescriptor LAT_OF_1ST_POINT = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.ESRI, "Latitude_Of_1st_Point")
},
0, -90, 90, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@code longitudeOf1stPoint}
* parameter value. Valid values range is from -180 to 180°. Default value is 0.
*/
public static final ParameterDescriptor LONG_OF_1ST_POINT = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.ESRI, "Longitude_Of_1st_Point")
},
0, -180, 180, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@code latitudeOf2ndPoint}
* parameter value. Valid values range is from -90 to 90°. Default value is 0.
*/
public static final ParameterDescriptor LAT_OF_2ND_POINT = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.ESRI, "Latitude_Of_2nd_Point")
},
0, -90, 90, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@code longitudeOf2ndPoint}
* parameter value. Valid values range is from -180 to 180°. Default value is 0.
*/
public static final ParameterDescriptor LONG_OF_2ND_POINT = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.ESRI, "Longitude_Of_2nd_Point")
},
0, -180, 180, NonSI.DEGREE_ANGLE);
/**
* The parameters group.
*/
static final ParameterDescriptorGroup PARAMETERS = createDescriptorGroup(new NamedIdentifier[] {
new NamedIdentifier(Citations.ESRI, "Hotine_Oblique_Mercator_Two_Point_Center"),
new NamedIdentifier(Citations.GEOTOOLS, NAME)
}, new ParameterDescriptor[] {
SEMI_MAJOR, SEMI_MINOR,
LAT_OF_1ST_POINT, LONG_OF_1ST_POINT,
LAT_OF_2ND_POINT, LONG_OF_2ND_POINT,
LATITUDE_OF_CENTRE, SCALE_FACTOR,
FALSE_EASTING, FALSE_NORTHING
});
/**
* Constructs a new provider.
*/
public Provider_TwoPoint() {
super(PARAMETERS);
}
/**
* Constructs a new provider.
*
* @param params A description of parameters.
*/
protected Provider_TwoPoint(final ParameterDescriptorGroup params) {
super(params);
}
/**
* Creates a transform from the specified group of parameter values.
*
* @param parameters The group of parameter values.
* @return The created math transform.
* @throws ParameterNotFoundException if a required parameter was not found.
*/
@Override
protected MathTransform createMathTransform(final ParameterValueGroup parameters)
throws ParameterNotFoundException
{
final Collection descriptors = PARAMETERS.descriptors();
return new ObliqueMercator(parameters, descriptors, true, false);
}
}
}