sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ama...@apache.org
Subject [sis] 25/45: WIP(SQLStore): work on bbox filter and conversion between envelope and geometry
Date Tue, 12 Nov 2019 16:44:52 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 c18417df63238fcef64affbed4e50b2f41e004b5
Author: Alexis Manin <amanin@apache.org>
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()
+        );
+    }
+}


Mime
View raw message