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 ebcb1d58a78c09717101e4a0b6d55f8641dd31ca Author: Alexis Manin <[email protected]> AuthorDate: Wed Oct 9 18:13:24 2019 +0200 feat(Feature): add naïve implementation of ST_Intersects --- .../java/org/apache/sis/filter/ST_Envelope.java | 10 +- .../java/org/apache/sis/filter/ST_Intersects.java | 114 +++++++++++++++++++++ .../test/java/org/apache/sis/filter/SQLMMTest.java | 89 +++++++++++++++- 3 files changed, 209 insertions(+), 4 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Envelope.java b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Envelope.java index 98990ae..4178baf 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Envelope.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Envelope.java @@ -1,5 +1,6 @@ package org.apache.sis.filter; +import java.util.Collections; import java.util.function.Function; import org.opengis.feature.AttributeType; @@ -8,6 +9,7 @@ import org.opengis.feature.PropertyType; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.geometry.Envelope; +import org.opengis.geometry.Geometry; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.metadata.extent.GeographicBoundingBox; @@ -77,7 +79,7 @@ public class ST_Envelope extends AbstractFunction implements FeatureExpression { } result = new ImmutableEnvelope(tmpResult); - resultType = new DefaultAttributeType(null, Envelope.class, 1, 1, null); + resultType = new DefaultAttributeType(Collections.singletonMap("name", "ST_Envelope"), Envelope.class, 1, 1, null); } @Override @@ -154,6 +156,12 @@ public class ST_Envelope extends AbstractFunction implements FeatureExpression { .orElseThrow(() -> new IllegalArgumentException("No geometry provider found to read WKT")); } + // First, we check if the envelope is already available. If not, we try to compute it. + if (value instanceof Geometry) { + final Envelope env = ((Geometry) value).getEnvelope(); + if (env != null) return env; + } + return Geometries.getEnvelope(value); } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Intersects.java b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Intersects.java new file mode 100644 index 0000000..407d291 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Intersects.java @@ -0,0 +1,114 @@ +package org.apache.sis.filter; + +import java.util.function.Predicate; + +import org.opengis.filter.FilterVisitor; +import org.opengis.filter.expression.Expression; +import org.opengis.filter.expression.Literal; +import org.opengis.filter.spatial.Intersects; +import org.opengis.geometry.Geometry; + +import org.apache.sis.internal.feature.GeometryWrapper; + +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; + +import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty; +import static org.apache.sis.util.ArgumentChecks.ensureNonNull; + +/** + * TODO: check CRS + * TODO: refine once Geometry API is stable. + */ +public class ST_Intersects implements Intersects { + + public static final String NAME = "ST_Intersect"; + + final Expression left; + final Expression right; + + private final Predicate intersects; + + public ST_Intersects(Expression[] parameters) { + ensureNonEmpty("Parameters", parameters); + if (parameters.length != 2) throw new IllegalArgumentException("2 parameters are expected for intersection, but "+parameters.length+" are provided"); + + left = parameters[0]; + right = parameters[1]; + ensureNonNull("Left operand", left); + ensureNonNull("Right operand", right); + if (left instanceof Literal && right instanceof Literal) { + intersects = constantOp((Literal) left, (Literal) right); + } else if (left instanceof Literal) { + intersects = intersect(right, (Literal) left); + } else if (right instanceof Literal) { + intersects = intersect(left, (Literal) right); + } else intersects = this::nonOptimizedIntersects; + } + + private boolean nonOptimizedIntersects(Object candidate) { + final Object leftEval = left.evaluate(candidate); + final Object rightEval = right.evaluate(candidate); + if (leftEval == null || rightEval == null) return false; + return toJTS(leftEval).intersects(toJTS(rightEval)); + } + + private static org.locationtech.jts.geom.Geometry toJTS(Object value) { + if (value instanceof GeometryWrapper) value = ((GeometryWrapper) value).geometry; + if (value instanceof org.locationtech.jts.geom.Geometry) return (org.locationtech.jts.geom.Geometry) value; + throw new UnsupportedOperationException("Unsupported geometry type: "+value.getClass().getCanonicalName()); + } + + private Predicate constantOp(Literal left, Literal right) { + final boolean result = left.getValue() != null + && right.getValue() != null + && toJTS(left.getValue()).intersects(toJTS(right.getValue())); + return it -> result; + } + + private static Predicate intersect(Expression left, Literal right) { + Object value = right.getValue(); + ensureNonNull("Literal value", value); + // TODO: make more consistent strategy once Geometry API is stable. + if (value instanceof GeometryWrapper) value = ((GeometryWrapper) value).geometry; + if (value instanceof org.locationtech.jts.geom.Geometry) { + final PreparedGeometry pg = new PreparedGeometryFactory().create((org.locationtech.jts.geom.Geometry) value); + return it -> { + Object val = left.evaluate(it); + if (val == null) return false; + return pg.intersects(toJTS(val)); + }; + } else if (value instanceof Geometry) { + final Geometry geom = (Geometry) value; + return it -> { + final Geometry newVal = left.evaluate(it, Geometry.class); + if (newVal == null) { + final Object testVal = left.evaluate(it); + if (testVal == null) return false; + throw new UnsupportedOperationException("Unsupported geometry type: "+testVal.getClass().getCanonicalName()); + } + return geom.intersects(newVal); + }; + } else throw new UnsupportedOperationException("Unsupported geometry type: "+value.getClass().getCanonicalName()); + } + + @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); + } +} 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 9929d04..75a2fbc 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 @@ -21,18 +21,27 @@ 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.filter.expression.Literal; +import org.opengis.filter.expression.PropertyName; +import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.feature.builder.FeatureTypeBuilder; +import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.test.TestCase; +import org.apache.sis.util.NullArgumentException; 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.LineString; +import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.Point; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** @@ -40,6 +49,9 @@ import static org.junit.Assert.fail; * @author Johann Sorel (Geomatys) */ public class SQLMMTest extends TestCase { + + private static final String P_NAME = "geom"; + /** * The factory to use for creating the objects to test. */ @@ -78,7 +90,7 @@ public class SQLMMTest extends TestCase { //check result final Object newGeom = fct.evaluate(feature); - Assert.assertTrue(newGeom instanceof Point); + assertTrue(newGeom instanceof Point); final Point trs = (Point) newGeom; Assert.assertEquals(outCrs, trs.getUserData()); Assert.assertEquals(30.0, trs.getX(), 0.0); @@ -90,7 +102,7 @@ public class SQLMMTest extends TestCase { //check result final Object newGeom = fct.evaluate(feature); - Assert.assertTrue(newGeom instanceof Point); + assertTrue(newGeom instanceof Point); final Point trs = (Point) newGeom; Assert.assertEquals(outCrs, trs.getUserData()); Assert.assertEquals(30.0, trs.getX(), 0.0); @@ -98,6 +110,7 @@ public class SQLMMTest extends TestCase { } } + @Test public void ST_Envelope() { try { new ST_Envelope(new Expression[2]); @@ -113,6 +126,76 @@ public class SQLMMTest extends TestCase { // expected behavior } - // TODO: update SIS version then add test cases. + final LineString pt = gf.createLineString(new Coordinate[]{ + new Coordinate(12, 3.3), + new Coordinate(13.1, 4.4), + new Coordinate(12.02, 5.7) + }); + ST_Envelope operator = new ST_Envelope(new Expression[]{factory.literal(pt)}); + final GeneralEnvelope expectedEnv = new GeneralEnvelope(2); + expectedEnv.setEnvelope(12, 3.3, 13.1, 5.7); + Envelope evaluated = (Envelope) operator.evaluate(null); + assertTrue(String.format("Bad result:%nExpected: %s%nBut got: %s", expectedEnv.toString(), evaluated.toString()), expectedEnv.equals(evaluated, 1e-10, false)); + evaluated = (Envelope) operator.evaluate(null); + assertTrue(String.format("Bad result:%nExpected: %s%nBut got: %s", expectedEnv.toString(), evaluated.toString()), expectedEnv.equals(evaluated, 1e-10, false)); + + // After testing literal data, we'll now try to extract data from a feature. + final Feature f = mock(); + f.setPropertyValue(P_NAME, pt); + operator = new ST_Envelope(new Expression[]{factory.property(P_NAME)}); + evaluated = (Envelope) operator.evaluate(f); + assertTrue(String.format("Bad result:%nExpected: %s%nBut got: %s", expectedEnv.toString(), evaluated.toString()), expectedEnv.equals(evaluated, 1e-10, false)); + } + + @Test + public void ST_Intersects() { + try { + new ST_Intersects(null); + fail("ST_Intersects operator should accept exactly 2 parameters"); + } catch (NullArgumentException e) { + // expected behavior + } + + try { + new ST_Intersects(new Expression[3]); + fail("ST_Intersects operator should accept exactly 2 parameters"); + } catch (IllegalArgumentException e) { + // expected behavior + } + + final Coordinate start = new Coordinate(0, 0.1); + final Coordinate second = new Coordinate(1.2, 0.2); + final LinearRing ring = gf.createLinearRing(new Coordinate[]{ + start, second, new Coordinate(0.7, 0.8), start + }); + + final Literal lring = factory.literal(ring); + ST_Intersects st = new ST_Intersects(new Expression[]{factory.literal(gf.createPoint(new Coordinate(2, 4))), lring}); + // Ensure argument nullity does not modify behavior + assertFalse("Unexpected intersection", st.evaluate(null)); + assertFalse("Unexpected intersection", st.evaluate(new Object())); + + // Border should intersect + final Feature f = mock(); + f.setPropertyValue(P_NAME, gf.createPoint(second)); + final PropertyName geomName = factory.property(P_NAME); + st = new ST_Intersects(new Expression[]{geomName, lring}); + assertTrue("Border point should intersect triangle", st.evaluate(f)); + // Ensure inverting expression does not modify behavior. + st = new ST_Intersects(new Expression[]{lring, geomName}); + assertTrue("Border point should intersect triangle", st.evaluate(f)); + + // TODO: add CRS checking tests. + } + + /** + * + * @return A feature with a single property of {@link Object any} type named after + */ + private static Feature mock() { + final FeatureTypeBuilder ftb = new FeatureTypeBuilder().setName("mock"); + ftb.addAttribute(Object.class).setName(P_NAME); + final FeatureType mockType = ftb.build(); + return mockType.newInstance(); } }
