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 c18417df63238fcef64affbed4e50b2f41e004b5 Author: Alexis Manin <[email protected]> AuthorDate: Fri Oct 4 17:07:02 2019 +0200 WIP(SQLStore): work on bbox filter and conversion between envelope and geometry --- .../main/java/org/apache/sis/feature/Features.java | 53 +++++--- .../java/org/apache/sis/filter/ST_Envelope.java | 135 ++++++++++++++++++++- .../apache/sis/internal/feature/Geometries.java | 74 ++++++++++- .../java/org/apache/sis/internal/feature/JTS.java | 2 +- .../sis/internal/feature/WrapResolution.java | 53 ++++++++ .../sis/internal/sql/feature/ANSIInterpreter.java | 3 +- .../sql/feature/FilterInterpreterTest.java | 35 ++++++ 7 files changed, 333 insertions(+), 22 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java b/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java index 4852a3f..7b9448d 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java @@ -16,29 +16,32 @@ */ package org.apache.sis.feature; -import org.opengis.util.GenericName; -import org.opengis.util.NameFactory; -import org.opengis.util.InternationalString; -import org.opengis.metadata.maintenance.ScopeCode; -import org.opengis.metadata.quality.ConformanceResult; -import org.opengis.metadata.quality.DataQuality; -import org.opengis.metadata.quality.Element; -import org.opengis.metadata.quality.Result; -import org.apache.sis.util.Static; -import org.apache.sis.util.iso.DefaultNameFactory; -import org.apache.sis.internal.system.DefaultFactories; -import org.apache.sis.internal.feature.Resources; +import java.util.Optional; -// Branch-dependent imports import org.opengis.feature.Attribute; import org.opengis.feature.AttributeType; import org.opengis.feature.Feature; -import org.opengis.feature.FeatureType; import org.opengis.feature.FeatureAssociationRole; +import org.opengis.feature.FeatureType; import org.opengis.feature.IdentifiedType; import org.opengis.feature.InvalidPropertyValueException; import org.opengis.feature.Operation; import org.opengis.feature.PropertyType; +import org.opengis.metadata.maintenance.ScopeCode; +import org.opengis.metadata.quality.ConformanceResult; +import org.opengis.metadata.quality.DataQuality; +import org.opengis.metadata.quality.Element; +import org.opengis.metadata.quality.Result; +import org.opengis.util.GenericName; +import org.opengis.util.InternationalString; +import org.opengis.util.NameFactory; + +import org.apache.sis.internal.feature.Resources; +import org.apache.sis.internal.system.DefaultFactories; +import org.apache.sis.util.Static; +import org.apache.sis.util.iso.DefaultNameFactory; + +// Branch-dependent imports /** @@ -224,4 +227,26 @@ public final class Features extends Static { } } } + + + /** + * Test if given property type is an attribute as defined by {@link AttributeType}, or if it produces one as an + * {@link Operation#getResult() operation result}. It it is, we return the found attribute. + * + * @param input the data type to unravel the attribute from. + * @return The found attribute or an empty shell if we cannot find any. + */ + public static Optional<AttributeType<?>> castOrUnwrap(IdentifiedType input) { + // In case an operation also implements attribute type, we check it first. + // TODO : cycle detection ? + while (!(input instanceof AttributeType) && input instanceof Operation) { + input = ((Operation)input).getResult(); + } + + if (input instanceof AttributeType) { + return Optional.of((AttributeType)input); + } + + return Optional.empty(); + } } 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 6adf144..0fa2761 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,30 +1,159 @@ package org.apache.sis.filter; +import java.util.function.Function; + +import org.opengis.feature.AttributeType; import org.opengis.feature.FeatureType; 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.MismatchedDimensionException; +import org.opengis.metadata.extent.GeographicBoundingBox; +import org.apache.sis.feature.DefaultAttributeType; +import org.apache.sis.feature.Features; +import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.geometry.ImmutableEnvelope; +import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.internal.feature.FeatureExpression; +import org.apache.sis.internal.feature.Geometries; + +import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** + * Naïve implementation of SQLMM ST_Envelope operation. Compute bounding box of a geometry. Coordinate reference + * system unchanged. * + * @author Alexis Manin (Geomatys) */ public class ST_Envelope extends AbstractFunction implements FeatureExpression { public static final String NAME = "ST_Envelope"; + private final Worker worker; public ST_Envelope(Expression[] parameters) { super(NAME, parameters, null); - if (parameters.length > 1) throw new IllegalArgumentException("Only one parameter is accepted: source Geometry"); + if (parameters == null || parameters.length != 1) throw new MismatchedDimensionException( + String.format( + "Single parameter expected for %s operation: source Geometry. However, %d arguments were provided", + NAME, parameters == null? 0 : parameters.length + ) + ); + + final Expression parameter = parameters[0]; + if (parameter instanceof Literal) worker = new LiteralEnvelope((Literal) parameter); + else if (parameter instanceof FeatureExpression) worker = new FeatureEnvelope((FeatureExpression) parameter); + else throw new UnsupportedOperationException("Given parameter must either be a literal or a feature expression"); } @Override public Object evaluate(Object object) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 03/10/2019 + return worker.evaluate(object); } @Override public PropertyType expectedType(FeatureType type) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 03/10/2019 + return worker.type(type); + } + + /** + * An implementation of ST_Envelope working on a literal. It is a special case where computation can be done only + * once at built time, save both CPU time and memory, by caching result as a unique reference. Also, it allows to + * merge parameter validation with real computation, ensuring that operator instance will consistently return result + */ + private class LiteralEnvelope implements Worker { + + final Envelope result; + final AttributeType resultType; + + public LiteralEnvelope(Literal source) { + Object value = source == null? null : source.getValue(); + ensureNonNull("Source value", value); + final Envelope tmpResult = tryGet(value); + + if (tmpResult == null) { + throw new IllegalArgumentException("Given value is of unsupported type: "+value.getClass()); + } + + result = new ImmutableEnvelope(tmpResult); + resultType = new DefaultAttributeType(null, Envelope.class, 1, 1, null); + } + + @Override + public PropertyType type(FeatureType target) { + return resultType; + } + + @Override + public Envelope evaluate(Object target) { + return result; + } + } + + private class FeatureEnvelope implements Worker { + + final FeatureExpression source; + final Function evaluator; + + private FeatureEnvelope(FeatureExpression source) { + this.source = source; + if (source instanceof Expression) { + final Expression exp = (Expression) source; + evaluator = exp::evaluate; + } else if (source instanceof Function) { + evaluator = (Function) source; + } else throw new UnsupportedOperationException("Cannot create envelope operation from a feature expression which is not a function"); + } + + @Override + public PropertyType type(FeatureType target) { + final PropertyType expressionType = source.expectedType(target); + final AttributeType<?> attr = Features.castOrUnwrap(expressionType) + .orElseThrow(() -> new UnsupportedOperationException("Cannot evaluate given expression because it does not create attribute values")); + // If given expression evaluates directly to a bbox, there's no need for a conversion step. + if (Envelope.class.isAssignableFrom(attr.getValueClass())) { + return expressionType; + } + + final int minOccurs = attr.getMinimumOccurs(); + final AttributeType<?> crsCharacteristic = attr.characteristics().get(AttributeConvention.CRS_CHARACTERISTIC); + AttributeType[] crsParam = crsCharacteristic == null? null : new AttributeType[]{crsCharacteristic}; + return new DefaultAttributeType<>(null, Envelope.class, Math.min(1, minOccurs), 1, null, crsParam); + } + + @Override + public Envelope evaluate(Object target) { + final Object extractedValue = evaluator.apply(target); + if (extractedValue == null) return null; + final Envelope env = tryGet(extractedValue); + if (env == null) throw new RuntimeException("A value is present, but its envelope cannot be determined"); + if (env.getCoordinateReferenceSystem() == null) { + // TODO: how to determine CRS ? + } + + return env; + } + } + + private interface Worker { + PropertyType type(FeatureType target); + Envelope evaluate(Object target); + } + + private static Envelope tryGet(Object value) { + if (value == null) return null; + + if (value instanceof GeographicBoundingBox) { + return new GeneralEnvelope((GeographicBoundingBox)value); + } else if (value instanceof Envelope) { + return (Envelope) value; + } else if (value instanceof CharSequence) { + // Maybe it's a WKT format, so we will try to read it + value = Geometries.fromWkt(value.toString()) + .orElseThrow(() -> new IllegalArgumentException("No geometry provider found to read WKT")); + } + + return Geometries.getEnvelope(value); } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java index df36cf6..8f4fcfb 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java @@ -29,6 +29,7 @@ import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.internal.system.Loggers; import org.apache.sis.math.Vector; import org.apache.sis.setup.GeometryLibrary; +import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.logging.Logging; @@ -150,12 +151,12 @@ public abstract class Geometries<G> { return false; } - public static Optional<Geometry> toGeometry(final Envelope env) { - return findStrategy(g -> g.tryConvertToGeometry(env)) + public static Optional<Geometry> toGeometry(final Envelope env, WrapResolution wraparound) { + return findStrategy(g -> g.tryConvertToGeometry(env, wraparound)) .map(result -> new GeometryWrapper(result, env)); } - abstract Object tryConvertToGeometry(final Envelope env); + abstract Object tryConvertToGeometry(final Envelope env, WrapResolution wraparound); /** * If the given point is an implementation of this library, returns its coordinate. @@ -243,6 +244,16 @@ public abstract class Geometries<G> { .orElse(null); } + public static Optional<?> fromWkt(String wkt) { + return findStrategy(g -> { + try { + return g.parseWKT(wkt); + } catch (Exception e) { + throw new BackingStoreException(e); + } + }); + } + /** * If the given geometry is the type supported by this {@code Geometries} instance, * returns its WKT representation. Otherwise returns {@code null}. @@ -331,4 +342,61 @@ public abstract class Geometries<G> { return Optional.empty(); } + + private Object envelope2Polygon(final Envelope env, WrapResolution resolution) { + double[] ordinates; + double[] secondEnvelopeIfSplit = null; + if (WrapResolution.NONE.equals(resolution)) { + ordinates = new double[] { + env.getMinimum(0), + env.getMinimum(1), + env.getMaximum(0), + env.getMaximum(1) + }; + } else { + final boolean xWrap = env.getMinimum(0) > env.getMaximum(0); + final boolean yWrap = env.getMinimum(1) > env.getMaximum(1); + + //TODO + switch (resolution) { + case EXPAND: + case SPLIT: + case CONTIGUOUS: + default: throw new IllegalArgumentException("Unknown or unset wrap resolution: "+resolution); + } + + } + + + double minX = ordinates[0]; + double minY = ordinates[1]; + double maxX = ordinates[2]; + double maxY = ordinates[3]; + Vector[] points = { + Vector.create(new double[]{minX, minY}), + Vector.create(new double[]{minX, maxY}), + Vector.create(new double[]{maxX, maxY}), + Vector.create(new double[]{maxX, minY}), + Vector.create(new double[]{minX, minY}) + }; + + final G mainRect = createPolyline(2, points); + if (secondEnvelopeIfSplit != null) { + minX = secondEnvelopeIfSplit[0]; + minY = secondEnvelopeIfSplit[1]; + maxX = secondEnvelopeIfSplit[2]; + maxY = secondEnvelopeIfSplit[3]; + Vector[] points2 = { + Vector.create(new double[]{minX, minY}), + Vector.create(new double[]{minX, maxY}), + Vector.create(new double[]{maxX, maxY}), + Vector.create(new double[]{maxX, minY}), + Vector.create(new double[]{minX, minY}) + }; + final G secondRect = createPolyline(2, points2); + // TODO: merge then send back + } + + return mainRect; + } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java index c48cdd7..034bd65 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java @@ -114,7 +114,7 @@ final class JTS extends Geometries<Geometry> { } @Override - Geometry tryConvertToGeometry(org.opengis.geometry.Envelope env) { + Geometry tryConvertToGeometry(org.opengis.geometry.Envelope env, final WrapResolution resolution) { final int dim = env.getDimension(); if (dim > 2) throw new UnsupportedOperationException("Cannot manage more than 2 dimensions, but input envelope has "+dim); throw new UnsupportedOperationException("Not yet"); diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/WrapResolution.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/WrapResolution.java new file mode 100644 index 0000000..af99204 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/WrapResolution.java @@ -0,0 +1,53 @@ +package org.apache.sis.internal.feature; + +public enum WrapResolution { + /** + * Convert the coordinates without checking the antemeridian. + * If the envelope crosses the antemeridian (lower corner values {@literal >} upper corner values) + * the created polygon will be wrong since it will define a different area then the envelope. + * Use this method only knowing the envelopes do not cross the antemeridian. + * + * Example : + * ENV(+170 +10, -170 -10) + * POLYGON(+170 +10, -170 +10, -170 -10, +170 -10, +170 +10) + * + */ + NONE, + /** + * Convert the coordinates checking the antemeridian. + * If the envelope crosses the antemeridian (lower corner values {@literal >} upper corner values) + * the created polygon will go from axis minimum value to axis maximum value. + * This ensure the create polygon contains the envelope but is wider. + * + * Example : + * ENV(+170 +10, -170 -10) + * POLYGON(-180 +10, +180 +10, +180 -10, -180 -10, -180 +10) + */ + EXPAND, + /** + * Convert the coordinates checking the antemeridian. + * If the envelope crosses the antemeridian (lower corner values {@literal >} upper corner values) + * the created polygon will be cut in 2 polygons on each side of the coordinate system. + * This ensure the create polygon exactly match the envelope but with a more + * complex geometry. + * + * Example : + * ENV(+170 +10, -170 -10) + * MULTI-POLYGON( + * (-180 +10, -170 +10, -170 -10, -180 -10, -180 +10) + * (+170 +10, +180 +10, +180 -10, +170 -10, +170 +10) + * ) + */ + SPLIT, + /** + * Convert the coordinates checking the antemeridian. + * If the envelope crosses the antemeridian (lower corner values {@literal >} upper corner values) + * the created polygon coordinate will increase over the antemeridian making + * a contiguous geometry. + * + * Example : + * ENV(+170 +10, -170 -10) + * POLYGON(+170 +10, +190 +10, +190 -10, +170 -10, +170 +10) + */ + CONTIGUOUS +} diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java index aea1253..3a0c864 100644 --- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java +++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/ANSIInterpreter.java @@ -41,6 +41,7 @@ import org.opengis.util.LocalName; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.internal.feature.Geometries; +import org.apache.sis.internal.feature.WrapResolution; import org.apache.sis.util.iso.Names; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; @@ -514,7 +515,7 @@ public class ANSIInterpreter implements FilterVisitor, ExpressionVisitor { } final GeneralEnvelope env = new GeneralEnvelope(lower, upper); env.setCoordinateReferenceSystem(source.getCoordinateReferenceSystem()); - return Geometries.toGeometry(env) + return Geometries.toGeometry(env, WrapResolution.SPLIT) .orElseThrow(() -> new UnsupportedOperationException("No geometry implementation available")); } 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 new file mode 100644 index 0000000..9aea3a0 --- /dev/null +++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/FilterInterpreterTest.java @@ -0,0 +1,35 @@ +package org.apache.sis.internal.sql.feature; + +import org.opengis.filter.FilterFactory2; +import org.opengis.filter.spatial.BBOX; + +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.junit.Test; + +public class FilterInterpreterTest { + private static final FilterFactory2 FF = new DefaultFilterFactory(); + + @Test + public void testGeometricFilter() { + final ANSIInterpreter interpreter = new ANSIInterpreter(); + final BBOX filter = FF.bbox(FF.property("Toto"), new GeneralEnvelope(new DefaultGeographicBoundingBox(-12.3, 2.1, 43.3, 51.7))); + final Object result = filter.accept(interpreter, null); + Assert.assertTrue("Result filter should be a text", result instanceof CharSequence); + Assert.assertEquals( + "Filter as SQL condition: ", + "ST_Intersect(" + + "ST_Envelope(\"Toto\"), " + + "ST_Envelope(" + + "ST_GeomFromText(" + + "POLYGON((-12.3 43.3, -12.3 51.7, 2.1 51.7, 2.1 43.3, -12.3 43.3))" + + ")" + + ")" + + ")", + result.toString() + ); + } +}
