This is an automated email from the ASF dual-hosted git repository.

amanin 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 6722ec6  lfix(Feature): improve envelope operation to ensure attribute 
envelopes have a CRS defined if possible
6722ec6 is described below

commit 6722ec6172dcb7b3329e9917bb0e0f436e4ea536
Author: Alexis Manin <[email protected]>
AuthorDate: Fri May 29 18:04:18 2020 +0200

    lfix(Feature): improve envelope operation to ensure attribute envelopes 
have a CRS defined if possible
---
 .../org/apache/sis/feature/EnvelopeOperation.java  |  34 +++--
 .../apache/sis/feature/EnvelopeOperationTest.java  | 154 +++++++++++++++++++++
 2 files changed, 173 insertions(+), 15 deletions(-)

diff --git 
a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java 
b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
index b932010..ad14ad0 100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java
@@ -22,6 +22,7 @@ import java.util.Map;
 import java.util.LinkedHashMap;
 import java.util.Objects;
 import java.util.Optional;
+import org.apache.sis.util.Utilities;
 import org.opengis.util.GenericName;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
@@ -130,7 +131,7 @@ final class EnvelopeOperation extends AbstractOperation {
         final String characteristicName = 
AttributeConvention.CRS_CHARACTERISTIC.toString();
         /*
          * Get all property names without duplicated values. If a property is 
a link to an attribute,
-         * then the key will be the name of the referenced attribute instead 
than the operation name.
+         * then the key will be the name of the referenced attribute instead 
of the operation name.
          * The intent is to avoid querying the same geometry twice if the 
attribute is also specified
          * explicitly in the array of properties.
          *
@@ -190,10 +191,8 @@ final class EnvelopeOperation extends AbstractOperation {
                     if (crs == null) {
                         crs = value;                                    // 
Fallback if default geometry has no CRS.
                     }
-                    final CoordinateOperation op = CRS.findOperation(value, 
crs, null);
-                    if (!op.getMathTransform().isIdentity()) {
-                        attributeToCRS[i] = op;
-                    }
+                    // even in case of identity operation, we keep it to be 
able to fetch characteristic CRS of the attribute.
+                    attributeToCRS[i] = CRS.findOperation(value, crs, null);
                 }
             }
         }
@@ -287,7 +286,7 @@ final class EnvelopeOperation extends AbstractOperation {
             final String[] attributeNames = 
EnvelopeOperation.this.attributeNames;
             GeneralEnvelope envelope = null;                                   
     // Union of all envelopes.
             for (int i=0; i<attributeNames.length; i++) {
-                Envelope genv;                                                 
     // Envelope of a single geometry.
+                GeneralEnvelope genv;                                          
     // Envelope of a single geometry.
                 final String name = attributeNames[i];
                 if (attributeToCRS == null) {
                     /*
@@ -319,28 +318,33 @@ final class EnvelopeOperation extends AbstractOperation {
                     try {
                         if (at == null) {
                             final CoordinateOperation op = attributeToCRS[i];
-                            if (op != null) {                           // 
Null operation means identity transform.
-                                genv = Envelopes.transform(op, genv);
+                            if (op != null) {
+                                // Ensure attribute envelope has a CRS by 
forcing CRS found as characteristic
+                                if (op.getMathTransform().isIdentity() && 
op.getSourceCRS() != null) genv.setCoordinateReferenceSystem(op.getSourceCRS());
+                                else genv = Envelopes.transform(op, genv);
                             }
                         } else {                                               
         // Should be a rare case.
                             final Object geomCRS = at.getValue();
                             if (!(geomCRS instanceof 
CoordinateReferenceSystem)) {
                                 throw new 
IllegalStateException(Errors.format(Errors.Keys.UnspecifiedCRS));
                             }
-                            ((GeneralEnvelope) 
genv).setCoordinateReferenceSystem((CoordinateReferenceSystem) geomCRS);
-                            genv = Envelopes.transform(genv, crs);
+                            
genv.setCoordinateReferenceSystem((CoordinateReferenceSystem) geomCRS);
+                            genv = 
GeneralEnvelope.castOrCopy(Envelopes.transform(genv, crs));
                         }
                     } catch (TransformException e) {
                         throw new 
IllegalStateException(Errors.format(Errors.Keys.CanNotTransformEnvelope), e);
                     }
                 }
+
+                /* Add current attribute envelope in result one. For now, we 
only allow union of envelopes in the same
+                 * crs because we were not able to deduce output space from 
feature type characteristics. However, in
+                 * the future, we could find a common space to use.
+                 */
                 if (envelope == null) {
-                    envelope = GeneralEnvelope.castOrCopy(genv);        // 
Should always be a cast without copy.
-                    // Ensure that default CRS is set in case envelope 
returned by geometry was not specified
-                    if (envelope.getCoordinateReferenceSystem() == null && crs 
!= null) envelope.setCoordinateReferenceSystem(crs);
-                } else {
+                    envelope = genv;
+                } else if 
(Utilities.equalsIgnoreMetadata(genv.getCoordinateReferenceSystem(), 
envelope.getCoordinateReferenceSystem())) {
                     envelope.add(genv);
-                }
+                } else throw new 
IllegalStateException(Errors.format(Errors.Keys.MismatchedCRS));
             }
             return envelope;
         }
