This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit d94bbdd49113b9c889542a7eab63cd318521430a Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Aug 27 17:14:32 2021 +0200 Add an `Optimization.setFeatureType(…)` method and update some filter/expression implementations for taking advantage of it. --- .../apache/sis/filter/BinaryGeometryFilter.java | 51 +++++++-- .../java/org/apache/sis/filter/LeafExpression.java | 2 +- .../java/org/apache/sis/filter/Optimization.java | 60 ++++++++++- .../java/org/apache/sis/filter/PropertyValue.java | 120 +++++++++++++++++++-- .../sis/internal/filter/sqlmm/TwoGeometries.java | 40 +++++++ .../org/apache/sis/filter/LogicalFunctionTest.java | 35 ++++++ .../internal/filter/sqlmm/RegistryTestCase.java | 25 +++++ 7 files changed, 309 insertions(+), 24 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryGeometryFilter.java b/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryGeometryFilter.java index 50199f0..e1afc00 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryGeometryFilter.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryGeometryFilter.java @@ -21,10 +21,12 @@ import java.util.Arrays; import javax.measure.Unit; import javax.measure.IncommensurableException; import org.opengis.util.FactoryException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import org.apache.sis.internal.feature.Geometries; import org.apache.sis.internal.feature.GeometryWrapper; import org.apache.sis.internal.feature.SpatialOperationContext; +import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.internal.filter.Node; import org.apache.sis.util.ArgumentChecks; @@ -32,9 +34,12 @@ import org.apache.sis.util.ArgumentChecks; import org.opengis.filter.Filter; import org.opengis.filter.Literal; import org.opengis.filter.Expression; +import org.opengis.filter.ValueReference; import org.opengis.filter.SpatialOperator; import org.opengis.filter.BinarySpatialOperator; import org.opengis.filter.InvalidFilterValueException; +import org.opengis.feature.FeatureType; +import org.opengis.feature.PropertyNotFoundException; /** @@ -175,17 +180,23 @@ abstract class BinaryGeometryFilter<R,G> extends Node implements SpatialOperator */ @Override public final Filter<? super R> optimize(final Optimization optimization) { - final Expression<? super R, ?> geometry1 = unwrap(expression1); - final Expression<? super R, ?> geometry2 = unwrap(expression2); - final Expression<? super R, ?> effective1 = optimization.apply(geometry1); - final Expression<? super R, ?> effective2 = optimization.apply(geometry2); - final Literal<? super R, ?> literal; - final boolean immediate; // true if the filter should be evaluated immediately. - final boolean literalIsNull; // true if one of the literal value is null. + Expression<? super R, ?> geometry1 = unwrap(expression1); + Expression<? super R, ?> geometry2 = unwrap(expression2); + Expression<? super R, ?> effective1 = optimization.apply(geometry1); + Expression<? super R, ?> effective2 = optimization.apply(geometry2); + Expression<? super R, ?> other; // The expression which is not literal. + Expression<? super R, GeometryWrapper<G>> wrapper; + Literal<? super R, ?> literal; + boolean immediate; // true if the filter should be evaluated immediately. + boolean literalIsNull; // true if one of the literal value is null. if (effective2 instanceof Literal<?,?>) { + other = effective1; + wrapper = expression2; literal = (Literal<? super R, ?>) effective2; immediate = (effective1 instanceof Literal<?,?>); } else if (effective1 instanceof Literal<?,?>) { + other = effective2; + wrapper = expression1; literal = (Literal<? super R, ?>) effective1; immediate = false; } else { @@ -197,6 +208,31 @@ abstract class BinaryGeometryFilter<R,G> extends Node implements SpatialOperator // If the literal has no value, then the filter will always evaluate to a negative result. result = negativeResult(); } else { + /* + * If we are optimizing for a feature type, and if the other expression is a property value, + * then try to fetch the CRS of the property values. If we can transform the literal to that + * CRS, do it now in order to avoid doing this transformation for all feature instances. + */ + final FeatureType featureType = optimization.getFeatureType(); + if (featureType != null && other instanceof ValueReference<?,?>) try { + final CoordinateReferenceSystem targetCRS = AttributeConvention.getCRSCharacteristic( + featureType, featureType.getProperty(((ValueReference<?,?>) other).getXPath())); + if (targetCRS != null) { + final GeometryWrapper<G> geometry = wrapper.apply(null); + final GeometryWrapper<G> transformed = geometry.transform(targetCRS); + if (geometry != transformed) { + literal = Optimization.literal(transformed); + if (literal == effective1) effective1 = literal; + else effective2 = literal; + } + } + } catch (PropertyNotFoundException | TransformException e) { + warning(e, true); + } + /* + * If one of the "effective" parameter has been modified, recreate a new filter. + * If all operands are literal, we can evaluate that filter immediately. + */ Filter<? super R> filter = this; if ((effective1 != geometry1) || (effective2 != geometry2)) { filter = recreate(effective1, effective2); @@ -204,7 +240,6 @@ abstract class BinaryGeometryFilter<R,G> extends Node implements SpatialOperator if (!immediate) { return filter; } - // If all operands are literal, we can evaluate the expression immediately. result = filter.test(null); } return result ? Filter.include() : Filter.exclude(); diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java b/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java index cf21ee4..0bd07b6 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java @@ -190,7 +190,7 @@ abstract class LeafExpression<R,V> extends Node implements FeatureExpression<R,V */ @Override public Expression<? super R, ? extends V> optimize(final Optimization optimization) { - return new Literal<>(getValue()); + return Optimization.literal(getValue()); } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/Optimization.java b/core/sis-feature/src/main/java/org/apache/sis/filter/Optimization.java index b582bce..e52bc88 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/Optimization.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/Optimization.java @@ -32,6 +32,7 @@ import org.opengis.filter.Literal; import org.opengis.filter.Expression; import org.opengis.filter.LogicalOperator; import org.opengis.filter.LogicalOperatorName; +import org.opengis.feature.FeatureType; /** @@ -44,11 +45,15 @@ import org.opengis.filter.LogicalOperatorName; * <li>Immediate evaluation of expressions where all parameters are literal values.</li> * </ul> * - * Current version does not yet provide configuration options. - * But this class is the place where such options may be added in the future. + * The following options can enable some additional optimizations: * - * <p>This class is <strong>not</strong> thread-safe. A new instance shall be created - * for each thread applying optimizations. Example:</p> + * <ul> + * <li>The type of the {@code Feature} instances to be filtered.</li> + * </ul> + * + * <h2>Usage in multi-threads context</h2> + * This class is <strong>not</strong> thread-safe. + * A new instance shall be created for each thread applying optimizations. Example: * * {@preformat java * Filter<R> filter = ...; @@ -81,6 +86,11 @@ public class Optimization { private static final Object COMPUTING = Void.TYPE; /** + * The type of feature instances to be filtered, or {@code null} if unknown. + */ + private FeatureType featureType; + + /** * Filters and expressions already optimized. Also used for avoiding never-ending loops. * The map is created when first needed. * @@ -99,6 +109,29 @@ public class Optimization { } /** + * Returns the type of feature instances to be filtered, or {@code null} if unknown. + * This is the last value specified by a call to {@link #setFeatureType(FeatureType)}. + * The default value is {@code null}. + * + * @return the type of feature instances to be filtered, or {@code null} if unknown. + */ + public FeatureType getFeatureType() { + return featureType; + } + + /** + * Sets the type of feature instances to be filtered. + * If this type is known in advance, specifying it may allow to compute more specific + * {@link org.apache.sis.util.ObjectConverter}s or to apply some geometry reprojection + * in advance. + * + * @param type the type of feature instances to be filtered, or {@code null} if unknown. + */ + public void setFeatureType(final FeatureType type) { + featureType = type; + } + + /** * Optimizes or simplifies the given filter. If the given instance implements the {@link OnFilter} interface, * then its {@code optimize(this)} method is invoked. Otherwise this method returns the given filter as-is. * @@ -172,6 +205,7 @@ public class Optimization { Expression<? super R, ?> e = expressions.get(i); unchanged &= (e == (e = optimization.apply(e))); immediate &= (e instanceof Literal<?,?>); + effective[i] = e; } if (immediate) { return test(null) ? Filter.include() : Filter.exclude(); @@ -275,9 +309,10 @@ public class Optimization { Expression<? super R, ?> e = parameters.get(i); unchanged &= (e == (e = optimization.apply(e))); immediate &= (e instanceof Literal<?,?>); + effective[i] = e; } if (immediate) { - return new LeafExpression.Literal<>(apply(null)); + return literal(apply(null)); } else if (unchanged) { return this; } else { @@ -387,4 +422,19 @@ public class Optimization { } throw new IllegalArgumentException(); } + + /** + * Creates a constant, literal value that can be used in expressions. + * This is a helper methods for optimizations that simplified an expression to a constant value. + * + * @param <R> the type of resources used as inputs. + * @param <V> the type of the value of the literal. + * @param value the literal value. May be {@code null}. + * @return a literal for the given value. + * + * @see DefaultFilterFactory#literal(Object) + */ + public static <R,V> Literal<R,V> literal(final V value) { + return new LeafExpression.Literal<>(value); + } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java index 9da399d..594999a 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java @@ -19,6 +19,7 @@ package org.apache.sis.filter; import java.util.Collection; import java.util.Collections; import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.ObjectConverter; import org.apache.sis.util.ObjectConverters; import org.apache.sis.util.UnconvertibleObjectException; import org.apache.sis.feature.builder.FeatureTypeBuilder; @@ -29,6 +30,7 @@ import org.apache.sis.feature.builder.AttributeTypeBuilder; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; import org.opengis.feature.PropertyType; +import org.opengis.feature.AttributeType; import org.opengis.feature.IdentifiedType; import org.opengis.feature.Operation; import org.opengis.feature.PropertyNotFoundException; @@ -38,7 +40,7 @@ import org.opengis.filter.ValueReference; /** * Expression whose value is computed by retrieving the value indicated by the provided name. - * A property name does not store any value; it acts as an indirection to a property value of + * This expression does not store any value; it acts as an indirection to a property value of * the evaluated feature. * * @author Johann Sorel (Geomatys) @@ -83,7 +85,7 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> implements Val if (type == Object.class) { return (PropertyValue<V>) new AsObject(name); } else { - return new Typed<>(type, name); + return new Converted<>(type, name); } } @@ -104,14 +106,32 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> implements Val } /** + * Returns the type of values fetched from {@link Feature} instance. + * This is the type before conversion to the {@linkplain #getValueClass() target type}. + */ + protected Class<?> getSourceClass() { + return Object.class; + } + + /** * Returns an expression that provides values as instances of the specified class. */ @Override @SuppressWarnings("unchecked") public final <N> Expression<Feature,N> toValueType(final Class<N> type) { - return type.isAssignableFrom(getValueClass()) ? (PropertyValue<N>) this : create(name, type); + if (type.isAssignableFrom(getValueClass())) { + return (PropertyValue<N>) this; + } + final Class<?> source = getSourceClass(); + if (source != Object.class) { + return new CastedAndConverted<>(source, type, name); + } + return create(name, type); } + + + /** * An expression fetching property values as {@code Object}. * This expression does not need to apply any type conversion. @@ -143,15 +163,19 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> implements Val } } + + + /** * An expression fetching property values as an object of specified type. + * The value is converted from {@link Object} to the specified type. */ - private static final class Typed<V> extends PropertyValue<V> { + private static class Converted<V> extends PropertyValue<V> implements Optimization.OnExpression<Feature,V> { /** For cross-version compatibility. */ private static final long serialVersionUID = -1436865010478207066L; /** The desired type of values. */ - private final Class<V> type; + protected final Class<V> type; /** * Creates a new expression retrieving values from a property of the given name. @@ -159,13 +183,16 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> implements Val * @param type the desired type for the expression result. * @param name the name of the property to fetch. */ - Typed(final Class<V> type, final String name) { + protected Converted(final Class<V> type, final String name) { super(name); this.type = type; } - /** Returns the type of values computed by this expression. */ - @Override public Class<V> getValueClass() { + /** + * Returns the type of values computed by this expression. + */ + @Override + public final Class<V> getValueClass() { return type; } @@ -187,10 +214,33 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> implements Val } /** - * Provides the expected type of values produced by this expression when a feature of the given type is evaluated. + * Tries to optimize this expression. If an {@link ObjectConverter} can be determined in advance + * for the {@linkplain Optimization#getFeatureType() feature type for which to optimize}, + * then a specialized expression is returned. Otherwise this method returns {@code this}. + */ + @Override + public final Expression<Feature, ? extends V> optimize(final Optimization optimization) { + final FeatureType featureType = optimization.getFeatureType(); + if (featureType != null) try { + final PropertyType property = featureType.getProperty(name); + if (property instanceof AttributeType<?>) { + final Class<?> source = ((AttributeType<?>) property).getValueClass(); + if (source != null && source != Object.class && !source.isAssignableFrom(getSourceClass())) { + return new CastedAndConverted<>(source, type, name); + } + } + } catch (PropertyNotFoundException e) { + warning(e, true); + } + return this; + } + + /** + * Provides the expected type of values produced by this expression + * when a feature of the given type is evaluated. */ @Override - public PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) { + public final PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) { final PropertyTypeBuilder p = super.expectedType(valueType, addTo); if (p instanceof AttributeTypeBuilder<?>) { final AttributeTypeBuilder<?> a = (AttributeTypeBuilder<?>) p; @@ -225,4 +275,54 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> implements Val } return addTo.addProperty(type); } + + + + + /** + * An expression fetching property values as an object of specified type. + * The value is first casted from {@link Object} to the expected source type, + * then converted to the specified target type. + */ + private static final class CastedAndConverted<S,V> extends Converted<V> { + /** For cross-version compatibility. */ + private static final long serialVersionUID = -58453954752151703L; + + /** The source type before conversion. */ + private final Class<S> source; + + /** The conversion from source type to the type to be returned. */ + private final ObjectConverter<? super S, ? extends V> converter; + + /** Creates a new expression retrieving values from a property of the given name. */ + CastedAndConverted(final Class<S> source, final Class<V> type, final String name) { + super(type, name); + this.source = source; + converter = ObjectConverters.find(source, type); + } + + /** + * Returns the type of values fetched from {@link Feature} instance. + */ + @Override + protected Class<S> getSourceClass() { + return source; + } + + /** + * Returns the value of the property of the given name. + * If no value is found for the given feature, then this method returns {@code null}. + */ + @Override + public V apply(final Feature instance) { + if (instance != null) try { + return converter.apply(source.cast(instance.getPropertyValue(name))); + } catch (PropertyNotFoundException e) { + warning(e, true); + } catch (ClassCastException | UnconvertibleObjectException e) { + warning(e, false); + } + return null; + } + } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/TwoGeometries.java b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/TwoGeometries.java index 2bdc3a9..69efa59 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/TwoGeometries.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/TwoGeometries.java @@ -18,12 +18,19 @@ package org.apache.sis.internal.filter.sqlmm; import java.util.List; import java.util.Arrays; +import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; +import org.apache.sis.filter.Optimization; +import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.internal.feature.Geometries; import org.apache.sis.internal.feature.GeometryWrapper; // Branch-dependent imports +import org.opengis.feature.FeatureType; +import org.opengis.feature.PropertyNotFoundException; import org.opengis.filter.Expression; +import org.opengis.filter.Literal; +import org.opengis.filter.ValueReference; /** @@ -69,6 +76,39 @@ class TwoGeometries<R,G> extends SpatialFunction<R> { } /** + * If the CRS of the first argument is known in advance and the second argument is a literal, + * transforms the second geometry to the CRS of the first argument. The transformed geometry + * is always the second argument because according SQLMM specification, operations shall be + * executed in the CRS of the first argument. + */ + @Override + public Expression<? super R, ?> optimize(final Optimization optimization) { + final FeatureType featureType = optimization.getFeatureType(); + if (featureType != null) { + final Expression<? super R, ?> p1 = unwrap(geometry1); + if (p1 instanceof ValueReference<?,?> && unwrap(geometry2) instanceof Literal<?,?>) try { + final CoordinateReferenceSystem targetCRS = AttributeConvention.getCRSCharacteristic( + featureType, featureType.getProperty(((ValueReference<?,?>) p1).getXPath())); + if (targetCRS != null) { + final GeometryWrapper<G> literal = geometry2.apply(null); + if (literal != null) { + final GeometryWrapper<G> tr = literal.transform(targetCRS); + if (tr != literal) { + @SuppressWarnings({"unchecked","rawtypes"}) + final Expression<? super R, ?>[] effective = getParameters().toArray(new Expression[0]); // TODO: use generator in JDK9. + effective[1] = Optimization.literal(tr); + return recreate(effective); + } + } + } + } catch (PropertyNotFoundException | TransformException e) { + warning(e, true); + } + } + return super.optimize(optimization); + } + + /** * Returns a handler for the library of geometric objects used by this expression. */ @Override diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFunctionTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFunctionTest.java index e0ffeca..1e327a5 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFunctionTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFunctionTest.java @@ -29,6 +29,8 @@ import static org.apache.sis.test.Assert.*; // Branch-dependent imports import org.opengis.feature.Feature; +import org.opengis.feature.FeatureType; +import org.opengis.filter.Expression; import org.opengis.filter.Filter; import org.opengis.filter.Literal; import org.opengis.filter.FilterFactory; @@ -39,6 +41,7 @@ import org.opengis.filter.LogicalOperator; * Tests {@link LogicalFunction} implementations. * * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) * @version 1.1 * @since 1.1 * @module @@ -184,4 +187,36 @@ public final strictfp class LogicalFunctionTest extends TestCase { assertSame("Second optimization should have no effect.", optimized, new Optimization().apply(optimized)); assertSame("Expression should have been evaluated now.", expected, optimized); } + + /** + * Tests {@link Optimization} applied on logical filters when the {@link FeatureType} is known. + */ + @Test + public void testFeatureOptimization() { + final String attribute = "population"; + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.addAttribute(String.class).setName(attribute); + final FeatureType type = ftb.setName("Test").build(); + final Feature instance = type.newInstance(); + instance.setPropertyValue("population", "1000"); + /* + * Prepare an expression which divide the population value by 5. + */ + final Expression<Feature,Number> e = factory.divide(factory.property(attribute, Integer.class), factory.literal(5)); + final Optimization optimization = new Optimization(); + assertSame(e, optimization.apply(e)); // No optimization. + assertEquals(200, e.apply(instance).intValue()); + /* + * Notify the optimizer that property values will be of `String` type. + * The optimizer should compute an `ObjectConverter` in advance. + */ + optimization.setFeatureType(type); + final Expression<? super Feature, ? extends Number> opt = optimization.apply(e); + assertEquals(200, e.apply(instance).intValue()); + assertNotSame(e, opt); + + final PropertyValue<?> p = (PropertyValue<?>) opt.getParameters().get(0); + assertEquals(String.class, p.getSourceClass()); + assertEquals(Integer.class, p.getValueClass()); + } } diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/filter/sqlmm/RegistryTestCase.java b/core/sis-feature/src/test/java/org/apache/sis/internal/filter/sqlmm/RegistryTestCase.java index 3877e4f..e6eddba 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/internal/filter/sqlmm/RegistryTestCase.java +++ b/core/sis-feature/src/test/java/org/apache/sis/internal/filter/sqlmm/RegistryTestCase.java @@ -17,6 +17,7 @@ package org.apache.sis.internal.filter.sqlmm; import java.util.Arrays; +import org.opengis.geometry.DirectPosition; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; @@ -463,6 +464,30 @@ public abstract strictfp class RegistryTestCase<G> extends TestCase { } /** + * Tests {@link Optimization} on an arbitrary expression on feature instances. + */ + @Test + public void testFeatureOptimization() { + geometry = library.createPoint(10, 30); + setGeometryCRS(HardCodedCRS.WGS84_LATITUDE_FIRST); + function = factory.function("ST_Union", factory.property(P_NAME), factory.literal(geometry)); + + final Optimization optimization = new Optimization(); + final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); + ftb.addAttribute(library.pointClass).setName(P_NAME).setCRS(HardCodedCRS.WGS84); + optimization.setFeatureType(ftb.setName("Test").build()); + final Expression<? super Feature, ?> optimized = optimization.apply(function); + assertNotSame("Optimization should produce a new expression.", function, optimized); + /* + * Get the second parameter, which should be a literal, and get the point coordinates. + * Verify that the order is swapped compared to the order at the beginning of this method. + */ + final Object literal = optimized.getParameters().get(1).apply(null); + final DirectPosition point = library.castOrWrap(literal).getCentroid(); + assertArrayEquals(new double[] {30, 10}, point.getCoordinate(), STRICT); + } + + /** * Executed after each test for verifying that no unexpected log message has been emitted. */ @After