sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ama...@apache.org
Subject [sis] 07/22: feat(SQLStore): work on bbox filter and conversion between envelope and geometry
Date Thu, 14 Nov 2019 11:46:41 GMT
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 <amanin@apache.org>
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()
+        );
+    }
+}


Mime
View raw message