diff --git 
a/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
 
b/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
index 79482ee..e0b9739 100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/feature/EnvelopeOperationTest.java
@@ -20,8 +20,21 @@ import java.util.Arrays;
 import java.util.Map;
 import java.util.Collections;
 import com.esri.core.geometry.Point;
+import com.esri.core.geometry.Polyline;
 import com.esri.core.geometry.Polygon;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.feature.builder.CharacteristicTypeBuilder;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.feature.Geometries;
+import org.apache.sis.internal.feature.GeometryWrapper;
+import org.apache.sis.referencing.CRS;
+import org.opengis.feature.Attribute;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
 import org.opengis.geometry.Envelope;
+import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.internal.feature.AttributeConvention;
@@ -50,6 +63,13 @@ import org.opengis.feature.PropertyType;
  */
 @DependsOn(LinkOperationTest.class)
 public final strictfp class EnvelopeOperationTest extends TestCase {
+
+    private static final AttributeType<CoordinateReferenceSystem> 
CRS_CHARACTERISTIC = new FeatureTypeBuilder()
+            .addAttribute(CoordinateReferenceSystem.class)
+            .setName(AttributeConvention.CRS_CHARACTERISTIC)
+            .setMinimumOccurs(0)
+            .build();
+
     /**
      * Creates a feature type with a bounds operation.
      * The feature contains the following properties:
@@ -171,4 +191,138 @@ public final strictfp class EnvelopeOperationTest extends 
TestCase {
     public void testSparseFeature() throws FactoryException {
         run(new SparseFeature(school(2)));
     }
+
+    /**
+     * If no characteristic is defined on properties, but geometries define 
different ones, we should return an
+     * error, because it is an ambiguous case (Note: In the future, we could 
try to push them all in a
+     * {@link CRS#suggestCommonTarget(GeographicBoundingBox, 
CoordinateReferenceSystem...) common space}.
+     */
+    @Test
+    public void no_characteristic_but_different_geometry_crs() {
+        try {
+            final Envelope env = new 
CRSManagementUtil().test(HardCodedCRS.WGS84, HardCodedCRS.NTF);
+            fail("Ambiguity in CRS should have caused an error,  a value has 
been returned: "+env);
+        } catch (IllegalStateException e) {
+            // Expected behavior
+        }
+    }
+
+    /**
+     * When CRS is not in characteristics, but can be found on geometries, 
returned envelope should match it.
+     */
+    @Test
+    public void same_crs_on_geometries() {
+        final Envelope env = new CRSManagementUtil().test(HardCodedCRS.WGS84, 
HardCodedCRS.WGS84);
+        assertEquals(HardCodedCRS.WGS84, env.getCoordinateReferenceSystem());
+    }
+
+    /**
+     * When referencing is defined neither in characteristics nor on 
geometries, we should assume all geometries are
+     * expressed in the same space. Therefore, an envelope with no CRS should 
be returned.
+     */
+    @Test
+    public void no_crs_defined() {
+        Envelope env = new CRSManagementUtil().test(null, null);
+        assertNull(env.getCoordinateReferenceSystem());
+    }
+
+    /**
+     * Ensure that returned envelope CRS is the default one specified by 
property type characteristics if no geometry
+     * defines its CRS.
+     */
+    @Test
+    public void feature_type_characteristic_defines_crs() {
+        final Envelope env = new CRSManagementUtil(HardCodedCRS.WGS84, false, 
HardCodedCRS.WGS84, false)
+                .test(null, null);
+        assertEquals(HardCodedCRS.WGS84, env.getCoordinateReferenceSystem());
+    }
+
+    @Test
+    public void feature_characteristic_define_crs() {
+        final CRSManagementUtil environment = new CRSManagementUtil(null, 
true, null, true);
+        Envelope env = environment
+                .test(HardCodedCRS.WGS84, true, HardCodedCRS.WGS84, true);
+        assertEquals(HardCodedCRS.WGS84, env.getCoordinateReferenceSystem());
+
+        try {
+            env = environment.test(HardCodedCRS.WGS84, true, HardCodedCRS.NTF, 
true);
+            fail("Envelope should not be computed due to different CRS in 
geometries: "+env);
+        } catch (IllegalStateException e) {
+            // expected behavior
+        }
+    }
+
+    private static class CRSManagementUtil {
+        final FeatureType type;
+
+        CRSManagementUtil() {
+            this(null, false, null, false);
+        }
+
+        /**
+         * Create a feature type containing two geometric fields. If given CRS 
are non null, they will be specified as
+         * default CRS of each field through property type CRS characteristic.
+         * @param defaultCrs1 Default CRS of first property
+         * @param forceCharacteristic1 True if we want a CRS characteristic 
even with a null CRS. False to omit
+         *                             characteristic i defaultCrs1 is null.
+         * @param defaultCrs2 Default CRS for second property
+         * @param forceCharacteristic2 True if we want a CRS characteristic 
even with a null CRS. False to omit
+         *                             characteristic i defaultCrs2 is null.
+         */
+        CRSManagementUtil(final CoordinateReferenceSystem defaultCrs1, boolean 
forceCharacteristic1, final CoordinateReferenceSystem defaultCrs2, boolean 
forceCharacteristic2) {
+            final FeatureTypeBuilder builder = new 
FeatureTypeBuilder().setName("test");
+            final AttributeTypeBuilder<GeometryWrapper> g1 = 
builder.addAttribute(GeometryWrapper.class).setName("g1");
+            if (defaultCrs1 != null || forceCharacteristic1) 
g1.setCRS(defaultCrs1);
+            g1.addRole(AttributeRole.DEFAULT_GEOMETRY);
+
+            final AttributeTypeBuilder<GeometryWrapper> g2 = 
builder.addAttribute(GeometryWrapper.class).setName("g2");
+            if (defaultCrs2 != null || forceCharacteristic2) 
g2.setCRS(defaultCrs2);
+
+            type = builder.build();
+        }
+
+        /**
+         * Compute the envelope of this feature, and ensure that lower/upper 
coordinates are well-defined.
+         * The result is returned, so user can check the coordinate reference 
system on it.
+         *
+         * @param c1 CRS to put on the first geometry (not on property 
characteristic, but geometry itself)
+         * @param c2 CRS to put on the second geometry (not on property 
characteristic, but geometry itself)
+         * @return A non null envelope, result of the envelope operation.
+         */
+        Envelope test(final CoordinateReferenceSystem c1, final 
CoordinateReferenceSystem c2) {
+            return test(c1, false, c2, false);
+        }
+
+        Envelope test(final CoordinateReferenceSystem c1, final boolean 
c1AsCharacteristic, final CoordinateReferenceSystem c2, final boolean 
c2AsCharacteristic) {
+            final GeometryWrapper g1 = Geometries.wrap(new Point(4, 4))
+                    .orElseThrow(() -> new IllegalStateException("Cannot load 
ESRI binding"));
+            final GeometryWrapper g2 = Geometries.wrap(new Polyline(new 
Point(2, 2), new Point(3, 3)))
+                    .orElseThrow(() -> new IllegalStateException("Cannot load 
ESRI binding"));
+
+            Feature f = type.newInstance();
+            set(f, "g1", g1, c1, c1AsCharacteristic);
+            set(f, "g2", g2, c2, c2AsCharacteristic);
+
+            Object result = f.getPropertyValue("sis:envelope");
+            assertNotNull(result);
+            assertTrue(result instanceof Envelope);
+            Envelope env = (Envelope) result;
+            assertArrayEquals(new double[]{2, 2}, 
env.getLowerCorner().getCoordinate(), 1e-4);
+            assertArrayEquals(new double[]{4, 4}, 
env.getUpperCorner().getCoordinate(), 1e-4);
+            return env;
+        }
+
+        private void set(final Feature target, final String propertyName, 
final GeometryWrapper geometry, final CoordinateReferenceSystem crs, final 
boolean asCharacteristic) {
+            if (asCharacteristic) {
+                final Attribute g1p = (Attribute) 
target.getProperty(propertyName);
+                final Attribute<CoordinateReferenceSystem> crsCharacteristic = 
CRS_CHARACTERISTIC.newInstance();
+                crsCharacteristic.setValue(crs);
+                
g1p.characteristics().put(AttributeConvention.CRS_CHARACTERISTIC.toString(), 
crsCharacteristic);
+                g1p.setValue(geometry);
+            } else {
+                geometry.setCoordinateReferenceSystem(crs);
+                target.setPropertyValue(propertyName, geometry);
+            }
+        }
+    }
 }

Reply via email to