/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-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.
*/
package org.geotools.referencing.operation.projection;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.Collection;
import javax.measure.unit.Unit;
import javax.measure.unit.NonSI;
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.referencing.operation.ConicProjection;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.geotools.metadata.iso.citation.Citations;
import org.geotools.referencing.NamedIdentifier;
import org.geotools.referencing.operation.projection.ObliqueMercator.Provider;
import org.geotools.referencing.operation.projection.ObliqueMercator.Provider_TwoPoint;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.referencing.operation.transform.ConcatenatedTransform;
import org.geotools.resources.i18n.ErrorKeys;
import static java.lang.Math.*;
/**
* Krovak Oblique Conformal Conic projection (EPSG code 9819). This projection is used in the
* Czech Republic and Slovakia under the name "Krovak" projection. The geographic coordinates
* on the ellipsoid are first reduced to conformal coordinates on the conformal (Gaussian) sphere.
* These spherical coordinates are then projected onto the oblique cone and converted to grid
* coordinates. The pseudo standard parallel is defined on the conformal sphere after its rotation,
* to obtain the oblique aspect of the projection. It is then the parallel on this sphere at which
* the map projection is true to scale; on the ellipsoid it maps as a complex curve.
*
* The compulsory parameters are just the ellipsoid characteristics. All other parameters are
* optional and have defaults to match the common usage with Krovak projection.
*
* In general the axis of Krovak projection are defined as westing and southing (not easting and
* northing) and they are also reverted, so if the value of projected coordinates should (and in
* y, x order in Krovak) be positive the 'Axis' parameter for projection
* should be defined explicitly like this (in wkt):
*
*
PROJCS["S-JTSK (Ferro) / Krovak",
* .
* .
* .
* PROJECTION["Krovak"]
* PARAMETER["semi_major", 6377397.155],
* PARAMETER["semi_minor", 6356078.963],
* UNIT["meter",1.0],
* AXIS["x", WEST],
* AXIS["y", SOUTH]]
*
* Axis in Krovak:
*
* y<------------------+
* |
* Czech. Rep. |
* |
* x
*
*
* By default, the axis are 'easting, northing' so the values of projected coordinates are negative
* (and in y, x order in Krovak - it is cold Krovak GIS version).
*
* References:
*
*
*
* @see Krovak on RemoteSensing.org
* @see Krovak on "Coordinate
* Conversions and Transformations including Formulas"
* @see Krovak on POSC
*
* @since 2.4
* @version $Id$
*
*
* @source $URL$
* @author Jan Jezek
* @author Martin Desruisseaux
*/
public class Krovak extends MapProjection {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -8359105634355342212L;
/**
* Maximum number of iterations for iterative computations.
*/
private static final int MAXIMUM_ITERATIONS = 15;
/**
* When to stop the iteration.
*/
private static final double ITERATION_TOLERANCE = 1E-11;
/**
* Azimuth of the centre line passing through the centre of the projection.
* This is equals to the co-latitude of the cone axis at point of intersection
* with the ellipsoid.
*/
protected final double azimuth;
/**
* Parameter used by ESRI - scale for X Axis
*/
protected double x_scale;
/**
* Parameter used by ESRI - scale for Y Axis
*/
protected double y_scale;
/**
* Parameter used by ESRI - rotation
*/
protected double xy_plane_rotation;
/**
* Variable to decide if ESRI parameters were used
*/
boolean esriDefinition;
private MathTransform axisTransform = null;
/**
* Latitude of pseudo standard parallel.
*/
protected final double pseudoStandardParallel;
/**
* Useful variables calculated from parameters defined by user.
*/
private final double sinAzim, cosAzim, n, tanS2, alfa, hae, k1, ka, ro0, rop;
/**
* Useful constant - 45° in radians.
*/
private static final double s45 = 0.785398163397448;
/**
* 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.
*/
protected Krovak(final ParameterValueGroup parameters) throws ParameterNotFoundException {
this(parameters, true);
}
/**
* Constructs a new map projection from the supplied parameters.
*
* @param parameters The parameter values in standard units.
* @param esriDefinition true if ESRI parameters are specified.
* @throws ParameterNotFoundException if a mandatory parameter is missing.
*/
protected Krovak(final ParameterValueGroup parameters, boolean esriDefinition) throws ParameterNotFoundException {
super(parameters);
this.esriDefinition = true;//esriDefinition;
final Collection expected = getParameterDescriptors().descriptors();
// Fetch parameters from user input.
latitudeOfOrigin = doubleValue(expected, Provider.LATITUDE_OF_CENTER, parameters);
centralMeridian = doubleValue(expected, Provider.LONGITUDE_OF_CENTER, parameters);
azimuth = doubleValue(expected, Provider.AZIMUTH, parameters);
pseudoStandardParallel = doubleValue(expected, Provider.PSEUDO_STANDARD_PARALLEL, parameters);
scaleFactor = doubleValue(expected, Provider.SCALE_FACTOR, parameters);
x_scale = doubleValue(expected, Provider.X_SCALE, parameters);
y_scale = doubleValue(expected, Provider.Y_SCALE, parameters);
xy_plane_rotation = doubleValue(expected, Provider.XY_PLANE_ROTATION, parameters);
/**
* Check if there are parameters for axis swapping used by ESRI - if so then
* set variable so the proper ParameterDescriptorGroup will be returned by
* getParameterDescriptors()
*/
if ( Double.isNaN(doubleValue(expected, Provider.X_SCALE,parameters)) &&
Double.isNaN(doubleValue(expected, Provider.Y_SCALE,parameters)) &&
Double.isNaN(doubleValue(expected, Provider.XY_PLANE_ROTATION,parameters))){
this.esriDefinition = false;
} else {
axisTransform = createAffineTransform(x_scale, y_scale, xy_plane_rotation);
}
ensureLatitudeInRange (Provider.LATITUDE_OF_CENTER, latitudeOfOrigin, false);
ensureLongitudeInRange(Provider.LONGITUDE_OF_CENTER, centralMeridian, false);
// Calculates useful constants.
sinAzim = sin(azimuth);
cosAzim = cos(azimuth);
n = sin(pseudoStandardParallel);
tanS2 = tan(pseudoStandardParallel / 2 + s45);
final double sinLat, cosLat, cosL2, u0;
sinLat = sin(latitudeOfOrigin);
cosLat = cos(latitudeOfOrigin);
cosL2 = cosLat * cosLat;
alfa = sqrt(1 + ((excentricitySquared * (cosL2*cosL2)) / (1 - excentricitySquared)));
hae = alfa * excentricity / 2;
u0 = asin(sinLat / alfa);
final double g, esl;
esl = excentricity * sinLat;
g = pow((1 - esl) / (1 + esl), (alfa * excentricity) / 2);
k1 = pow(tan(latitudeOfOrigin/2 + s45), alfa) * g / tan(u0/2 + s45);
ka = pow(1 / k1, -1 / alfa);
final double radius;
radius = sqrt(1 - excentricitySquared) / (1 - (excentricitySquared * (sinLat * sinLat)));
ro0 = scaleFactor * radius / tan(pseudoStandardParallel);
rop = ro0 * pow(tanS2, n);
}
private MathTransform createAffineTransform(double x_scale, double y_scale, double xy_plane_rotation){
/**
* calculates matrix coefficients form geometric coefficients
*/
double a00 = x_scale * Math.cos(xy_plane_rotation);
double a01 = -y_scale * Math.sin(xy_plane_rotation);
double a10 = x_scale* Math.sin(xy_plane_rotation);
double a11 = y_scale * Math.cos(xy_plane_rotation);
AffineTransform at = new AffineTransform(a00, a10, a01, a11, 0d, 0d);
MathTransform theAffineTransform = new AffineTransform2D(at);
return theAffineTransform;
}
/**
* {@inheritDoc}
*/
public ParameterDescriptorGroup getParameterDescriptors() {
return (esriDefinition) ? Provider.PARAMETERS_ESRI : Provider.PARAMETERS;
// return Provider.PARAMETERSESRI;
}
/**
* {@inheritDoc}
*/
@Override
public ParameterValueGroup getParameterValues() {
final Collection expected = getParameterDescriptors().descriptors();
final ParameterValueGroup values = super.getParameterValues();
set(expected, Provider.LATITUDE_OF_CENTER, values, latitudeOfOrigin );
set(expected, Provider.LONGITUDE_OF_CENTER, values, centralMeridian );
set(expected, Provider.AZIMUTH, values, azimuth );
set(expected, Provider.PSEUDO_STANDARD_PARALLEL, values, pseudoStandardParallel);
set(expected, Provider.SCALE_FACTOR, values, scaleFactor );
set(expected, Provider.X_SCALE, values, x_scale );
set(expected, Provider.Y_SCALE, values, y_scale );
set(expected, Provider.XY_PLANE_ROTATION, values, xy_plane_rotation );
return values;
}
/**
* Transforms the specified (λ,φ) coordinates
* (units in radians) and stores the result in {@code ptDst} (linear distance
* on a unit sphere).
*/
protected Point2D transformNormalized(final double lambda, final double phi, Point2D ptDst)
throws ProjectionException
{
final double esp = excentricity * sin(phi);
final double gfi = pow(((1. - esp) / (1. + esp)), hae);
final double u = 2 * (atan(pow(tan(phi/2 + s45), alfa) / k1 * gfi) - s45);
final double deltav = -lambda * alfa;
final double cosU = cos(u);
final double s = asin((cosAzim * sin(u)) + (sinAzim * cosU * cos(deltav)));
final double d = asin(cosU * sin(deltav) / cos(s));
final double eps = n * d;
final double ro = rop / pow(tan(s/2 + s45), n);
// x and y are reverted.
final double y = -(ro * cos(eps));
final double x = -(ro * sin(eps));
double[] result = {x, y};
/**
* swap axis if required
*/
if (axisTransform!=null){
try {
axisTransform.transform(new double[] {x, y}, 0, result,0, 1 );
} catch (TransformException e) {
throw new ProjectionException(e);
}
}
if (ptDst != null) {
ptDst.setLocation(result[0], result[1]);
return ptDst;
}
return new Point2D.Double(result[0], result[1]);
}
/**
* Transforms the specified (x,y) coordinate
* and stores the result in {@code ptDst}.
*/
protected Point2D inverseTransformNormalized(final double x, final double y, Point2D ptDst)
throws ProjectionException
{
// x -> southing, y -> westing
double[] result = {x, y};
/**
* swap axis if required
*/
if (axisTransform!=null){
try {
axisTransform.transform(new double[] {x, y}, 0, result,0, 1 );
} catch (TransformException e) {
throw new ProjectionException(e);
}
}
final double ro = hypot(result[0], result[1]);
final double eps = atan2(-result[0], -result[1]);
final double d = eps / n;
final double s = 2 * (atan(pow(ro0/ro, 1/n) * tanS2) - s45);
final double cs = cos(s);
final double u = asin((cosAzim * sin(s)) - (sinAzim * cs * cos(d)));
final double kau = ka * pow(tan((u / 2.) + s45), 1 / alfa);
final double deltav = asin((cs * sin(d)) / cos(u));
final double lambda = -deltav / alfa;
double phi = 0;
double fi1 = u;
// iteration calculation
for (int i=MAXIMUM_ITERATIONS;;) {
fi1 = phi;
final double esf = excentricity * sin(fi1);
phi = 2. * (atan(kau * pow((1. + esf) / (1. - esf), excentricity/2.)) - s45);
if (abs(fi1 - phi) <= ITERATION_TOLERANCE) {
break;
}
if (--i < 0) {
throw new ProjectionException(ErrorKeys.NO_CONVERGENCE);
}
}
if (ptDst != null) {
ptDst.setLocation(lambda, phi);
return ptDst;
}
return new Point2D.Double(lambda, phi);
}
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// PROVIDERS ////////
//////// ////////
//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
/**
* The {@linkplain org.geotools.referencing.operation.MathTransformProvider math transform
* provider} for an {@linkplain Krovak Krovak} projection (EPSG code 9819).
*
* @since 2.4
* @version $Id$
* @author Jan Jezek
*
* @see org.geotools.referencing.operation.DefaultMathTransformFactory
*/
public static class Provider extends AbstractProvider {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -278392856661204734L;
/**
* The operation parameter descriptor for the {@linkplain #latitudeOfOrigin
* latitude of origin} parameter value. Valid values range is from -90 to 90.
* Default value is 49.5.
*/
public static final ParameterDescriptor LATITUDE_OF_CENTER = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "latitude_of_center"),
new NamedIdentifier(Citations.EPSG, "Latitude of projection centre"),
new NamedIdentifier(Citations.EPSG, "Latitude of origin"),
new NamedIdentifier(Citations.GEOTIFF, "CenterLat")
}, 49.5, -90, 90, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@linkplain #centralMeridian central
* meridian} parameter value. Valid values range is from -180 to 180. Default value
* is 24�50' (= 42�50' from Ferro prime meridian).
*/
public static final ParameterDescriptor LONGITUDE_OF_CENTER = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "longitude_of_center"),
new NamedIdentifier(Citations.EPSG, "Longitude of projection centre"),
new NamedIdentifier(Citations.EPSG, "Longitude of origin"),
new NamedIdentifier(Citations.GEOTIFF, "CenterLong")
}, 42.5-17.66666666666667, -180, 180, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@linkplain #azimuth azimuth} parameter
* value. Valid values range is from -90 to 90. Default value is 30.28813972222.
*/
public static final ParameterDescriptor AZIMUTH = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "azimuth"),
new NamedIdentifier(Citations.EPSG, "Azimuth of initial line"),
new NamedIdentifier(Citations.EPSG, "Co-latitude of cone axis"),
new NamedIdentifier(Citations.GEOTIFF, "AzimuthAngle"),
new NamedIdentifier(Citations.ESRI, "Azimuth"),
}, 30.28813972222222, 0, 360, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the pseudo {@linkplain #pseudoStandardParallel
* pseudo standard parallel} parameter value. Valid values range is from -90 to 90.
* Default value is 78.5.
*/
public static final ParameterDescriptor PSEUDO_STANDARD_PARALLEL =
createDescriptor(new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "pseudo_standard_parallel_1"),
new NamedIdentifier(Citations.EPSG, "Latitude of Pseudo Standard Parallel"),
new NamedIdentifier(Citations.ESRI, "Pseudo_Standard_Parallel_1")
}, 78.5, -90, 90, NonSI.DEGREE_ANGLE);
/**
* The operation parameter descriptor for the {@link #scaleFactor scaleFactor}
* parameter value. Valid values range is from 0 to infinity. Default value is 1.
*/
public static final ParameterDescriptor SCALE_FACTOR = createDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "scale_factor"),
new NamedIdentifier(Citations.EPSG, "Scale factor on pseudo standard parallel"),
new NamedIdentifier(Citations.GEOTIFF, "ScaleAtCenter"),
new NamedIdentifier(Citations.OGC, "Scale_Factor")
}, 0.9999, 0, Double.POSITIVE_INFINITY, Unit.ONE);
/**
* ESRI Parameter for scale of X axis in projected coordinate system.
*/
public static final ParameterDescriptor X_SCALE = createOptionalDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.ESRI,"X_Scale"),
},
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Unit.ONE);
/**
* ESRI Parameter for scale of Y axis in projected coordinate system.
*/
public static final ParameterDescriptor Y_SCALE = createOptionalDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.ESRI,"Y_Scale"),
},
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Unit.ONE);
/**
* ESRI Parameter for rotation of projected coordinate system.
*/
public static final ParameterDescriptor XY_PLANE_ROTATION = createOptionalDescriptor(
new NamedIdentifier[] {
new NamedIdentifier(Citations.ESRI, "XY_Plane_Rotation"),
},
-360, 360, NonSI.DEGREE_ANGLE);
/**
* The parameters group.
*/
static final ParameterDescriptorGroup PARAMETERS_ESRI = createDescriptorGroup(new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "Krovak"),
new NamedIdentifier(Citations.GEOTIFF, "Krovak"),
new NamedIdentifier(Citations.EPSG, "Krovak Oblique Conformal Conic"),
new NamedIdentifier(Citations.EPSG, "Krovak Oblique Conic Conformal"),
new NamedIdentifier(Citations.EPSG, "9819"),
new NamedIdentifier(Citations.ESRI, "Krovak"),
},
new ParameterDescriptor[] {
SEMI_MAJOR, SEMI_MINOR, LATITUDE_OF_CENTER, LONGITUDE_OF_CENTER,
AZIMUTH, PSEUDO_STANDARD_PARALLEL, SCALE_FACTOR,
FALSE_EASTING, FALSE_NORTHING, X_SCALE, Y_SCALE, XY_PLANE_ROTATION
});
/**
* The parameters group.
*/
static final ParameterDescriptorGroup PARAMETERS = createDescriptorGroup(new NamedIdentifier[] {
new NamedIdentifier(Citations.OGC, "Krovak"),
new NamedIdentifier(Citations.GEOTIFF, "Krovak"),
new NamedIdentifier(Citations.EPSG, "Krovak Oblique Conformal Conic"),
new NamedIdentifier(Citations.EPSG, "Krovak Oblique Conic Conformal"),
new NamedIdentifier(Citations.EPSG, "9819"),
new NamedIdentifier(Citations.ESRI, "Krovak"),
},
new ParameterDescriptor[] {
SEMI_MAJOR, SEMI_MINOR, LATITUDE_OF_CENTER, LONGITUDE_OF_CENTER,
AZIMUTH, PSEUDO_STANDARD_PARALLEL, SCALE_FACTOR,
FALSE_EASTING, FALSE_NORTHING,
});
/**
* Constructs a new provider.
*/
public Provider() {
super(PARAMETERS_ESRI);
}
/**
* Constructs a new provider.
*/
public Provider(final ParameterDescriptorGroup params) {
super(params);
}
/**
* Returns the operation type for this map projection.
*/
@Override
public Class getOperationType() {
return ConicProjection.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.
*/
public MathTransform createMathTransform(final ParameterValueGroup parameters)
throws ParameterNotFoundException
{
return new Krovak(parameters, false);
}
}
/**
* Returns a hash value for this projection.
*/
@Override
public int hashCode() {
final long code = Double.doubleToLongBits(azimuth) ^
Double.doubleToLongBits(pseudoStandardParallel);
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 Krovak that = (Krovak) object;
return equals(azimuth, that.azimuth) &&
equals(pseudoStandardParallel, that.pseudoStandardParallel);
}
return false;
}
}