This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 31126879d2c75a84d68146c449b9673eb4608f10 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed May 3 19:18:09 2023 +0200 Retrofit `GroupAsPolylineOperation` together with all other feature operations. It required a generalization for working on attributes as well as associations. --- .../apache/sis/feature/DefaultAssociationRole.java | 8 +- .../org/apache/sis/feature/FeatureOperations.java | 51 ++++- .../main/java/org/apache/sis/feature/Features.java | 39 +++- .../sis/feature/GroupAsPolylineOperation.java | 248 +++++++++++++++++++++ .../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 +- .../sis/internal/feature/GeometriesTestCase.java | 2 +- .../storage/gpx/GroupAsPolylineOperation.java | 211 ------------------ .../org/apache/sis/internal/storage/gpx/Types.java | 31 ++- 13 files changed, 365 insertions(+), 274 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java index b7f0bbbbc3..690fbee171 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java @@ -153,12 +153,12 @@ public class DefaultAssociationRole extends FieldType implements FeatureAssociat * String namespace = "My model"; * GenericName nameOfA = Names.createTypeName(namespace, ":", "Feature type A"); * GenericName nameOfB = Names.createTypeName(namespace, ":", "Feature type B"); - * FeatureType typeA = new DefaultFeatureType(nameOfA, false, null, - * new DefaultAssociationRole(Names.createLocalName("Association to B"), nameOfB), + * FeatureType typeA = new DefaultFeatureType(Map.of(NAME_KEY, nameOfA), false, null, + * new DefaultAssociationRole(Map.of(NAME_KEY, "Association to B"), nameOfB, 1, 1), * // More properties if desired. * ); - * FeatureType typeB = new DefaultFeatureType(nameOfB, false, null, - * new DefaultAssociationRole(Names.createLocalName("Association to A"), featureA), + * FeatureType typeB = new DefaultFeatureType(Map.of(NAME_KEY, nameOfB), false, null, + * new DefaultAssociationRole(Map.of(NAME_KEY, "Association to A"), featureA, 1, 1), * // More properties if desired. * ); * } diff --git 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 index db15ddcb83..7986a36733 100644 --- 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,6 +27,7 @@ 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; @@ -249,7 +250,7 @@ public final class FeatureOperations extends Static { * * <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. * * @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}. @@ -265,12 +266,54 @@ public final class FeatureOperations extends Static { 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) + { + 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 * {@link org.opengis.filter.Expression}, * but more generic functions are accepted as well. * + * <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 <V> the type of values computed by the expression and assigned to the feature property. * @param identification the name of the operation, together with optional information. * @param expression the expression to evaluate on feature instances. @@ -284,7 +327,7 @@ public final class FeatureOperations extends Static { final AttributeType<? super V> resultType) { ArgumentChecks.ensureNonNull("expression", expression); - ArgumentChecks.ensureNonNull("result", resultType); + ArgumentChecks.ensureNonNull("resultType", resultType); return POOL.unique(ExpressionOperation.create(identification, expression, resultType)); } @@ -295,6 +338,10 @@ public final class FeatureOperations extends Static { * This method casts or converts the expression to the expected type by a call to * {@link Expression#toValueType(Class)}. * + * <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 <V> the type of values computed by the expression and assigned to the feature property. * @param identification the name of the operation, together with optional information. * @param expression the expression to evaluate on feature instances. diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java b/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java index 4d6eca4a0c..cbf2f98185 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java @@ -49,7 +49,7 @@ import org.opengis.feature.PropertyType; * @author Martin Desruisseaux (Geomatys) * @author Johann Sorel (Geomatys) * @author Alexis Manin (Geomatys) - * @version 1.1 + * @version 1.4 * @since 0.5 */ public final class Features extends Static { @@ -138,13 +138,42 @@ public final class Features extends Static { * * @since 1.1 */ + @SuppressWarnings("unchecked") public static Optional<AttributeType<?>> toAttribute(IdentifiedType type) { - if (!(type instanceof AttributeType<?>)) { + return toIdentifiedType(type, (Class) AttributeType.class); + } + + /** + * Returns the given type as a {@link 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() + * 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); + } + + /** + * Implementation of {@link #toAttribute(IdentifiedType)} and {@link #toAssociation(IdentifiedType)}. + */ + private static <T> Optional<T> toIdentifiedType(IdentifiedType type, final Class<T> target) { + if (!target.isInstance(type)) { if (!(type instanceof Operation)) { return Optional.empty(); } type = ((Operation) type).getResult(); - if (!(type instanceof AttributeType<?>)) { + if (!target.isInstance(type)) { if (!(type instanceof Operation)) { return Optional.empty(); } @@ -154,14 +183,14 @@ public final class Features extends Static { * would be thread freeze, we check as a safety. */ final Map<IdentifiedType,Boolean> done = new IdentityHashMap<>(4); - while (!((type = ((Operation) type).getResult()) instanceof AttributeType<?>)) { + while (!target.isInstance(type = ((Operation) type).getResult())) { if (!(type instanceof Operation) || done.put(type, Boolean.TRUE) != null) { return Optional.empty(); } } } } - return Optional.of((AttributeType<?>) type); + return Optional.of(target.cast(type)); } /** diff --git 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 new file mode 100644 index 0000000000..ee206a37d1 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/GroupAsPolylineOperation.java @@ -0,0 +1,248 @@ +/* + * 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) { + FeatureAssociationRole association = Features.toAssociation(components).orElse(null); + if (association != null && association.getMaximumOccurs() == 1) { + components = association; + } else { + association = null; + AttributeType<?> attribute = Features.toAttribute(components).orElse(null); + if (attribute == null) { + throw new IllegalArgumentException(Resources.format(Resources.Keys.IllegalPropertyType_2, + components.getName(), components.getClass())); + } + if (attribute.getMaximumOccurs() <= 1) { + return new LinkOperation(identification, components); + } + components = attribute; + } + return new GroupAsPolylineOperation(identification, Geometries.implementation(library), components, association != null); + } + + /** + * 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) + { + 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() { + 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) { + 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, + * 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) { + 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); + } + }; + } + 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 --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java index 8fe1787c4a..ffd04dfef6 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java @@ -19,7 +19,6 @@ package org.apache.sis.internal.feature; import java.io.Serializable; import java.nio.ByteBuffer; import java.util.Optional; -import java.util.Iterator; import java.util.logging.Logger; import org.opengis.geometry.Envelope; import org.opengis.geometry.DirectPosition; @@ -37,7 +36,6 @@ import org.apache.sis.internal.system.Loggers; import org.apache.sis.math.Vector; import org.apache.sis.setup.GeometryLibrary; import org.apache.sis.util.resources.Errors; -import org.apache.sis.util.Classes; /** @@ -566,37 +564,6 @@ public abstract class Geometries<G> implements Serializable { return result; } - /** - * Merges a sequence of points or polylines into a single polyline instances. - * Each previous polyline will be a separated path in the new polyline instances. - * The implementation returned by this method is an instance of {@link #rootClass}. - * - * <p>Contrarily to other methods in this class, this method does <strong>not</strong> unwrap - * the geometries contained in {@link GeometryWrapper}. It is caller responsibility to do so - * if needed.</p> - * - * @param paths the points or polylines to merge in a single polyline object. - * @return the merged polyline, or {@code null} if the given iterator has no element. - * @throws ClassCastException if collection elements are not instances of a supported library, - * or not all elements are instances of the same library. - */ - public static Object mergePolylines(final Iterator<?> paths) { - while (paths.hasNext()) { - final Object first = paths.next(); - if (first != null) { - final Optional<GeometryWrapper<?>> w = wrap(first); - if (w.isPresent()) return w.get().mergePolylines(paths); - /* - * Use the same exception type than `mergePolylines(…)` implementations. - * Also the same type than exception occurring elsewhere in the code of - * the caller (GroupAsPolylineOperation). - */ - throw new ClassCastException(Errors.format(Errors.Keys.UnsupportedType_1, Classes.getClass(first))); - } - } - return null; - } - /** * Creates a wrapper for the given geometry instance. * The given object shall be an instance of {@link #rootClass}. diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryWrapper.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryWrapper.java index 9644e35484..096e0d1314 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryWrapper.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryWrapper.java @@ -57,7 +57,7 @@ import org.opengis.filter.InvalidFilterValueException; * change without warning in future Apache SIS version. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.4 * * @param <G> root class of geometry instances of the underlying library (i.e. {@link Geometries#rootClass}). * This is not necessarily the class of the wrapped geometry returned by {@link #implementation()}. @@ -185,10 +185,10 @@ public abstract class GeometryWrapper<G> extends AbstractGeometry implements Geo * (it is caller responsibility to unwrap if needed).</p> * * @param paths the points or polylines to merge in a single polyline instance. - * @return the merged polyline (may be the wrapper geometry but never {@code null}). + * @return the merged polyline (may be the underlying geometry of {@code this} but never {@code null}). * @throws ClassCastException if collection elements are not instances of the point or geometry class. */ - protected abstract G mergePolylines(final Iterator<?> paths); + public abstract G mergePolylines(final Iterator<?> paths); /** * Applies a filter predicate between this geometry and another geometry. diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/esri/Wrapper.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/esri/Wrapper.java index c4c5d63885..68799cd376 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/esri/Wrapper.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/esri/Wrapper.java @@ -158,7 +158,7 @@ final class Wrapper extends GeometryWithCRS<Geometry> { * @throws ClassCastException if an element in the iterator is not an ESRI geometry. */ @Override - protected Geometry mergePolylines(final Iterator<?> polylines) { + public Geometry mergePolylines(final Iterator<?> polylines) { final Polyline path = new Polyline(); boolean lineTo = false; add: for (Geometry next = geometry;;) { @@ -181,7 +181,7 @@ add: for (Geometry next = geometry;;) { lineTo = false; } /* - * 'polylines.hasNext()' check is conceptually part of 'for' instruction, + * `polylines.hasNext()` check is conceptually part of `for` instruction, * except that we need to skip this condition during the first iteration. */ do if (!polylines.hasNext()) break add; diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PointWrapper.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PointWrapper.java index 7a8369092d..10db10fd4b 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PointWrapper.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PointWrapper.java @@ -119,7 +119,7 @@ final class PointWrapper extends GeometryWithCRS<Shape> { * @throws ClassCastException if an element in the iterator is not a {@link Shape} or a {@link Point2D}. */ @Override - protected Shape mergePolylines(final Iterator<?> polylines) { + public Shape mergePolylines(final Iterator<?> polylines) { return Wrapper.mergePolylines(point, polylines); } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Wrapper.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Wrapper.java index 8b545d752e..8326eef294 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Wrapper.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Wrapper.java @@ -155,7 +155,7 @@ final class Wrapper extends GeometryWithCRS<Shape> { * @throws ClassCastException if an element in the iterator is not a {@link Shape} or a {@link Point2D}. */ @Override - protected Shape mergePolylines(final Iterator<?> polylines) { + public Shape mergePolylines(final Iterator<?> polylines) { return mergePolylines(geometry, polylines); } diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/Wrapper.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/Wrapper.java index 547543d23e..b4c76fea54 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/Wrapper.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/Wrapper.java @@ -281,7 +281,7 @@ final class Wrapper extends GeometryWrapper<Geometry> { * @throws ClassCastException if an element in the iterator is not a JTS geometry. */ @Override - protected Geometry mergePolylines(final Iterator<?> polylines) { + public Geometry mergePolylines(final Iterator<?> polylines) { final List<Coordinate> coordinates = new ArrayList<>(); final List<Geometry> lines = new ArrayList<>(); boolean isFloat = true; diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometriesTestCase.java b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometriesTestCase.java index a73c1f30e3..43503a539f 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometriesTestCase.java +++ b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometriesTestCase.java @@ -120,7 +120,7 @@ public abstract class GeometriesTestCase extends TestCase { } /** - * Tests {@link Geometries#mergePolylines(Iterator)} (or actually tests its strategy). + * Tests {@link GeometryWrapper#mergePolylines(Iterator)} (or actually tests its strategy). * This method verifies the polylines by a call to {@link GeometryWrapper#getEnvelope()}. * Subclasses should perform more extensive tests by verifying the {@link #geometry} field. */ diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/GroupAsPolylineOperation.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/GroupAsPolylineOperation.java deleted file mode 100644 index 6303da7ccc..0000000000 --- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/GroupAsPolylineOperation.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * 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.internal.storage.gpx; - -import java.util.Map; -import java.util.Iterator; -import java.util.Collection; -import org.opengis.parameter.ParameterDescriptorGroup; -import org.opengis.parameter.ParameterValueGroup; -import org.apache.sis.feature.AbstractAttribute; -import org.apache.sis.feature.AbstractOperation; -import org.apache.sis.feature.DefaultAttributeType; -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.util.resources.Errors; - -// Branch-dependent imports -import org.opengis.feature.Feature; -import org.opengis.feature.Property; -import org.opengis.feature.Attribute; -import org.opengis.feature.AttributeType; - - -/** - * Creates a single (Multi){@code Polyline} instance from a sequence of points or polylines stored in another property. - * This base class expects a sequence of {@code Point} or {@code Polyline} instances as input. - * The single (Multi){@code Polyline} instance is re-computed every time this property is requested. - * - * <h2>Examples</h2> - * <p><i>Polylines created from points:</i> - * a boat that record it's position every hour. - * The list of all positions is stored in an attribute with [0 … ∞] multiplicity. - * This class will extract each position and create a line as a new attribute. - * Any change applied to the positions will be visible on the line.</p> - * - * <p><i>Polylines created from other polylines:</i> - * a boat that record track every hour. - * The list of all tracks is stored in an attribute with [0 … ∞] multiplicity. - * This class will extract each track and create a polyline as a new attribute. - * Any change applied to the tracks will be visible on the polyline.</p> - * - * @author Johann Sorel (Geomatys) - * @author Martin Desruisseaux (Geomatys) - * @version 0.8 - * @since 0.8 - */ -final class GroupAsPolylineOperation extends AbstractOperation { - /** - * For cross-version compatibility. - */ - private static final long serialVersionUID = 7898989085371304159L; - - /** - * The parameter descriptor for the "Group polylines" operation, which does not take any parameter. - */ - private static final ParameterDescriptorGroup EMPTY_PARAMS = FeatureUtilities.parameters("GroupPolylines"); - - /** - * Name of the property to follow in order to get the geometries to add to a polyline. - * This property shall be a feature association, usually with [0 … ∞] multiplicity. - */ - private final String association; - - /** - * The expected result type to be returned by {@link #getResult()}. - */ - @SuppressWarnings("serial") - private final AttributeType<?> result; - - /** - * Creates a new operation which will look for geometries in the given feature association. - * - * @param identification name and other information to be given to this operation. - * @param association name of the property to follow in order to get the geometries to add to a polyline. - * @param result the expected result type to be returned by {@link #getResult()}. - */ - GroupAsPolylineOperation(final Map<String,?> identification, final String association, final AttributeType<?> result) { - super(identification); - this.association = association; - this.result = result; - } - - /** - * Creates the {@code result} argument for the constructor. This creation is provided in a separated method - * because the same instance will be shared by many {@code GroupAsPolylineOperation} instances. - * - * @param geometries accessor to the geometry implementation in use (Java2D, ESRI or JTS). - */ - static <G> AttributeType<? extends G> getResult(final Geometries<G> geometries) { - return new DefaultAttributeType<>(Map.of(NAME_KEY, AttributeConvention.ENVELOPE_PROPERTY), - geometries.polylineClass, 1, 1, null); - } - - /** - * Returns an empty parameter descriptor group. - */ - @Override - public ParameterDescriptorGroup getParameters() { - return EMPTY_PARAMS; - } - - /** - * Returns the expected result type. - */ - @Override - public final AttributeType<?> getResult() { - return result; - } - - /** - * Executes the operation on the specified feature with the specified parameters. - * If the geometries have changed since last time this method has been invoked, - * the result will be recomputed. - */ - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public final Property apply(Feature feature, ParameterValueGroup parameters) { - return new Result(feature, association, result); - } - - - /** - * The attribute resulting from execution if 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, - * 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 static final class Result<G> extends AbstractAttribute<G> { - /** - * For cross-version compatibility. - */ - private static final long serialVersionUID = -8872834506769732436L; - - /** - * The feature on which to execute the operation. - */ - @SuppressWarnings("serial") // Most SIS implementations are serializable. - private final Feature feature; - - /** - * Name of the property to follow in order to get the geometries to add to a polyline. - * This property shall be a feature association, usually with [0 … ∞] multiplicity. - */ - private final String association; - - /** - * 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 Feature feature, final String association, final AttributeType<G> result) { - super(result); - this.feature = feature; - this.association = association; - } - - /** - * 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. - * This exception should not happen since we use {@link #feature} in contexts where types are known. - */ - @Override - public G getValue() { - if (geometry == null) { - final Iterator<?> it = ((Collection<?>) feature.getPropertyValue(association)).iterator(); - final Object geom = Geometries.mergePolylines(new Iterator<Object>() { - @Override public boolean hasNext() { - return it.hasNext(); - } - - @Override public Object next() { - return ((Feature) it.next()).getPropertyValue(AttributeConvention.GEOMETRY); - } - }); - geometry = getType().getValueClass().cast(geom); - } - return geometry; - } - - /** - * Does not allow modification of this attribute. - */ - @Override - public void setValue(G value) { - throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, Attribute.class)); - } - } -} diff --git 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 index bdefc58aad..c552af357d 100644 --- 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 @@ -21,7 +21,6 @@ import java.util.Locale; import java.util.Map; import java.util.HashMap; import java.time.temporal.Temporal; -import org.opengis.util.ScopedName; import org.opengis.util.GenericName; import org.opengis.util.NameFactory; import org.opengis.util.FactoryException; @@ -35,6 +34,7 @@ import org.apache.sis.storage.FeatureNaming; import org.apache.sis.storage.IllegalNameException; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.feature.AbstractIdentifiedType; +import org.apache.sis.feature.DefaultAssociationRole; import org.apache.sis.feature.FeatureOperations; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.builder.PropertyTypeBuilder; @@ -47,8 +47,8 @@ import org.apache.sis.util.ResourceInternationalString; import org.apache.sis.util.iso.DefaultNameFactory; // Branch-dependent imports -import org.opengis.feature.AttributeType; import org.opengis.feature.FeatureType; +import org.opengis.feature.Operation; /** @@ -57,7 +57,8 @@ import org.opengis.feature.FeatureType; * nevertheless allows definition of alternative {@code Types} with names created by different factories. * * @author Johann Sorel (Geomatys) - * @version 0.8 + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 * @since 0.8 */ final class Types { @@ -132,8 +133,7 @@ final class Types { { geometries = Geometries.implementation(library); final Map<String,InternationalString[]> resources = new HashMap<>(); - final ScopedName geomName = AttributeConvention.GEOMETRY_PROPERTY; - final Map<String,?> geomInfo = Map.of(AbstractIdentifiedType.NAME_KEY, geomName); + final Map<String,?> geomInfo = Map.of(AbstractIdentifiedType.NAME_KEY, AttributeConvention.GEOMETRY_PROPERTY); final Map<String,?> envpInfo = Map.of(AbstractIdentifiedType.NAME_KEY, AttributeConvention.ENVELOPE_PROPERTY); /* * The parent of all FeatureTypes to be created in this constructor. @@ -180,7 +180,7 @@ final class Types { * └──────────────────┴────────────────┴───────────────────────┴──────────────┘ */ builder.clear().setSuperTypes(parent).setNameSpace(Tags.PREFIX).setName("WayPoint"); - builder.addAttribute(GeometryType.POINT).setName(geomName) + builder.addAttribute(GeometryType.POINT).setName(AttributeConvention.GEOMETRY_PROPERTY) .setCRS(CommonCRS.WGS84.normalizedGeographic()) .addRole(AttributeRole.DEFAULT_GEOMETRY); builder.setDefaultMultiplicity(0, 1); @@ -221,8 +221,7 @@ final class Types { * │ rtept │ WayPoint │ gpx:wptType │ [0 … ∞] │ * └────────────────┴────────────────┴───────────────────────┴──────────────┘ */ - final AttributeType<?> groupResult = GroupAsPolylineOperation.getResult(geometries); - GroupAsPolylineOperation groupOp = new GroupAsPolylineOperation(geomInfo, Tags.ROUTE_POINTS, groupResult); + Operation 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)); @@ -247,7 +246,7 @@ final class Types { * │ trkpt │ WayPoint │ gpx:wptType │ [0 … ∞] │ * └────────────────┴──────────┴─────────────┴──────────────┘ */ - groupOp = new GroupAsPolylineOperation(geomInfo, Tags.TRACK_POINTS, groupResult); + groupOp = groupAsPolyline(geomInfo, Tags.TRACK_POINTS, wayPoint); builder.clear().setSuperTypes(parent).setNameSpace(Tags.PREFIX).setName("TrackSegment"); builder.addProperty(groupOp); builder.addProperty(FeatureOperations.envelope(envpInfo, null, groupOp)); @@ -272,7 +271,7 @@ final class Types { * │ trkseg │ TrackSegment │ gpx:trksegType │ [0 … ∞] │ * └────────────────┴────────────────┴───────────────────────┴──────────────┘ */ - groupOp = new GroupAsPolylineOperation(geomInfo, Tags.TRACK_SEGMENTS, groupResult); + groupOp = groupAsPolyline(geomInfo, Tags.TRACK_SEGMENTS, trackSegment); builder.clear().setSuperTypes(parent).setNameSpace(Tags.PREFIX).setName("Track"); builder.addProperty(groupOp); builder.addProperty(FeatureOperations.envelope(envpInfo, null, groupOp)); @@ -317,4 +316,16 @@ final class Types { } 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) { + var c = new DefaultAssociationRole(Map.of(DefaultAssociationRole.NAME_KEY, components), type, 1, 1); + return FeatureOperations.groupAsPolyline(geomInfo, geometries.library, c); + } }