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 349d1f53de7cf63a1bb4dfe06341ad357e71b3a9 Author: Alexis Manin <[email protected]> AuthorDate: Thu Oct 3 18:05:58 2019 +0200 feat(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 | 159 +++++++++++++++++++++ .../java/org/apache/sis/internal/feature/ESRI.java | 29 ++-- .../apache/sis/internal/feature/Geometries.java | 145 ++++++++++++++----- .../java/org/apache/sis/internal/feature/JTS.java | 31 ++-- .../sis/internal/feature/WrapResolution.java | 53 +++++++ .../sis/internal/sql/feature/ANSIInterpreter.java | 156 +++++++++++++++++--- .../sql/feature/FilterInterpreterTest.java | 35 +++++ 8 files changed, 573 insertions(+), 88 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 new file mode 100644 index 0000000..0fa2761 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Envelope.java @@ -0,0 +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 == 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) { + return worker.evaluate(object); + } + + @Override + public PropertyType expectedType(FeatureType type) { + 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/ESRI.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java index bbd8780..601b6b7 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java @@ -17,22 +17,26 @@ package org.apache.sis.internal.feature; import java.util.Iterator; -import com.esri.core.geometry.Geometry; + +import org.opengis.geometry.Envelope; + +import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.math.Vector; +import org.apache.sis.setup.GeometryLibrary; +import org.apache.sis.util.Classes; + import com.esri.core.geometry.Envelope2D; +import com.esri.core.geometry.Geometry; import com.esri.core.geometry.MultiPath; -import com.esri.core.geometry.Polyline; -import com.esri.core.geometry.Polygon; +import com.esri.core.geometry.OperatorExportToWkt; +import com.esri.core.geometry.OperatorImportFromWkt; import com.esri.core.geometry.Point; import com.esri.core.geometry.Point2D; import com.esri.core.geometry.Point3D; -import com.esri.core.geometry.WktImportFlags; +import com.esri.core.geometry.Polygon; +import com.esri.core.geometry.Polyline; import com.esri.core.geometry.WktExportFlags; -import com.esri.core.geometry.OperatorImportFromWkt; -import com.esri.core.geometry.OperatorExportToWkt; -import org.apache.sis.geometry.GeneralEnvelope; -import org.apache.sis.setup.GeometryLibrary; -import org.apache.sis.math.Vector; -import org.apache.sis.util.Classes; +import com.esri.core.geometry.WktImportFlags; /** @@ -84,6 +88,11 @@ final class ESRI extends Geometries<Geometry> { return null; } + @Override + Object tryConvertToGeometry(Envelope env) { + throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 03/10/2019 + } + /** * If the given point is an implementation of this library, returns its coordinate. * Otherwise returns {@code null}. If non-null, the returned array may have a length of 2 or 3. 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 a30b2a2..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 @@ -17,13 +17,20 @@ package org.apache.sis.internal.feature; import java.util.Iterator; +import java.util.Optional; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.LogRecord; -import org.apache.sis.util.logging.Logging; -import org.apache.sis.internal.system.Loggers; + +import org.opengis.geometry.Envelope; +import org.opengis.geometry.Geometry; + import org.apache.sis.geometry.GeneralEnvelope; -import org.apache.sis.setup.GeometryLibrary; +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; /** @@ -144,6 +151,13 @@ public abstract class Geometries<G> { return false; } + 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, WrapResolution wraparound); + /** * If the given point is an implementation of this library, returns its coordinate. * Otherwise returns {@code null}. @@ -163,11 +177,7 @@ public abstract class Geometries<G> { * @see #createPoint(double, double) */ public static double[] getCoordinate(final Object point) { - for (Geometries<?> g = implementation; g != null; g = g.fallback) { - double[] coord = g.tryGetCoordinate(point); - if (coord != null) return coord; - } - return null; + return findStrategy(g -> g.tryGetCoordinate(point)).orElse(null); } /** @@ -186,11 +196,7 @@ public abstract class Geometries<G> { * is not a recognized geometry or its envelope is empty. */ public static GeneralEnvelope getEnvelope(final Object geometry) { - for (Geometries<?> g = implementation; g != null; g = g.fallback) { - GeneralEnvelope env = g.tryGetEnvelope(geometry); - if (env != null) return env; - } - return null; + return findStrategy(g -> g.tryGetEnvelope(geometry)).orElse(null); } /** @@ -208,18 +214,19 @@ public abstract class Geometries<G> { * object is not a recognized geometry. */ public static String toString(final Object geometry) { - for (Geometries<?> g = implementation; g != null; g = g.fallback) { - String s = g.tryGetLabel(geometry); - if (s != null) { - GeneralEnvelope env = g.tryGetEnvelope(geometry); - if (env != null) { - final String bbox = env.toString(); - s += bbox.substring(bbox.indexOf('(')); - } - return s; + return findStrategy(g -> g.tryToString(geometry)).orElse(null); + } + + private String tryToString(Object geometry) { + String s = tryGetLabel(geometry); + if (s != null) { + GeneralEnvelope env = tryGetEnvelope(geometry); + if (env != null) { + final String bbox = env.toString(); + s += bbox.substring(bbox.indexOf('(')); } } - return null; + return s; } /** @@ -233,11 +240,18 @@ public abstract class Geometries<G> { * @return the Well Known Text for the given geometry, or {@code null} if the given object is unrecognized. */ public static String formatWKT(Object geometry, double flatness) { - for (Geometries<?> g = implementation; g != null; g = g.fallback) { - String wkt = g.tryFormatWKT(geometry, flatness); - if (wkt != null) return wkt; - } - return null; + return findStrategy(g -> g.tryFormatWKT(geometry, flatness)) + .orElse(null); + } + + public static Optional<?> fromWkt(String wkt) { + return findStrategy(g -> { + try { + return g.parseWKT(wkt); + } catch (Exception e) { + throw new BackingStoreException(e); + } + }); } /** @@ -304,13 +318,8 @@ public abstract class Geometries<G> { while (paths.hasNext()) { final Object first = paths.next(); if (first != null) { - for (Geometries<?> g = implementation; g != null; g = g.fallback) { - final Object merged = g.tryMergePolylines(first, paths); - if (merged != null) { - return merged; - } - } - throw unsupported(2); + return findStrategy(g -> g.tryMergePolylines(first, paths)) + .orElseThrow(() -> unsupported(2)); } } return null; @@ -324,4 +333,70 @@ public abstract class Geometries<G> { static UnsupportedOperationException unsupported(final int dimension) { return new UnsupportedOperationException(Resources.format(Resources.Keys.UnsupportedGeometryObject_1, dimension)); } + + private static <T> Optional<T> findStrategy(final Function<Geometries<?>, T> op) { + for (Geometries<?> g = implementation; g != null; g = g.fallback) { + final T result = op.apply(g); + if (result != null) return Optional.of(result); + } + + 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 20e2f6d..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 @@ -16,24 +16,26 @@ */ package org.apache.sis.internal.feature; -import java.util.List; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; + +import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.math.Vector; +import org.apache.sis.setup.GeometryLibrary; +import org.apache.sis.util.Classes; + import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Point; -import org.locationtech.jts.geom.Polygon; -import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.io.WKTReader; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.io.ParseException; -import org.apache.sis.geometry.GeneralEnvelope; -import org.apache.sis.setup.GeometryLibrary; -import org.apache.sis.math.Vector; -import org.apache.sis.util.Classes; +import org.locationtech.jts.io.WKTReader; /** @@ -111,6 +113,13 @@ final class JTS extends Geometries<Geometry> { return null; } + @Override + 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"); + } + /** * If the given point is an implementation of this library, returns its coordinate. * Otherwise returns {@code null}. If non-null, the returned array may have a length of 2 or 3. 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 48c7d63..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 @@ -1,9 +1,12 @@ package org.apache.sis.internal.sql.feature; +import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.function.BooleanSupplier; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.opengis.filter.*; import org.opengis.filter.expression.Add; @@ -19,6 +22,7 @@ import org.opengis.filter.expression.PropertyName; import org.opengis.filter.expression.Subtract; import org.opengis.filter.spatial.BBOX; import org.opengis.filter.spatial.Beyond; +import org.opengis.filter.spatial.BinarySpatialOperator; import org.opengis.filter.spatial.Contains; import org.opengis.filter.spatial.Crosses; import org.opengis.filter.spatial.DWithin; @@ -29,9 +33,15 @@ import org.opengis.filter.spatial.Overlaps; import org.opengis.filter.spatial.Touches; import org.opengis.filter.spatial.Within; import org.opengis.filter.temporal.*; +import org.opengis.geometry.Envelope; +import org.opengis.geometry.Geometry; +import org.opengis.metadata.extent.GeographicBoundingBox; import org.opengis.util.GenericName; 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; @@ -107,7 +117,15 @@ public class ANSIInterpreter implements FilterVisitor, ExpressionVisitor { @Override public Object visit(PropertyIsBetween filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + final CharSequence propertyExp = evaluateMandatory(filter.getExpression(), extraData); + final CharSequence lowerExp = evaluateMandatory(filter.getLowerBoundary(), extraData); + final CharSequence upperExp = evaluateMandatory(filter.getUpperBoundary(), extraData); + + return new StringBuilder(propertyExp) + .append(" BETWEEN ") + .append(lowerExp) + .append(" AND ") + .append(upperExp); } @Override @@ -158,61 +176,92 @@ public class ANSIInterpreter implements FilterVisitor, ExpressionVisitor { return evaluateMandatory(filter.getExpression(), extraData) + " = NULL"; } + /* + * SPATIAL FILTERS + */ + @Override public Object visit(BBOX filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + final CharSequence left = evaluateMandatory(filter.getExpression1(), extraData); + final CharSequence right = evaluateMandatory(filter.getExpression2(), extraData); + + // TODO: In case source expression is already an envelope, we do not need to force envelope conversion. It would + // only be micro-optimisation however. + boolean leftToEnvelope = true; + boolean rightToEnvelope = true; + + final StringBuilder sb = new StringBuilder("ST_Intersects("); + if (leftToEnvelope) { + sb.append("ST_Envelope(").append(left).append(')'); + } else sb.append(left); + + sb.append(", "); + + if (rightToEnvelope) { + sb.append("ST_Envelope(").append(right).append(')'); + } else sb.append(right); + + return sb.append(')'); } @Override public Object visit(Beyond filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + // TODO: ISO SQL specifies that unit of distance could be specified. However, PostGIS documentation does not + // talk about it. For now, we'll fallback on Java implementation until we're sure how to perform native + // operation properly. + throw new UnsupportedOperationException("Not yet: unit management ambiguous"); } @Override public Object visit(Contains filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + return function("ST_Contains", filter, extraData); } @Override public Object visit(Crosses filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + return function("ST_Crosses", filter, extraData); } @Override public Object visit(Disjoint filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + return function("ST_Disjoint", filter, extraData); } @Override public Object visit(DWithin filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + // TODO: as for beyond, unit determination is a bit complicated. + throw new UnsupportedOperationException("Not yet: unit management to handle properly"); } @Override public Object visit(Equals filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + return function("ST_Equals", filter, extraData); } @Override public Object visit(Intersects filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + return function("ST_Intersects", filter, extraData); } @Override public Object visit(Overlaps filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + return function("ST_Overlaps", filter, extraData); } @Override public Object visit(Touches filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + return function("ST_Touches", filter, extraData); } @Override public Object visit(Within filter, Object extraData) { - throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 + return function("ST_Within", filter, extraData); } + /* + * TEMPORAL OPERATORS + */ + @Override public Object visit(After filter, Object extraData) { throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 30/09/2019 @@ -333,12 +382,25 @@ public class ANSIInterpreter implements FilterVisitor, ExpressionVisitor { */ protected static CharSequence format(Literal candidate) { - final Object value = candidate == null ? null : candidate.getValue(); + Object value = candidate == null ? null : candidate.getValue(); if (value == null) return "NULL"; else if (value instanceof CharSequence) { final String asStr = value.toString(); asStr.replace("'", "''"); return "'"+asStr+"'"; + } else if (value instanceof Number || value instanceof Boolean) { + return value.toString(); + } + + // geometric special cases + if (value instanceof GeographicBoundingBox) { + value = new GeneralEnvelope((GeographicBoundingBox)value); + } + if (value instanceof Envelope) { + value = asGeometry((Envelope)value); + } + if (value instanceof Geometry) { + return format((Geometry)value); } throw new UnsupportedOperationException("Not supported yet: Literal value of type "+value.getClass()); @@ -392,20 +454,40 @@ public class ANSIInterpreter implements FilterVisitor, ExpressionVisitor { + ")"; } + protected CharSequence function(Object extraData, final String fnName, Supplier<Expression>... parameters) { + return Arrays.stream(parameters) + .map(Supplier::get) + .map(exp -> evaluateMandatory(exp, extraData)) + .collect(Collectors.joining(", ", fnName+'(', ")")); + } + + private CharSequence function(String fnName, BinarySpatialOperator filter, Object extraData) { + return function(extraData, fnName, filter::getExpression1, filter::getExpression2); + } + protected CharSequence evaluateMandatory(final Filter candidate, Object extraData) { final Object exp = candidate == null ? null : candidate.accept(this, extraData); - if (isNonEmptyText(exp)) return (CharSequence) exp; - else throw new IllegalArgumentException("Filter evaluate to an empty text: "+candidate); + return asNonEmptyText(exp) + .orElseThrow(() -> new IllegalArgumentException("Filter evaluate to an empty text: "+candidate)); } protected CharSequence evaluateMandatory(final Expression candidate, Object extraData) { final Object exp = candidate == null ? null : candidate.accept(this, extraData); - if (isNonEmptyText(exp)) return (CharSequence) exp; - else throw new IllegalArgumentException("Expression evaluate to an empty text: "+candidate); + return asNonEmptyText(exp) + .orElseThrow(() -> new IllegalArgumentException("Expression evaluate to an empty text: "+candidate)); + } + + protected static Optional<CharSequence> asNonEmptyText(final Object toCheck) { + if (toCheck instanceof CharSequence) { + final CharSequence asCS = (CharSequence) toCheck; + if (asCS.length() > 0) return Optional.of(asCS); + } + + return Optional.empty(); } protected static boolean isNonEmptyText(final Object toCheck) { - return toCheck instanceof CharSequence && ((CharSequence) toCheck).length() > 0; + return asNonEmptyText(toCheck).isPresent(); } private static void ensureMatchCase(BinaryComparisonOperator filter) { @@ -420,4 +502,42 @@ public class ANSIInterpreter implements FilterVisitor, ExpressionVisitor { if (extraData instanceof StringBuilder) return ((StringBuilder) extraData).append(toAdd); return toAdd; } + + protected static Geometry asGeometry(final Envelope source) { + final double[] lower = source.getLowerCorner().getCoordinate(); + final double[] upper = source.getLowerCorner().getCoordinate(); + for (int i = 0 ; i < lower.length ; i++) { + if (Double.isNaN(lower[i]) || Double.isNaN(upper[i])) { + throw new IllegalArgumentException("Cannot use envelope containing NaN for filter"); + } + lower[i] = clampInfinity(lower[i]); + upper[i] = clampInfinity(upper[i]); + } + final GeneralEnvelope env = new GeneralEnvelope(lower, upper); + env.setCoordinateReferenceSystem(source.getCoordinateReferenceSystem()); + return Geometries.toGeometry(env, WrapResolution.SPLIT) + .orElseThrow(() -> new UnsupportedOperationException("No geometry implementation available")); + } + + protected static CharSequence format(final Geometry source) { + // TODO: find a better approximation of desired "flatness" + final Envelope env = source.getEnvelope(); + final double flatness = 0.05 * IntStream.range(0, env.getDimension()) + .mapToDouble(env::getSpan) + .average() + .orElseThrow(() -> new IllegalArgumentException("Given geometry envelope dimension is 0")); + return new StringBuilder("ST_GeomFromText(") + .append(Geometries.formatWKT(source, flatness)) + .append(')'); + } + + protected static double clampInfinity(final double candidate) { + if (candidate == Double.NEGATIVE_INFINITY) { + return -Double.MAX_VALUE; + } else if (candidate == Double.POSITIVE_INFINITY) { + return Double.MAX_VALUE; + } + + return candidate; + } } 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() + ); + } +}
