You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/05/03 17:19:35 UTC
[sis] 02/02: Retrofit `GroupAsPolylineOperation` together with all other feature operations. It required a generalization for working on attributes as well as associations.
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 <ma...@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);
+ }
}