You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/03/18 23:34:55 UTC

[sis] branch geoapi-4.0 updated: Add a test case for the fix provided in previous commit.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new ab6393c  Add a test case for the  fix provided in previous commit.
ab6393c is described below

commit ab6393c7253adfb2ecce268c0764b80a5356e92e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Mar 19 00:28:00 2021 +0100

    Add a test case for the  fix provided in previous commit.
---
 .../org/apache/sis/image/ResamplingGridTest.java   | 12 +++-
 .../java/org/apache/sis/geometry/Envelopes.java    | 36 ++++++++--
 .../java/org/apache/sis/geometry/Shapes2D.java     |  2 +-
 .../operation/CoordinateOperationRegistry.java     |  2 +-
 .../org/apache/sis/geometry/TransformTestCase.java | 44 +++++++++---
 .../operation/HardCodedConversions.java            | 81 ++++++++++++++++------
 6 files changed, 135 insertions(+), 42 deletions(-)

diff --git a/core/sis-feature/src/test/java/org/apache/sis/image/ResamplingGridTest.java b/core/sis-feature/src/test/java/org/apache/sis/image/ResamplingGridTest.java
index 54cdd7d..f6bacc2 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/image/ResamplingGridTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/image/ResamplingGridTest.java
@@ -182,13 +182,21 @@ public final strictfp class ResamplingGridTest extends TestCase {
     }
 
     /**
+     * Creates a <cite>Lambert Conic Conformal (1SP)</cite> projection
+     * with a Latitude of natural origin arbitrarily set to 40.
+     */
+    private static MathTransform lambertProjection() {
+        return HardCodedConversions.createCRS(HardCodedConversions.LAMBERT).getConversionFromBase().getMathTransform();
+    }
+
+    /**
      * Tests {@link ResamplingGrid} with a Lambert Conic Conformal projection.
      *
      * @throws TransformException if an error occurred while transforming a coordinate.
      */
     @Test
     public void testLambert() throws TransformException {
-        final MathTransform projection = HardCodedConversions.lambert().getConversionFromBase().getMathTransform();
+        final MathTransform projection = lambertProjection();
         final Rectangle domain = new Rectangle(-20, 30, 40, 20);
         final MathTransform2D tr = compare("Lambert", projection, domain);
         assertInstanceOf("Expected a non-linear transform.", ResamplingGrid.class, tr);
@@ -205,7 +213,7 @@ public final strictfp class ResamplingGridTest extends TestCase {
      */
     @Test
     public void testLambertOnSmallArea() throws TransformException {
-        final MathTransform projection = HardCodedConversions.lambert().getConversionFromBase().getMathTransform();
+        final MathTransform projection = lambertProjection();
         final Rectangle2D domain = new Rectangle2D.Double(-20, 50, 0.025, 0.025);
         final MathTransform2D tr = compare("Lambert (small area)", projection, domain);
         assertInstanceOf("Expected a linear transform.", AffineTransform2D.class, tr);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
index 16285ae..5d318bb 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Envelopes.java
@@ -106,6 +106,18 @@ import static org.apache.sis.util.StringBuilders.trimFractionalPart;
  */
 public final class Envelopes extends Static {
     /**
+     * Fraction of the axis span to accept as close enough to an envelope boundary. This is used for coordinates
+     * that are suppose to be on a boundary, for checking if it is really on the boundary side where it should be.
+     * For example on the longitude axis, bounds are -180° and +180° with wraparound meaning and the span is 360°.
+     * A {@code SPAN_FRACTION_AS_BOUND} value of 0.25 means that we accept a margin of 0.25 × 360° = 90° on each
+     * side: longitudes between -180 and -90 are clipped to the -180° bounds, and longitudes between +180 and +90
+     * and clipped to the +180° bounds. We use a large fraction because we use it in contexts where the longitude
+     * is not supposed to be in the envelope interior. We could use a 0.5 value for clipping to the nearest bound,
+     * but a smaller value is used for safety.
+     */
+    static final double SPAN_FRACTION_AS_BOUND = 0.25;
+
+    /**
      * Do not allow instantiation of this class.
      */
     private Envelopes() {
@@ -800,7 +812,7 @@ poles:  for (int i=0; i<dimension; i++) {
                         if (CoordinateOperations.isWrapAround(axis)) {
                             revertPt = mt.transform(sourcePt, revertPt);
                             final double delta = Math.abs(revertPt.getOrdinate(i) - extremum);
-                            if (!(delta < 0.25 * (axis.getMaximumValue() - axis.getMinimumValue()))) {
+                            if (!(delta < SPAN_FRACTION_AS_BOUND * (axis.getMaximumValue() - axis.getMinimumValue()))) {
                                 continue;
                             }
                         }
@@ -888,11 +900,23 @@ poles:  for (int i=0; i<dimension; i++) {
                         try {
                             sourcePt = inverse.transform(targetPt, sourcePt);
                             if (sourceBox.contains(sourcePt)) {
-                                revertPt = mt.transform(sourcePt, revertPt);
-                                final double delta = Math.abs(revertPt.getOrdinate(wrapAroundDimension) - value);
-                                if (delta < 0.25 * (max - min)) {
-                                    transformed.add(targetPt);
-                                }
+                                /*
+                                 * The `value` limit is typically the 180°E or 180°W longitude value. We could test
+                                 * its validity as below (see similar code in other loop above for explanation):
+                                 *
+                                 *     revertPt = mt.transform(sourcePt, revertPt);
+                                 *     final double delta = Math.abs(revertPt.getOrdinate(wrapAroundDimension) - value);
+                                 *     if (delta < SPAN_FRACTION_AS_BOUND * (max - min)) {
+                                 *         transformed.add(targetPt);
+                                 *     }
+                                 *
+                                 * But we don't because this block is executed when another coordinate is at its bounds,
+                                 * and that other coordinate is usually the latitude at 90°S or 90°N. In such case, all
+                                 * longitude values are the same point on Earth (a pole) and can be anything. Comparing
+                                 * `revertPt` coordinate with `value` is meaningless. In order to keep above check, we
+                                 * would need to determine if `targetPt` is at a pole. But we have fully reliable way.
+                                 */
+                                transformed.add(targetPt);
                             }
                         } catch (TransformException exception) {
                             if (warning == null) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Shapes2D.java b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Shapes2D.java
index 927eba5..7459d34 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/Shapes2D.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/Shapes2D.java
@@ -496,7 +496,7 @@ public final class Shapes2D extends Static {
                 if (CoordinateOperations.isWrapAround(axis)) {
                     revertPt = mt.transform(sourcePt, revertPt);
                     final double delta = Math.abs((dimension == 0 ? revertPt.getX() : revertPt.getY()) - extremum);
-                    if (!(delta < 0.25 * (axis.getMaximumValue() - axis.getMinimumValue()))) {
+                    if (!(delta < Envelopes.SPAN_FRACTION_AS_BOUND * (axis.getMaximumValue() - axis.getMinimumValue()))) {
                         continue;
                     }
                 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index dcbcb97..7b8a5af 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -691,7 +691,7 @@ class CoordinateOperationRegistry {
      * @param  operation  the operation to invert, or {@code null}.
      * @return the inverse of {@code operation}, or {@code null} if none.
      * @throws NoninvertibleTransformException if the operation is not invertible.
-     * @throws FactoryException if the operation creation failed for an other reason.
+     * @throws FactoryException if the operation creation failed for another reason.
      */
     private CoordinateOperation inverse(final CoordinateOperation operation)
             throws NoninvertibleTransformException, FactoryException
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java
index d38fa26..0514d7f 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/geometry/TransformTestCase.java
@@ -48,7 +48,7 @@ import static org.apache.sis.test.ReferencingAssert.*;
  * All tests performed by this class are two-dimensional.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.1
  *
  * @param <G>  the type of geometric objects, either {@link GeneralEnvelope} or {@link java.awt.geom.Rectangle2D}.
  *
@@ -136,16 +136,7 @@ public abstract strictfp class TransformTestCase<G> extends TestCase {
     @Test
     @DependsOnMethod("testTransform")
     public final void testTransformOverPole() throws FactoryException, TransformException {
-        final ProjectedCRS sourceCRS = (ProjectedCRS) CRS.fromWKT(
-                "PROJCS[“WGS 84 / Antarctic Polar Stereographic”,\n" +
-                "  GEOGCS[“WGS 84”,\n" +
-                "    DATUM[“World Geodetic System 1984”,\n" +
-                "      SPHEROID[“WGS 84”, 6378137.0, 298.257223563]],\n" +
-                "    PRIMEM[“Greenwich”, 0.0],\n" +
-                "    UNIT[“degree”, 0.017453292519943295]],\n" +
-                "  PROJECTION[“Polar Stereographic (variant B)”],\n" +
-                "  PARAMETER[“standard_parallel_1”, -71.0],\n" +
-                "  UNIT[“m”, 1.0]]");
+        final ProjectedCRS    sourceCRS  = HardCodedConversions.createCRS(HardCodedConversions.POLAR_STEREOGRAPHIC);
         final GeographicCRS   targetCRS  = sourceCRS.getBaseCRS();
         final Conversion      conversion = inverse(sourceCRS.getConversionFromBase());
         final MathTransform2D transform  = (MathTransform2D) conversion.getMathTransform();
@@ -244,9 +235,40 @@ public abstract strictfp class TransformTestCase<G> extends TestCase {
     }
 
     /**
+     * Tests conversion from a UTM projection to geographic CRS where the resulting envelope crosses the anti-meridian.
+     * Contrarily to {@link #testTransformOverAntiMeridian()}, the longitude range is outside the [-180 … +180]° range.
+     * This is because the projection has a large central meridian which is added to the result.
+     *
+     * @throws FactoryException if an error occurred while creating the operation.
+     * @throws TransformException if an error occurred while transforming the envelope.
+     *
+     * @since 1.1
+     */
+    @Test
+    @DependsOnMethod("testTransformOverAntiMeridian")
+    public void testProjectionOutsideLongitudeRange() throws FactoryException, TransformException {
+        final ProjectedCRS    sourceCRS  = HardCodedConversions.createCRS(HardCodedConversions.UTM);
+        final GeographicCRS   targetCRS  = sourceCRS.getBaseCRS();
+        final Conversion      conversion = inverse(sourceCRS.getConversionFromBase());
+        final G rectangle = createFromExtremums(sourceCRS,
+                -402748, 7965673,                               // Computed by SIS (not validated by external authority).
+                1312383, 9912935);
+
+        // Longitude span anti-meridian (-214° to 45°).
+        final G expected = createFromExtremums(targetCRS,
+                -213.637, 70.141,
+                 -44.959, 89.147);
+
+        final G actual = transform(conversion, rectangle);
+        assertGeometryEquals(expected, actual, 0.001, 0.001);
+    }
+
+    /**
      * Returns the inverse of the given conversion. This method is not strictly correct
      * since we reuse the properties (name, aliases, etc.) from the given conversion.
      * However those properties are not significant for the purpose of this test.
+     *
+     * @see org.apache.sis.referencing.operation.CoordinateOperationRegistry#inverse(SingleOperation)
      */
     private static Conversion inverse(final Conversion conversion) throws NoninvertibleTransformException {
         return new DefaultConversion(IdentifiedObjects.getProperties(conversion), conversion.getTargetCRS(),
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/HardCodedConversions.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/HardCodedConversions.java
index 48878fc..aa50924 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/HardCodedConversions.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/HardCodedConversions.java
@@ -23,7 +23,9 @@ import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.operation.OperationMethod;
 import org.apache.sis.internal.referencing.provider.Mercator1SP;
+import org.apache.sis.internal.referencing.provider.TransverseMercator;
 import org.apache.sis.internal.referencing.provider.LambertConformal1SP;
+import org.apache.sis.internal.referencing.provider.PolarStereographicB;
 import org.apache.sis.referencing.crs.DefaultProjectedCRS;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.referencing.cs.HardCodedCS;
@@ -45,8 +47,39 @@ public final strictfp class HardCodedConversions {
     public static final DefaultConversion MERCATOR;
     static {
         final OperationMethod method = new Mercator1SP();
-        MERCATOR = new DefaultConversion(Collections.singletonMap(OperationMethod.NAME_KEY, "Mercator"),
-                method, null, method.getParameters().createValue());
+        MERCATOR = create("Mercator", method, method.getParameters().createValue());
+    }
+
+    /**
+     * A defining conversion for a <cite>Universal Transverse Mercator zone 9</cite> projection.
+     * Pseudo Well-Known Text for the {@link org.opengis.referencing.operation.MathTransform}:
+     *
+     * {@preformat wkt
+     *   Param_MT["Transverse Mercator",
+     *       Parameter["Longitude of natural origin", -129, Unit["degree"]],
+     *       Parameter["Scale factor at natural origin", 0.9996],
+     *       Parameter["False easting", 500000, Unit["metre"]]]]
+     * }
+     */
+    public static final DefaultConversion UTM;
+    static {
+        final OperationMethod method = new TransverseMercator();
+        final ParameterValueGroup pg = method.getParameters().createValue();
+        pg.parameter("Longitude of natural origin").setValue(-129);
+        pg.parameter("Scale factor at natural origin").setValue(0.9996);
+        pg.parameter("False easting").setValue(500000);
+        UTM = create("UTM zone 9N", method, pg);
+    }
+
+    /**
+     * A defining conversion for a <cite>Antarctic Polar Stereographic</cite> projection.
+     */
+    public static final DefaultConversion POLAR_STEREOGRAPHIC;
+    static {
+        final OperationMethod method = new PolarStereographicB();
+        final ParameterValueGroup pg = method.getParameters().createValue();
+        pg.parameter("Latitude of standard parallel").setValue(-71);
+        POLAR_STEREOGRAPHIC = create("Antarctic Polar Stereographic", method, pg);
     }
 
     /**
@@ -58,8 +91,14 @@ public final strictfp class HardCodedConversions {
         final OperationMethod method = new LambertConformal1SP();
         final ParameterValueGroup pg = method.getParameters().createValue();
         pg.parameter("Latitude of natural origin").setValue(40);
-        LAMBERT = new DefaultConversion(Collections.singletonMap(OperationMethod.NAME_KEY, "Lambert Conic Conformal"),
-                method, null, pg);
+        LAMBERT = create("Lambert Conic Conformal", method, pg);
+    }
+
+    /**
+     * Creates a defining conversion of the given name with given parameter values.
+     */
+    private static DefaultConversion create(final String name, final OperationMethod method, final ParameterValueGroup pg) {
+        return new DefaultConversion(Collections.singletonMap(OperationMethod.NAME_KEY, name), method, null, pg);
     }
 
     /**
@@ -69,6 +108,20 @@ public final strictfp class HardCodedConversions {
     }
 
     /**
+     * Creates a two-dimension CRS using the given conversion on the WGS84 datum.
+     * This CRS uses (<var>easting</var>, <var>northing</var>) coordinates in metres.
+     * The base CRS uses (<var>longitude</var>, <var>latitude</var>) axes
+     * and the prime meridian is Greenwich.
+     *
+     * @param  conversion  the defining conversion as one of the constant defined in this class.
+     * @return two-dimensional projection using the given method.
+     */
+    public static DefaultProjectedCRS createCRS(final DefaultConversion conversion) {
+        return new DefaultProjectedCRS(Collections.singletonMap(ProjectedCRS.NAME_KEY, conversion.getName()),
+                HardCodedCRS.WGS84, conversion, HardCodedCS.PROJECTED);
+    }
+
+    /**
      * A two-dimensional Mercator projection using the WGS84 datum.
      * This CRS uses (<var>easting</var>, <var>northing</var>) coordinates in metres.
      * The base CRS uses (<var>longitude</var>, <var>latitude</var>) axes
@@ -80,8 +133,7 @@ public final strictfp class HardCodedConversions {
      * @return two-dimensional Mercator projection.
      */
     public static DefaultProjectedCRS mercator() {
-        return new DefaultProjectedCRS(name("Mercator"),
-                HardCodedCRS.WGS84, HardCodedConversions.MERCATOR, HardCodedCS.PROJECTED);
+        return createCRS(MERCATOR);
     }
 
     /**
@@ -94,7 +146,7 @@ public final strictfp class HardCodedConversions {
      */
     public static DefaultProjectedCRS mercator3D() {
         return new DefaultProjectedCRS(name("Mercator (3D)"),
-                HardCodedCRS.WGS84_3D, HardCodedConversions.MERCATOR, HardCodedCS.PROJECTED_3D);
+                HardCodedCRS.WGS84_3D, MERCATOR, HardCodedCS.PROJECTED_3D);
     }
 
     /**
@@ -105,24 +157,11 @@ public final strictfp class HardCodedConversions {
      * @return two- or three-dimensional Mercator projection.
      */
     public static DefaultProjectedCRS mercator(final GeographicCRS baseCRS) {
-        return new DefaultProjectedCRS(name("Mercator (other)"), baseCRS, HardCodedConversions.MERCATOR,
+        return new DefaultProjectedCRS(name("Mercator (other)"), baseCRS, MERCATOR,
                 baseCRS.getCoordinateSystem().getDimension() == 3 ? HardCodedCS.PROJECTED_3D : HardCodedCS.PROJECTED);
     }
 
     /**
-     * A two-dimensional Lambert Conic Conformal (1SP) projection using the WGS84 datum.
-     * This CRS uses (<var>easting</var>, <var>northing</var>) coordinates in metres.
-     * The base CRS uses (<var>longitude</var>, <var>latitude</var>) axes
-     * and the prime meridian is Greenwich.
-     *
-     * @return two-dimensional Lambert Conic Conformal (1SP) projection.
-     */
-    public static DefaultProjectedCRS lambert() {
-        return new DefaultProjectedCRS(name("Lambert Conic Conformal"),
-                HardCodedCRS.WGS84, HardCodedConversions.LAMBERT, HardCodedCS.PROJECTED);
-    }
-
-    /**
      * Puts the given name in a property map CRS constructors.
      */
     private static Map<String,?> name(final String name) {