This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit c8ed9467ae089b2d3a9327cb23c1a1be6a4cef65 Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Apr 29 18:55:53 2022 +0200 Port `LambertAzimuthalEqualArea` from Geotk. We rewrote the equations from EPSG guidance notes. https://issues.apache.org/jira/browse/SIS-228 --- .../provider/LambertAzimuthalEqualArea.java | 186 +++++++++++ .../LambertAzimuthalEqualAreaSpherical.java | 77 +++++ .../java/org/apache/sis/referencing/Builder.java | 14 +- .../operation/projection/EqualAreaProjection.java | 29 +- .../projection/LambertAzimuthalEqualArea.java | 283 ++++++++++++++++ ...g.opengis.referencing.operation.OperationMethod | 2 + .../referencing/provider/ProvidersTest.java | 2 + .../projection/LambertAzimuthalEqualAreaTest.java | 357 +++++++++++++++++++++ .../sis/test/suite/ReferencingTestSuite.java | 1 + 9 files changed, 938 insertions(+), 13 deletions(-) diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualArea.java new file mode 100644 index 0000000000..18f34bacd1 --- /dev/null +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualArea.java @@ -0,0 +1,186 @@ +/* + * 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 javax.xml.bind.annotation.XmlTransient; +import org.opengis.parameter.ParameterDescriptor; +import org.opengis.parameter.ParameterDescriptorGroup; +import org.opengis.referencing.operation.PlanarProjection; +import org.apache.sis.metadata.iso.citation.Citations; +import org.apache.sis.parameter.ParameterBuilder; +import org.apache.sis.parameter.Parameters; +import org.apache.sis.referencing.operation.projection.NormalizedProjection; + + +/** + * The provider for "<cite>Lambert Azimuthal Equal Area</cite>" projection (EPSG:9820). + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * + * @see <a href="http://geotiff.maptools.org/proj_list/lambert_azimuthal_equal_area.html">GeoTIFF parameters for Lambert Azimuthal Equal Area</a> + * + * @since 1.2 + * @module + */ +@XmlTransient +public class LambertAzimuthalEqualArea extends MapProjection { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 3360095493388421774L; + + /** + * The EPSG identifier, to be preferred to the name when available. + * The {@code IDENTIFIER_OF_BASE} name is for avoiding confusion with + * the {@code IDENTIFIER} name used in subclasses. + */ + public static final String IDENTIFIER_OF_BASE = "9820"; + + /** + * The operation parameter descriptor for the <cite>Latitude of natural origin</cite> (φ₀) parameter value. + * Valid values range is [-90 … 90]°. The default value is 0° + * + * <!-- 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_center </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> + */ + public 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> longitude_of_center </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 = Equirectangular.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 = Equirectangular.FALSE_NORTHING; + + /** + * The group of all parameters expected by this coordinate operation. + */ + private static final ParameterDescriptorGroup PARAMETERS; + static { + final ParameterBuilder builder = builder(); + LATITUDE_OF_ORIGIN = createLatitude(renameAlias(builder, Equirectangular.LATITUDE_OF_ORIGIN, + Citations.OGC, AlbersEqualArea.LATITUDE_OF_FALSE_ORIGIN), true); + + LONGITUDE_OF_ORIGIN = createLongitude(renameAlias(builder, Equirectangular.LONGITUDE_OF_ORIGIN, + Citations.OGC, AlbersEqualArea.LONGITUDE_OF_FALSE_ORIGIN)); + + PARAMETERS = builder + .addIdentifier(IDENTIFIER_OF_BASE) + .addName( "Lambert Azimuthal Equal Area") + .addName(Citations.OGC, "Lambert_Azimuthal_Equal_Area") + .addName(Citations.ESRI, "Lambert_Azimuthal_Equal_Area") + .addName(Citations.NETCDF, "LambertAzimuthalEqualArea") + .addName(Citations.GEOTIFF, "CT_LambertAzimEqualArea") + .addName(Citations.PROJ4, "laea") + .addIdentifier(Citations.GEOTIFF, "10") + .addIdentifier(Citations.MAP_INFO, "4") + .createGroupForMapProjection( + LATITUDE_OF_ORIGIN, + LONGITUDE_OF_ORIGIN, + FALSE_EASTING, + FALSE_NORTHING); + } + + /** + * Constructs a new provider. + */ + public LambertAzimuthalEqualArea() { + super(PARAMETERS); + } + + /** + * Constructs a math transform provider from a set of parameters. + * + * @param parameters the set of parameters (never {@code null}). + */ + LambertAzimuthalEqualArea(final ParameterDescriptorGroup parameters) { + super(parameters); + } + + /** + * Returns the operation type for this map projection. + * + * @return {@code PlanarProjection.class} + */ + @Override + public Class<PlanarProjection> getOperationType() { + return PlanarProjection.class; + } + + /** + * {@inheritDoc} + * + * @return the map projection created from the given parameter values. + */ + @Override + protected final NormalizedProjection createProjection(final Parameters parameters) { + return new org.apache.sis.referencing.operation.projection.LambertAzimuthalEqualArea(this, parameters); + } +} diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualAreaSpherical.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualAreaSpherical.java new file mode 100644 index 0000000000..6be0502cd2 --- /dev/null +++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/LambertAzimuthalEqualAreaSpherical.java @@ -0,0 +1,77 @@ +/* + * 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 javax.xml.bind.annotation.XmlTransient; +import org.apache.sis.parameter.ParameterBuilder; +import org.opengis.parameter.ParameterDescriptor; +import org.opengis.parameter.ParameterDescriptorGroup; + + +/** + * The provider for "<cite>Lambert Azimuthal Equal Area (Spherical)</cite>" projection (EPSG:1027). + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +@XmlTransient +public final class LambertAzimuthalEqualAreaSpherical extends LambertAzimuthalEqualArea { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -8356440804799174833L; + + /** + * The EPSG identifier, to be preferred to the name when available. + */ + public static final String IDENTIFIER = "1027"; + + /** + * The group of all parameters expected by this coordinate operation. + */ + static final ParameterDescriptorGroup PARAMETERS; + static { + final ParameterBuilder builder = builder(); + + final ParameterDescriptor<Double> latitudeOfOrigin = createLatitude(builder + .addNamesAndIdentifiers(LATITUDE_OF_ORIGIN).setDeprecated(true) + .addName("Spherical latitude of origin"), true); + + final ParameterDescriptor<Double> longitudeOfOrigin = createLongitude(builder + .addNamesAndIdentifiers(LONGITUDE_OF_ORIGIN).setDeprecated(true) + .addName("Spherical longitude of origin")); + + PARAMETERS = builder + .addIdentifier(IDENTIFIER) + .addName("Lambert Azimuthal Equal Area (Spherical)") + .setDeprecated(true).addIdentifier("9821").setDeprecated(false) + .createGroupForMapProjection( + latitudeOfOrigin, + longitudeOfOrigin, + FALSE_EASTING, + FALSE_NORTHING); + } + + /** + * Constructs a new provider. + */ + public LambertAzimuthalEqualAreaSpherical() { + super(PARAMETERS); + } +} diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java index ae12007d43..81e054b5c2 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/Builder.java @@ -985,18 +985,16 @@ public abstract class Builder<B extends Builder<B>> { } /** - * Sets whether the next {@code Identifier} or {@code IdentifiedObject}s to create shall be considered deprecated. - * Deprecated objects exist in some {@linkplain org.opengis.referencing.AuthorityFactory authority factories} like - * the EPSG database. - * - * <p>Note that this method does not apply to name and identifiers, which have their own - * {@code addDeprecatedFoo(…)} methods.</p> + * Sets whether the next {@code GenericName}s, {@code Identifier}s or {@code IdentifiedObject}s + * to create shall be considered deprecated. + * Deprecated objects exist in some {@linkplain org.opengis.referencing.AuthorityFactory authority factories} + * like the EPSG database. * * <p><b>Lifetime:</b> * Deprecation status is cleared after a {@code createXXX(…)} method has been invoked.</p> * - * @param deprecated {@code true} if the next names, identifiers and identified objects should be - * considered deprecated, or {@code false} otherwise. + * @param deprecated {@code true} if the next names, identifiers and identified + * objects should be considered deprecated, or {@code false} otherwise. * @return {@code this}, for method call chaining. * * @see AbstractIdentifiedObject#isDeprecated() diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java index 47a47558c4..3abebb6a42 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/EqualAreaProjection.java @@ -32,7 +32,7 @@ import static org.apache.sis.math.MathFunctions.atanh; * hierarchy with {@link ConformalProjection} and {@link EqualAreaProjection} being two distinct classes.</p> * * @author Martin Desruisseaux (Geomatys) - * @version 1.0 + * @version 1.2 * @since 0.8 * @module */ @@ -40,7 +40,7 @@ abstract class EqualAreaProjection extends NormalizedProjection { /** * For cross-version compatibility. */ - private static final long serialVersionUID = 2221537810038553082L; + private static final long serialVersionUID = 5880625564193782957L; /** * The threshold value of {@link #eccentricity} at which we consider the accuracy of the @@ -65,7 +65,7 @@ abstract class EqualAreaProjection extends NormalizedProjection { * * <div class="note"><b>Serialization note:</b> * we do not strictly need to serialize those fields since they could be computed after deserialization. - * Bu we serialize them anyway in order to simplify a little bit this class (it allows us to keep those + * But we serialize them anyway in order to simplify a little bit this class (it allows us to keep those * fields final) and because values computed after deserialization could be slightly different than the * ones computed after construction if a future version of the constructor uses the double-double values * provided by {@link Initializer}.</div> @@ -74,8 +74,21 @@ abstract class EqualAreaProjection extends NormalizedProjection { /** * Value of {@link #qm(double)} function (part of Snyder equation (3-12)) at pole (sinφ = 1). + * The <var>q</var><sub>p</sub> constant is defined by EPSG guidance note as: + * + * <blockquote>(1 – ℯ²)⋅([1 / (1 – ℯ²)] – {[1/(2ℯ)] ln [(1 – ℯ) / (1 + ℯ)]})</blockquote> + * + * But in this class, we omit the (1 – ℯ²) factor. + * + * <h4>Spherical case</h4> + * In the spherical case (ℯ=0), the value is exactly 2. */ - private final double qmPolar; + final double qmPolar; + + /** + * {@code true} if {@link #eccentricity} is zero. + */ + final boolean isSpherical; /** * {@code true} if the {@link #eccentricity} value is greater than or equals to the eccentricity threshold, @@ -90,6 +103,7 @@ abstract class EqualAreaProjection extends NormalizedProjection { */ EqualAreaProjection(final Initializer initializer) { super(initializer); + isSpherical = (eccentricitySquared == 0); final double e2 = eccentricitySquared; final double e4 = e2 * e2; final double e6 = e2 * e4; @@ -135,6 +149,7 @@ abstract class EqualAreaProjection extends NormalizedProjection { c4β = other.c4β; c6β = other.c6β; qmPolar = other.qmPolar; + isSpherical = other.isSpherical; useIterations = other.useIterations; } @@ -165,7 +180,7 @@ abstract class EqualAreaProjection extends NormalizedProjection { * Check for zero eccentricity is required because qm_ellipsoid(sinφ) would * simplify to sinφ + atanh(0) / 0 == sinφ + 0/0, thus producing NaN. */ - return (eccentricity == 0) ? 2*sinφ : qm_ellipsoid(sinφ); + return isSpherical ? 2*sinφ : qm_ellipsoid(sinφ); } /** @@ -202,6 +217,10 @@ abstract class EqualAreaProjection extends NormalizedProjection { * have a sufficient amount of terms for achieving the centimetric precision, so we "finish" it by the * iterative method. The series expansion is nevertheless useful for reducing the number of iterations. * + * <h4>Spherical case</h4> + * In the spherical case, this method returns {@code asin(y/2)}. This method does not use that + * simplification for the spherical case. This optimization is left to the caller if desired. + * * @param y in the cylindrical case, this is northing on the normalized ellipsoid. * @return the latitude in radians. */ diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualArea.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualArea.java new file mode 100644 index 0000000000..8a4cac76ee --- /dev/null +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualArea.java @@ -0,0 +1,283 @@ +/* + * 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.referencing.operation.projection; + +import java.util.EnumMap; +import org.opengis.parameter.ParameterDescriptor; +import org.opengis.referencing.operation.Matrix; +import org.opengis.referencing.operation.OperationMethod; +import org.apache.sis.referencing.operation.matrix.Matrix2; +import org.apache.sis.referencing.operation.matrix.MatrixSIS; +import org.apache.sis.referencing.operation.transform.ContextualParameters; +import org.apache.sis.internal.util.DoubleDouble; +import org.apache.sis.parameter.Parameters; +import org.apache.sis.util.Workaround; + +import static java.lang.Math.*; +import static org.apache.sis.internal.referencing.Formulas.fastHypot; +import static org.apache.sis.internal.referencing.provider.LambertAzimuthalEqualArea.*; + + +/** + * <cite>Lambert Azimuthal Equal Area</cite> projection (EPSG code 9820). + * See the following references for an overview: + * <ul> + * <li><a href="https://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection">Lambert Azimuthal projection on Wikipedia</a></li> + * <li><a href="https://mathworld.wolfram.com/LambertAzimuthalEqual-AreaProjection.html">Lambert Azimuthal Equal-Area projection on MathWorld</a></li> + * </ul> + * + * @author Martin Desruisseaux (Geomatys) + * @author Rémi Maréchal (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public class LambertAzimuthalEqualArea extends EqualAreaProjection { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 7419101460426922558L; + + /** + * Sine and cosine of authalic latitude of origin. In the spherical case, + * the authalic latitude ß and the geodetic latitude φ are the same. + * + * <h4>Possible simplification</h4> + * In equatorial case, sin(ß₀)=0 and cos(ß₀)=1. We could do special cases with simplifications in the + * {@code transform(…)} formulas, but the result does not seem simpler enough to worth code branching. + * + * <p>In the polar case, sin(ß₀)=1 and cos(ß₀)=0. But the equations become indeterminate (we get 0/0) + * and a different set of equations must be used.</p> + */ + private final double sinß0, cosß0; + + /** + * {@code true} if the projection is at a pole. + * This implementation does not need to distinguish between North and South pole. + * Formulas in this class are for the South case only, and this class handles the + * North case by reverting the sign of φ before conversion and y after conversion. + */ + private final boolean polar; + + /** + * Creates a Lambert Azimuthal Equal Area projection from the given parameters. + * + * @param method description of the projection parameters. + * @param parameters the parameter values of the projection to create. + */ + public LambertAzimuthalEqualArea(final OperationMethod method, final Parameters parameters) { + this(initializer(method, parameters)); + } + + /** + * Work around for RFE #4093999 in Sun's bug database + * ("Relax constraint on placement of this()/super() call in constructors"). + */ + @SuppressWarnings("fallthrough") + @Workaround(library="JDK", version="1.7") + private static Initializer initializer(final OperationMethod method, final Parameters parameters) { + final EnumMap<ParameterRole, ParameterDescriptor<Double>> roles = new EnumMap<>(ParameterRole.class); + roles.put(ParameterRole.CENTRAL_MERIDIAN, LONGITUDE_OF_ORIGIN); + roles.put(ParameterRole.FALSE_EASTING, FALSE_EASTING); + roles.put(ParameterRole.FALSE_NORTHING, FALSE_NORTHING); + return new Initializer(method, parameters, roles, null); + } + + /** + * Work around for RFE #4093999 in Sun's bug database + * ("Relax constraint on placement of this()/super() call in constructors"). + */ + @Workaround(library="JDK", version="1.7") + private LambertAzimuthalEqualArea(final Initializer initializer) { + super(initializer); + final double φ0 = toRadians(initializer.getAndStore(LATITUDE_OF_ORIGIN)); + final double sinφ0 = sin(φ0); + final double cosφ0 = cos(φ0); + polar = abs(sinφ0) == 1; // sin(φ) == 1 implies a tolerance ∆φ≈1E-6°. + sinß0 = qm(sinφ0) / qmPolar; + cosß0 = sqrt(1 - sinß0*sinß0); + /* + * In the polar case we have cos(φ₀) ≈ 0 and cos(ß₀) ≈ 0, which cause D = 0/0. + * Trying to evaluate the indeterminate with L'Hôpital's rule produce infinity. + * Consequently a different set of formulas for the polar form must be used not + * only here but also in the `transform(…)` and `inverseTransform(…)` methods. + */ + final MatrixSIS denormalize = getContextualParameters().getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION); + if (polar) { + final DoubleDouble c = initializer.axisLengthRatio(); + denormalize.convertBefore(0, c, null); + denormalize.convertBefore(1, c, null); + if (φ0 > 0) { // North pole case: use South pole formulas with sign of φ and y reverted. + final MatrixSIS normalize = getContextualParameters().getMatrix(ContextualParameters.MatrixRole.NORMALIZATION); + normalize .convertBefore(1, -1, null); + denormalize.convertBefore(1, -1, null); + } + } else { + final double D = cosφ0 / (sqrt(1 - eccentricitySquared*(sinφ0*sinφ0)) * cosß0); + final double Rq2 = (1 - eccentricitySquared) * qmPolar/2; + denormalize.convertBefore(0, D, null); + denormalize.convertBefore(1, Rq2/D, null); + } + } + + /** + * Creates a new projection initialized to the same parameters than the given one. + */ + LambertAzimuthalEqualArea(final LambertAzimuthalEqualArea other) { + super(other); + sinß0 = other.sinß0; + cosß0 = other.cosß0; + polar = other.polar; + } + + /** + * Returns the names of additional internal parameters which need to be taken in account when + * comparing two {@code LambertAzimuthalEqualArea} projections or formatting them in debug mode. + */ + @Override + final String[] getInternalParameterNames() { + return new String[] {"ß₀"}; + } + + /** + * Returns the values of additional internal parameters which need to be taken in account when + * comparing two {@code LambertAzimuthalEqualArea} projections or formatting them in debug mode. + */ + @Override + final double[] getInternalParameterValues() { + return new double[] {asin(sinß0)}; + } + + /** + * Converts the specified (λ,φ)) coordinate (units in radians) and stores the result in {@code dstPts}. + * In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}. + * + * @return the matrix of the projection derivative at the given source position, + * or {@code null} if the {@code derivate} argument is {@code false}. + * @throws ProjectionException if the coordinate can not be converted. + */ + @Override + public Matrix transform(final double[] srcPts, final int srcOff, + final double[] dstPts, final int dstOff, + final boolean derivate) throws ProjectionException + { + final double λ = srcPts[srcOff ]; + final double φ = srcPts[srcOff+1]; + final double sinλ = sin(λ); + final double cosλ = cos(λ); + final double sinφ = sin(φ); + final double qm = qm(sinφ); + if (!polar) { + final double sinß = qm / qmPolar; + final double cosß = sqrt(1 - sinß*sinß); + final double c = sinß0*sinß + cosß0*cosß*cosλ + 1; + /* + * The `c` factor is 0 when projecting the antipodal point of origin. + * The antipodal point is actually all points on a circle of radius 2. + * We can not return a single point, so NaN seems a safer value. + */ + double B = sqrt(2 / c); + if (B == Double.POSITIVE_INFINITY) { + B = Double.NaN; + } + final double x = cosß*sinλ; // (x,y)×B = (E,N) + final double y = cosß0*sinß - sinß0*cosß*cosλ; + if (dstPts != null) { + dstPts[dstOff ] = B * x; + dstPts[dstOff+1] = B * y; + } + /* + * End of map projection. Compute the derivative if it was requested. + */ + if (!derivate) { + return null; + } + final double dsinß_dφ = dqm_dφ(sinφ, cos(φ)) / qmPolar; + final double dcosß_dφ = -dsinß_dφ * (sinß/cosß); + final double cosλdcosß = dcosß_dφ * cosλ; + final double dy_dλ = sinß0*x; + final double db_dλ = cosß0*x / (2*c); + final double dy_dφ = (cosß0*dsinß_dφ - sinß0*cosλdcosß); + final double db_dφ = -(sinß0*dsinß_dφ + cosß0*cosλdcosß) / (2*c); + return new Matrix2( + B * (cosλ + db_dλ*sinλ)*cosß, + B * (dcosß_dφ + db_dφ*cosß)*sinλ, + B * (dy_dλ + db_dλ*y), + B * (dy_dφ + db_dφ*y)); + } + /* + * Polar case needs special code because formula for the oblique case become indeterminate. + * North pole case is handled with the same formula, but with sign of φ reversed by the normalize + * affine transform (which cause the sign of q to be also reversed). After the transform, sign of + * y will be reversed by the denormalize affine transform, so it should not be reversed here. + */ + double ρ = sqrt(qmPolar + qm); + if (sinφ == 1) { + ρ = Double.NaN; // Antipodal point. + } + final double x = ρ * sinλ; + final double y = ρ * cosλ; + if (dstPts != null) { + dstPts[dstOff ] = x; + dstPts[dstOff+1] = y; + } + if (!derivate) { + return null; + } + final double db_dφ = dqm_dφ(sinφ, cos(φ)) / (2*ρ); + return new Matrix2(y, db_dφ * sinλ, + -x, db_dφ * cosλ); + } + + /** + * Converts the specified (<var>x</var>,<var>y</var>) coordinates and stores the (λ,φ) result in {@code dstPts}. + * + * @throws ProjectionException if the point can not be converted. + */ + @Override + protected void inverseTransform(final double[] srcPts, final int srcOff, + final double[] dstPts, final int dstOff) + throws ProjectionException + { + double x = srcPts[srcOff ]; + double y = srcPts[srcOff+1]; + double sinß_qp; // sin(ß) × qmPolar + if (polar) { + sinß_qp = (x*x + y*y) - qmPolar; + } else { + final double ρ = fastHypot(x, y); + final double C = 2*asin(ρ / 2); + final double cosC = cos(C); + final double sinC = sin(C); + final double ysinC = y*sinC; + sinß_qp = cosC*sinß0; + if (cosC != 1) { // cos(C) == 1 implies y/ρ = 0/0. + sinß_qp += ysinC*cosß0/ρ; + } + sinß_qp *= qmPolar; + y = ρ*cosC*cosß0 - ysinC*sinß0; + x *= sinC; + } + dstPts[dstOff ] = atan2(x, y); + dstPts[dstOff+1] = isSpherical ? asin(sinß_qp/2) : φ(sinß_qp); + } + + /* + * We do not provide a specialized sub-class for the spherical case + * because the simplifications are too small. + */ +} diff --git a/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod b/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod index 0e11fa01f6..52f61edf69 100644 --- a/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod +++ b/core/sis-referencing/src/main/resources/META-INF/services/org.opengis.referencing.operation.OperationMethod @@ -42,6 +42,8 @@ org.apache.sis.internal.referencing.provider.LambertConformalBelgium org.apache.sis.internal.referencing.provider.LambertConformalMichigan org.apache.sis.internal.referencing.provider.LambertCylindricalEqualArea org.apache.sis.internal.referencing.provider.LambertCylindricalEqualAreaSpherical +org.apache.sis.internal.referencing.provider.LambertAzimuthalEqualArea +org.apache.sis.internal.referencing.provider.LambertAzimuthalEqualAreaSpherical org.apache.sis.internal.referencing.provider.AlbersEqualArea org.apache.sis.internal.referencing.provider.TransverseMercator org.apache.sis.internal.referencing.provider.TransverseMercatorSouth diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java index 4810aa159a..fb24e65e70 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/ProvidersTest.java @@ -91,6 +91,8 @@ public final strictfp class ProvidersTest extends TestCase { LambertConformalMichigan.class, LambertCylindricalEqualArea.class, LambertCylindricalEqualAreaSpherical.class, + LambertAzimuthalEqualArea.class, + LambertAzimuthalEqualAreaSpherical.class, AlbersEqualArea.class, TransverseMercator.class, TransverseMercatorSouth.class, diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualAreaTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualAreaTest.java new file mode 100644 index 0000000000..72427c2dac --- /dev/null +++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/LambertAzimuthalEqualAreaTest.java @@ -0,0 +1,357 @@ +/* + * 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.referencing.operation.projection; + +import org.opengis.util.FactoryException; +import org.opengis.referencing.operation.TransformException; +import org.apache.sis.referencing.operation.transform.CoordinateDomain; +import org.apache.sis.referencing.operation.transform.MathTransformFactoryMock; +import org.apache.sis.internal.referencing.provider.MapProjection; +import org.apache.sis.internal.metadata.ReferencingServices; +import org.apache.sis.parameter.Parameters; +import org.junit.Test; + +import static java.lang.Double.NaN; +import static java.lang.StrictMath.*; +import static org.apache.sis.math.MathFunctions.SQRT_2; +import static org.apache.sis.internal.referencing.Formulas.LINEAR_TOLERANCE; +import static org.apache.sis.internal.referencing.Formulas.ANGULAR_TOLERANCE; +import static org.junit.Assert.*; + + +/** + * Tests the {@link LambertAzimuthalEqualArea} class. We test using various values + * of the latitude of origin, which is the only parameter impacting the internal + * coefficients of that class (except for the eccentricity). + * + * @author Martin Desruisseaux (Geomatys) + * @author Rémi Maréchal (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public final strictfp class LambertAzimuthalEqualAreaTest extends MapProjectionTestCase { + /** + * The radius of the sphere used in sphere test cases. + */ + private static final double SPHERE_RADIUS = ReferencingServices.AUTHALIC_RADIUS; + + /** + * Returns the provider for the map projection to tesT. + */ + private static MapProjection provider(final boolean elliptical) { + return elliptical ? new org.apache.sis.internal.referencing.provider.LambertAzimuthalEqualArea() + : new org.apache.sis.internal.referencing.provider.LambertAzimuthalEqualAreaSpherical(); + } + + /** + * Creates and validates a new instance of {@link LambertAzimuthalEqualArea}. + * + * @param elliptical {@code false} for a sphere, or {@code true} for WGS84 ellipsoid. + * @param latitudeOfOrigin the latitude of origin. + * @param complete whether to create the full projection, working on degrees instead of radians. + * @throws FactoryException if an error occurred while creating the map projection. + */ + private void createProjection(final boolean elliptical, final double latitudeOfOrigin, final boolean complete) + throws FactoryException + { + final MapProjection provider = provider(elliptical); + final Parameters parameters = parameters(provider, elliptical); + parameters.parameter("latitude_of_origin").setValue(latitudeOfOrigin); + LambertAzimuthalEqualArea projection = new LambertAzimuthalEqualArea(provider, parameters); + if (complete) { + transform = projection.createMapProjection(new MathTransformFactoryMock(provider)); + } else { + transform = projection; + } + validate(); + } + + /** + * Tests oblique case with a point computed with PROJ. + * Command line was: + * + * <pre>cs2cs "EPSG:4326" +to +type=crs +proj=laea +no_defs +lat_0=90 +lon_0=0 +datum=WGS84 <<< "72 50"</pre> + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testPolar() throws FactoryException, TransformException { + tolerance = LINEAR_TOLERANCE; + createProjection(true, 90, true); + final double[] point = new double[] {50, 72}; + final double[] expected = new double[] {1533302.80, -1286593.82}; + verifyTransform(point, expected); + } + + /** + * Tests oblique case with a point computed with PROJ. + * Command line was: + * + * <pre>cs2cs "EPSG:4326" +to +type=crs +proj=laea +no_defs +lat_0=37 +lon_0=0 +datum=WGS84 <<< "25 -9"</pre> + * + * Note that {@link #runGeoapiTest()} performs a similar test. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testOblique() throws FactoryException, TransformException { + tolerance = LINEAR_TOLERANCE; + createProjection(true, 37, true); + final double[] point = new double[] {-9, 25}; + final double[] expected = new double[] {-911656.53, -1288191.92}; + verifyTransform(point, expected); + } + + /** + * Tests self-consistency of the unitary projection on a sphere. + * The projection works with radians on a sphere of radius 1. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testConsistencyOfUnitaryOnSphere() throws FactoryException, TransformException { + final double delta = toRadians(100.0 / 60) / 1852; // Approximatively 100 metres. + derivativeDeltas = new double[] {delta, delta}; + tolerance = ANGULAR_TOLERANCE; + for (int φ = -90; φ <= 90; φ += 15) { + createProjection(false, φ, false); + verifyInDomain(CoordinateDomain.GEOGRAPHIC_RADIANS, 862247543); + } + } + + /** + * Tests self-consistency of the unitary projection on an ellipse. + * The projection works with radians on an ellipsoid of semi-major axis length 1. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testConsistencyOfUnitaryOnEllipse() throws FactoryException, TransformException { + final double delta = toRadians(100.0 / 60) / 1852; // Approximatively 100 metres. + derivativeDeltas = new double[] {delta, delta}; + tolerance = ANGULAR_TOLERANCE; + for (int φ = -90; φ <= 90; φ += 15) { + createProjection(true, φ, false); + verifyInDomain(CoordinateDomain.GEOGRAPHIC_RADIANS, 484117986); + } + } + + /** + * Tests the projection at a few particular points in the oblique case. + * The particular points are the origin and the (almost) antipodal points. + * The projection works with radians on a sphere of radius 1. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testParticularPointsWithObliqueOnSphere() throws FactoryException, TransformException { + testParticularPointsWithOblique(false); + } + + /** + * Tests the projection at a few particular points in the oblique case. + * The particular points are the origin and the (almost) antipodal points. + * The projection works with degrees on an ellipsoid of semi-major axis length 1. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testParticularPointsWithObliqueOnEllipse() throws FactoryException, TransformException { + testParticularPointsWithOblique(true); + } + + /** + * Implementation of {@link #testParticularPointsWithObliqueOnSphere()} + * and {@link #testParticularPointsWithObliqueOnEllipse()}. + */ + private void testParticularPointsWithOblique(final boolean elliptical) throws FactoryException, TransformException { + tolerance = LINEAR_TOLERANCE; + createProjection(elliptical, 45, true); + + // Projects the origin. + final double[] point = new double[] {0, 45}; + final double[] expected = new double[] {0, 0}; + verifyTransform(point, expected); + + // Project the antipode. + point[0] = 180; + point[1] = -45; + transform.transform(point, 0, point, 0, 1); + assertEquals("E", NaN, point[0], tolerance); + assertEquals("N", NaN, point[1], tolerance); + + // Project the almost-antipode. + point[0] = 180; + point[1] = -44.999; + transform.transform(point, 0, point, 0, 1); + assertEquals("E", 0, point[0], tolerance); + assertEquals("N", 2*SPHERE_RADIUS, point[1], 0.002*SPHERE_RADIUS); + } + + /** + * Tests the projection at a few extreme points in the polar case. + * The projection works with radians on a sphere of radius 1. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testParticularPointsWithPolarOnSphere() throws FactoryException, TransformException { + testParticularPointsWithPolar(false); + } + + /** + * Tests the projection at a few extreme points in the polar case. + * The projection works with radians on an ellipsoid of semi-major axis length 1. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testParticularPointsWithPolarOnEllipse() throws FactoryException, TransformException { + testParticularPointsWithPolar(true); + } + + /** + * Implementation of {@link #testParticularPointsWithPolarOnSphere()} + * and {@link #testParticularPointsWithPolarOnEllipse()}. + */ + private void testParticularPointsWithPolar(final boolean elliptical) throws FactoryException, TransformException { + tolerance = LINEAR_TOLERANCE; + createProjection(elliptical, 90, true); + /* + * Project the origin. Result should be (0,0). Do not test the inverse projection + * because the longitude could be anything and still be the North pole. We test that + * by projecting again with an other longitude, set to 45°, and expect the same result. + */ + final double[] point = new double[] {0, 90}; + final double[] expected = new double[] {0, 0}; + isInverseTransformSupported = false; + verifyTransform(point, expected); + transform.inverse().transform(expected, 0, point, 0, 1); + assertEquals("λ", 0, point[0], 180); + assertEquals("φ", 90, point[1], ANGULAR_TOLERANCE); + /* + * Same point as above (i.e. projection origin), but using a different longitude value. + * Same expected result because longitude should have no effect at a pole. + */ + point[0] = 45; + point[1] = 90; + verifyTransform(point, expected); + transform.inverse().transform(expected, 0, point, 0, 1); + assertEquals("λ", 45, point[0], 180); + assertEquals("φ", 90, point[1], ANGULAR_TOLERANCE); + /* + * Project a point on the equator, at 0° and at 180° longitude. + * Result should be (0, √2) positive or negative depending on the longitude. + */ + point[0] = 0; expected[0] = 0; + point[1] = 0; expected[1] = -SQRT_2*SPHERE_RADIUS; + isInverseTransformSupported = true; + if (elliptical) tolerance = 0.5; // Because the use of `SPHERE_RADIUS` is approximate. + verifyTransform(point, expected); + + point[0] = 180; expected[0] = 0; + point[1] = 0; expected[1] = SQRT_2*SPHERE_RADIUS; + verifyTransform(point, expected); + /* + * Project the antipode. Result would be (0, -2) if this operation was allowed. + * Actually the formulas would work, but every points on a circle or radius 2 + * would be the pole so returning a single value may not be appropriate. + */ + point[0] = 0; + point[1] = -90; + transform.transform(point, 0, point, 0, 1); + assertEquals("E", NaN, point[0], tolerance); + assertEquals("N", NaN, point[1], tolerance); + } + + /** + * Tests consistency when converting a point forward then backward. + * The projection works with radians on a sphere of radius 1. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testRoundtrip() throws FactoryException, TransformException { + tolerance = ANGULAR_TOLERANCE; + createProjection(false, -75, false); + final double[] point = new double[] {toRadians(-90), toRadians(-8)}; + transform.transform(point, 0, point, 0, 1); + transform.inverse().transform(point, 0, point, 0, 1); + assertEquals(-90, toDegrees(point[0]), tolerance); + assertEquals( -8, toDegrees(point[1]), tolerance); + } + + /** + * Creates a projection and tests the derivatives at a few points. + * The projection works with radians on an ellipsoid of semi-major axis length 1. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + */ + @Test + public void testDerivative() throws FactoryException, TransformException { + tolerance = 1E-9; + final double delta = toRadians(100.0 / 60) / 1852; // Approximatively 100 metres. + derivativeDeltas = new double[] {delta, delta}; + + // Polar projection. + createProjection(true, 90, false); + verifyDerivative(toRadians(-6), toRadians(80)); + + // Intentionally above the pole. + verifyDerivative(toRadians(-6), toRadians(100)); + + // Polar projection, spherical formulas. + createProjection(false, 90, false); + verifyDerivative(toRadians(-6), toRadians(85)); + + // Equatorial projection, spherical formulas. + createProjection(false, 0, false); + verifyDerivative(toRadians(3), toRadians(4)); + + // Oblique projection, ellipsoidal formulas. + createProjection(true, 8, false); + verifyDerivative(toRadians(-6), toRadians(2)); + + // Oblique projection, spherical formulas. + createProjection(false, 8, false); + verifyDerivative(toRadians(-6), toRadians(2)); + } + + /** + * Runs the test defined in the GeoAPI-conformance module. + * + * @throws FactoryException if an error occurred while creating the map projection. + * @throws TransformException if an error occurred while projecting a coordinate. + * + * @see org.opengis.test.referencing.ParameterizedTransformTest#testLambertAzimuthalEqualArea() + */ + @Test + public void runGeoapiTest() throws FactoryException, TransformException { + createGeoApiTest(provider(true)).testLambertAzimuthalEqualArea(); + } +} diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java index 22a252697f..9417060f15 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java @@ -192,6 +192,7 @@ import org.junit.BeforeClass; org.apache.sis.referencing.operation.projection.EqualAreaProjectionTest.class, org.apache.sis.referencing.operation.projection.CylindricalEqualAreaTest.class, org.apache.sis.referencing.operation.projection.AlbersEqualAreaTest.class, + org.apache.sis.referencing.operation.projection.LambertAzimuthalEqualAreaTest.class, org.apache.sis.referencing.operation.projection.MeridianArcTest.class, org.apache.sis.referencing.operation.projection.SinusoidalTest.class, org.apache.sis.referencing.operation.projection.PolyconicTest.class,
