sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ama...@apache.org
Subject [sis] 09/22: feat(Feature): add BBOX filter
Date Thu, 14 Nov 2019 11:46:43 GMT
This is an automated email from the ASF dual-hosted git repository.

amanin pushed a commit to branch refactor/sql-store
in repository https://gitbox.apache.org/repos/asf/sis.git

commit a78680336ae483714488243c398bf830e4e4cb7d
Author: Alexis Manin <amanin@apache.org>
AuthorDate: Tue Oct 8 17:55:31 2019 +0200

    feat(Feature): add BBOX filter
---
 .../java/org/apache/sis/filter/DefaultBBOX.java    | 200 +++++++++++++++++++++
 .../apache/sis/filter/DefaultFilterFactory.java    |  39 +++-
 .../test/java/org/apache/sis/filter/SQLMMTest.java |  33 +++-
 .../sql/feature/FilterInterpreterTest.java         |   3 +-
 .../org/apache/sis/test/suite/SQLTestSuite.java    |   3 +-
 5 files changed, 268 insertions(+), 10 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultBBOX.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultBBOX.java
new file mode 100644
index 0000000..a465388
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultBBOX.java
@@ -0,0 +1,200 @@
+package org.apache.sis.filter;
+
+import java.util.function.Predicate;
+
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.filter.FilterVisitor;
+import org.opengis.filter.expression.Expression;
+import org.opengis.filter.expression.Literal;
+import org.opengis.filter.spatial.BBOX;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.extent.GeographicBoundingBox;
+
+import org.apache.sis.feature.Features;
+import org.apache.sis.geometry.AbstractEnvelope;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.ImmutableEnvelope;
+import org.apache.sis.internal.feature.Geometries;
+import org.apache.sis.util.NullArgumentException;
+
+/**
+ * @implNote AMBIGUITY : Description of BBOX operator from <a href="http://docs.opengeospatial.org/is/09-026r2/09-026r2.html#60">
+ *     filter encoding 2.0.2</a> is rather succinct, and do not well explain if both
tested expressions must be
+ *     envelopes, or if we should test an envelope against a real geometry. What we will
do in this implementation is
+ *     testing bbox only, because the test for a bbox against a complex geometry can be realized
using ST_Intersect
+ *     operator.
+ *
+ * Border management: From above reference, bbox should be equivalent to Not ST_Disjoint
(from SQLMM/ISO:19125).
+ * Disjoint operation specifies that both geometry boundaries must not touch (their intersection
is an empty space), so
+ * we will consider as valid envelopes with only a common boundary.
+ *
+ *     TODO: CRS check.
+ */
+final class DefaultBBOX implements BBOX {
+
+    final Expression left;
+    final Expression right;
+
+    private final Predicate intersects;
+
+    DefaultBBOX(Expression left, Expression right) {
+        if (left == null && right == null) {
+            throw new NullArgumentException(
+                    "Both arguments are null, but at least one must be given " +
+                            "(as stated in OGC Filter encoding corrigendum 2.0.2, section
7.8.3.2)."
+            );
+        }
+
+        this.left = left;
+        this.right = right;
+
+        if (left instanceof Literal) {
+            intersects = asOptimizedTest((Literal)left, right);
+        } else if (right instanceof Literal) {
+            intersects = asOptimizedTest((Literal)right, left);
+        } else intersects = this::nonOptimizedIntersect;
+    }
+
+    @Override
+    public Expression getExpression1() {
+        return left;
+    }
+
+    @Override
+    public Expression getExpression2() {
+        return right;
+    }
+
+    @Override
+    public boolean evaluate(Object object) {
+        return intersects.test(object);
+    }
+
+    @Override
+    public Object accept(FilterVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+
+    private boolean nonOptimizedIntersect(Object candidate) {
+        Envelope leftEval = left == null? null : asEnvelope(left, candidate);
+        Envelope rightEval = left == null? null : asEnvelope(right, candidate);
+        if (left == null) {
+            return multiIntersect(candidate, rightEval);
+        } else if (right == null) {
+            return multiIntersect(candidate, leftEval);
+        }
+
+        /* OGC Filter encoding corrigendum 2.0.2 section 7.8.3.4 states that false must be
returned if any of the
+         * operand is null. It does not state what to do if both are null, but we'll follow
the same behavior.
+         */
+        if (leftEval == null || rightEval == null) return false;
+        return GeneralEnvelope.castOrCopy(leftEval).intersects(rightEval);
+    }
+
+    private static boolean intersect(final Object candidate, final Expression valueExtractor,
final AbstractEnvelope constEnvelope) {
+        final Envelope candidateEnv = asEnvelope(valueExtractor, candidate);
+        if (candidateEnv == null) return false;
+        return constEnvelope.intersects(candidateEnv, true);
+    }
+
+    /**
+     * Ensure that all geometric properties in given candidate intersect input envelope.
This method tries to match OGC
+     * Filter Encoding corrigendum 2.0.2 section 7.8.3.2 that says if one of the expressions
given at built is null, we
+     * have to ensure all geometric properties of the candidate intersect the other expression.
+     *
+     * @param candidate The object to extract all geometric properties from.
+     * @param fixed
+     * @return
+     */
+    private static boolean multiIntersect(Object candidate, Envelope fixed) {
+        // TODO: We could optimize by caching feature-type properties. The best way would
be an initialisation
+        // procedure freezing target data type, but I'm no sure such a mechanism would be
possible.
+        final GeneralEnvelope constEnv = GeneralEnvelope.castOrCopy(fixed);
+        if (candidate instanceof Feature) {
+            final Feature f = (Feature) candidate;
+            final FeatureType type = f.getType();
+            /* Note: for now, we could have doublons, but have no simple mean to eliminate
link operations. Relying on
+             * convention naming is too risky, as some drivers could use it directly on their
attributes, or create a
+             * computational operation (create point from numeric columns, reproject geometry,
etc.). In such case, we
+             * would drop valuable information.
+             */
+            return type.getProperties(true)
+                    .stream()
+                    .filter(p
+                            -> Features.castOrUnwrap(p)
+                            .map(AttributeType::getValueClass)
+                            .filter(Geometries::isKnownType)
+                            .isPresent()
+                    )
+                    .map(p -> p.getName().toString())
+                    .map(f::getPropertyValue)
+                    .map(Geometries::getEnvelope)
+                    .allMatch(fEnv -> fEnv != null && constEnv.intersects(fEnv,
true));
+        } else if (candidate instanceof Envelope) {
+            return constEnv.intersects((Envelope) candidate);
+        } else {
+            final Envelope env = Geometries.getEnvelope(candidate);
+            if (env == null) throw new UnsupportedOperationException(
+                    "Candidate type unsupported: "+candidate == null? "null" : candidate.getClass().getCanonicalName()
+            );
+            return constEnv.intersects(env);
+        }
+    }
+
+    private static Envelope asEnvelope(final Expression evaluator, final Object data) {
+        Envelope eval = evaluator.evaluate(data, Envelope.class);
+        if (eval == null) {
+            final Object tmpVal = evaluator.evaluate(data);
+            if (tmpVal instanceof Envelope) {
+                eval = (Envelope) tmpVal;
+            } else if (tmpVal instanceof GeographicBoundingBox) {
+                eval = new GeneralEnvelope((GeographicBoundingBox) tmpVal);
+            } else {
+                eval = Geometries.getEnvelope(tmpVal);
+            }
+        }
+
+        return eval;
+    }
+
+    private static Predicate asOptimizedTest(Literal constant, Expression other) {
+        final ImmutableEnvelope constEnv = new ImmutableEnvelope(asEnvelope(constant, null));
+        return other == null? it -> multiIntersect(it, constEnv) : it -> intersect(it,
other, constEnv);
+    }
+
+    /*
+     * DEPRECATED OPERATIONS: NOT IMPLEMENTED
+     */
+
+    @Override
+    public String getPropertyName() {
+        throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)"
on 08/10/2019
+    }
+
+    @Override
+    public String getSRS() {
+        throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)"
on 08/10/2019
+    }
+
+    @Override
+    public double getMinX() {
+        throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)"
on 08/10/2019
+    }
+
+    @Override
+    public double getMinY() {
+        throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)"
on 08/10/2019
+    }
+
+    @Override
+    public double getMaxX() {
+        throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)"
on 08/10/2019
+    }
+
+    @Override
+    public double getMaxY() {
+        throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)"
on 08/10/2019
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
index b1b1197..b8c90a3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
@@ -53,9 +53,16 @@ import org.opengis.filter.spatial.Within;
 import org.opengis.filter.temporal.*;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.Geometry;
