This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sis.git
commit b98a31fa9b860e69acc8fa89357e24f52d63bffc Merge: 410b3a1f6a b486f990bc Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sat May 6 16:59:39 2023 +0200 Merge branch 'geoapi-3.1'. .../org/apache/sis/feature/AbstractFeature.java | 12 +- .../apache/sis/feature/DefaultAssociationRole.java | 8 +- .../java/org/apache/sis/feature/DenseFeature.java | 1 + .../org/apache/sis/feature/EnvelopeOperation.java | 20 +- .../apache/sis/feature/ExpressionOperation.java | 66 +++++- .../org/apache/sis/feature/FeatureOperations.java | 74 +++++-- .../main/java/org/apache/sis/feature/Features.java | 39 +++- .../sis/feature/GroupAsPolylineOperation.java | 242 +++++++++++++++++++++ .../java/org/apache/sis/feature/LinkOperation.java | 2 +- .../org/apache/sis/feature/OperationResult.java | 66 ++++++ .../java/org/apache/sis/feature/SparseFeature.java | 1 + .../apache/sis/feature/StringJoinOperation.java | 12 +- .../org/apache/sis/filter/AssociationValue.java | 12 + .../org/apache/sis/filter/ConvertFunction.java | 12 + .../java/org/apache/sis/filter/LeafExpression.java | 26 +++ .../java/org/apache/sis/filter/Optimization.java | 49 ++++- .../java/org/apache/sis/filter/PropertyValue.java | 1 + .../sis/internal/coverage/j2d/ObservableImage.java | 2 +- .../sis/internal/feature/FeatureExpression.java | 19 +- .../apache/sis/internal/feature/Geometries.java | 33 --- .../sis/internal/feature/GeometryWrapper.java | 6 +- .../apache/sis/internal/feature/esri/Wrapper.java | 4 +- .../sis/internal/feature/j2d/PointWrapper.java | 2 +- .../apache/sis/internal/feature/j2d/Wrapper.java | 2 +- .../apache/sis/internal/feature/jts/Wrapper.java | 2 +- .../java/org/apache/sis/internal/filter/Node.java | 87 ++++++++ .../apache/sis/{ => internal}/filter/XPath.java | 8 +- .../apache/sis/feature/FeatureOperationsTest.java | 2 +- .../sis/feature/GroupAsPolylineOperationTest.java | 65 ++++++ .../org/apache/sis/filter/LogicalFilterTest.java | 26 +++ .../test/java/org/apache/sis/filter/XPathTest.java | 1 + .../sis/internal/feature/GeometriesTestCase.java | 2 +- .../apache/sis/test/suite/FeatureTestSuite.java | 1 + .../java/org/apache/sis/math/FunctionProperty.java | 85 +++++++- .../org/apache/sis/math/FunctionPropertyTest.java} | 30 ++- .../apache/sis/test/suite/UtilityTestSuite.java | 3 +- .../java/org/apache/sis/storage/FeatureQuery.java | 112 +++++++--- .../java/org/apache/sis/storage/FeatureSubset.java | 6 +- .../org/apache/sis/storage/FeatureQueryTest.java | 8 +- .../storage/gpx/GroupAsPolylineOperation.java | 207 ------------------ .../org/apache/sis/internal/storage/gpx/Types.java | 31 ++- 41 files changed, 1009 insertions(+), 378 deletions(-) diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java index fb74497a08,becfe57dfb..b80b9eea0d --- a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java @@@ -427,10 -446,13 +434,13 @@@ public abstract class AbstractFeature i * Executes the parameterless operation of the given name and sets the value of its result. * This method is the complement of {@link #getOperationValue(String)} for subclasses where * some properties may be operations. Not all operations accept assignments, - * but the {@linkplain FeatureOperations#link link} operation for instance does. + * but the {@linkplain FeatureOperations#link link} and + * {@linkplain FeatureOperations#compound compound} operations (for instances) do. + * Whether an operation is writable or not depends on whether the computed {@link Property} + * supports {@link Attribute#setValue(Object)} or {@link FeatureAssociation#setValue(Feature)}. * * @param name the name of the operation to execute. The caller is responsible to ensure that the - * property type for that name is an instance of {@link Operation}. + * property type for that name is an instance of {@link AbstractOperation}. * @param value the value to assign to the result of the named operation. * @throws IllegalStateException if the operation of the given name does not accept assignment. * diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java index db3bdd0056,88594a2fa2..05335b0335 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java @@@ -277,9 -281,8 +272,8 @@@ final class EnvelopeOperation extends A /** * Creates a new attribute for the given feature. */ - Result(final Feature feature) { + Result(final AbstractFeature feature) { - super(resultType); - this.feature = feature; + super(resultType, feature); } /** diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java index 0d0531ee49,7b897b979a..2d9cc34278 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java @@@ -58,12 -63,13 +58,12 @@@ final class ExpressionOperation<V> exte * The expression on which to delegate the execution of this operation. */ @SuppressWarnings("serial") // Not statically typed as serializable. - private final Function<AbstractFeature, ? extends V> expression; - private final Function<? super Feature, ? extends V> expression; ++ private final Function<? super AbstractFeature, ? extends V> expression; /** * The type of result of evaluating the expression. */ - private final DefaultAttributeType<? super V> result; - @SuppressWarnings("serial") // Apache SIS implementations are serializable. - private final AttributeType<V> resultType; ++ private final DefaultAttributeType<V> resultType; /** * The name of all feature properties that are known to be read by the expression. @@@ -78,17 -84,35 +78,35 @@@ * * @param identification the name of the operation, together with optional information. * @param expression the expression to evaluate on feature instances. - * @param result type of values computed by the expression. + * @param resultType type of values computed by the expression. */ - ExpressionOperation(final Map<String,?> identification, - final Function<AbstractFeature, ? extends V> expression, - final DefaultAttributeType<? super V> result) + static <V> AbstractOperation create(final Map<String,?> identification, - final Function<? super Feature, ? extends V> expression, - final AttributeType<? super V> resultType) ++ final Function<? super AbstractFeature, ? extends V> expression, ++ final DefaultAttributeType<? super V> resultType) + { + if (expression instanceof ValueReference<?,?>) { + final String xpath = ((ValueReference<?,?>) expression).getXPath(); + if (xpath.equals(resultType.getName().toString())) { + return new LinkOperation(identification, resultType); + } + } + return new ExpressionOperation<>(identification, expression, resultType); + } + + /** + * Creates a generic operation when no optimized case has been identifier. + */ + private ExpressionOperation(final Map<String,?> identification, - final Function<? super Feature, ? extends V> expression, - final AttributeType<V> resultType) ++ final Function<? super AbstractFeature, ? extends V> expression, ++ final DefaultAttributeType<V> resultType) { super(identification); this.expression = expression; - this.result = result; + this.resultType = resultType; if (expression instanceof Expression<?,?>) { - dependencies = DependencyFinder.search((Expression<AbstractFeature,?>) expression); + @SuppressWarnings("unchecked") - var c = (Expression<Feature,?>) expression; // Cast is okay because we will not pass or request any `Feature` instance. ++ var c = (Expression<AbstractFeature,?>) expression; // Cast is okay because we will not pass or request any `Feature` instance. + dependencies = DependencyFinder.search(c); } else { dependencies = Set.of(); } @@@ -106,8 -130,8 +124,8 @@@ * Returns the expected result type. */ @Override - public IdentifiedType getResult() { + public AbstractIdentifiedType getResult() { - return result; + return resultType; } /** @@@ -128,10 -152,34 +146,34 @@@ * @return the computed property from the given feature. */ @Override - public Property apply(final Feature feature, ParameterValueGroup parameters) { + public Property apply(final AbstractFeature feature, ParameterValueGroup parameters) { - final AbstractAttribute<? super V> instance = result.newInstance(); - instance.setValue(expression.apply(feature)); - return instance; + return new Result(feature); + } + + /** + * The attributes that delegates computation to the expression. + * Value is calculated each time it is accessed. + */ + private final class Result extends OperationResult<V> { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -19004252522001532L; + + /** + * Creates a new attribute for the given feature. + */ - Result(final Feature feature) { ++ Result(final AbstractFeature feature) { + super(resultType, feature); + } + + /** + * Delegates the computation to the user-supplied expression. + */ + @Override + public V getValue() { + return expression.apply(feature); + } } /** diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java index 98b451ddd7,7986a36733..3e79790812 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java @@@ -27,9 -27,15 +27,10 @@@ import org.apache.sis.util.Static import org.apache.sis.util.UnconvertibleObjectException; import org.apache.sis.util.collection.WeakHashSet; import org.apache.sis.util.resources.Errors; + import org.apache.sis.setup.GeometryLibrary; // Branch-dependent imports -import org.opengis.feature.Feature; -import org.opengis.feature.Operation; -import org.opengis.feature.PropertyType; -import org.opengis.feature.AttributeType; -import org.opengis.feature.FeatureAssociationRole; -import org.opengis.filter.Expression; +import org.apache.sis.filter.Expression; /** @@@ -252,12 -250,8 +253,12 @@@ public final class FeatureOperations ex * * <h4>Read/write behavior</h4> * This operation is read-only. Calls to {@code Attribute.setValue(Envelope)} will result in an - * {@link IllegalStateException} to be thrown. + * {@link UnsupportedOperationException} to be thrown. * + * <div class="warning"><b>Warning:</b> + * The type of {@code geometryAttributes} elements will be changed to {@code PropertyType} + * if and when such interface will be defined in GeoAPI.</div> + * * @param identification the name and other information to be given to the operation. * @param crs the Coordinate Reference System in which to express the envelope, or {@code null}. * @param geometryAttributes the operation or attribute type from which to get geometry values. @@@ -272,6 -266,44 +273,44 @@@ return POOL.unique(new EnvelopeOperation(identification, crs, geometryAttributes)); } + /** + * Creates a single geometry from a sequence of points or polylines stored in another property. + * When evaluated, this operation reads a feature property containing a sequence of {@code Point}s or {@code Polyline}s. + * Those geometries shall be instances of the specified geometry library (e.g. JTS or ESRI). + * The merged geometry is usually a {@code Polyline}, + * unless the sequence of source geometries is empty or contains a single element. + * The merged geometry is re-computed every time that the operation is evaluated. + * + * <h4>Examples</h4> + * <p><i>Polylines created from points:</i> + * a boat that record it's position every hour. + * The input is a list of all positions stored in an attribute with [0 … ∞] multiplicity. + * This operation will extract each position and create a line as a new attribute.</p> + * + * <p><i>Polylines created from other polylines:</i> + * a boat that record track every hour. + * The input is a list of all tracks stored in an attribute with [0 … ∞] multiplicity. + * This operation will extract each track and create a polyline as a new attribute.</p> + * + * <h4>Read/write behavior</h4> + * This operation is read-only. Calls to {@code Attribute.setValue(…)} + * will result in an {@link UnsupportedOperationException} to be thrown. + * + * @param identification the name of the operation, together with optional information. + * @param library the library providing the implementations of geometry objects to read and write. + * @param components attribute, association or operation providing the geometries to group as a polyline. + * @return a feature operation which computes its values by merging points or polylines. + * + * @since 1.4 + */ - public static Operation groupAsPolyline(final Map<String,?> identification, final GeometryLibrary library, - final PropertyType components) ++ public static AbstractOperation groupAsPolyline(final Map<String,?> identification, final GeometryLibrary library, ++ final AbstractIdentifiedType components) + { + ArgumentChecks.ensureNonNull("library", library); + ArgumentChecks.ensureNonNull("components", components); + return POOL.unique(GroupAsPolylineOperation.create(identification, library, components)); + } + /** * Creates an operation which delegates the computation to a given expression. * The {@code expression} argument should generally be an instance of @@@ -286,12 -322,13 +329,13 @@@ * * @since 1.4 */ - public static <V> AbstractOperation expression(final Map<String,?> identification, - final Function<AbstractFeature, ? extends V> expression, - final DefaultAttributeType<? super V> result) - public static <V> Operation function(final Map<String,?> identification, - final Function<? super Feature, ? extends V> expression, - final AttributeType<? super V> resultType) ++ public static <V> AbstractOperation function(final Map<String,?> identification, ++ final Function<? super AbstractFeature, ? extends V> expression, ++ final DefaultAttributeType<? super V> resultType) { ArgumentChecks.ensureNonNull("expression", expression); - return POOL.unique(new ExpressionOperation<>(identification, expression, result)); + ArgumentChecks.ensureNonNull("resultType", resultType); + return POOL.unique(ExpressionOperation.create(identification, expression, resultType)); } /** @@@ -310,10 -351,10 +358,10 @@@ * * @since 1.4 */ - public static <V> AbstractOperation expressionToResult(final Map<String,?> identification, - final Expression<AbstractFeature, ?> expression, - final DefaultAttributeType<V> result) - public static <V> Operation expression(final Map<String,?> identification, - final Expression<? super Feature, ?> expression, - final AttributeType<V> resultType) ++ public static <V> AbstractOperation expression(final Map<String,?> identification, ++ final Expression<? super AbstractFeature, ?> expression, ++ final DefaultAttributeType<V> resultType) { - return expression(identification, expression.toValueType(result.getValueClass()), result); + return function(identification, expression.toValueType(resultType.getValueClass()), resultType); } } diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/Features.java index becc02779f,cbf2f98185..a583ba3dd2 --- 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 @@@ -126,14 -138,43 +126,43 @@@ public final class Features extends Sta * * @since 1.1 */ + @SuppressWarnings("unchecked") - public static Optional<AttributeType<?>> toAttribute(IdentifiedType type) { - return toIdentifiedType(type, (Class) AttributeType.class); + public static Optional<DefaultAttributeType<?>> toAttribute(AbstractIdentifiedType type) { - if (!(type instanceof DefaultAttributeType<?>)) { ++ return toIdentifiedType(type, (Class) DefaultAttributeType.class); + } + + /** - * Returns the given type as a {@link FeatureAssociationRole} by casting if possible, or by getting the result type ++ * Returns the given type as a {@code FeatureAssociationRole} by casting if possible, or by getting the result type + * of an operation. More specifically this method returns the first of the following types which apply: + * + * <ul> - * <li>If the given type is an instance of {@link FeatureAssociationRole}, then it is returned as-is.</li> - * <li>If the given type is an instance of {@link Operation} and the {@linkplain Operation#getResult() - * result type} is an {@link FeatureAssociationRole}, then that result type is returned.</li> - * <li>If the given type is an instance of {@link Operation} and the {@linkplain Operation#getResult() ++ * <li>If the given type is an instance of {@code FeatureAssociationRole}, then it is returned as-is.</li> ++ * <li>If the given type is an instance of {@code Operation} and the {@code Operation.getResult() ++ * result type} is an {@code FeatureAssociationRole}, then that result type is returned.</li> ++ * <li>If the given type is an instance of {@code Operation} and the {@code Operation.getResult() + * result type} is another operation, then the above check is performed recursively.</li> + * </ul> + * + * @param type the data type to express as an attribute type. + * @return the association role, or empty if this method cannot find any. + * + * @since 1.4 + */ - public static Optional<FeatureAssociationRole> toAssociation(IdentifiedType type) { - return toIdentifiedType(type, FeatureAssociationRole.class); ++ public static Optional<DefaultAssociationRole> toAssociation(AbstractIdentifiedType type) { ++ return toIdentifiedType(type, DefaultAssociationRole.class); + } + + /** - * Implementation of {@link #toAttribute(IdentifiedType)} and {@link #toAssociation(IdentifiedType)}. ++ * Implementation of {@code toAttribute(IdentifiedType)} and {@code toAssociation(IdentifiedType)}. + */ - private static <T> Optional<T> toIdentifiedType(IdentifiedType type, final Class<T> target) { ++ private static <T> Optional<T> toIdentifiedType(AbstractIdentifiedType type, final Class<T> target) { + if (!target.isInstance(type)) { - if (!(type instanceof Operation)) { + if (!(type instanceof AbstractOperation)) { return Optional.empty(); } - type = ((Operation) type).getResult(); + type = ((AbstractOperation) type).getResult(); - if (!(type instanceof DefaultAttributeType<?>)) { + if (!target.isInstance(type)) { - if (!(type instanceof Operation)) { + if (!(type instanceof AbstractOperation)) { return Optional.empty(); } /* @@@ -141,9 -182,9 +170,9 @@@ * contain a cycle. However, given that the consequence of an infinite cycle here * would be thread freeze, we check as a safety. */ - final Map<IdentifiedType,Boolean> done = new IdentityHashMap<>(4); - while (!target.isInstance(type = ((Operation) type).getResult())) { - if (!(type instanceof Operation) || done.put(type, Boolean.TRUE) != null) { + final Map<AbstractIdentifiedType,Boolean> done = new IdentityHashMap<>(4); - while (!((type = ((AbstractOperation) type).getResult()) instanceof DefaultAttributeType<?>)) { ++ while (!target.isInstance(type = ((AbstractOperation) type).getResult())) { + if (!(type instanceof AbstractOperation) || done.put(type, Boolean.TRUE) != null) { return Optional.empty(); } } diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/GroupAsPolylineOperation.java index 0000000000,59f515554d..d6e8b972fa mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/GroupAsPolylineOperation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/GroupAsPolylineOperation.java @@@ -1,0 -1,250 +1,242 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.sis.feature; + + import java.util.Map; + import java.util.Collection; + import java.util.Iterator; + import java.util.EnumMap; + import org.opengis.parameter.ParameterDescriptorGroup; + import org.opengis.parameter.ParameterValueGroup; + import org.apache.sis.internal.feature.AttributeConvention; + import org.apache.sis.internal.feature.FeatureUtilities; + import org.apache.sis.internal.feature.Geometries; + import org.apache.sis.internal.feature.GeometryWrapper; + import org.apache.sis.internal.feature.Resources; + import org.apache.sis.setup.GeometryLibrary; + -// Branch-dependent imports -import org.opengis.feature.Feature; -import org.opengis.feature.Property; -import org.opengis.feature.PropertyType; -import org.opengis.feature.AttributeType; -import org.opengis.feature.FeatureAssociationRole; -import org.opengis.feature.Operation; - + + /** + * Creates a single (Multi){@code Polyline} instance from a sequence of points or polylines stored in another property. + * This is the implementation of {@link FeatureOperations#groupAsPolyline FeatureOperations.groupAsPolyline(…)}. + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 0.8 + */ + final class GroupAsPolylineOperation extends AbstractOperation { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -1995248173704801739L; + + /** + * The parameter descriptor for the "Group polylines" operation, which does not take any parameter. + */ + private static final ParameterDescriptorGroup EMPTY_PARAMS = FeatureUtilities.parameters("GroupAsPolyline"); + + /** + * Name of the property to follow in order to get the geometries to add to a polyline. + * This property can be an attribute, operation or feature association, + * usually with [0 … ∞] multiplicity. + */ + private final String propertyName; + + /** + * Whether the property giving components is an association to feature instances. + */ + private final boolean isFeatureAssociation; + + /** + * The geometry library. + */ + private final Geometries<?> geometries; + + /** + * The {@link #resultType} for each library, created when first needed. + * Used for sharing the same instance for all operations using the same library. + */ + private static final EnumMap<GeometryLibrary, DefaultAttributeType<?>> TYPES = new EnumMap<>(GeometryLibrary.class); + + /** + * Returns an operation which will group into a single geometry all geometries contained in the specified property. + * + * @param identification the name of the operation, together with optional information. + * @param library the library providing the implementations of geometry objects to read and write. + * @param components attribute, association or operation providing the geometries to group as a polyline. + */ - static Operation create(final Map<String,?> identification, final GeometryLibrary library, PropertyType components) { ++ static AbstractOperation create(final Map<String,?> identification, final GeometryLibrary library, AbstractIdentifiedType components) { + if (components instanceof LinkOperation) { + components = ((LinkOperation) components).result; + } + final boolean isFeatureAssociation; - if (components instanceof AttributeType<?>) { - if (((AttributeType<?>) components).getMaximumOccurs() <= 1) { ++ if (components instanceof DefaultAttributeType<?>) { ++ if (((DefaultAttributeType<?>) components).getMaximumOccurs() <= 1) { + return new LinkOperation(identification, components); + } + isFeatureAssociation = false; + } else { - isFeatureAssociation = (components instanceof FeatureAssociationRole) - && ((FeatureAssociationRole) components).getMaximumOccurs() == 1; ++ isFeatureAssociation = (components instanceof DefaultAssociationRole) ++ && ((DefaultAssociationRole) components).getMaximumOccurs() == 1; + if (!isFeatureAssociation) { + throw new IllegalArgumentException(Resources.format(Resources.Keys.IllegalPropertyType_2, + components.getName(), components.getClass())); + } + } + return new GroupAsPolylineOperation(identification, Geometries.implementation(library), components, isFeatureAssociation); + } + + /** + * Creates an operation which will group into a single polyline all geometries contained in the specified property. + * This constructor shall be invoked only after the {@code source} is known to contain collection, i.e. the maximum + * number of occurrences of attribute values or feature instances is greater than 1. + */ + private GroupAsPolylineOperation(final Map<String,?> identification, final Geometries<?> geometries, - final PropertyType components, final boolean isFeatureAssociation) ++ final AbstractIdentifiedType components, final boolean isFeatureAssociation) + { + super(identification); + this.geometries = geometries; + this.propertyName = components.getName().toString(); + this.isFeatureAssociation = isFeatureAssociation; + } + + /** + * Returns an empty parameter descriptor group. + */ + @Override + public ParameterDescriptorGroup getParameters() { + return EMPTY_PARAMS; + } + + /** + * Returns the expected result type. + */ + @Override - public final AttributeType<?> getResult() { ++ public final DefaultAttributeType<?> getResult() { + synchronized (TYPES) { + return TYPES.computeIfAbsent(geometries.library, (library) -> { + var name = Map.of(AbstractIdentifiedType.NAME_KEY, AttributeConvention.ENVELOPE_PROPERTY); + return new DefaultAttributeType<>(name, geometries.polylineClass, 1, 1, null); + }); + } + } + + /** + * Executes the operation on the specified feature. + */ + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) - public final Property apply(Feature feature, ParameterValueGroup parameters) { ++ public final Property apply(AbstractFeature feature, ParameterValueGroup parameters) { + return new Result<>(getResult(), feature); + } + + + /** + * The attribute resulting from execution of the {@link GroupAsPolylineOperation}. + * The value is computed when first requested, then cached for this {@code Result} instance only. - * Note that the cache is not used when {@link #apply(Feature, ParameterValueGroup)} is invoked, ++ * Note that the cache is not used when {@code apply(Feature, ParameterValueGroup)} is invoked, + * causing a new value to be computed again. The intent is to behave as if the operation has been + * executed at {@code apply(…)} invocation time, even if we deferred the actual execution. + * + * @param <G> the root geometry class (implementation-dependent). + */ + private final class Result<G> extends OperationResult<G> { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 5558751012506417903L; + + /** + * The result, computed when first needed. + */ + private transient G geometry; + + /** + * Creates a new result for an execution on the given feature. + * The actual computation is deferred to the first call of {@link #getValue()}. + */ - Result(final AttributeType<G> resultType, final Feature feature) { ++ Result(final DefaultAttributeType<G> resultType, final AbstractFeature feature) { + super(resultType, feature); + } + + /** + * Computes the geometry from all points or polylines found in the associated feature. + * + * @throws ClassCastException if a feature, a property value or a geometry is not of the expected class. + */ + @Override + public G getValue() { + if (geometry == null) { + geometry = compute(); + } + return geometry; + } + + /** + * Computes the geometry when first needed. + */ + private G compute() { + /* + * Cast to `Collection` should be safe if the constructor + * ensured that `Features.getMaximumOccurs(property) > 1`. + */ + Iterator<?> paths = ((Collection<?>) feature.getPropertyValue(propertyName)).iterator(); + if (isFeatureAssociation) { + final Iterator<?> it = paths; + paths = new Iterator<Object>() { + @Override public boolean hasNext() { + return it.hasNext(); + } + + @Override public Object next() { - return ((Feature) it.next()).getPropertyValue(AttributeConvention.GEOMETRY); ++ return ((AbstractFeature) it.next()).getPropertyValue(AttributeConvention.GEOMETRY); + } + }; + } + while (paths.hasNext()) { + GeometryWrapper<?> first = geometries.castOrWrap(paths.next()); + if (first != null) { + final Object geom = first.mergePolylines(paths); + return getType().getValueClass().cast(geom); + } + } + return null; + } + } + + /** + * Computes a hash-code value for this operation. + */ + @Override + public int hashCode() { + return super.hashCode() + propertyName.hashCode() + geometries.hashCode(); + } + + /** + * Compares this operation with the given object for equality. + */ + @Override + public boolean equals(final Object obj) { + if (super.equals(obj)) { + final GroupAsPolylineOperation that = (GroupAsPolylineOperation) obj; + return propertyName.equals(that.propertyName) && + geometries.equals(that.geometries); + } + return false; + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java index fde190b24d,d195ebe596..90d6ac766c --- a/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java @@@ -48,7 -54,8 +48,7 @@@ final class LinkOperation extends Abstr /** * The type of the result. */ - private final AbstractIdentifiedType result; - @SuppressWarnings("serial") // Most SIS implementations are serializable. - final PropertyType result; ++ final AbstractIdentifiedType result; /** * The name of the referenced attribute or feature association. diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java index 0000000000,b05f884c54..130ffe453d mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java @@@ -1,0 -1,71 +1,66 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.sis.feature; + + import org.apache.sis.util.resources.Errors; + -// Branch-dependent imports -import org.opengis.feature.Feature; -import org.opengis.feature.Attribute; -import org.opengis.feature.AttributeType; - + + /** + * Base class of attributes that are the result of a feature operation. + * This base class is defined for making easier to identify where computations are done. + * + * @todo A future version may provide caching services, methods for taking a snapshot, <i>etc.</i> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * + * @param <V> the type of attribute values. + * + * @since 1.4 + */ + abstract class OperationResult<V> extends AbstractAttribute<V> { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 1418917854672134381L; + + /** + * The feature instance to use as a source for computing the result. + */ + @SuppressWarnings("serial") // Most SIS implementations are serializable. - protected final Feature feature; ++ protected final AbstractFeature feature; + + /** + * Creates a new operation for a result of the given type. + * + * @param type information about the attribute (base Java class, domain of values, <i>etc.</i>). + */ - protected OperationResult(final AttributeType<V> type, final Feature feature) { ++ protected OperationResult(final DefaultAttributeType<V> type, final AbstractFeature feature) { + super(type); + this.feature = feature; + } + + /** + * Retro-propagate an operation result to the properties in the source feature instance. + * This is an optional operation. + * The default implementation unconditionally throws an {@link UnsupportedOperationException}. + */ + @Override + public void setValue(V value) { - throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, Attribute.class)); ++ throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, AbstractAttribute.class)); + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java index 09b83ae978,3bc72ab684..8c9ce69271 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java @@@ -318,9 -326,8 +313,8 @@@ final class StringJoinOperation extend /** * Creates a new attribute for the given feature. */ - Result(final Feature feature) { + Result(final AbstractFeature feature) { - super(resultType); - this.feature = feature; + super(resultType, feature); } /** diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java index a3f20b3055,49cdb7f41e..f72ddebc54 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java @@@ -25,15 -25,16 +25,16 @@@ import java.util.StringJoiner import org.apache.sis.feature.Features; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.builder.PropertyTypeBuilder; + import org.apache.sis.math.FunctionProperty; // Branch-dependent imports -import org.opengis.feature.Feature; -import org.opengis.feature.FeatureAssociationRole; -import org.opengis.feature.FeatureType; -import org.opengis.feature.PropertyType; -import org.opengis.feature.PropertyNotFoundException; -import org.opengis.filter.Expression; -import org.opengis.filter.ValueReference; +import org.opengis.util.ScopedName; +import org.apache.sis.feature.AbstractFeature; +import org.apache.sis.feature.AbstractIdentifiedType; +import org.apache.sis.feature.DefaultAssociationRole; +import org.apache.sis.feature.DefaultFeatureType; +import org.apache.sis.internal.geoapi.filter.Name; +import org.apache.sis.internal.geoapi.filter.ValueReference; /** @@@ -103,10 -99,21 +104,21 @@@ final class AssociationValue<V> extend * Returns the class of resources expected by this expression. */ @Override - public final Class<Feature> getResourceClass() { - return Feature.class; + public final Class<AbstractFeature> getResourceClass() { + return AbstractFeature.class; } + /** + * Returns the manner in which values are computed from given resources. + * This method assumes an initially empty set of properties, then adds the transitive properties. + * This method does not inherit directly the properties of the {@linkplain #accessor} because it + * does not operate on the same resource, so the non-transitive function properties may not hold. + */ + @Override + public Set<FunctionProperty> properties() { + return transitiveProperties(accessor.getParameters()); + } + /** * For {@link #toString()} implementation. */ diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java index aac4113826,c7801d8f02..4a0f212dc0 --- 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 @@@ -29,10 -30,12 +30,11 @@@ import org.apache.sis.internal.feature. import org.apache.sis.internal.filter.Node; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.builder.PropertyTypeBuilder; + import org.apache.sis.math.FunctionProperty; // Branch-dependent imports -import org.opengis.feature.FeatureType; -import org.opengis.feature.AttributeType; -import org.opengis.filter.Expression; +import org.apache.sis.feature.DefaultFeatureType; +import org.apache.sis.feature.DefaultAttributeType; /** @@@ -79,7 -92,11 +91,11 @@@ abstract class LeafExpression<R,V> exte * @param <R> the type of resources used as inputs. * @param <V> the type of value computed by the expression. */ - static class Literal<R,V> extends LeafExpression<R,V> implements org.opengis.filter.Literal<R,V> { + static class Literal<R,V> extends LeafExpression<R,V> implements org.apache.sis.internal.geoapi.filter.Literal<R,V> { + /** The properties of this function, which returns constants. */ + private static final Set<FunctionProperty> CONSTANT = + Set.of(FunctionProperty.ORDER_PRESERVING, FunctionProperty.ORDER_REVERSING); + /** For cross-version compatibility. */ private static final long serialVersionUID = -8383113218490957822L; diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java index bc345fff42,34acd54f98..677c6e1559 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java @@@ -16,16 -16,20 +16,21 @@@ */ package org.apache.sis.internal.feature; - import org.apache.sis.filter.Expression; + import java.util.Set; -import org.opengis.feature.FeatureType; -import org.opengis.feature.AttributeType; -import org.opengis.filter.Literal; -import org.opengis.filter.Expression; -import org.opengis.filter.ValueReference; + import org.apache.sis.math.FunctionProperty; import org.apache.sis.filter.Optimization; import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.builder.PropertyTypeBuilder; import org.apache.sis.feature.builder.AttributeTypeBuilder; +import org.apache.sis.feature.DefaultFeatureType; +import org.apache.sis.internal.geoapi.filter.Literal; +import org.apache.sis.internal.geoapi.filter.ValueReference; + import org.apache.sis.internal.filter.Node; + ++// Branch-dependent imports ++import org.apache.sis.filter.Expression; + /** * OGC expressions or other functions operating on feature instances. @@@ -36,9 -40,9 +41,9 @@@ * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.4 * - * @param <R> the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs. + * @param <R> the type of resources (e.g. {@code Feature}) used as inputs. * @param <V> the type of values computed by the expression. * * @since 1.0 diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/Node.java index 5e615957e1,0dedb78d65..8c2ccd3bdc --- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/Node.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/Node.java @@@ -217,9 -222,93 +220,93 @@@ public abstract class Node implements S if (expression instanceof GeometryConverter<?,?>) { return ((GeometryConverter<?,G>) expression).library; } - throw new InvalidFilterValueException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression)); + throw new IllegalArgumentException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression)); } + /** + * The set of all properties that may be present in {@link FeatureExpression#properties()} + * when the only available information is the list of parameters. When we do not know how + * an expression is using the parameters, the function properties should be the empty set. + * When combining a function properties with the properties inherited from the parameters, + * the only properties that can be added to an initially empty set are the properties that + * are {@linkplain FunctionProperty#concatenate(Set, Set) concatenated} with the logical + * {@code OR} operation. In the current {@link FunctionProperty} enumeration, the only + * property handled that way is {@code VOLATILE}. + */ + private static final Set<FunctionProperty> TRANSITIVE_PROPERTIES = Set.of(FunctionProperty.VOLATILE); + + /** + * Whether the given set of properties contains the {@link #TRANSITIVE_PROPERTIES} singleton value. + * If a future version recognizes more properties, the return type will no longer be a boolean. + */ + private static boolean isVolatile(final Set<FunctionProperty> properties) { + return properties.contains(FunctionProperty.VOLATILE); + } + + /** + * Whether the combination of the function properties of all given expression is {@link #TRANSITIVE_PROPERTIES}. + * This method assumes that {@code TRANSITIVE_PROPERTIES} is a singleton and that the property can be combined + * by a logical {@code OR} operation. + * + * @param operands the expressions from which to get the function properties. + * @return whether is present the single function property that may appear. + */ + private static <R> boolean isVolatile(final Iterable<Expression<R,?>> operands) { + for (final Expression<R,?> operand : operands) { + if (operand instanceof FeatureExpression<?,?>) { + if (isVolatile(((FeatureExpression<?,?>) operand).properties())) { + return true; // Short-circuit for `OR` logical operation. + } + } else { + if (isVolatile(operand.getParameters())) { + return true; // Short-circuit for `OR` logical operation. + } + } + } + return false; + } + + /** + * Returns the manner in which values are computed from resources. + * This method delegates to {@link FeatureExpression#properties()} if possible. + * Otherwise this method assumes that the intrinsic properties of the given expression are unknown, + * and inherits from the parameters only the properties that can be added to an initially empty set. + * + * @param function the expression for which to query function properties, or {@code null}. + * @return the manners in which values are computed from resources. + */ + public static Set<FunctionProperty> properties(final Expression<?,?> function) { + if (function instanceof FeatureExpression<?,?>) { + return ((FeatureExpression<?,?>) function).properties(); + } else if (function != null) { + return transitiveProperties(function.getParameters()); + } else { + return Set.of(); + } + } + + /** + * Returns the manner in which values are computed from resources in an expression having the given operands. + * This method assumes that the intrinsic properties of the parent expression or parent filter are unknown, + * and inherits from the operands only the properties that can be added to an initially empty set. + * + * <p>Note that {@code transitiveProperties(function.getParameters())} is <strong>not</strong> equivalent to + * {@code properties(function)}. It is rather equivalent to the following code, where the parent expression + * is not the final step of a chain of operations, and the next step has no known properties:</p> + * + * {@snippet lang="java" : + * FunctionProperty.concatenate(transitiveProperties(operands), Set.of()); + * } + * + * @param <R> the type of resources. + * @param operands the operands from which to inherit function properties. + * @return the manners in which values are computed from resources. + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") // Because immutable. + public static <R> Set<FunctionProperty> transitiveProperties(final Iterable<Expression<R,?>> operands) { + return isVolatile(operands) ? TRANSITIVE_PROPERTIES : Set.of(); + } + /** * Returns the children of this node, or an empty collection if none. This is used * for information purpose, for example in order to build a string representation. diff --cc core/sis-feature/src/test/java/org/apache/sis/feature/GroupAsPolylineOperationTest.java index 0000000000,e3285521d3..62255e450a mode 000000,100644..100644 --- a/core/sis-feature/src/test/java/org/apache/sis/feature/GroupAsPolylineOperationTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/feature/GroupAsPolylineOperationTest.java @@@ -1,0 -1,71 +1,65 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.sis.feature; + + import java.util.Map; + import java.util.Arrays; + import com.esri.core.geometry.Point; + import com.esri.core.geometry.Polyline; + import org.apache.sis.feature.builder.FeatureTypeBuilder; + import org.apache.sis.setup.GeometryLibrary; + + // Test dependencies + import org.apache.sis.test.TestCase; + import org.junit.Test; + + import static org.junit.Assert.*; + -// Branch-dependent imports -import org.opengis.feature.Attribute; -import org.opengis.feature.Feature; -import org.opengis.feature.Operation; -import org.opengis.feature.Property; - + + /** + * Tests {@link GroupAsPolylineOperation}. + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * @since 0.4 + */ + public final class GroupAsPolylineOperationTest extends TestCase { + /** + * Tests a feature with a sequence of points. + */ + @Test + public void testPoints() { + final FeatureTypeBuilder builder = new FeatureTypeBuilder().setName("test"); + builder.addAttribute(Point.class).setMaximumOccurs(10).setName("points"); - final Feature feature = builder.build().newInstance(); ++ final AbstractFeature feature = builder.build().newInstance(); + feature.setPropertyValue("points", Arrays.asList( + new Point(-6, 4), + new Point(12, 7), + new Point( 8, 6))); + - final Operation group = FeatureOperations.groupAsPolyline(Map.of("name", "polyline"), ++ final AbstractOperation group = FeatureOperations.groupAsPolyline(Map.of("name", "polyline"), + GeometryLibrary.ESRI, feature.getType().getProperty("points")); + - final Property result = group.apply(feature, null); - final Object value = ((Attribute<?>) result).getValue(); ++ final var result = group.apply(feature, null); ++ final Object value = ((AbstractAttribute<?>) result).getValue(); + final Polyline poly = (Polyline) value; + assertEquals(-6, poly.getPoint(0).getX(), STRICT); + assertEquals( 7, poly.getPoint(1).getY(), STRICT); + assertEquals( 8, poly.getPoint(2).getX(), STRICT); + } + } diff --cc core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java index 6cadd74277,8555b04741..a28a3dec78 --- a/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java @@@ -97,6 -103,21 +99,21 @@@ public final class LogicalFilterTest ex assertInstanceOf("Predicate.negate()", LogicalFilter.Not.class, operand.negate()); } + /** + * Tests a filter having a volatile expression. + */ + @Test + public void testVolatile() { - final var literal = new LeafExpression.Literal<Feature,Object>("test") { ++ final var literal = new LeafExpression.Literal<AbstractFeature,Object>("test") { + @Override public Set<FunctionProperty> properties() { + return Set.of(FunctionProperty.VOLATILE); + } + }; - final Filter<Feature> operand = factory.isNull(literal); - final LogicalOperator<Feature> filter = factory.not(operand); ++ final Filter<AbstractFeature> operand = factory.isNull(literal); ++ final Filter<AbstractFeature> filter = factory.not(operand); + assertTrue(isVolatile(filter)); + } + /** * Implementation of {@link #testAnd()} and {@link #testOr()}. * diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java index a95ce19519,1d34f86d1d..c4b1749677 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java @@@ -112,7 -118,7 +114,7 @@@ public class FeatureQuery extends Quer * @see #setSelection(Filter) */ @SuppressWarnings("serial") // Most SIS implementations are serializable. - private Filter<AbstractFeature> selection; - private Filter<? super Feature> selection; ++ private Filter<? super AbstractFeature> selection; /** * The number of feature instances to skip from the beginning. @@@ -193,12 -199,12 +195,12 @@@ * @throws IllegalArgumentException if a property is duplicated. */ @SafeVarargs - public final void setProjection(final Expression<AbstractFeature, ?>... properties) { - public final void setProjection(final Expression<? super Feature, ?>... properties) { ++ public final void setProjection(final Expression<? super AbstractFeature, ?>... properties) { NamedExpression[] wrappers = null; if (properties != null) { wrappers = new NamedExpression[properties.length]; for (int i=0; i<wrappers.length; i++) { - final Expression<AbstractFeature, ?> e = properties[i]; - final Expression<? super Feature, ?> e = properties[i]; ++ final Expression<? super AbstractFeature, ?> e = properties[i]; ArgumentChecks.ensureNonNullElement("properties", i, e); wrappers[i] = new NamedExpression(e); } @@@ -274,7 -299,7 +295,7 @@@ * * @param selection the filter, or {@code null} if none. */ - public void setSelection(final Filter<AbstractFeature> selection) { - public void setSelection(final Filter<? super Feature> selection) { ++ public void setSelection(final Filter<? super AbstractFeature> selection) { this.selection = selection; } @@@ -285,7 -310,7 +306,7 @@@ * * @return the filter, or {@code null} if none. */ - public Filter<AbstractFeature> getSelection() { - public Filter<? super Feature> getSelection() { ++ public Filter<? super AbstractFeature> getSelection() { return selection; } @@@ -485,7 -533,7 +535,7 @@@ * Never {@code null}. */ @SuppressWarnings("serial") - public final Expression<AbstractFeature,?> expression; - public final Expression<? super Feature, ?> expression; ++ public final Expression<? super AbstractFeature, ?> expression; /** * The name to assign to the expression result, or {@code null} if unspecified. @@@ -508,7 -556,7 +558,7 @@@ * * @param expression the literal, value reference or expression to be retrieved by a {@code Query}. */ - public NamedExpression(final Expression<AbstractFeature,?> expression) { - public NamedExpression(final Expression<? super Feature, ?> expression) { ++ public NamedExpression(final Expression<? super AbstractFeature, ?> expression) { this(expression, (GenericName) null); } @@@ -518,7 -566,7 +568,7 @@@ * @param expression the literal, value reference or expression to be retrieved by a {@code Query}. * @param alias the name to assign to the expression result, or {@code null} if unspecified. */ - public NamedExpression(final Expression<AbstractFeature,?> expression, final GenericName alias) { - public NamedExpression(final Expression<? super Feature,?> expression, final GenericName alias) { ++ public NamedExpression(final Expression<? super AbstractFeature,?> expression, final GenericName alias) { this(expression, alias, ProjectionType.STORED); } @@@ -529,7 -577,7 +579,7 @@@ * @param expression the literal, value reference or expression to be retrieved by a {@code Query}. * @param alias the name to assign to the expression result, or {@code null} if unspecified. */ - public NamedExpression(final Expression<AbstractFeature,?> expression, final String alias) { - public NamedExpression(final Expression<? super Feature,?> expression, final String alias) { ++ public NamedExpression(final Expression<? super AbstractFeature,?> expression, final String alias) { ArgumentChecks.ensureNonNull("expression", expression); this.expression = expression; this.alias = (alias != null) ? Names.createLocalName(null, null, alias) : null; @@@ -545,7 -593,7 +595,7 @@@ * * @since 1.4 */ - public NamedExpression(final Expression<AbstractFeature,?> expression, final GenericName alias, ProjectionType type) { - public NamedExpression(final Expression<? super Feature,?> expression, final GenericName alias, ProjectionType type) { ++ public NamedExpression(final Expression<? super AbstractFeature,?> expression, final GenericName alias, ProjectionType type) { ArgumentChecks.ensureNonNull("expression", expression); ArgumentChecks.ensureNonNull("type", type); this.expression = expression; @@@ -689,7 -739,7 +741,7 @@@ * For each property, get the expected type (mandatory) and its name (optional). * A default name will be computed if no alias were explicitly given by user. */ - final Expression<AbstractFeature,?> expression = item.expression; - final Expression<? super Feature,?> expression = item.expression; ++ final Expression<? super AbstractFeature,?> expression = item.expression; final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(expression); final PropertyTypeBuilder resultType; if (fex == null || (resultType = fex.expectedType(valueType, ftb)) == null) { @@@ -739,18 -789,20 +791,20 @@@ } while (!names.add(text.toString())); name = Names.createLocalName(null, null, text); } - resultType.setName(name); /* - * If the attribute that we just added should be virtual, - * replace the attribute by an operation. + * If the attribute that we just added should be virtual, replace the attribute by an operation. + * We need to keep the property name computed by `fex.expectedType(…)` for the operation result, + * because that name is the name of the link to create if the operation is `ValueReference`. */ - if (item.type == ProjectionType.VIRTUAL && resultType instanceof AttributeTypeBuilder<?>) { + if (item.type == ProjectionType.COMPUTING && resultType instanceof AttributeTypeBuilder<?>) { final var ab = (AttributeTypeBuilder<?>) resultType; - final AttributeType<?> storedType = ab.build(); + final DefaultAttributeType<?> storedType = ab.build(); if (ftb.properties().remove(resultType)) { final var properties = Map.of(AbstractOperation.NAME_KEY, name); - ftb.addProperty(FeatureOperations.expressionToResult(properties, expression, storedType)); + ftb.addProperty(FeatureOperations.expression(properties, expression, storedType)); } + } else { + resultType.setName(name); } } return ftb.build(); diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java index 5a1b87e95d,5b2e67c900..825fecfaff --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java @@@ -110,7 -110,7 +110,7 @@@ final class FeatureSubset extends Abstr /* * Apply filter. */ - final Filter<AbstractFeature> selection = query.getSelection(); - final Filter<? super Feature> selection = query.getSelection(); ++ final Filter<? super AbstractFeature> selection = query.getSelection(); if (selection != null && !selection.equals(Filter.include())) { stream = stream.filter(selection); } @@@ -139,10 -139,10 +139,10 @@@ * Transform feature instances. * Note: "projection" here is in relational database sense, not map projection. */ - final FeatureQuery.NamedExpression[] projection = query.getProjection(); + final FeatureQuery.NamedExpression[] projection = query.getStoredProjection(); if (projection != null) { @SuppressWarnings({"unchecked", "rawtypes"}) - final Expression<AbstractFeature,?>[] expressions = new Expression[projection.length]; - final Expression<? super Feature,?>[] expressions = new Expression[projection.length]; ++ final Expression<? super AbstractFeature,?>[] expressions = new Expression[projection.length]; for (int i=0; i<expressions.length; i++) { expressions[i] = projection[i].expression; } diff --cc storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java index 04073e0a50,32ce1bd5fd..6fbf0b8c80 --- a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java @@@ -308,8 -328,8 +309,8 @@@ public final class FeatureQueryTest ext /** * Shortcut for creating expression for a projection computed on-the-fly. */ - private static FeatureQuery.NamedExpression virtualProjection(final Expression<Feature, ?> expression, final String alias) { + private static FeatureQuery.NamedExpression virtualProjection(final Expression<AbstractFeature, ?> expression, final String alias) { - return new FeatureQuery.NamedExpression(expression, Names.createLocalName(null, null, alias), FeatureQuery.ProjectionType.VIRTUAL); + return new FeatureQuery.NamedExpression(expression, Names.createLocalName(null, null, alias), FeatureQuery.ProjectionType.COMPUTING); } /** diff --cc storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Types.java index 195e9bbace,c552af357d..c8e39408af --- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Types.java +++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Types.java @@@ -47,8 -47,8 +47,8 @@@ import org.apache.sis.util.ResourceInte import org.apache.sis.util.iso.DefaultNameFactory; // Branch-dependent imports -import org.opengis.feature.FeatureType; -import org.opengis.feature.Operation; ++import org.apache.sis.feature.AbstractOperation; +import org.apache.sis.feature.DefaultFeatureType; - import org.apache.sis.feature.DefaultAttributeType; /** @@@ -221,8 -221,7 +221,7 @@@ final class Types * │ rtept │ WayPoint │ gpx:wptType │ [0 … ∞] │ * └────────────────┴────────────────┴───────────────────────┴──────────────┘ */ - final DefaultAttributeType<?> groupResult = GroupAsPolylineOperation.getResult(geometries); - GroupAsPolylineOperation groupOp = new GroupAsPolylineOperation(geomInfo, Tags.ROUTE_POINTS, groupResult); - Operation groupOp = groupAsPolyline(geomInfo, Tags.ROUTE_POINTS, wayPoint); ++ AbstractOperation groupOp = groupAsPolyline(geomInfo, Tags.ROUTE_POINTS, wayPoint); builder.clear().setSuperTypes(parent).setNameSpace(Tags.PREFIX).setName("Route"); builder.addProperty(groupOp); builder.addProperty(FeatureOperations.envelope(envpInfo, null, groupOp)); @@@ -317,4 -316,16 +316,16 @@@ } return builder.build(); } + + /** + * Creates a new operation which will group the geometries in the given property into a single polyline. + * + * @param geomInfo the name of the operation, together with optional information. + * @param components name of the property providing the geometries to group as a polyline. + * @param type type of the property identified by {@code components}. + */ - private Operation groupAsPolyline(final Map<String,?> geomInfo, final String components, final FeatureType type) { ++ private AbstractOperation groupAsPolyline(final Map<String,?> geomInfo, final String components, final DefaultFeatureType type) { + var c = new DefaultAssociationRole(Map.of(DefaultAssociationRole.NAME_KEY, components), type, 1, 1); + return FeatureOperations.groupAsPolyline(geomInfo, geometries.library, c); + } }