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 2019/06/18 23:39:05 UTC

[sis] branch geoapi-4.0 updated (35bb3c4 -> d44bfad)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from 35bb3c4  Post-merge fix: some pending API were not recognized (if an interface existed in GeoAPI 3.0 but a new parent has been added in GeoAPI 3.1).
     new 693a3d1  fix(netcdf): Fix border behavior to ensure enclosing when reading a subgrid.
     new 4f4ef95  Minor adjustment of imports order.
     new 2f67744  feat(Storage): add a feature set decoration, allowing to view a sequence of feature sets as a single one.
     new ea26103  Refactor pull request #18: - Move code common between ConcatenatedFeatureSet and JoinFeatureSet in a new common parent: AggregatedFeatureSet. - Populate the `Metadata.lineage` node in ISO 19115 metadata with information about the source FeatureSets. - Implement ConcatenatedFeatureSet.subset(Query) by delegating the query to source feature sets. - Modify the algorithm finding common parent type: instead of counting occurrences (those numbers may be wrong in context of mul [...]
     new d44bfad  Merge branch 'feat/featureset-concatenation' into geoapi-4.0 (pull request #18).

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/sis/feature/CommonParentFinder.java | 177 ++++++++++++++++
 .../org/apache/sis/feature/DefaultFeatureType.java |   6 +
 .../main/java/org/apache/sis/feature/Features.java |  20 +-
 .../iso/maintenance/DefaultScopeDescription.java   |  79 ++++++-
 .../apache/sis/internal/netcdf/RasterResource.java |   6 +-
 .../sis/internal/storage/AbstractResource.java     |   2 +-
 .../sis/internal/storage/AggregatedFeatureSet.java | 128 +++++++++++
 .../internal/storage/ConcatenatedFeatureSet.java   | 233 +++++++++++++++++++++
 .../sis/internal/storage/JoinFeatureSet.java       |  56 ++---
 .../sis/internal/storage/MetadataBuilder.java      |  55 ++++-
 .../storage/ConcatenatedFeatureSetTest.java        | 185 ++++++++++++++++
 .../apache/sis/test/suite/StorageTestSuite.java    |   1 +
 12 files changed, 900 insertions(+), 48 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/feature/CommonParentFinder.java
 create mode 100644 storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
 create mode 100644 storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
 create mode 100644 storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ConcatenatedFeatureSetTest.java


[sis] 02/05: fix(netcdf): Fix border behavior to ensure enclosing when reading a subgrid.

Posted by de...@apache.org.
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 693a3d1b0fdb741e115519457e31924c9553b112
Author: Alexis Manin <al...@gmail.com>
AuthorDate: Mon Jun 17 18:03:57 2019 +0200

    fix(netcdf): Fix border behavior to ensure enclosing when reading a subgrid.
---
 .../main/java/org/apache/sis/internal/netcdf/RasterResource.java    | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 5685be5..2bff917 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -24,6 +24,8 @@ import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.Buffer;
 import java.awt.image.DataBuffer;
+
+import org.apache.sis.coverage.grid.GridRoundingMode;
 import org.opengis.util.GenericName;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
@@ -561,7 +563,9 @@ public final class RasterResource extends AbstractGridResource implements Resour
         final SampleDimension[] bands = new SampleDimension[rangeIndices.getNumBands()];
         int[] bandOffsets = null;                                                   // By default, all bands start at index 0.
         try {
-            GridDerivation targetGeometry = gridGeometry.derive().subgrid(domain);
+            GridDerivation targetGeometry = gridGeometry.derive()
+                    .rounding(GridRoundingMode.ENCLOSING)
+                    .subgrid(domain);
             GridExtent     areaOfInterest = targetGeometry.getIntersection();       // Pixel indices of data to read.
             int[]          subsamplings   = targetGeometry.getSubsamplings();       // Slice to read or subsampling to apply.
             int            numBuffers     = bands.length;                           // By default, one variable per band.


[sis] 05/05: Merge branch 'feat/featureset-concatenation' into geoapi-4.0 (pull request #18).

Posted by de...@apache.org.
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 d44bfad55561e327bc6b0a1fb95b4c98ff3a8598
Merge: 4f4ef95 ea26103
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jun 19 01:34:54 2019 +0200

    Merge branch 'feat/featureset-concatenation' into geoapi-4.0 (pull request #18).

 .../org/apache/sis/feature/CommonParentFinder.java | 177 ++++++++++++++++
 .../org/apache/sis/feature/DefaultFeatureType.java |   6 +
 .../main/java/org/apache/sis/feature/Features.java |  20 +-
 .../iso/maintenance/DefaultScopeDescription.java   |  79 ++++++-
 .../sis/internal/storage/AbstractResource.java     |   2 +-
 .../sis/internal/storage/AggregatedFeatureSet.java | 128 +++++++++++
 .../internal/storage/ConcatenatedFeatureSet.java   | 233 +++++++++++++++++++++
 .../sis/internal/storage/JoinFeatureSet.java       |  56 ++---
 .../sis/internal/storage/MetadataBuilder.java      |  55 ++++-
 .../storage/ConcatenatedFeatureSetTest.java        | 185 ++++++++++++++++
 .../apache/sis/test/suite/StorageTestSuite.java    |   1 +
 11 files changed, 895 insertions(+), 47 deletions(-)


[sis] 03/05: Refactor pull request #18: - Move code common between ConcatenatedFeatureSet and JoinFeatureSet in a new common parent: AggregatedFeatureSet. - Populate the `Metadata.lineage` node in ISO 19115 metadata with information about the source FeatureSets. - Implement ConcatenatedFeatureSet.subset(Query) by delegating the query to source feature sets. - Modify the algorithm finding common parent type: instead of counting occurrences (those numbers may be wrong in context of multi-inheritance), re [...]

Posted by de...@apache.org.
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 ea26103bc962f8d39c174633e78b69bb83f5c894
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jun 19 01:23:16 2019 +0200

    Refactor pull request #18:
    - Move code common between ConcatenatedFeatureSet and JoinFeatureSet in a new common parent: AggregatedFeatureSet.
    - Populate the `Metadata.lineage` node in ISO 19115 metadata with information about the source FeatureSets.
    - Implement ConcatenatedFeatureSet.subset(Query) by delegating the query to source feature sets.
    - Modify the algorithm finding common parent type: instead of counting occurrences (those numbers may be wrong in context of multi-inheritance), rely on FeatureType.isAssignableFrom(FeatureType) checks. HashMap are used for avoiding to duplicate checks when the same FeatureType appears more than once.
---
 .../org/apache/sis/feature/CommonParentFinder.java | 177 +++++++++
 .../org/apache/sis/feature/DefaultFeatureType.java |   6 +
 .../main/java/org/apache/sis/feature/Features.java |  20 +-
 .../iso/maintenance/DefaultScopeDescription.java   |  79 +++-
 .../sis/internal/storage/AbstractResource.java     |   2 +-
 .../sis/internal/storage/AggregatedFeatureSet.java | 128 +++++++
 .../internal/storage/ConcatenatedFeatureSet.java   | 398 ++++++++++-----------
 .../sis/internal/storage/JoinFeatureSet.java       |  56 +--
 .../sis/internal/storage/MetadataBuilder.java      |  55 ++-
 .../storage/ConcatenatedFeatureSetTest.java        | 177 ++++-----
 .../apache/sis/test/suite/StorageTestSuite.java    |   1 +
 11 files changed, 752 insertions(+), 347 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/CommonParentFinder.java b/core/sis-feature/src/main/java/org/apache/sis/feature/CommonParentFinder.java
new file mode 100644
index 0000000..18c30b0
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/CommonParentFinder.java
@@ -0,0 +1,177 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.function.Predicate;
+
+// Branch-dependent imports
+import org.opengis.feature.FeatureType;
+
+
+/**
+ * Finds a feature type common to all given types. This is either one of the given types, or a parent common to all types.
+ * A feature <var>F</var> is considered a common parent if <code>F.{@link DefaultFeatureType#isAssignableFrom(FeatureType)
+ * isAssignableFrom}(type)</code> returns {@code true} for all elements <var>type</var> in the given array.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class CommonParentFinder {
+    /**
+     * Finds a feature type common to all given types. See class javadoc.
+     *
+     * @param  types  types for which to find a common type.
+     * @return a feature type which is assignable from all given types, or {@code null} if none.
+     */
+    static FeatureType select(final Iterable<? extends FeatureType> types) {
+        /*
+         * Get a set of unique feature types. Since the process done in this method may be relatively costly,
+         * we want to avoid doing the same work twice. The purpose of Boolean value will be explained later.
+         */
+        final Map<FeatureType,Boolean> allTypes = new LinkedHashMap<>();
+        types.forEach((type) -> allTypes.putIfAbsent(type, Boolean.FALSE));
+        allTypes.remove(null);
+        int count = allTypes.size();
+        final FeatureType[] required = allTypes.keySet().toArray(new FeatureType[count]);
+        /*
+         * We are going to iterate over the `required` array many times. For performance reason, this array should be as
+         * short as possible. We can make it shorter by removing any element A which is assignable to another element B,
+         * since "B assignable from A" implies that a common parent assignable from B will also be assignable from A.
+         * If this simplification results in an array of only one element, we are done.
+         */
+        for (int i=0; i<count; i++) {
+            final FeatureType parent = required[i];
+            for (int j=count; --j >= 0;) {                 // Reverse order so that removed elements do not impact index j.
+                if (j != i && parent.isAssignableFrom(required[j])) {
+                    System.arraycopy(required, j+1, required, j, --count - j);      // If A is assignable from B, remove B.
+                    required[count] = null;
+                    if (j < i) i--;                         // If removed element was before i, then i indices are shifted.
+                }
+            }
+        }
+        switch (count) {
+            case 0: return null;
+            case 1: return required[0];
+        }
+        /*
+         * If we reach this point, no element in the given types is a parent of all other types.
+         * We need to build a set of all parent types.
+         */
+        return new CommonParentFinder(allTypes, required, count).select();
+    }
+
+    /**
+     * All feature types examined by this class. Values are whether a feature type is retained
+     * as a candidate for common parent. Those values are initially {@link Boolean#FALSE} then
+     * updated after we find that a type {@linkplain #isAssignableFromAll is assignable from
+     * all required types}.
+     */
+    private final Map<FeatureType,Boolean> allTypes;
+
+    /**
+     * The features types which must be assignable to the common parent.
+     * This array may not contain all feature types given by user, since we try to remove redundant elements.
+     */
+    private final FeatureType[] required;
+
+    /**
+     * Number of valid elements in the {@link #required} array.
+     */
+    private final int count;
+
+    /**
+     * Creates a finder for a common parent of the given types.
+     * Invokes {@link #select()} after construction time for getting the parent.
+     */
+    private CommonParentFinder(final Map<FeatureType,Boolean> allTypes, final FeatureType[] required, final int count) {
+        this.allTypes = allTypes;
+        this.required = required;
+        this.count    = count;
+        for (int i=0; i<count; i++) {
+            scanParents(required[i]);
+        }
+    }
+
+    /**
+     * Returns {@code true} if the given parent candidate is assignable from all required types.
+     * The feature type to be returned by {@link #select()} must met that condition.
+     */
+    private boolean isAssignableFromAll(final FeatureType parent) {
+        for (int i=0; i<count; i++) {
+            final FeatureType type = required[i];
+            if (type != parent && !parent.isAssignableFrom(type)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Verifies if the given type has a parent which is assignable from all required types.
+     * This method verifies recursively parents of parents, skipping types that have already
+     * been examined in a previous invocation of this method.
+     */
+    private void scanParents(final FeatureType type) {
+        for (final FeatureType parent : type.getSuperTypes()) {
+            if (allTypes.putIfAbsent(parent, Boolean.FALSE) == null) {
+                if (isAssignableFromAll(parent)) {
+                    allTypes.put(parent, Boolean.TRUE);     // Found a candidate.
+                    skipParents(parent);                    // All parents of this candidate can be skipped.
+                } else {
+                    scanParents(parent);                    // Verify if a parent is assignable.
+                }
+            }
+        }
+    }
+
+    /**
+     * Invoked when the given feature type is assignable from all required types.
+     * There is no need to verify the parents since they are not going to be a better match.
+     */
+    private void skipParents(final FeatureType type) {
+        assert isAssignableFromAll(type);
+        for (final FeatureType parent : type.getSuperTypes()) {
+            if (Boolean.TRUE.equals(allTypes.put(parent, Boolean.FALSE))) {
+                // If `parent` was previously a candidate, its parents have already been set to `FALSE`.
+            } else {
+                skipParents(parent);
+            }
+        }
+    }
+
+    /**
+     * Invoked after all feature types have been examined. This method removes all features types that
+     * are not parent of required types, then select the one having the greatest number of properties.
+     */
+    FeatureType select() {
+        allTypes.values().removeIf(Predicate.isEqual(Boolean.FALSE));
+        FeatureType best = null;
+        int numProperties = 0;
+        for (final FeatureType type : allTypes.keySet()) {
+            final int n = type.getProperties(true).size();
+            if (best == null || n > numProperties) {
+                best = type;
+                numProperties = n;
+            }
+        }
+        return best;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
index a1007cf..19144b5 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
@@ -665,6 +665,12 @@ public class DefaultFeatureType extends AbstractIdentifiedType implements Featur
      * However as a safety, this method also checks that all properties in this feature type is assignable
      * from a property of the same name in the given type.
      *
+     * <div class="section">Constraints:</div>
+     * <ul>
+     *   <li>If <var>A</var> is assignable from <var>B</var> and <var>B</var> is assignable from <var>C</var>,
+     *       then <var>A</var> is assignable from <var>C</var>.</li>
+     * </ul>
+     *
      * <div class="note"><b>Analogy:</b>
      * if we compare {@code FeatureType} to {@link Class} in the Java language, then this method is equivalent
      * to {@link Class#isAssignableFrom(Class)}.</div>
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 461b0e2..4852a3f 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
@@ -33,6 +33,7 @@ import org.apache.sis.internal.feature.Resources;
 import org.opengis.feature.Attribute;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
 import org.opengis.feature.FeatureAssociationRole;
 import org.opengis.feature.IdentifiedType;
 import org.opengis.feature.InvalidPropertyValueException;
@@ -45,7 +46,7 @@ import org.opengis.feature.PropertyType;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.5
  * @module
  */
@@ -119,6 +120,23 @@ public final class Features extends Static {
     }
 
     /**
+     * Finds a feature type common to all given types, or returns {@code null} if none is found.
+     * The return value is either one of the given types, or a parent common to all types.
+     * A feature <var>F</var> is considered a common parent if <code>F.{@link DefaultFeatureType#isAssignableFrom
+     * isAssignableFrom}(type)</code> returns {@code true} for all elements <var>type</var> in the given array.
+     *
+     * @param  types  types for which to find a common type, or {@code null}.
+     * @return a feature type which is assignable from all given types, or {@code null} if none.
+     *
+     * @see FeatureType#isAssignableFrom(FeatureType)
+     *
+     * @since 1.0
+     */
+    public static FeatureType findCommonParent(final Iterable<? extends FeatureType> types) {
+        return (types != null) ? CommonParentFinder.select(types) : null;
+    }
+
+    /**
      * Returns the name of the type of values that the given property can take.
      * The type of value can be a {@link Class}, a {@link org.opengis.feature.FeatureType}
      * or another {@code PropertyType} depending on given argument:
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/maintenance/DefaultScopeDescription.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/maintenance/DefaultScopeDescription.java
index 2000a20..81f7d47 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/maintenance/DefaultScopeDescription.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/maintenance/DefaultScopeDescription.java
@@ -30,6 +30,7 @@ import org.apache.sis.internal.jaxb.Context;
 import org.apache.sis.internal.system.Semaphores;
 import org.apache.sis.util.collection.CheckedContainer;
 import org.apache.sis.util.resources.Messages;
+import org.apache.sis.util.iso.Types;
 import org.apache.sis.xml.Namespaces;
 
 import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
@@ -207,7 +208,8 @@ public class DefaultScopeDescription extends ISOMetadata implements ScopeDescrip
      */
     @SuppressWarnings("unchecked")
     private static Set<CharSequence> cast(final Object value) {
-        assert ((CheckedContainer<?>) value).getElementType() == CharSequence.class;
+        assert !(value instanceof CheckedContainer<?>) ||
+                ((CheckedContainer<?>) value).getElementType() == CharSequence.class;
         return (Set<CharSequence>) value;
     }
 
@@ -268,6 +270,37 @@ public class DefaultScopeDescription extends ISOMetadata implements ScopeDescrip
     }
 
     /**
+     * Returns an indication of which property is set, or {@code null} if unknown.
+     * This method returns one of the following values depending which property has been set:
+     *
+     * <table class="sis">
+     *   <caption>hierarchical level of the data</caption>
+     *   <tr><th>Scope code</th> <th>Getter method</th></tr>
+     *   <tr><td>{@link ScopeCode#DATASET        DATASET}</td>        <td>{@link #getDataset()}</td></tr>
+     *   <tr><td>{@link ScopeCode#FEATURE_TYPE   FEATURE_TYPE}</td>   <td>{@link #getFeatures()}</td></tr>
+     *   <tr><td>{@link ScopeCode#ATTRIBUTE_TYPE ATTRIBUTE_TYPE}</td> <td>{@link #getAttributes()}</td></tr>
+     *   <tr><td>{@link ScopeCode#FEATURE        FEATURE}</td>        <td>{@link #getFeatureInstances()}</td></tr>
+     *   <tr><td>{@link ScopeCode#ATTRIBUTE      ATTRIBUTE}</td>      <td>{@link #getAttributeInstances()}</td></tr>
+     * </table>
+     *
+     * @return an identification of the property which is set, or {@code null} if unknown.
+     *
+     * @see #setLevelDescription(ScopeCode, Set)
+     *
+     * @since 1.0
+     */
+    public ScopeCode getLevel() {
+        switch (property) {
+            case DATASET:             return ScopeCode.DATASET;
+            case FEATURES:            return ScopeCode.FEATURE_TYPE;
+            case ATTRIBUTES:          return ScopeCode.ATTRIBUTE_TYPE;
+            case FEATURE_INSTANCES:   return ScopeCode.FEATURE;
+            case ATTRIBUTE_INSTANCES: return ScopeCode.ATTRIBUTE;
+        }
+        return null;
+    }
+
+    /**
      * Returns the dataset to which the information applies.
      *
      * <div class="note"><b>Example:</b>
@@ -468,4 +501,48 @@ public class DefaultScopeDescription extends ISOMetadata implements ScopeDescrip
             value = newValue;
         }
     }
+
+    /**
+     * Dispatches the given values to a setter method determined by the given hierarchical level.
+     * The mapping between scope codes and {@code ScopeDescription} properties is documented in the {@link #getLevel()} method.
+     * If the given scope code is not one of the listed codes, then the "other" property is used.
+     *
+     * @param  level      an identification of the property which is set, or {@code null} if unknown.
+     * @param  newValues  the values to set, or {@code null} if none.
+     *
+     * @see #getLevel()
+     *
+     * @since 1.0
+     */
+    public void setLevelDescription(final ScopeCode level, final Set<? extends CharSequence> newValues) {
+        if (ScopeCode.DATASET.equals(level)) {
+            String description = null;
+            if (newValues != null) {
+                for (CharSequence value : newValues) {
+                    if (value != null) {
+                        description = value.toString();
+                        break;
+                    }
+                }
+            }
+            setDataset(description);
+        } else if (ScopeCode.FEATURE_TYPE.equals(level)) {
+            setFeatures(newValues);
+        } else if (ScopeCode.ATTRIBUTE_TYPE.equals(level)) {
+            setAttributes(newValues);
+        } else if (ScopeCode.FEATURE.equals(level)) {
+            setFeatureInstances(newValues);
+        } else if (ScopeCode.ATTRIBUTE.equals(level)) {
+            setAttributeInstances(newValues);
+        } else {
+            InternationalString description = null;
+            if (newValues != null) {
+                for (CharSequence value : newValues) {
+                    description = Types.toInternationalString(value);
+                    if (description != null) break;
+                }
+            }
+            setOther(description);
+        }
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
index 73ed965..39d1086 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
@@ -60,7 +60,7 @@ public abstract class AbstractResource implements Resource, Localized {
     /**
      * The set of registered warning listeners for the data store, or {@code null} if none.
      */
-    private final WarningListeners<DataStore> listeners;
+    final WarningListeners<DataStore> listeners;
 
     /**
      * Creates a new resource.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
new file mode 100644
index 0000000..ce317fb
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.util.logging.WarningListeners;
+
+// Branch-dependent imports
+import org.opengis.feature.FeatureType;
+
+
+/**
+ * A feature set made from the aggregation of other feature sets. The features may be aggregated in different ways,
+ * depending on the subclass. The aggregation may be all features from one set followed by all features from another set,
+ * or it may be features of the two sets merged together in a way similar to SQL JOIN statement.
+ *
+ * <p>This class provides default implementations of {@link #getEnvelope()} and {@link #getMetadata()}.
+ * Subclasses need to implement {@link #dependencies()}.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class AggregatedFeatureSet extends AbstractFeatureSet {
+    /**
+     * Creates a new aggregated feature set.
+     *
+     * @param  listeners  the set of registered warning listeners for the data store, or {@code null} if none.
+     */
+    protected AggregatedFeatureSet(final WarningListeners<DataStore> listeners) {
+        super(listeners);
+    }
+
+    /**
+     * Returns all feature set used by this aggregation. This method is invoked for implementation of
+     * {@link #getEnvelope()} and {@link #createMetadata(MetadataBuilder)}.
+     *
+     * @return all feature sets in this aggregation.
+     */
+    abstract Collection<FeatureSet> dependencies();
+
+    /**
+     * Adds the envelopes of the aggregated feature sets in the given list. If some of the feature sets
+     * are themselves aggregated feature sets, then this method traverses them recursively. We compute
+     * the union of all envelopes at once after we got all envelopes.
+     */
+    private void getEnvelopes(final List<Envelope> addTo) throws DataStoreException {
+        for (final FeatureSet fs : dependencies()) {
+            if (fs instanceof AggregatedFeatureSet) {
+                ((AggregatedFeatureSet) fs).getEnvelopes(addTo);
+            } else {
+                // No need to filter null envelopes since Envelopes.union(…) will ignore them.
+                addTo.add(fs.getEnvelope());
+            }
+        }
+    }
+
+    /**
+     * Returns the union of the envelope in all aggregated feature set, or {@code null} if none.
+     * This method tries to find a CRS common to all feature sets. If no common CRS can be found,
+     * an exception is thrown.
+     *
+     * <div class="note"><b>Implementation note:</b>
+     * the envelope is recomputed every time this method is invoked. The result is not cached because
+     * the envelope of {@code FeatureSet} sources may change between invocations of this method.
+     * The cost should not be excessive if the sources cache themselves their envelopes.</div>
+     *
+     * @return union of envelopes of both sides, or {@code null}.
+     * @throws DataStoreException if an error occurred while computing the envelope.
+     */
+    @Override
+    public Envelope getEnvelope() throws DataStoreException {
+        final List<Envelope> envelopes = new ArrayList<>();
+        getEnvelopes(envelopes);
+        try {
+            return Envelopes.union(envelopes.toArray(new Envelope[envelopes.size()]));
+        } catch (TransformException e) {
+            /*
+             * Since Envelopes.union(…) tried to find a common CRS,
+             * failure to transform an envelope may be important.
+             */
+            throw new DataStoreReferencingException(e);
+        }
+    }
+
+    /**
+     * Invoked the first time that {@link #getMetadata()} is invoked. The default implementation adds
+     * the information documented in {@link AbstractFeatureSet#createMetadata(MetadataBuilder)}, then
+     * adds the dependencies as lineages.
+     *
+     * @param  metadata  the builder where to set metadata properties.
+     * @throws DataStoreException if an error occurred while reading metadata from the data stores.
+     */
+    @Override
+    protected void createMetadata(final MetadataBuilder metadata) throws DataStoreException {
+        super.createMetadata(metadata);
+        for (final FeatureSet fs : dependencies()) {
+            final FeatureType type = fs.getType();
+            metadata.addSource(fs.getMetadata(), ScopeCode.FEATURE_TYPE,
+                    (type == null) ? null : new CharSequence[] {type.getName().toInternationalString()});
+        }
+    }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
index e7dded1..bfa7012 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
@@ -1,271 +1,233 @@
+/*
+ * 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;
 
-import org.apache.sis.geometry.AbstractEnvelope;
-import org.apache.sis.geometry.GeneralEnvelope;
-import org.apache.sis.storage.DataStoreException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Collection;
+import java.util.stream.Stream;
+import org.apache.sis.feature.Features;
 import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.event.ChangeEvent;
-import org.apache.sis.storage.event.ChangeListener;
-import org.apache.sis.util.NullArgumentException;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.internal.util.CollectionsExt;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.storage.Query;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
-import org.opengis.referencing.operation.TransformException;
-import org.opengis.util.GenericName;
 
-import java.lang.ref.WeakReference;
-import java.util.ArrayDeque;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Stream;
-
-import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
 
 /**
- * Expose a sequence of {@link FeatureSet feature sets} as a single one. A few notes :
+ * Exposes a sequence of {@link FeatureSet}s as a single one. A few notes:
  * <ol>
- *     <li>The feature set concatenation must be built with a non-empty array or collection of feature set. It is copied
- *     as is in a unmodifiable list, meaning that doublons won't be removed, and iteration order is driven by input
- *     sequence. </li>
- *     <li>All input feature sets must share a common type, or at least a common super-type. If you want to sequence
+ *   <li>The feature set concatenation must be built with a non-empty array or collection of feature set.
+ *     It is copied verbatim in an unmodifiable list, meaning that duplicated instances won't be removed,
+ *     and iteration order is driven by input sequence.</li>
+ *   <li>All input feature sets must share a common type, or at least a common super-type. If you want to sequence
  *     sets which does not share any common parent, please pre-process them to modify their public type.</li>
- *     <li>For now, event system is disabled, meaning that trying to attach a listener to the feature set concatenation
- *     will throw an {@link UnsupportedOperationException}. It's meant to evolve.</li>
+ *   <li>The {@linkplain #getIdentifier() identifier} is the name of above-cited common type.
+ *     It may result in the same identifier than one of the given sources,
+ *     but this is okay if this {@code FeatureSet} has a different {@code DataStore} parent than the sources.</li>
  * </ol>
+ *
+ * @author  Alexis Manin (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
  */
-public class ConcatenatedFeatureSet implements FeatureSet {
-
-    private final List<FeatureSet> sources;
-    private final FeatureType commonType;
-
+public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
     /**
-     * Temporarily cache union of underlying feature set envelopes. However, only a weak reference is used, because
-     * we cannot prevent subsets to be updated externally. Keeping a temporary reference allows for an eventual update
-     * of the concatenated envelope. The same logic is applied for {@link #getMetadata() metadata accessor}.
+     * The sequence of feature sets whose feature instances will be returned.
      */
-    private WeakReference<Envelope> cachedEnvelope;
+    private final List<FeatureSet> sources;
 
     /**
-     * Cache metadata only temporarily, because it depends on {@link #getEnvelope() envelope accessor}. For more
-     * information about why the cache is only temporary, please read {@link #cachedEnvelope} documentation.
+     * The most specific feature type common to all feature sets in the {@linkplain #sources} list.
      */
-    private WeakReference<Metadata> cachedMetadata;
+    private final FeatureType commonType;
 
     /**
-     * See {@link #ConcatenatedFeatureSet(FeatureSet...)}.
+     * Creates a new feature set as a concatenation of the sequence of features given by the {@code sources}.
+     * This constructor does not verify that the given {@code sources} array contains at least two elements;
+     * this verification must be done by the caller. This constructor retains the given {@code sources} array
+     * by direct reference; clone, if desired, shall be done by the caller.
+     *
+     * @param  listeners  the set of registered warning listeners for the data store, or {@code null} if none.
+     * @param  sources    the sequence of feature sets to expose in a single set.
+     *                    Must neither be null, empty nor contain a single element only.
+     * @throws DataStoreException if given feature sets does not share any common type.
      */
-    public ConcatenatedFeatureSet(final Collection<FeatureSet> sources) throws DataStoreException {
-        this(sources == null? null : sources.toArray(new FeatureSet[sources.size()]));
+    protected ConcatenatedFeatureSet(final WarningListeners<DataStore> listeners, final FeatureSet[] sources) throws DataStoreException {
+        super(listeners);
+        for (int i=0; i<sources.length; i++) {
+            ArgumentChecks.ensureNonNullElement("sources", i, sources[i]);
+        }
+        this.sources = UnmodifiableArrayList.wrap(sources);
+        final FeatureType[] types = new FeatureType[sources.length];
+        for (int i=0; i<types.length; i++) {
+            types[i] = sources[i].getType();
+        }
+        commonType = Features.findCommonParent(Arrays.asList(types));
+        if (commonType == null) {
+            // TODO: localize.
+            throw new DataStoreContentException("Cannot find a common super type across all feature sets to concatenate");
+        }
+        /*
+         * TODO: we should add listeners on source feature sets. By doing it, we could be notified of changes,
+         *       allowing a better update strategy for envelope, metadata and type.
+         */
     }
 
     /**
-     * Create a new feature set being a view of the sequence of given sets.
+     * Creates a new feature set as a concatenation of the sequence of features given by the {@code sources}.
+     * The given array shall be non-empty. If the array contains only 1 element, that element is returned.
      *
-     * @param sources The sequence of feature set to expose in a single set. Must neither be null, empty nor contains
-     *                a single element only.
-     * @throws DataStoreException If given feature sets does not share any common type.
+     * @param  sources  the sequence of feature sets to expose in a single set.
+     * @return the concatenation of given feature set.
+     * @throws DataStoreException if given feature sets does not share any common type.
      */
-    public ConcatenatedFeatureSet(final FeatureSet... sources) throws DataStoreException {
-        ensureNonNull("Sources feature sets", sources);
-        if (sources.length < 1) {
-            throw new IllegalArgumentException("Given feature set sequence is empty.");
-        } else if (sources.length == 1) {
-            throw new IllegalArgumentException("You are trying to concatenate a single feature set.");
+    public static FeatureSet create(final FeatureSet... sources) throws DataStoreException {
+        ArgumentChecks.ensureNonEmpty("sources", sources);
+        if (sources.length == 1) {
+            final FeatureSet fs = sources[0];
+            ArgumentChecks.ensureNonNullElement("sources", 0, fs);
+            return fs;
+        } else {
+            return new ConcatenatedFeatureSet(null, sources.clone());
         }
-
-        final FeatureSet[] copy = Arrays.copyOf(sources, sources.length);
-        this.sources = Collections.unmodifiableList(Arrays.asList(copy));
-        commonType = checkType(this.sources);
-        // TODO: we should add listeners on source feature sets. By doing it, we could be notified of changes, allowing
-        // a better update strategy for envelope, metadata and type.
-    }
-
-    @Override
-    public FeatureType getType() {
-        return commonType;
     }
 
-    @Override
-    public Stream<Feature> features(boolean parallel) {
-        final Stream<FeatureSet> sets = parallel? sources.parallelStream() : sources.stream();
-        return sets.flatMap(set -> {
-            try {
-                return set.features(parallel);
-            } catch (DataStoreException e) {
-                throw new BackingStoreException(e);
+    /**
+     * Creates a new feature set as a concatenation of the sequence of features given by the {@code sources}.
+     * The given collection shall be non-empty. If the collection contains only 1 element, that element is returned.
+     *
+     * @param  sources  the sequence of feature sets to expose in a single set.
+     * @return the concatenation of given feature set.
+     * @throws DataStoreException if given feature sets does not share any common type.
+     */
+    public static FeatureSet create(final Collection<? extends FeatureSet> sources) throws DataStoreException {
+        ArgumentChecks.ensureNonNull("sources", sources);
+        final int size = sources.size();
+        switch (size) {
+            case 0: throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "sources"));
+            case 1: {
+                final FeatureSet fs = CollectionsExt.first(sources);
+                ArgumentChecks.ensureNonNullElement("sources", 0, fs);
+                return fs;
             }
-        });
-    }
-
-    @Override
-    public synchronized Envelope getEnvelope() throws DataStoreException {
-        Envelope result = cachedEnvelope == null? null : cachedEnvelope.get();
-        if (result == null) {
-            result = computeEnvelope();
-            if (result != null)
-                cachedEnvelope = new WeakReference<>(result);
-        }
-
-        return result;
-    }
-
-    private Envelope computeEnvelope() throws DataStoreException {
-        Envelope tmpEnv = sources.get(0).getEnvelope();
-        if (tmpEnv == null)
-            return null;
-
-        final GeneralEnvelope envelope = new GeneralEnvelope(tmpEnv);
-        for (int i = 1 ; i < sources.size() ; i++) {
-            tmpEnv = sources.get(i).getEnvelope();
-            if (tmpEnv == null) {
-                return null;
+            default: {
+                return new ConcatenatedFeatureSet(null, sources.toArray(new FeatureSet[size]));
             }
-            envelope.add(tmpEnv);
         }
+    }
 
-        return envelope;
+    /**
+     * Returns all feature set used by this aggregation. This method is invoked for implementation of
+     * {@link #getEnvelope()} and {@link #createMetadata(MetadataBuilder)}.
+     */
+    @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")         // Safe because the list is unmodifiable.
+    final List<FeatureSet> dependencies() {
+        return sources;
     }
 
+    /**
+     * Returns the most specific feature type common to all feature sets given to the constructor.
+     *
+     * @return the common type of all features returned by this set.
+     */
     @Override
-    public GenericName getIdentifier() {
-        return null;
+    public FeatureType getType() {
+        return commonType;
     }
 
+    /**
+     * Returns an estimation of the number of features in this set, or {@code null} if unknown.
+     * This is the sum of the estimations provided by all source sets, or {@code null} if at least
+     * one source could not provide an estimation.
+     *
+     * @return estimation of the number of features, or {@code null}.
+     */
     @Override
-    public synchronized Metadata getMetadata() throws DataStoreException {
-        Metadata result = cachedMetadata == null ? null : cachedMetadata.get();
-        if (result == null) {
-            final MetadataBuilder builder = new MetadataBuilder();
-            builder.addFeatureType(getType(), null);
-            final Envelope env = getEnvelope();
-            if (env != null) {
-                try {
-                    builder.addExtent(AbstractEnvelope.castOrCopy(env));
-                } catch (TransformException e) {
-                    // TODO: log error, or use a warning listener. It's fine if the extent cannot be computed
+    protected Integer getFeatureCount() {
+        int sum = 0;
+        for (final FeatureSet fs : sources) {
+            if (fs instanceof AbstractFeatureSet) {
+                final Integer count = ((AbstractFeatureSet) fs).getFeatureCount();
+                if (count != null && count >= 0) {
+                    sum += count;
+                    if (sum < 0) {                          // Integer overflow.
+                        sum = Integer.MAX_VALUE;
+                        break;
+                    }
+                    continue;
                 }
             }
-
-            result = builder.build(true);
-            cachedMetadata = new WeakReference<>(result);
+            return null;                 // A source can not provide estimation.
         }
-
-        return result;
-    }
-
-    @Override
-    public <T extends ChangeEvent> void addListener(ChangeListener<? super T> listener, Class<T> eventType) {
-        throw new UnsupportedOperationException();
+        return sum;
     }
 
+    /**
+     * Returns a stream of all features contained in this concatenated dataset. If the {@code parallel} argument
+     * is {@code false}, then datasets are traversed in the order they were specified at construction time.
+     * If the {@code parallel} argument is {@code true}, then datasets are traversed in no determinist order.
+     * If an error occurred while reading the feature instances from a source, then the error is wrapped in a
+     * {@link BackingStoreException}.
+     *
+     * @param  parallel  {@code true} for a parallel stream, or {@code false} for a sequential stream.
+     * @return all features contained in this dataset.
+     */
     @Override
-    public <T extends ChangeEvent> void removeListener(ChangeListener<? super T> listener, Class<T> eventType) {
-        throw new UnsupportedOperationException();
+    public Stream<Feature> features(final boolean parallel) {
+        final Stream<FeatureSet> sets = parallel ? sources.parallelStream() : sources.stream();
+        return sets.flatMap(set -> {
+            try {
+                return set.features(parallel);
+            } catch (DataStoreException e) {
+                throw new BackingStoreException(e);
+            }
+        });
     }
 
     /**
-     * Find a feature type common to all given feature sets. It means we will search for a super type used by all input
-     * feature sets.
-     *
-     * @implNote A graph approach would surely be the best way to resolve this problem. However, due to the lack of
-     * standard tools in the jdk, we'd rather build a map with every distinct encountered type/super type. For each, we
-     * associate the number of occurrences found, as the maximum number of inheritors for this type. When done, we
-     * simply give back a type with an encounter count equal to the number of input feature sets, and the least possible
-     *  number of inheritors.
+     * Requests a subset of features and/or feature properties from this resource.
      *
-     * @param sources The set of feature sets to find a common type for. Must not be null.
-     * @return A feature type which can be found in every encountered feature set, as super type or direct one.
-     * @throws DataStoreException If an error occurs while reading data types from source feature sets.
-     * @throws IllegalArgumentException If we cannot find a common type, or input collection is empty.
-     * @throws NullArgumentException If input collection is null.
+     * @param  query  definition of feature and feature properties filtering applied at reading time.
+     * @return resulting subset of features (never {@code null}).
+     * @throws DataStoreException if an error occurred while processing the query.
      */
-    private static FeatureType checkType(final Collection<FeatureSet> sources) throws DataStoreException, IllegalArgumentException, NullArgumentException {
-        ensureNonNull("Source sets", sources);
-
-        final java.util.Iterator<FeatureSet> it = sources.iterator();
-        if (!it.hasNext())
-            throw new IllegalArgumentException("Cannot find a common data type from an empty collection.");
-
-        FeatureType nextType = it.next().getType();
-
-        final Map<FeatureType, Indirection> typeMatchCount = new HashMap<>();
-        // Coupling Stacks allow us to properly browse and document indirection levels in encountered data types.
-        Deque<FeatureType> typeStack = new ArrayDeque<>();
-        Deque<FeatureType> nextLevel = new ArrayDeque<>();
-
-        nextLevel.push(nextType);
-        int indirection = -1;
-        do {
-            indirection++;
-            // Micro opti : re-use the same two deques to avoid instantiating/resizing lots of arrays.
-            final Deque tmp = typeStack;
-            typeStack = nextLevel;
-            nextLevel = tmp;
-
-            do {
-                final FeatureType type = typeStack.pop();
-                typeMatchCount.put(type, new Indirection(indirection, 1));
-                nextLevel.addAll(type.getSuperTypes());
-            } while (!typeStack.isEmpty());
-        } while (!nextLevel.isEmpty());
-
-        while (it.hasNext()) {
-            boolean hasBeenUpdated = false;
-            nextLevel.push(it.next().getType());
-            indirection = -1;
-            do {
-                indirection++;
-                // Micro opti : re-use the same two deques to avoid instantiating/resizing lots of arrays.
-                final java.util.Deque tmp = typeStack;
-                typeStack = nextLevel;
-                nextLevel = tmp;
-
-                do {
-                    final int tmpIndi = indirection;
-                    final FeatureType type = typeStack.pop();
-                    final Indirection indi = typeMatchCount
-                            .computeIfPresent(type, (key, oldVal) -> oldVal.merge(new Indirection(tmpIndi, 1)));
-                    if (indi != null) hasBeenUpdated = true;
-                    nextLevel.addAll(type.getSuperTypes());
-                } while (!typeStack.isEmpty());
-            } while (!nextLevel.isEmpty());
-
-            // short-circuit here, if no feature type has the same count as already encounterd types.
-            if (!hasBeenUpdated)
-                throw new IllegalArgumentException("No common feature nor super type found across input feature sets");
-        }
-
-        final int requiredMatch = sources.size();
-        return typeMatchCount.entrySet().stream()
-                .filter(stat -> stat.getValue().matchCount == requiredMatch)
-                .min(Comparator.comparingInt(stat -> stat.getValue().indirection))
-                .map(Map.Entry::getKey)
-                .orElseThrow(() -> new IllegalArgumentException("Cannot find a common super type across all feature sets to concatenate"));
-    }
-
-    private static class Indirection {
-        int indirection;
-        int matchCount;
-
-        private Indirection(int indirection, final int matchCount) {
-            this.indirection = indirection;
-            this.matchCount = matchCount;
-        }
-
-        private Indirection merge(final Indirection other) {
-            if (other.indirection > indirection)
-                indirection = other.indirection;
-            matchCount += other.matchCount;
-
-            return this;
+    @Override
+    public FeatureSet subset(final Query query) throws DataStoreException {
+        final FeatureSet[] subsets = new FeatureSet[sources.size()];
+        boolean modified = false;
+        for (int i=0; i<subsets.length; i++) {
+            FeatureSet source = sources.get(i);
+            subsets[i] = source.subset(query);
+            modified |= subsets[i] != source;
         }
+        return modified ? new ConcatenatedFeatureSet(listeners, subsets) : this;
     }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java
index 27e22a4..bc501b5 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java
@@ -18,16 +18,13 @@ package org.apache.sis.internal.storage;
 
 import java.util.Map;
 import java.util.List;
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Spliterator;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 import org.opengis.util.GenericName;
-import org.opengis.geometry.Envelope;
-import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.feature.FeatureOperations;
 import org.apache.sis.feature.DefaultFeatureType;
 import org.apache.sis.feature.DefaultAssociationRole;
@@ -36,7 +33,6 @@ import org.apache.sis.internal.storage.query.SimpleQuery;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.collection.Containers;
@@ -79,7 +75,7 @@ import org.apache.sis.filter.DefaultFilterFactory;
  * @since   1.0
  * @module
  */
-public class JoinFeatureSet extends AbstractFeatureSet {
+public class JoinFeatureSet extends AggregatedFeatureSet {
     /**
      * Specifies whether values on both sides are required (inner join), or only one side (outer join).
      */
@@ -268,6 +264,19 @@ public class JoinFeatureSet extends AbstractFeatureSet {
     }
 
     /**
+     * Returns the two feature sets used by this {@code JoinFeatureSet}.
+     * The "main" dependency is at index 0 and the other dependency at index 1.
+     *
+     * @return the dependencies in a list of size 2 with the "main" dependency first.
+     */
+    @Override
+    final List<FeatureSet> dependencies() {
+        final FeatureSet[] sets = new FeatureSet[] {left, right};
+        if (swapSides) ArraysExt.swap(sets, 0, 1);
+        return Arrays.asList(sets);
+    }
+
+    /**
      * Specifies whether values on both sides are required (inner join), or only one side (outer join).
      *
      * @return whether values on both sides are required (inner join), or only one side (outer join).
@@ -289,41 +298,6 @@ public class JoinFeatureSet extends AbstractFeatureSet {
     }
 
     /**
-     * Returns the union of the envelope on both sides, or {@code null} if none.
-     * The coordinate reference system is the CRS of the "main" side.
-     *
-     * @return union of envelopes of both sides, or {@code null}.
-     * @throws DataStoreException if an error occurred while computing the envelope.
-     */
-    @Override
-    public Envelope getEnvelope() throws DataStoreException {
-        final List<Envelope> envelopes = new ArrayList<>();
-        getEnvelopes(envelopes);
-        try {
-            return Envelopes.union(envelopes.toArray(new Envelope[envelopes.size()]));
-        } catch (TransformException e) {
-            throw new DataStoreReferencingException(e);
-        }
-    }
-
-    /**
-     * Adds the envelopes of the two joined feature set in the given list. If some of the feature sets
-     * are themselves joined feature sets, then this method traverses them recursively. We compute the
-     * union of all envelopes at once after we got all envelopes.
-     */
-    private void getEnvelopes(final List<Envelope> addTo) throws DataStoreException {
-        boolean side = swapSides;
-        do {
-            final FeatureSet f = side ? right : left;
-            if (f instanceof JoinFeatureSet) {
-                ((JoinFeatureSet) f).getEnvelopes(addTo);
-            } else {
-                addTo.add(f.getEnvelope());
-            }
-        } while ((side = !side) != swapSides);
-    }
-
-    /**
      * Returns a stream of all features contained in this dataset.
      *
      * @param  parallel  {@code true} for a parallel stream (if supported), or {@code false} for a sequential stream.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
index 133c6ea..34424a3 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
@@ -21,8 +21,10 @@ import java.util.Date;
 import java.util.Locale;
 import java.util.Optional;
 import java.util.Iterator;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -35,12 +37,14 @@ import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.Geometry;
+import org.opengis.metadata.Metadata;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Role;
 import org.opengis.metadata.citation.DateType;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.citation.CitationDate;
 import org.opengis.metadata.citation.OnLineFunction;
+import org.opengis.metadata.identification.Identification;
 import org.opengis.metadata.spatial.GCP;
 import org.opengis.metadata.spatial.Dimension;
 import org.opengis.metadata.spatial.DimensionNameType;
@@ -65,8 +69,9 @@ import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.geometry.AbstractEnvelope;
+import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.internal.simple.SimpleDuration;
+import org.apache.sis.geometry.AbstractEnvelope;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.iso.DefaultIdentifier;
 import org.apache.sis.metadata.iso.DefaultMetadataScope;
@@ -127,7 +132,6 @@ import org.apache.sis.util.iso.Names;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.measure.Units;
 
-import static java.util.Collections.singleton;
 import static org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_DAY;
 
 // Branch-dependent imports
@@ -2633,7 +2637,7 @@ parse:      for (int i = 0; i < length;) {
             event.setContext(Context.ACQUISITION);
             event.setTime(time);
             final DefaultOperation op = new DefaultOperation();
-            op.setSignificantEvents(singleton(event));
+            op.setSignificantEvents(Collections.singleton(event));
             op.setType(OperationType.REAL);
             op.setStatus(Progress.COMPLETED);
             addIfNotPresent(acquisition().getOperations(), op);
@@ -2738,6 +2742,51 @@ parse:      for (int i = 0; i < length;) {
     }
 
     /**
+     * Adds information about a source of data used for producing the resource.
+     * Storage location is:
+     *
+     * <ul>
+     *   <li>{@code metadata/resourceLineage/source/scope/level}</li>
+     *   <li>{@code metadata/resourceLineage/source/scope/extent}</li>
+     *   <li>{@code metadata/resourceLineage/source/scope/levelDescription/*}</li>
+     *   <li>{@code metadata/resourceLineage/source/citation}</li>
+     *   <li>{@code metadata/resourceLineage/source/sourceReferenceSystem}</li>
+     *   <li>{@code metadata/resourceLineage/source/sourceSpatialResolution}</li>
+     * </ul>
+     *
+     * <div class="note"><b>Example:</b>
+     * if a {@code FeatureSet} is the aggregation of two other {@code FeatureSet} resources,
+     * then this method can be invoked twice with the metadata of each source {@code FeatureSet}.
+     * If the aggregated data are features, then {@code level} should be {@link ScopeCode#FEATURE}.</div>
+     *
+     * @param  metadata  the metadata of the source, or {@code null} if none.
+     * @param  level     hierarchical level of the source (e.g. feature). Should not be null.
+     * @param  features  names of dataset, features or attributes used in the source.
+     */
+    public final void addSource(final Metadata metadata, final ScopeCode level, final CharSequence... features) {
+        if (metadata != null) {
+            final DefaultSource source = new DefaultSource();
+            final DefaultScope scope = new DefaultScope(level);
+            source.setSourceReferenceSystem(CollectionsExt.first(metadata.getReferenceSystemInfo()));
+            for (final Identification id : metadata.getIdentificationInfo()) {
+                source.setSourceCitation(id.getCitation());
+                source.setSourceSpatialResolution(CollectionsExt.first(id.getSpatialResolutions()));
+                scope.setExtents(id.getExtents());
+                if (features != null && features.length != 0) {
+                    final DefaultScopeDescription sd = new DefaultScopeDescription();
+                    sd.setLevelDescription(level, new LinkedHashSet<>(Arrays.asList(features)));
+                    scope.getLevelDescription().add(sd);
+                }
+                source.setScope(scope.isEmpty() ? null : scope);
+                if (!source.isEmpty()) {
+                    addIfNotPresent(lineage().getSources(), source);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
      * Adds information about the procedure, process and algorithm applied in a process step.
      * If a processing was already defined with a different identifier, then a new processing
      * instance will be created. Storage location is:
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ConcatenatedFeatureSetTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ConcatenatedFeatureSetTest.java
index fbbbff4..90bdb2c 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ConcatenatedFeatureSetTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ConcatenatedFeatureSetTest.java
@@ -1,40 +1,67 @@
+/*
+ * 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;
 
+import java.util.Arrays;
+import java.util.Collections;
+import org.opengis.metadata.Metadata;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.util.NullArgumentException;
+import org.apache.sis.test.DependsOnMethod;
+import org.apache.sis.test.TestCase;
 import org.junit.Test;
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.metadata.Metadata;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.function.Consumer;
 
 import static org.junit.Assert.*;
 
-public class ConcatenatedFeatureSetTest {
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+
 
+/**
+ * Tests {@link ConcatenatedFeatureSet}.
+ *
+ * @author  Alexis Manin (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class ConcatenatedFeatureSetTest extends TestCase {
+    /**
+     * Tests the concatenation of two feature sets having the same feature type.
+     *
+     * @throws DataStoreException if an error occurred while concatenating the feature sets.
+     */
     @Test
-    public void testSimple() throws DataStoreException {
-        System.out.println(System.getenv("BASH"));
+    public void testSameType() throws DataStoreException {
         final FeatureTypeBuilder builder = new FeatureTypeBuilder();
         builder.addAttribute(String.class).setName("label");
         builder.addAttribute(Integer.class).setName("amount");
         builder.setName("first");
 
         final FeatureType ft = builder.build();
-
         final FeatureSet fs1 = new MemoryFeatureSet(null, ft, Arrays.asList(ft.newInstance(), ft.newInstance()));
         final FeatureSet fs2 = new MemoryFeatureSet(null, ft, Arrays.asList(ft.newInstance()));
-
-        final ConcatenatedFeatureSet cfs = new ConcatenatedFeatureSet(fs1, fs2);
+        final FeatureSet cfs = ConcatenatedFeatureSet.create(fs1, fs2);
 
         final FeatureType cType = cfs.getType();
-        assertNotNull("Concatenation feature type", cType);
-        assertEquals("Concatenation feature type", ft, cType);
+        assertSame("getType()", ft, cType);
 
         final Metadata md = cfs.getMetadata();
         assertNotNull("Concatenation metadata", md);
@@ -43,10 +70,17 @@ public class ConcatenatedFeatureSetTest {
         assertEquals("Number of features", 3, count);
     }
 
+    /**
+     * Tests the concatenation of two feature sets having different feature types.
+     *
+     * @throws DataStoreException if an error occurred while concatenating the feature sets.
+     */
     @Test
+    @DependsOnMethod("testSameType")
     public void testCommonSuperType() throws DataStoreException {
-        /* First, we prepare two types sharing a common ancestor. We'll create two types using same properties, so we
-         * can ensure that all data is exposed upon traversal, not only data defined in the super type.
+        /*
+         * First, we prepare two types sharing a common ancestor. We'll create two types using same properties,
+         * so we can ensure that all data is exposed upon traversal, not only data defined in the super type.
          */
         final FeatureTypeBuilder builder = new FeatureTypeBuilder();
         builder.addAttribute(Integer.class).setName("value");
@@ -58,11 +92,8 @@ public class ConcatenatedFeatureSetTest {
         builder.setSuperTypes(superType);
         builder.addAttribute(String.class).setName("label");
 
-        builder.setName("t1");
-        final FeatureType t1 = builder.build();
-
-        builder.setName("t2");
-        final FeatureType t2 = builder.build();
+        final FeatureType t1 = builder.setName("t1").build();
+        final FeatureType t2 = builder.setName("t2").build();
 
         // Populate a feature set for first type.
         final Feature t1f1 = t1.newInstance();
@@ -75,7 +106,7 @@ public class ConcatenatedFeatureSetTest {
 
         final FeatureSet t1fs = new MemoryFeatureSet(null, t1, Arrays.asList(t1f1, t1f2));
 
-        // Populate a feature set for second type
+        // Populate a feature set for second type.
         final Feature t2f1 = t2.newInstance();
         t2f1.setPropertyValue("value", 3);
         t2f1.setPropertyValue("label", "second-first");
@@ -84,89 +115,71 @@ public class ConcatenatedFeatureSetTest {
         t2f2.setPropertyValue("value", 4);
         t2f2.setPropertyValue("label", "second-second");
 
-        final FeatureSet t2fs = new MemoryFeatureSet(null, t1, Arrays.asList(t2f1, t2f2));
-
-        /* First, we'll test that total sum of value property is coherent with initialized features. After that, we will
-         * ensure that we can get back the right labels for each subtype.
+        final FeatureSet t2fs = new MemoryFeatureSet(null, t2, Arrays.asList(t2f1, t2f2));
+        /*
+         * First, we'll test that total sum of value property is coherent with initialized features.
+         * After that, we will ensure that we can get back the right labels for each subtype.
          */
-        final ConcatenatedFeatureSet set = new ConcatenatedFeatureSet(t1fs, t2fs);
+        final FeatureSet set = ConcatenatedFeatureSet.create(t1fs, t2fs);
         final int sum = set.features(true)
                 .mapToInt(f -> (int) f.getPropertyValue("value"))
                 .sum();
-        assertEquals("Sum of feature \'value\' property", 12, sum);
+        assertEquals("Sum of feature `value` property", 12, sum);
 
         final Object[] t1labels = set.features(false)
                 .filter(f -> t1.equals(f.getType()))
                 .map(f -> f.getPropertyValue("label"))
                 .toArray();
-        assertArrayEquals("First type labels", new String[]{"first-first", "first-second"}, t1labels);
-
+        assertArrayEquals("First type labels", new String[] {"first-first", "first-second"}, t1labels);
 
         final Object[] t2labels = set.features(false)
                 .filter(f -> t2.equals(f.getType()))
                 .map(f -> f.getPropertyValue("label"))
                 .toArray();
-        assertArrayEquals("First type labels", new String[]{"second-first", "second-second"}, t2labels);
+        assertArrayEquals("First type labels", new String[] {"second-first", "second-second"}, t2labels);
     }
 
+    /**
+     * Tests the concatenation of two feature sets having no common parent.
+     * Creation of {@link ConcatenatedFeatureSet} is expected to fail.
+     */
     @Test
+    @DependsOnMethod("testCommonSuperType")
     public void noCommonType() {
         final FeatureTypeBuilder builder = new FeatureTypeBuilder();
         builder.setName("super");
         final FeatureType mockSuperType = builder.build();
-
-        builder.setName("first");
-        builder.setSuperTypes(mockSuperType);
-        final FeatureType firstType = builder.build();
-
-        builder.clear();
-        builder.setName("second");
-        final FeatureType secondType = builder.build();
-
-        expectArgumentFailure(
-                () -> new ConcatenatedFeatureSet(
-                        new MemoryFeatureSet(null, firstType, Collections.EMPTY_LIST),
-                        new MemoryFeatureSet(null, secondType, Collections.EMPTY_LIST)
-                ),
-                concatenation -> fail("Concatenation succeeded despite the lack of common type. Concatenation expose type: "+concatenation)
-        );
-    }
-
-    @Test
-    public void lackOfInput() {
-        expectArgumentFailure(
-                () -> new ConcatenatedFeatureSet(),
-                concatenation -> fail("An empty concatenation has been created. Type: "+concatenation.getType())
-        );
-
-        final FeatureTypeBuilder builder = new FeatureTypeBuilder();
-        builder.setName("mock");
-        final FeatureType mockType = builder.build();
-
-        expectArgumentFailure(
-                () -> new ConcatenatedFeatureSet(new MemoryFeatureSet(null, mockType, Collections.EMPTY_LIST)),
-                concatenation -> fail("A concatenation has been created from a single type: "+concatenation.getType())
-        );
-    }
-
-    private static <T> void expectArgumentFailure(DataStoreComputing<T> input, Consumer<T> onUnexpectedSuccess) {
-        final T computedValue;
+        final FeatureType firstType  = builder.setSuperTypes(mockSuperType).setName("first").build();
+        final FeatureType secondType = builder.clear().setName("second").build();
+        final FeatureSet fs1 = new MemoryFeatureSet(null, firstType,  Collections.emptyList());
+        final FeatureSet fs2 = new MemoryFeatureSet(null, secondType, Collections.emptyList());
         try {
-            computedValue = input.compute();
-        } catch (NullArgumentException|IllegalArgumentException e) {
-            // expected behavior
-            return;
+            FeatureSet concatenation = ConcatenatedFeatureSet.create(fs1, fs2);
+            fail("Concatenation succeeded despite the lack of common type. Result is:\n" + concatenation);
+        } catch (DataStoreContentException e) {
+            // Expected behavior.
         } catch (DataStoreException e) {
-            fail("Concatenation failed with an error reserved for subset access. " +
-                    "It should have used an null or illegal argument exception to denote the lack of input.");
-            return; // Useless, but it allows compiler to understand that computed value can be final.
+            fail("Concatenation failed with an error reserved for other kind of error.");
         }
-
-        onUnexpectedSuccess.accept(computedValue);
     }
 
-    @FunctionalInterface
-    private static interface DataStoreComputing<T> {
-        T compute() throws DataStoreException;
+    /**
+     * Tests that no concatenated transform is created from less than 2 types.
+     *
+     * @throws DataStoreException if an error occurred while invoking the create method.
+     */
+    @Test
+    public void lackOfInput() throws DataStoreException {
+        try {
+            final FeatureSet set = ConcatenatedFeatureSet.create();
+            fail("An empty concatenation has been created:\n" + set);
+        } catch (IllegalArgumentException e) {
+            // This is the expected exception.
+        }
+        final FeatureTypeBuilder builder = new FeatureTypeBuilder().setName("mock");
+        final FeatureType mockType = builder.build();
+        final FeatureSet fs1 = new MemoryFeatureSet(null, mockType, Collections.emptyList());
+        final FeatureSet set = ConcatenatedFeatureSet.create(fs1);
+        assertSame("A concatenation has been created from a single type.", fs1, set);
     }
 }
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
index da50e25..084d68c 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
@@ -53,6 +53,7 @@ import org.junit.BeforeClass;
     org.apache.sis.internal.storage.csv.StoreTest.class,
     org.apache.sis.internal.storage.folder.StoreTest.class,
     org.apache.sis.internal.storage.JoinFeatureSetTest.class,
+    org.apache.sis.internal.storage.ConcatenatedFeatureSetTest.class,
     org.apache.sis.storage.DataStoresTest.class
 })
 public final strictfp class StorageTestSuite extends TestSuite {


[sis] 04/05: Minor adjustment of imports order.

Posted by de...@apache.org.
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 4f4ef959acd81df3694dbd4cde00e546b9aee115
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jun 19 01:30:45 2019 +0200

    Minor adjustment of imports order.
---
 .../src/main/java/org/apache/sis/internal/netcdf/RasterResource.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 2bff917..8034aea 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -25,7 +25,6 @@ import java.nio.file.Path;
 import java.nio.Buffer;
 import java.awt.image.DataBuffer;
 
-import org.apache.sis.coverage.grid.GridRoundingMode;
 import org.opengis.util.GenericName;
 import org.opengis.referencing.operation.MathTransform1D;
 import org.opengis.referencing.operation.TransformException;
@@ -39,6 +38,7 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridDerivation;
+import org.apache.sis.coverage.grid.GridRoundingMode;
 import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;


[sis] 01/05: feat(Storage): add a feature set decoration, allowing to view a sequence of feature sets as a single one.

Posted by de...@apache.org.
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 2f67744dcd2c7e3fb74c286aeaec1d0d8e71e7fa
Author: Alexis Manin <al...@gmail.com>
AuthorDate: Mon Jun 17 15:27:32 2019 +0200

    feat(Storage): add a feature set decoration, allowing to view a sequence of feature sets as a single one.
---
 .../internal/storage/ConcatenatedFeatureSet.java   | 271 +++++++++++++++++++++
 .../storage/ConcatenatedFeatureSetTest.java        | 172 +++++++++++++
 2 files changed, 443 insertions(+)

diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
new file mode 100644
index 0000000..e7dded1
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
@@ -0,0 +1,271 @@
+package org.apache.sis.internal.storage;
+
+import org.apache.sis.geometry.AbstractEnvelope;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.event.ChangeEvent;
+import org.apache.sis.storage.event.ChangeListener;
+import org.apache.sis.util.NullArgumentException;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Metadata;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.GenericName;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+
+/**
+ * Expose a sequence of {@link FeatureSet feature sets} as a single one. A few notes :
+ * <ol>
+ *     <li>The feature set concatenation must be built with a non-empty array or collection of feature set. It is copied
+ *     as is in a unmodifiable list, meaning that doublons won't be removed, and iteration order is driven by input
+ *     sequence. </li>
+ *     <li>All input feature sets must share a common type, or at least a common super-type. If you want to sequence
+ *     sets which does not share any common parent, please pre-process them to modify their public type.</li>
+ *     <li>For now, event system is disabled, meaning that trying to attach a listener to the feature set concatenation
+ *     will throw an {@link UnsupportedOperationException}. It's meant to evolve.</li>
+ * </ol>
+ */
+public class ConcatenatedFeatureSet implements FeatureSet {
+
+    private final List<FeatureSet> sources;
+    private final FeatureType commonType;
+
+    /**
+     * Temporarily cache union of underlying feature set envelopes. However, only a weak reference is used, because
+     * we cannot prevent subsets to be updated externally. Keeping a temporary reference allows for an eventual update
+     * of the concatenated envelope. The same logic is applied for {@link #getMetadata() metadata accessor}.
+     */
+    private WeakReference<Envelope> cachedEnvelope;
+
+    /**
+     * Cache metadata only temporarily, because it depends on {@link #getEnvelope() envelope accessor}. For more
+     * information about why the cache is only temporary, please read {@link #cachedEnvelope} documentation.
+     */
+    private WeakReference<Metadata> cachedMetadata;
+
+    /**
+     * See {@link #ConcatenatedFeatureSet(FeatureSet...)}.
+     */
+    public ConcatenatedFeatureSet(final Collection<FeatureSet> sources) throws DataStoreException {
+        this(sources == null? null : sources.toArray(new FeatureSet[sources.size()]));
+    }
+
+    /**
+     * Create a new feature set being a view of the sequence of given sets.
+     *
+     * @param sources The sequence of feature set to expose in a single set. Must neither be null, empty nor contains
+     *                a single element only.
+     * @throws DataStoreException If given feature sets does not share any common type.
+     */
+    public ConcatenatedFeatureSet(final FeatureSet... sources) throws DataStoreException {
+        ensureNonNull("Sources feature sets", sources);
+        if (sources.length < 1) {
+            throw new IllegalArgumentException("Given feature set sequence is empty.");
+        } else if (sources.length == 1) {
+            throw new IllegalArgumentException("You are trying to concatenate a single feature set.");
+        }
+
+        final FeatureSet[] copy = Arrays.copyOf(sources, sources.length);
+        this.sources = Collections.unmodifiableList(Arrays.asList(copy));
+        commonType = checkType(this.sources);
+        // TODO: we should add listeners on source feature sets. By doing it, we could be notified of changes, allowing
+        // a better update strategy for envelope, metadata and type.
+    }
+
+    @Override
+    public FeatureType getType() {
+        return commonType;
+    }
+
+    @Override
+    public Stream<Feature> features(boolean parallel) {
+        final Stream<FeatureSet> sets = parallel? sources.parallelStream() : sources.stream();
+        return sets.flatMap(set -> {
+            try {
+                return set.features(parallel);
+            } catch (DataStoreException e) {
+                throw new BackingStoreException(e);
+            }
+        });
+    }
+
+    @Override
+    public synchronized Envelope getEnvelope() throws DataStoreException {
+        Envelope result = cachedEnvelope == null? null : cachedEnvelope.get();
+        if (result == null) {
+            result = computeEnvelope();
+            if (result != null)
+                cachedEnvelope = new WeakReference<>(result);
+        }
+
+        return result;
+    }
+
+    private Envelope computeEnvelope() throws DataStoreException {
+        Envelope tmpEnv = sources.get(0).getEnvelope();
+        if (tmpEnv == null)
+            return null;
+
+        final GeneralEnvelope envelope = new GeneralEnvelope(tmpEnv);
+        for (int i = 1 ; i < sources.size() ; i++) {
+            tmpEnv = sources.get(i).getEnvelope();
+            if (tmpEnv == null) {
+                return null;
+            }
+            envelope.add(tmpEnv);
+        }
+
+        return envelope;
+    }
+
+    @Override
+    public GenericName getIdentifier() {
+        return null;
+    }
+
+    @Override
+    public synchronized Metadata getMetadata() throws DataStoreException {
+        Metadata result = cachedMetadata == null ? null : cachedMetadata.get();
+        if (result == null) {
+            final MetadataBuilder builder = new MetadataBuilder();
+            builder.addFeatureType(getType(), null);
+            final Envelope env = getEnvelope();
+            if (env != null) {
+                try {
+                    builder.addExtent(AbstractEnvelope.castOrCopy(env));
+                } catch (TransformException e) {
+                    // TODO: log error, or use a warning listener. It's fine if the extent cannot be computed
+                }
+            }
+
+            result = builder.build(true);
+            cachedMetadata = new WeakReference<>(result);
+        }
+
+        return result;
+    }
+
+    @Override
+    public <T extends ChangeEvent> void addListener(ChangeListener<? super T> listener, Class<T> eventType) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T extends ChangeEvent> void removeListener(ChangeListener<? super T> listener, Class<T> eventType) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Find a feature type common to all given feature sets. It means we will search for a super type used by all input
+     * feature sets.
+     *
+     * @implNote A graph approach would surely be the best way to resolve this problem. However, due to the lack of
+     * standard tools in the jdk, we'd rather build a map with every distinct encountered type/super type. For each, we
+     * associate the number of occurrences found, as the maximum number of inheritors for this type. When done, we
+     * simply give back a type with an encounter count equal to the number of input feature sets, and the least possible
+     *  number of inheritors.
+     *
+     * @param sources The set of feature sets to find a common type for. Must not be null.
+     * @return A feature type which can be found in every encountered feature set, as super type or direct one.
+     * @throws DataStoreException If an error occurs while reading data types from source feature sets.
+     * @throws IllegalArgumentException If we cannot find a common type, or input collection is empty.
+     * @throws NullArgumentException If input collection is null.
+     */
+    private static FeatureType checkType(final Collection<FeatureSet> sources) throws DataStoreException, IllegalArgumentException, NullArgumentException {
+        ensureNonNull("Source sets", sources);
+
+        final java.util.Iterator<FeatureSet> it = sources.iterator();
+        if (!it.hasNext())
+            throw new IllegalArgumentException("Cannot find a common data type from an empty collection.");
+
+        FeatureType nextType = it.next().getType();
+
+        final Map<FeatureType, Indirection> typeMatchCount = new HashMap<>();
+        // Coupling Stacks allow us to properly browse and document indirection levels in encountered data types.
+        Deque<FeatureType> typeStack = new ArrayDeque<>();
+        Deque<FeatureType> nextLevel = new ArrayDeque<>();
+
+        nextLevel.push(nextType);
+        int indirection = -1;
+        do {
+            indirection++;
+            // Micro opti : re-use the same two deques to avoid instantiating/resizing lots of arrays.
+            final Deque tmp = typeStack;
+            typeStack = nextLevel;
+            nextLevel = tmp;
+
+            do {
+                final FeatureType type = typeStack.pop();
+                typeMatchCount.put(type, new Indirection(indirection, 1));
+                nextLevel.addAll(type.getSuperTypes());
+            } while (!typeStack.isEmpty());
+        } while (!nextLevel.isEmpty());
+
+        while (it.hasNext()) {
+            boolean hasBeenUpdated = false;
+            nextLevel.push(it.next().getType());
+            indirection = -1;
+            do {
+                indirection++;
+                // Micro opti : re-use the same two deques to avoid instantiating/resizing lots of arrays.
+                final java.util.Deque tmp = typeStack;
+                typeStack = nextLevel;
+                nextLevel = tmp;
+
+                do {
+                    final int tmpIndi = indirection;
+                    final FeatureType type = typeStack.pop();
+                    final Indirection indi = typeMatchCount
+                            .computeIfPresent(type, (key, oldVal) -> oldVal.merge(new Indirection(tmpIndi, 1)));
+                    if (indi != null) hasBeenUpdated = true;
+                    nextLevel.addAll(type.getSuperTypes());
+                } while (!typeStack.isEmpty());
+            } while (!nextLevel.isEmpty());
+
+            // short-circuit here, if no feature type has the same count as already encounterd types.
+            if (!hasBeenUpdated)
+                throw new IllegalArgumentException("No common feature nor super type found across input feature sets");
+        }
+
+        final int requiredMatch = sources.size();
+        return typeMatchCount.entrySet().stream()
+                .filter(stat -> stat.getValue().matchCount == requiredMatch)
+                .min(Comparator.comparingInt(stat -> stat.getValue().indirection))
+                .map(Map.Entry::getKey)
+                .orElseThrow(() -> new IllegalArgumentException("Cannot find a common super type across all feature sets to concatenate"));
+    }
+
+    private static class Indirection {
+        int indirection;
+        int matchCount;
+
+        private Indirection(int indirection, final int matchCount) {
+            this.indirection = indirection;
+            this.matchCount = matchCount;
+        }
+
+        private Indirection merge(final Indirection other) {
+            if (other.indirection > indirection)
+                indirection = other.indirection;
+            matchCount += other.matchCount;
+
+            return this;
+        }
+    }
+}
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ConcatenatedFeatureSetTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ConcatenatedFeatureSetTest.java
new file mode 100644
index 0000000..fbbbff4
--- /dev/null
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/ConcatenatedFeatureSetTest.java
@@ -0,0 +1,172 @@
+package org.apache.sis.internal.storage;
+
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.util.NullArgumentException;
+import org.junit.Test;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.metadata.Metadata;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Consumer;
+
+import static org.junit.Assert.*;
+
+public class ConcatenatedFeatureSetTest {
+
+    @Test
+    public void testSimple() throws DataStoreException {
+        System.out.println(System.getenv("BASH"));
+        final FeatureTypeBuilder builder = new FeatureTypeBuilder();
+        builder.addAttribute(String.class).setName("label");
+        builder.addAttribute(Integer.class).setName("amount");
+        builder.setName("first");
+
+        final FeatureType ft = builder.build();
+
+        final FeatureSet fs1 = new MemoryFeatureSet(null, ft, Arrays.asList(ft.newInstance(), ft.newInstance()));
+        final FeatureSet fs2 = new MemoryFeatureSet(null, ft, Arrays.asList(ft.newInstance()));
+
+        final ConcatenatedFeatureSet cfs = new ConcatenatedFeatureSet(fs1, fs2);
+
+        final FeatureType cType = cfs.getType();
+        assertNotNull("Concatenation feature type", cType);
+        assertEquals("Concatenation feature type", ft, cType);
+
+        final Metadata md = cfs.getMetadata();
+        assertNotNull("Concatenation metadata", md);
+
+        final long count = cfs.features(false).count();
+        assertEquals("Number of features", 3, count);
+    }
+
+    @Test
+    public void testCommonSuperType() throws DataStoreException {
+        /* First, we prepare two types sharing a common ancestor. We'll create two types using same properties, so we
+         * can ensure that all data is exposed upon traversal, not only data defined in the super type.
+         */
+        final FeatureTypeBuilder builder = new FeatureTypeBuilder();
+        builder.addAttribute(Integer.class).setName("value");
+        builder.setName("parent");
+
+        final FeatureType superType = builder.build();
+
+        builder.clear();
+        builder.setSuperTypes(superType);
+        builder.addAttribute(String.class).setName("label");
+
+        builder.setName("t1");
+        final FeatureType t1 = builder.build();
+
+        builder.setName("t2");
+        final FeatureType t2 = builder.build();
+
+        // Populate a feature set for first type.
+        final Feature t1f1 = t1.newInstance();
+        t1f1.setPropertyValue("value", 2);
+        t1f1.setPropertyValue("label", "first-first");
+
+        final Feature t1f2 = t1.newInstance();
+        t1f2.setPropertyValue("value", 3);
+        t1f2.setPropertyValue("label", "first-second");
+
+        final FeatureSet t1fs = new MemoryFeatureSet(null, t1, Arrays.asList(t1f1, t1f2));
+
+        // Populate a feature set for second type
+        final Feature t2f1 = t2.newInstance();
+        t2f1.setPropertyValue("value", 3);
+        t2f1.setPropertyValue("label", "second-first");
+
+        final Feature t2f2 = t2.newInstance();
+        t2f2.setPropertyValue("value", 4);
+        t2f2.setPropertyValue("label", "second-second");
+
+        final FeatureSet t2fs = new MemoryFeatureSet(null, t1, Arrays.asList(t2f1, t2f2));
+
+        /* First, we'll test that total sum of value property is coherent with initialized features. After that, we will
+         * ensure that we can get back the right labels for each subtype.
+         */
+        final ConcatenatedFeatureSet set = new ConcatenatedFeatureSet(t1fs, t2fs);
+        final int sum = set.features(true)
+                .mapToInt(f -> (int) f.getPropertyValue("value"))
+                .sum();
+        assertEquals("Sum of feature \'value\' property", 12, sum);
+
+        final Object[] t1labels = set.features(false)
+                .filter(f -> t1.equals(f.getType()))
+                .map(f -> f.getPropertyValue("label"))
+                .toArray();
+        assertArrayEquals("First type labels", new String[]{"first-first", "first-second"}, t1labels);
+
+
+        final Object[] t2labels = set.features(false)
+                .filter(f -> t2.equals(f.getType()))
+                .map(f -> f.getPropertyValue("label"))
+                .toArray();
+        assertArrayEquals("First type labels", new String[]{"second-first", "second-second"}, t2labels);
+    }
+
+    @Test
+    public void noCommonType() {
+        final FeatureTypeBuilder builder = new FeatureTypeBuilder();
+        builder.setName("super");
+        final FeatureType mockSuperType = builder.build();
+
+        builder.setName("first");
+        builder.setSuperTypes(mockSuperType);
+        final FeatureType firstType = builder.build();
+
+        builder.clear();
+        builder.setName("second");
+        final FeatureType secondType = builder.build();
+
+        expectArgumentFailure(
+                () -> new ConcatenatedFeatureSet(
+                        new MemoryFeatureSet(null, firstType, Collections.EMPTY_LIST),
+                        new MemoryFeatureSet(null, secondType, Collections.EMPTY_LIST)
+                ),
+                concatenation -> fail("Concatenation succeeded despite the lack of common type. Concatenation expose type: "+concatenation)
+        );
+    }
+
+    @Test
+    public void lackOfInput() {
+        expectArgumentFailure(
+                () -> new ConcatenatedFeatureSet(),
+                concatenation -> fail("An empty concatenation has been created. Type: "+concatenation.getType())
+        );
+
+        final FeatureTypeBuilder builder = new FeatureTypeBuilder();
+        builder.setName("mock");
+        final FeatureType mockType = builder.build();
+
+        expectArgumentFailure(
+                () -> new ConcatenatedFeatureSet(new MemoryFeatureSet(null, mockType, Collections.EMPTY_LIST)),
+                concatenation -> fail("A concatenation has been created from a single type: "+concatenation.getType())
+        );
+    }
+
+    private static <T> void expectArgumentFailure(DataStoreComputing<T> input, Consumer<T> onUnexpectedSuccess) {
+        final T computedValue;
+        try {
+            computedValue = input.compute();
+        } catch (NullArgumentException|IllegalArgumentException e) {
+            // expected behavior
+            return;
+        } catch (DataStoreException e) {
+            fail("Concatenation failed with an error reserved for subset access. " +
+                    "It should have used an null or illegal argument exception to denote the lack of input.");
+            return; // Useless, but it allows compiler to understand that computed value can be final.
+        }
+
+        onUnexpectedSuccess.accept(computedValue);
+    }
+
+    @FunctionalInterface
+    private static interface DataStoreComputing<T> {
+        T compute() throws DataStoreException;
+    }
+}