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 4865a1f535d2afcb36ef2ca59bb89246b532229c Author: Alexis Manin <[email protected]> AuthorDate: Thu Oct 3 18:05:58 2019 +0200 WIP(SQLStore): Start geometric function implementation. --- .../java/org/apache/sis/filter/ST_Envelope.java | 30 ++++ .../java/org/apache/sis/internal/feature/ESRI.java | 29 ++-- .../apache/sis/internal/feature/Geometries.java | 77 +++++----- .../java/org/apache/sis/internal/feature/JTS.java | 31 +++-- .../sis/internal/sql/feature/ANSIInterpreter.java | 155 ++++++++++++++++++--- 5 files changed, 248 insertions(+), 74 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Envelope.java b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Envelope.java new file mode 100644 index 0000000..6adf144 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ST_Envelope.java @@ -0,0 +1,30 @@ +package org.apache.sis.filter; + +import org.opengis.feature.FeatureType; +import org.opengis.feature.PropertyType; +import org.opengis.filter.expression.Expression; + +import org.apache.sis.internal.feature.FeatureExpression; + +/** + * + */ +public class ST_Envelope extends AbstractFunction implements FeatureExpression { + + public static final String NAME = "ST_Envelope"; + + public ST_Envelope(Expression[] parameters) { + super(NAME, parameters, null); + if (parameters.length > 1) throw new IllegalArgumentException("Only one parameter is accepted: source Geometry"); + } + + @Override + public Object evaluate(Object object) { + throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 03/10/2019 + } + + @Override + public PropertyType expectedType(FeatureType type) { + throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)" on 03/10/2019 + } +} 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..df36cf6 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,19 @@ 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.logging.Logging; /** @@ -144,6 +150,13 @@ public abstract class Geometries<G> { return false; } + public static Optional<Geometry> toGeometry(final Envelope env) { + return findStrategy(g -> g.tryConvertToGeometry(env)) + .map(result -> new GeometryWrapper(result, env)); + } + + abstract Object tryConvertToGeometry(final Envelope env); + /** * If the given point is an implementation of this library, returns its coordinate. * Otherwise returns {@code null}. @@ -163,11 +176,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 +195,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 +213,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 +239,8 @@ 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); } /** @@ -304,13 +307,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 +322,13 @@ 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(); + } } 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..c48cdd7 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 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/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..aea1253 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,14 @@ 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.util.iso.Names; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; @@ -107,7 +116,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 +175,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 +381,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 +453,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 +501,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) + .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; + } }
