blob: 8469aecb169b8ea214481f7f129a7dc751a38fa1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.internal.referencing.provider;
import java.awt.geom.AffineTransform;
import javax.xml.bind.annotation.XmlTransient;
import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.operation.CylindricalProjection;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.parameter.ParameterBuilder;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.ContextualParameters;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.internal.referencing.j2d.ParameterizedAffine;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.internal.util.Constants;
import static java.lang.Math.*;
/**
* The provider for <cite>"Equidistant Cylindrical (Spherical)"</cite> projection
* (EPSG:1029, <span class="deprecated">EPSG:9823</span>).
* In the particular case where the longitude of origin and the standard parallel are 0°,
* this projection is also known as <cite>"Plate Carrée"</cite>.
*
* <p>At the difference of most other map projection providers, this class does not extend {@link MapProjection}
* because it does not create non-liner kernel. Instead, the projection created by this class is implemented
* by an affine transform.</p>
*
* <p>This provider is <strong>not</strong> suitable for the <cite>Equidistant Cylindrical</cite> projection
* (EPSG:1028, <span class="deprecated">EPSG:9842</span>). EPSG defines Equidistant Cylindrical projection as
* the ellipsoidal case of this projection, which uses a more complicated formula than the affine transform
* used here.</p>
*
* <div class="note"><b>Note:</b>
* EPSG:1028 and 1029 are the current codes, while EPSG:9842 and 9823 are deprecated codes.
* The new and deprecated definitions differ only by their parameters. In the Apache SIS implementation,
* both current and legacy definitions are known, but the legacy names are marked as deprecated.</div>
*
* @author John Grange
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
*
* @see PseudoPlateCarree
* @see <a href="http://geotiff.maptools.org/proj_list/equirectangular.html">GeoTIFF parameters for Equirectangular</a>
*
* @since 0.6
* @module
*/
@XmlTransient
public final class Equirectangular extends AbstractProvider {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -278288251842178001L;
/**
* Name of this projection in EPSG geodetic dataset.
*
* @todo Remove with JDK9 after we introduce {@code getInstance()} method.
*/
public static final String NAME = "Equidistant Cylindrical (Spherical)";
/*
* ACCESS POLICY: Only formal EPSG parameters shall be public.
* Parameters that we add ourselves should be package-privated.
*/
/**
* The operation parameter descriptor for the <cite>Latitude of 1st standard parallel</cite> (φ₁) parameter value.
* Valid values range is (-90 … 90)° and default value is 0°.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Latitude of 1st standard parallel </td></tr>
* <tr><td> OGC: </td><td> standard_parallel_1 </td></tr>
* <tr><td> ESRI: </td><td> Standard_Parallel_1 </td></tr>
* <tr><td> NetCDF: </td><td> standard_parallel </td></tr>
* <tr><td> GeoTIFF: </td><td> StdParallel1 </td></tr>
* <tr><td> Proj4: </td><td> lat_ts </td></tr>
* </table>
* <b>Notes:</b>
* <ul>
* <li>Value domain: (-90.0 … 90.0)°</li>
* <li>No default value</li>
* </ul>
*/
public static final ParameterDescriptor<Double> STANDARD_PARALLEL;
/**
* The operation parameter descriptor for the <cite>Latitude of natural origin</cite> (φ₀) parameter value.
* Valid values range is (-90 … 90)° and default value is 0°.
*
* <p>In theory, this parameter should not be used and its value should be 0 in all cases.
* This parameter is included for completeness in CRS labeling only, and is declared optional.</p>
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Latitude of natural origin </td></tr>
* <tr><td> OGC: </td><td> latitude_of_origin </td></tr>
* <tr><td> ESRI: </td><td> Latitude_Of_Origin </td></tr>
* <tr><td> NetCDF: </td><td> latitude_of_projection_origin </td></tr>
* <tr><td> GeoTIFF: </td><td> CenterLat </td></tr>
* <tr><td> Proj4: </td><td> lat_0 </td></tr>
* </table>
* <b>Notes:</b>
* <ul>
* <li>Value restricted to 0</li>
* <li>No default value</li>
* <li>Optional</li>
* </ul>
*/
static final ParameterDescriptor<Double> LATITUDE_OF_ORIGIN;
/**
* The operation parameter descriptor for the <cite>Longitude of natural origin</cite> (λ₀) parameter value.
* Valid values range is [-180 … 180]° and default value is 0°.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> Longitude of natural origin </td></tr>
* <tr><td> OGC: </td><td> central_meridian </td></tr>
* <tr><td> ESRI: </td><td> Central_Meridian </td></tr>
* <tr><td> NetCDF: </td><td> longitude_of_projection_origin </td></tr>
* <tr><td> GeoTIFF: </td><td> CenterLong </td></tr>
* <tr><td> Proj4: </td><td> lon_0 </td></tr>
* </table>
*/
public static final ParameterDescriptor<Double> LONGITUDE_OF_ORIGIN;
/**
* The operation parameter descriptor for the <cite>False easting</cite> (FE) parameter value.
* Valid values range is unrestricted and default value is 0 metre.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> False easting </td></tr>
* <tr><td> OGC: </td><td> false_easting </td></tr>
* <tr><td> ESRI: </td><td> False_Easting </td></tr>
* <tr><td> NetCDF: </td><td> false_easting </td></tr>
* <tr><td> GeoTIFF: </td><td> FalseEasting </td></tr>
* <tr><td> Proj4: </td><td> x_0 </td></tr>
* </table>
*/
public static final ParameterDescriptor<Double> FALSE_EASTING;
/**
* The operation parameter descriptor for the <cite>False northing</cite> (FN) parameter value.
* Valid values range is unrestricted and default value is 0 metre.
*
* <!-- Generated by ParameterNameTableGenerator -->
* <table class="sis">
* <caption>Parameter names</caption>
* <tr><td> EPSG: </td><td> False northing </td></tr>
* <tr><td> OGC: </td><td> false_northing </td></tr>
* <tr><td> ESRI: </td><td> False_Northing </td></tr>
* <tr><td> NetCDF: </td><td> false_northing </td></tr>
* <tr><td> GeoTIFF: </td><td> FalseNorthing </td></tr>
* <tr><td> Proj4: </td><td> y_0 </td></tr>
* </table>
*/
public static final ParameterDescriptor<Double> FALSE_NORTHING;
/**
* The group of all parameters expected by this coordinate operation.
*/
static final ParameterDescriptorGroup PARAMETERS;
static {
final ParameterBuilder builder = builder();
STANDARD_PARALLEL = createLatitude(builder
.addIdentifier("8823")
.addIdentifier(Citations.GEOTIFF, "3078")
.addName("Latitude of 1st standard parallel")
.addName(Citations.OGC, Constants.STANDARD_PARALLEL_1)
.addName(Citations.ESRI, "Standard_Parallel_1")
.addName(Citations.NETCDF, Constants.STANDARD_PARALLEL)
.addName(Citations.GEOTIFF, "StdParallel1")
.addName(Citations.PROJ4, "lat_ts"), false);
LONGITUDE_OF_ORIGIN = createLongitude(builder
.addIdentifier("8802")
.addIdentifier(Citations.GEOTIFF, "3088")
.addName("Longitude of natural origin")
.addName(Citations.OGC, Constants.CENTRAL_MERIDIAN)
.addName(Citations.ESRI, "Central_Meridian")
.addName(Citations.NETCDF, "longitude_of_projection_origin")
.addName(Citations.GEOTIFF, "CenterLong")
.addName(Citations.PROJ4, "lon_0"));
FALSE_EASTING = createShift(builder
.addIdentifier("8806")
.addIdentifier(Citations.GEOTIFF, "3082")
.addName("False easting")
.addName(Citations.OGC, Constants.FALSE_EASTING)
.addName(Citations.ESRI, "False_Easting")
.addName(Citations.NETCDF, Constants.FALSE_EASTING)
.addName(Citations.GEOTIFF, "FalseEasting")
.addName(Citations.PROJ4, "x_0"));
FALSE_NORTHING = createShift(builder
.addIdentifier("8807")
.addIdentifier(Citations.GEOTIFF, "3083")
.addName("False northing")
.addName(Citations.OGC, Constants.FALSE_NORTHING)
.addName(Citations.ESRI, "False_Northing")
.addName(Citations.NETCDF, Constants.FALSE_NORTHING)
.addName(Citations.GEOTIFF, "FalseNorthing")
.addName(Citations.PROJ4, "y_0"));
/*
* "Latitude of natural origin" is not formally parameters of the "Equidistant Cylindrical (Spherical)"
* projection according EPSG:1029. But we declare it anyway (as an optional parameter) because it was
* part of the now deprecated EPSG:9823 definition (and also EPSG:9842, the ellipsoidal case), and we
* still see it in use sometime. However, taking inspiration from the practice done in "Mercator (1SP)"
* projection, we require that the parameter value must be zero.
*/
LATITUDE_OF_ORIGIN = createZeroConstant(builder // Was used by EPSG:9823 (also EPSG:9842).
.addIdentifier("8801")
.addIdentifier(Citations.GEOTIFF, "3089")
.addName("Latitude of natural origin")
.addName(Citations.OGC, Constants.LATITUDE_OF_ORIGIN)
.addName(Citations.ESRI, "Latitude_Of_Origin")
.addName(Citations.NETCDF, "latitude_of_projection_origin")
.addName(Citations.GEOTIFF, "CenterLat")
.addName(Citations.PROJ4, "lat_0")
.setRemarks(Resources.formatInternational(Resources.Keys.ConstantProjParameterValue_1, 0))
.setRequired(false));
// Do not declare the ESRI "Equidistant_Cylindrical" projection name below,
// for avoiding confusion with EPSG "Equidistant Cylindrical" ellipsoidal projection.
PARAMETERS = addIdentifierAndLegacy(builder, "1029", "9823") // 9823 uses deprecated parameter names
.addName( NAME)
.addName( "Plate Carrée") // Not formally defined by EPSG, but cited in documentation.
.addName(Citations.OGC, "Equirectangular")
.addName(Citations.ESRI, "Plate_Carree")
.addName(Citations.GEOTIFF, "CT_Equirectangular")
.addName(Citations.PROJ4, "eqc")
.addIdentifier(Citations.GEOTIFF, "17")
.createGroupForMapProjection(
STANDARD_PARALLEL,
LATITUDE_OF_ORIGIN, // Not formally an Equirectangular parameter.
LONGITUDE_OF_ORIGIN,
FALSE_EASTING,
FALSE_NORTHING);
}
/**
* Constructs a new provider.
*/
public Equirectangular() {
super(2, 2, PARAMETERS);
}
/**
* Returns the operation type for this map projection.
*
* @return {@code CylindricalProjection.class}
*/
@Override
public Class<CylindricalProjection> getOperationType() {
return CylindricalProjection.class;
}
/**
* Notifies {@code DefaultMathTransformFactory} that map projections require
* values for the {@code "semi_major"} and {@code "semi_minor"} parameters.
*
* @return 1, meaning that the operation requires a source ellipsoid.
*/
@Override
public final int getEllipsoidsMask() {
return 1;
}
/**
* Gets a parameter value identified by the given descriptor and stores it only if different than zero.
* This method performs the same work than {@code Initializer.getAndStore(ParameterDescriptor)} in the
* {@link org.apache.sis.referencing.operation.projection} package.
*
* @param source the parameters from which to read the value.
* @param target where to store the parameter values.
* @param descriptor the descriptor that specify the parameter names and desired units.
* @return the parameter value in the units given by the descriptor.
* @throws IllegalArgumentException if the given value is out of bounds.
*/
private static double getAndStore(final Parameters source, final ParameterValueGroup target,
final ParameterDescriptor<Double> descriptor) throws IllegalArgumentException
{
final double value = source.doubleValue(descriptor); // Apply a unit conversion if needed.
MapProjection.validate(descriptor, value); // Unconditional validation for all parameters.
if (value != 0) { // All default values in this class are zero.
target.parameter(descriptor.getName().getCode()).setValue(value);
}
return value;
}
/**
* Creates an Equirectangular projection from the specified group of parameter values. This method is an
* adaptation of {@link org.apache.sis.referencing.operation.projection.NormalizedProjection} constructor,
* reproduced in this method because we will create an affine transform instead than the usual projection
* classes.
*
* @param factory the factory to use if this constructor needs to create other math transforms.
* @param parameters the parameter values that define the transform to create.
* @return the map projection created from the given parameter values.
* @throws FactoryException if an error occurred while creating the math transform.
*/
@Override
public MathTransform createMathTransform(final MathTransformFactory factory, final ParameterValueGroup parameters)
throws FactoryException
{
final Parameters p = Parameters.castOrWrap(parameters);
final ContextualParameters context = new ContextualParameters(this);
double a = getAndStore(p, context, MapProjection.SEMI_MAJOR);
double b = getAndStore(p, context, MapProjection.SEMI_MINOR);
double λ0 = getAndStore(p, context, LONGITUDE_OF_ORIGIN);
double φ0 = getAndStore(p, context, LATITUDE_OF_ORIGIN);
double φ1 = getAndStore(p, context, STANDARD_PARALLEL);
double fe = getAndStore(p, context, FALSE_EASTING);
double fn = getAndStore(p, context, FALSE_NORTHING);
/*
* Perform following transformation, in that order. Note that following
* AffineTransform convention, the Java code appears in reverse order:
*
* 1) Subtract φ0 to the latitude.
* 2) Subtract λ0 to the longitude.
* 3) Convert degrees to radians.
* 4) Scale longitude by cos(φ1).
*/
φ1 = toRadians1);
final MatrixSIS normalize = context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
normalize.convertBefore(0, cos1), null);
context.normalizeGeographicInputs0)
.convertBefore(1, null, 0);
/*
* At this point, we usually invoke 'denormalize.convertAfter(…, a, …)' where 'a' (the semi-major axis length)
* is taken as the Earth radius (R). However quoting EPSG: "If the figure of the earth used is an ellipsoid
* rather than a sphere then R should be calculated as the radius of the conformal sphere at the projection
* origin at latitude φ1 using the formula for RC given in section 1.2, table 3".
*/
if (a != b) {
final double rs = b / a;
final double sinφ1 = sin1);
a = b / (1 - (1 - rs*rs) * (sinφ1*sinφ1));
}
final DoubleDouble k = DoubleDouble.createAndGuessError(a);
final MatrixSIS denormalize = context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
denormalize.convertAfter(0, k, DoubleDouble.createAndGuessError(fe));
denormalize.convertAfter(1, k, DoubleDouble.createAndGuessError(fn));
/*
* Creates the ConcatenatedTransform, letting the factory returns the cached instance
* if the caller already invoked this method previously (which usually do not happen).
*/
MathTransform mt = context.completeTransform(factory, MathTransforms.identity(2));
if (mt instanceof AffineTransform) { // Always true in Apache SIS implementation.
mt = new ParameterizedAffine((AffineTransform) mt, context, true);
}
return mt;
}
}