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);
+     }
  }

Reply via email to