blob: 059dfd3812a9a7af74456470dd1ad246d38ee904 [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.referencing.operation.projection;
import java.util.Random;
import java.math.BigDecimal;
import org.opengis.util.FactoryException;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.internal.referencing.provider.LambertConformal1SP;
import org.apache.sis.internal.referencing.provider.LambertConformal2SP;
import org.apache.sis.internal.referencing.provider.LambertConformalWest;
import org.apache.sis.internal.referencing.provider.LambertConformalBelgium;
import org.apache.sis.referencing.operation.transform.CoordinateDomain;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.DependsOn;
import org.apache.sis.test.TestUtilities;
import org.junit.Test;
import static java.lang.StrictMath.*;
import static java.lang.Double.*;
import static org.apache.sis.test.Assert.*;
// Branch-specific imports
import static org.junit.Assume.assumeTrue;
import static org.apache.sis.test.Assert.PENDING_NEXT_GEOAPI_RELEASE;
/**
* Tests the {@link LambertConicConformal} class. We test using various values of the latitude of origin.
* We do not test with various values of standard parallels, because it is just an other way to set
* the value of the <var>n</var> field in {@code LambertConicConformal}. As long as we make this value varying,
* the latitude of origin is the simplest approach.
*
* @author Martin Desruisseaux (Geomatys)
* @author Rémi Maréchal (Geomatys)
* @version 1.1
* @since 0.6
* @module
*/
@DependsOn(ConformalProjectionTest.class)
public final strictfp class LambertConicConformalTest extends MapProjectionTestCase {
/**
* Verifies the value of the constant used in <cite>"Lambert Conic Conformal (2SP Belgium)"</cite> projection.
*
* @see #testLambertConicConformalBelgium()
*/
@Test
public void verifyBelgeConstant() {
final DoubleDouble BELGE_A = (DoubleDouble) LambertConicConformal.belgeA();
BigDecimal a = new BigDecimal(BELGE_A.value);
a = a.add (new BigDecimal(BELGE_A.error));
a = a.multiply(new BigDecimal("57.29577951308232087679815481410517")); // Conversion from radians to degrees.
a = a.multiply(new BigDecimal(60 * 60)); // Conversion from degrees to seconds.
a = a.add (new BigDecimal("29.2985")); // The standard value.
assertTrue(abs(a.doubleValue()) < 1E-31);
}
/**
* Creates a new instance of {@link LambertConicConformal}. See the class javadoc for an explanation
* about why we ask only for the latitude of origin and not the standard parallels.
*
* @param ellipsoidal {@code false} for a sphere, or {@code true} for WGS84 ellipsoid.
* @param latitudeOfOrigin the latitude of origin, in decimal degrees.
*/
private void createNormalizedProjection(final boolean ellipsoidal, final double latitudeOfOrigin) {
final LambertConformal1SP method = new LambertConformal1SP();
final Parameters parameters = parameters(method, ellipsoidal);
parameters.getOrCreate(LambertConformal1SP.LATITUDE_OF_ORIGIN).setValue(latitudeOfOrigin);
transform = new LambertConicConformal(method, parameters);
if (!ellipsoidal) {
transform = new LambertConicConformal.Spherical((LambertConicConformal) transform);
}
tolerance = NORMALIZED_TOLERANCE;
validate();
}
/**
* Tests the WKT formatting of {@link NormalizedProjection}. For the Lambert Conformal projection, we expect
* the internal {@code n} parameter in addition to the eccentricity.
*
* <h4>Note on accuracy</h4>
* The value of the eccentricity parameter should be fully accurate because it is calculated using only the
* {@link Math#sqrt(double)} function (ignoring basic algebraic operations) which, according javadoc, must
* give the result closest to the true mathematical result. But the functions involved in the calculation of
* <var>n</var> do not have such strong guarantees. So we use a regular expression in this test for ignoring
* the 2 last digits of <var>n</var>.
*/
@Test
public void testNormalizedWKT() {
createNormalizedProjection(true, 40);
assertWktEqualsRegex("(?m)\\Q" +
"PARAM_MT[“Lambert conic conformal (radians domain)”,\n" +
" PARAMETER[“eccentricity”, 0.0818191908426215],\n" +
" PARAMETER[“n”, 0.64278760968653\\E\\d*\\Q]]\\E"); // 0.6427876096865393 in the original test.
}
/**
* Tests the projection at some special latitudes (0, ±π/2, NaN and others).
*
* @throws ProjectionException if an error occurred while projecting a point.
*/
@Test
public void testSpecialLatitudes() throws ProjectionException {
if (transform == null) { // May have been initialized by 'testSphericalCase'.
createNormalizedProjection(true, 40); // Elliptical case
}
final double INF = POSITIVE_INFINITY;
assertEquals ("Not a number", NaN, transform(NaN), NORMALIZED_TOLERANCE);
assertEquals ("Out of range", NaN, transform(+2), NORMALIZED_TOLERANCE);
assertEquals ("Out of range", NaN, transform(-2), NORMALIZED_TOLERANCE);
assertEquals ("Forward 0°N", 1, transform(0), NORMALIZED_TOLERANCE);
assertEquals ("Forward 90°S", 0, transform(-PI/2), NORMALIZED_TOLERANCE);
assertEquals ("Forward 90°N", INF, transform(+PI/2), NORMALIZED_TOLERANCE);
assertEquals ("Forward (90+ε)°S", 0, transform(nextDown(-PI/2)), NORMALIZED_TOLERANCE);
assertEquals ("Forward (90+ε)°N", INF, transform(nextUp (+PI/2)), NORMALIZED_TOLERANCE);
assertEquals ("Forward (90-ε)°S", 0, transform(nextUp (-PI/2)), 1E-10);
assertEquals ("Not a number", NaN, inverseTransform(NaN), NORMALIZED_TOLERANCE);
assertEquals ("Inverse 0", -PI/2, inverseTransform( 0), NORMALIZED_TOLERANCE);
assertEquals ("Inverse +1", 0, inverseTransform(+1), NORMALIZED_TOLERANCE);
assertEquals ("Inverse -1", 0, inverseTransform(-1), NORMALIZED_TOLERANCE);
assertEquals ("Inverse +∞", +PI/2, inverseTransform(INF), NORMALIZED_TOLERANCE);
assertEquals ("Inverse −∞", +PI/2, inverseTransform(-INF), NORMALIZED_TOLERANCE);
// Like the north case, but with sign inversed.
createNormalizedProjection(((LambertConicConformal) transform).eccentricity != 0, -40);
validate();
assertEquals ("Not a number", NaN, transform(NaN), NORMALIZED_TOLERANCE);
assertEquals ("Out of range", NaN, transform(+2), NORMALIZED_TOLERANCE);
assertEquals ("Out of range", NaN, transform(-2), NORMALIZED_TOLERANCE);
assertEquals ("Forward 0°N", 1, transform(0), NORMALIZED_TOLERANCE);
assertEquals ("Forward 90°N", INF, transform(+PI/2), NORMALIZED_TOLERANCE);
assertEquals ("Forward 90°S", 0, transform(-PI/2), NORMALIZED_TOLERANCE);
assertEquals ("Forward (90+ε)°N", INF, transform(nextUp (+PI/2)), NORMALIZED_TOLERANCE);
assertEquals ("Forward (90+ε)°S", 0, transform(nextDown(-PI/2)), NORMALIZED_TOLERANCE);
assertEquals ("Forward (90-ε)°S", 0, transform(nextUp (-PI/2)), 1E-10);
assertEquals ("Not a number", NaN, inverseTransform(NaN), NORMALIZED_TOLERANCE);
assertEquals ("Inverse 0", -PI/2, inverseTransform( 0), NORMALIZED_TOLERANCE);
assertEquals ("Inverse +∞", +PI/2, inverseTransform(INF), NORMALIZED_TOLERANCE);
assertEquals ("Inverse −∞", +PI/2, inverseTransform(-INF), NORMALIZED_TOLERANCE);
}
/**
* Tests the derivatives at a few points. This method compares the derivatives computed by
* the projection with an estimation of derivatives computed by the finite differences method.
*
* @throws TransformException if an error occurred while projecting a point.
*/
@Test
@DependsOnMethod("testSpecialLatitudes")
public void testDerivative() throws TransformException {
if (transform == null) { // May have been initialized by 'testSphericalCase'.
createNormalizedProjection(true, 40); // Elliptical case
}
final double delta = toRadians(100.0 / 60) / 1852; // Approximately 100 metres.
derivativeDeltas = new double[] {delta, delta};
tolerance = 1E-9;
verifyDerivative(toRadians( 0), toRadians( 0));
verifyDerivative(toRadians(15), toRadians(30));
verifyDerivative(toRadians(10), toRadians(60));
}
/**
* Tests the <cite>"Lambert Conic Conformal (1SP)"</cite> case (EPSG:9801).
* This test is defined in GeoAPI conformance test suite.
*
* @throws FactoryException if an error occurred while creating the map projection.
* @throws TransformException if an error occurred while projecting a coordinate.
*/
@Test
@DependsOnMethod({"testSpecialLatitudes", "testDerivative"})
public void testLambertConicConformal1SP() throws FactoryException, TransformException {
createGeoApiTest(new LambertConformal1SP()).testLambertConicConformal1SP();
}
/**
* Tests the <cite>"Lambert Conic Conformal (2SP)"</cite> case (EPSG:9802).
* This test is defined in GeoAPI conformance test suite.
*
* @throws FactoryException if an error occurred while creating the map projection.
* @throws TransformException if an error occurred while projecting a coordinate.
*/
@Test
@DependsOnMethod("testLambertConicConformal1SP")
public void testLambertConicConformal2SP() throws FactoryException, TransformException {
createGeoApiTest(new LambertConformal2SP()).testLambertConicConformal2SP();
}
/**
* Tests the <cite>"Lambert Conic Conformal (2SP Belgium)"</cite> case (EPSG:9803).
* This test is defined in GeoAPI conformance test suite.
*
* @throws FactoryException if an error occurred while creating the map projection.
* @throws TransformException if an error occurred while projecting a coordinate.
*/
@Test
@DependsOnMethod({"testLambertConicConformal2SP", "verifyBelgeConstant"})
public void testLambertConicConformalBelgium() throws FactoryException, TransformException {
createGeoApiTest(new LambertConformalBelgium()).testLambertConicConformalBelgium();
}
/**
* Tests the <cite>"Lambert Conic Conformal (2SP Michigan)"</cite> case (EPSG:1051).
* This test is defined in GeoAPI conformance test suite.
*
* @throws FactoryException if an error occurred while creating the map projection.
* @throws TransformException if an error occurred while projecting a coordinate.
*/
@Test
@DependsOnMethod("testLambertConicConformal2SP")
public void testLambertConicConformalMichigan() throws FactoryException, TransformException {
assumeTrue(PENDING_NEXT_GEOAPI_RELEASE); // Test not available in GeoAPI 3.0
}
/**
* Tests the <cite>"Lambert Conic Conformal (1SP West Orientated)"</cite> case (EPSG:9826)).
*
* @throws FactoryException if an error occurred while creating the map projection.
* @throws TransformException if an error occurred while projecting a coordinate.
*/
@Test
@DependsOnMethod("testLambertConicConformal1SP")
public void testLambertConicConformalWestOrientated() throws FactoryException, TransformException {
createCompleteProjection(new LambertConformal1SP(),
WGS84_A, // Semi-major axis length
WGS84_B, // Semi-minor axis length
0.5, // Central meridian
40, // Latitude of origin
NaN, // Standard parallel 1
NaN, // Standard parallel 2
0.997, // Scale factor
200, // False easting
100); // False northing
final MathTransform reference = transform;
createCompleteProjection(new LambertConformalWest(),
WGS84_A, // Semi-major axis length
WGS84_B, // Semi-minor axis length
0.5, // Central meridian
40, // Latitude of origin
NaN, // Standard parallel 1
NaN, // Standard parallel 2
0.997, // Scale factor
200, // False easting
100); // False northing
final Random random = TestUtilities.createRandomNumberGenerator();
final double[] sources = new double[20];
for (int i=0; i<sources.length;) {
sources[i++] = 20 * random.nextDouble(); // Longitude
sources[i++] = 10 * random.nextDouble() + 35; // Latitude
}
final double[] expected = new double[sources.length];
reference.transform(sources, 0, expected, 0, sources.length/2);
/*
* At this point, we have the source coordinates and the expected projected coordinates calculated
* by the "Lambert Conic Conformal (1SP)" method. Now convert those projected coordinates into the
* coordinates that we expect from the "Lambert Conic Conformal (1SP West Orientated)". If we had
* no false easting, we would just revert the sign of 'x' values. But because of the false easting,
* we expect an additional offset of two time that easting. This is because (quoting the EPSG guide):
*
* the term FE retains its definition, i.e. in the Lambert Conic Conformal (West Orientated)
* method it increases the Westing value at the natural origin.
* In this method it is effectively false westing (FW).
*
* So the conversion for this test case should be: W = 400 - E
*
* However our map projection "kernel" implementation does not reverse the sign of 'x' values,
* because this reversal is the job of a separated method (CoordinateSystems.swapAndScaleAxes)
* which does is work by examining the axis directions. So we the values that we expect are:
*
* expected = -W = E - 400
*/
for (int i=0; i<sources.length; i += 2) {
expected[i] -= 400;
}
tolerance = Formulas.LINEAR_TOLERANCE;
verifyTransform(sources, expected);
}
/**
* Performs the same tests than {@link #testSpecialLatitudes()} and {@link #testDerivative()},
* but using spherical formulas.
*
* @throws FactoryException if an error occurred while creating the map projection.
* @throws TransformException if an error occurred while projecting a coordinate.
*/
@Test
@DependsOnMethod({"testSpecialLatitudes", "testDerivative"})
public void testSphericalCase() throws FactoryException, TransformException {
createNormalizedProjection(false, 40); // Spherical case
testSpecialLatitudes();
testDerivative();
// Make sure that the above methods did not overwrote the 'transform' field.
assertEquals("transform.class", LambertConicConformal.Spherical.class, transform.getClass());
}
/**
* Verifies the consistency of elliptical formulas with the spherical formulas.
* This test compares the results of elliptical formulas with the spherical ones
* for some random points.
*
* @throws FactoryException if an error occurred while creating the map projection.
* @throws TransformException if an error occurred while projecting a coordinate.
*/
@Test
@DependsOnMethod("testSphericalCase")
public void compareEllipticalWithSpherical() throws FactoryException, TransformException {
createCompleteProjection(new LambertConformal1SP(),
6371007, // Semi-major axis length
6371007, // Semi-minor axis length
0.5, // Central meridian
40, // Latitude of origin
NaN, // Standard parallel 1
NaN, // Standard parallel 2
0.997, // Scale factor
200, // False easting
100); // False northing
tolerance = Formulas.LINEAR_TOLERANCE;
compareEllipticalWithSpherical(CoordinateDomain.GEOGRAPHIC_SAFE, 0);
}
/**
* Verifies that deserialized projections work as expected. This implies that deserialization
* recomputed the internal transient fields, especially the series expansion coefficients.
*
* @throws FactoryException if an error occurred while creating the map projection.
* @throws TransformException if an error occurred while projecting a coordinate.
*/
@Test
@DependsOnMethod("testLambertConicConformal1SP")
public void testSerialization() throws FactoryException, TransformException {
createNormalizedProjection(true, 40);
final double[] source = new double[] {
70*PI/180, 27*PI/180,
30*PI/180, 56*PI/180
};
final double[] target = new double[source.length];
transform.transform(source, 0, target, 0, source.length / 2);
transform = assertSerializedEquals(transform);
tolerance = Formulas.LINEAR_TOLERANCE;
verifyTransform(source, target);
}
}