+import org.opengis.referencing.NoSuchAuthorityCodeException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.FactoryException;
 import org.opengis.util.GenericName;
 
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.internal.feature.FunctionRegister;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.util.collection.BackingStoreException;
 
 
 /**
@@ -130,7 +137,35 @@ public class DefaultFilterFactory implements FilterFactory2 {
     public BBOX bbox(final Expression e, final double minx, final double miny,
             final double maxx, final double maxy, final String srs)
     {
-        throw new UnsupportedOperationException("Not supported yet.");
+        final CoordinateReferenceSystem crs = readCrs(srs);
+        final GeneralEnvelope env = new GeneralEnvelope(2);
+        env.setEnvelope(minx, miny, maxx, maxy);
+        if (crs != null) env.setCoordinateReferenceSystem(crs);
+        return bbox(e, new ImmutableEnvelope(env));
+    }
+
+    /**
+     * Try to decode a full {@link CoordinateReferenceSystem} from given text. First, we
try to interpret it as a code,
+     * and if it fails, we try to read it as a WKT.
+     *
+     * @param srs The text describing the system. If null or blank, a null value is returned.
+     * @return Possible null value if input text is empty.
+     * @throws BackingStoreException If an error occurs while decoding the text.
+     */
+    private static CoordinateReferenceSystem readCrs(String srs) {
+        if (srs == null || (srs = srs.trim()).isEmpty()) return null;
+        try {
+            return CRS.forCode(srs);
+        } catch (NoSuchAuthorityCodeException e) {
+            try {
+                return CRS.fromWKT(srs);
+            } catch (FactoryException bis) {
+                e.addSuppressed(bis);
+            }
+            throw new BackingStoreException(e);
+        } catch (FactoryException e) {
+            throw new BackingStoreException(e);
+        }
     }
 
     /**
@@ -138,7 +173,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public BBOX bbox(final Expression e, final Envelope bounds) {
-        throw new UnsupportedOperationException("Not supported yet.");
+        return new DefaultBBOX(e, literal(bounds));
     }
 
     /**
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/SQLMMTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/SQLMMTest.java
index d6ec945..9929d04 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/SQLMMTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/SQLMMTest.java
@@ -16,19 +16,24 @@
  */
 package org.apache.sis.filter;
 
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.expression.Expression;
+import org.opengis.filter.expression.Function;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.test.TestCase;
+
 import org.junit.Assert;
 import org.junit.Test;
 import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.Point;
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.filter.FilterFactory2;
-import org.opengis.filter.expression.Function;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+import static org.junit.Assert.fail;
 
 /**
  *
@@ -91,7 +96,23 @@ public class SQLMMTest extends TestCase {
             Assert.assertEquals(30.0, trs.getX(), 0.0);
             Assert.assertEquals(10.0, trs.getY(), 0.0);
         }
-
     }
 
+    public void ST_Envelope() {
+        try {
+            new ST_Envelope(new Expression[2]);
+            fail("ST_Envelope operator should accept a single parameter");
+        } catch (IllegalArgumentException e) {
+            // expected behavior
+        }
+
+        try {
+            new ST_Envelope(null);
+            fail("ST_Envelope operator should accept a single parameter");
+        } catch (IllegalArgumentException e) {
+            // expected behavior
+        }
+
+        // TODO: update SIS version then add test cases.
+    }
 }
diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/FilterInterpreterTest.java
b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/FilterInterpreterTest.java
index 9aea3a0..ac3325f 100644
--- a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/FilterInterpreterTest.java
+++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/FilterInterpreterTest.java
@@ -7,10 +7,11 @@ import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.test.Assert;
+import org.apache.sis.test.TestCase;
 
 import org.junit.Test;
 
-public class FilterInterpreterTest {
+public class FilterInterpreterTest extends TestCase {
     private static final FilterFactory2 FF = new DefaultFilterFactory();
 
     @Test
diff --git a/storage/sis-sqlstore/src/test/java/org/apache/sis/test/suite/SQLTestSuite.java
b/storage/sis-sqlstore/src/test/java/org/apache/sis/test/suite/SQLTestSuite.java
index 2ed67d8..3a24e21 100644
--- a/storage/sis-sqlstore/src/test/java/org/apache/sis/test/suite/SQLTestSuite.java
+++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/test/suite/SQLTestSuite.java
@@ -25,7 +25,8 @@ import org.junit.BeforeClass;
  * All tests from the {@code sis-sqlstore} module, in rough dependency order.
  */
 @Suite.SuiteClasses({
-    org.apache.sis.storage.sql.SQLStoreTest.class
+    org.apache.sis.storage.sql.SQLStoreTest.class,
+    org.apache.sis.internal.sql.feature.FilterInterpreterTest.class
 })
 public final strictfp class SQLTestSuite extends TestSuite {
     /**


Mime
View raw message