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 <[email protected]> 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 { /**
