You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by am...@apache.org on 2019/11/12 16:44:57 UTC
[sis] 30/45: WIP(Feature): BBOX filter
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 167d5f354693ebba58eda9ab8075f19334172480
Author: Alexis Manin <am...@apache.org>
AuthorDate: Wed Oct 9 13:06:38 2019 +0200
WIP(Feature): BBOX filter
---
.../java/org/apache/sis/filter/DefaultBBOX.java | 121 ++++++++++++++++++---
.../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, 176 insertions(+), 23 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
index 8f233ba..a465388 100644
--- 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
@@ -1,15 +1,23 @@
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 static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+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">
@@ -18,7 +26,10 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
* testing bbox only, because the test for a bbox against a complex geometry can be realized using ST_Intersect
* operator.
*
- * TODO: optimisations in case one of the two operators is a literal.
+ * 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 {
@@ -26,11 +37,24 @@ final class DefaultBBOX implements BBOX {
final Expression left;
final Expression right;
- public DefaultBBOX(Expression left, Expression right) {
- ensureNonNull("Left expression", left);
- ensureNonNull("Right 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
@@ -45,13 +69,80 @@ final class DefaultBBOX implements BBOX {
@Override
public boolean evaluate(Object object) {
- Envelope leftEval = asEnvelope(left, object);
- Envelope rightEval = asEnvelope(right, object);
- // If both are null, return a match.
- if (leftEval == rightEval) return true;
+ 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) {
@@ -68,11 +159,15 @@ final class DefaultBBOX implements BBOX {
return eval;
}
- @Override
- public Object accept(FilterVisitor visitor, Object extraData) {
- return visitor.visit(this, extraData);
+ 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
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 {
/**