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

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

commit 1ba4cc6ec5edc6d661c7603668d9948abf4345f7
Author: jsorel <[email protected]>
AuthorDate: Wed Oct 28 16:20:25 2026 +0100

    feat(DGGS): add CodedGeometry.derivate, support queries which are not 
directly build on the CodedGeometry
---
 .../storage/dggs/DiscreteGlobalGridGeometry.java   |  18 +-
 .../FeatureSetAsDiscreteGlobalGridResource.java    |   3 +-
 .../shared/GridAsDiscreteGlobalGridResource.java   |   3 +-
 .../shared/MemoryDiscreteGlobalGridResource.java   |  14 +-
 .../TiledDiscreteGlobalGridCoverageResource.java   |   3 +-
 .../org/apache/sis/storage/rs/CodedDerivation.java | 466 +++++++++++++++++++++
 .../org/apache/sis/storage/rs/CodedGeometry.java   |  17 +
 .../org/apache/sis/storage/rs/CodedResource.java   |   6 +
 .../rs/internal/shared/AddDimCodedResource.java    |   3 +-
 .../storage/rs/internal/shared/CodedResources.java |  82 ++++
 .../rs/internal/shared/CompoundCodedResource.java  |   1 +
 .../rs/internal/shared/GridAsCodedResource.java    |   1 +
 .../rs/internal/shared/MemoryCodedResource.java    |  12 +-
 .../rs/internal/shared/ReducedCodedResource.java   |   2 +
 .../apache/sis/storage/rs/CodedDerivationTest.java | 229 ++++++++++
 .../rs/internal/shared/CodedResourcesTest.java     | 123 ++++++
 16 files changed, 973 insertions(+), 10 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/DiscreteGlobalGridGeometry.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/DiscreteGlobalGridGeometry.java
index e79400d883..0893e4db2c 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/DiscreteGlobalGridGeometry.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/DiscreteGlobalGridGeometry.java
@@ -31,6 +31,7 @@ import org.apache.sis.geometries.operation.GeometryProcessor;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.dggs.DiscreteGlobalGrid;
 import org.apache.sis.referencing.dggs.DiscreteGlobalGridHierarchy;
 import org.apache.sis.referencing.dggs.DiscreteGlobalGridReferenceSystem;
@@ -39,6 +40,7 @@ import 
org.apache.sis.referencing.operation.transform.TransformFactoryException;
 import org.apache.sis.storage.rs.CodeTransform;
 import org.apache.sis.storage.rs.CodedGeometry;
 import org.apache.sis.storage.rs.internal.shared.CodeTransforms;
+import org.apache.sis.storage.rs.internal.shared.s2.S2;
 import org.apache.sis.util.Utilities;
 
 
@@ -245,9 +247,10 @@ public class DiscreteGlobalGridGeometry extends 
CodedGeometry {
             candidates = List.of(bases);
         }
 
+        if (candidates == null) return null;
         GeneralEnvelope all = null;
-        for (Object zone : candidates) {
-            try {
+        try {
+            for (Object zone : candidates) {
                 final Zone zo = coder.decode(zone);
                 final Envelope env = zo.getEnvelope();
                 if (env != null) {
@@ -257,9 +260,16 @@ public class DiscreteGlobalGridGeometry extends 
CodedGeometry {
                         all.add(env);
                     }
                 }
-            } catch (TransformException ex) {
-                geoExtent = null;
             }
+        } catch (TransformException ex) {
+            return null;
+        }
+        if (all != null) {
+            //zone envelopes are in CRS:84
+            if (all.getCoordinateReferenceSystem() == null) {
+                
all.setCoordinateReferenceSystem(CommonCRS.WGS84.normalizedGeographic());
+            }
+            geoExtent = S2.toGeographicExtent(S2.toS2Polygon(all));
         }
         return geoExtent;
     }
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/FeatureSetAsDiscreteGlobalGridResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/FeatureSetAsDiscreteGlobalGridResource.java
index 08032a9cfd..f15152242b 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/FeatureSetAsDiscreteGlobalGridResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/FeatureSetAsDiscreteGlobalGridResource.java
@@ -45,6 +45,7 @@ import org.apache.sis.storage.dggs.DiscreteGlobalGridGeometry;
 import org.apache.sis.storage.dggs.DiscreteGlobalGridResource;
 import org.apache.sis.storage.rs.CodedCoverage;
 import org.apache.sis.storage.rs.CodedGeometry;
+import org.apache.sis.storage.rs.internal.shared.CodedResources;
 
 
 /**
@@ -153,7 +154,7 @@ public final class FeatureSetAsDiscreteGlobalGridResource 
extends AbstractResour
     @Override
     public CodedCoverage read(CodedGeometry grid, int... range) throws 
DataStoreException {
         init();
-        final DiscreteGlobalGridGeometry geometry = 
DiscreteGlobalGridResource.toDiscreteGlobalGridGeometry(grid);
+        final DiscreteGlobalGridGeometry geometry = 
DiscreteGlobalGridResource.toDiscreteGlobalGridGeometry(CodedResources.align(this,
 grid));
 
         final Optional<Envelope> envelope = featureSet.getEnvelope();
         if (!envelope.isEmpty()) {
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/GridAsDiscreteGlobalGridResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/GridAsDiscreteGlobalGridResource.java
index 657801d12a..1d760da99a 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/GridAsDiscreteGlobalGridResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/GridAsDiscreteGlobalGridResource.java
@@ -43,6 +43,7 @@ import org.apache.sis.storage.dggs.DiscreteGlobalGridGeometry;
 import org.apache.sis.storage.dggs.DiscreteGlobalGridResource;
 import org.apache.sis.storage.rs.CodedCoverage;
 import org.apache.sis.storage.rs.CodedGeometry;
+import org.apache.sis.storage.rs.internal.shared.CodedResources;
 
 
 /**
@@ -108,7 +109,7 @@ public final class GridAsDiscreteGlobalGridResource extends 
AbstractResource imp
 
     @Override
     public CodedCoverage read(CodedGeometry grid, int... range) throws 
DataStoreException {
-        final DiscreteGlobalGridGeometry geometry = 
DiscreteGlobalGridResource.toDiscreteGlobalGridGeometry(grid);
+        final DiscreteGlobalGridGeometry geometry = 
DiscreteGlobalGridResource.toDiscreteGlobalGridGeometry(CodedResources.align(this,
 grid));
         final DiscreteGlobalGridCoverageProcessor processor = new 
DiscreteGlobalGridCoverageProcessor();
         try {
             return processor.resample(source, geometry, range);
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/MemoryDiscreteGlobalGridResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/MemoryDiscreteGlobalGridResource.java
index 578ee1fc50..7b14db13d0 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/MemoryDiscreteGlobalGridResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/MemoryDiscreteGlobalGridResource.java
@@ -17,6 +17,7 @@
 package org.apache.sis.storage.dggs.internal.shared;
 
 import java.util.List;
+import org.opengis.util.FactoryException;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.measure.NumberRange;
 import org.apache.sis.storage.AbstractResource;
@@ -25,6 +26,8 @@ import org.apache.sis.storage.dggs.DiscreteGlobalGridGeometry;
 import org.apache.sis.storage.dggs.DiscreteGlobalGridResource;
 import org.apache.sis.storage.rs.CodedCoverage;
 import org.apache.sis.storage.rs.CodedGeometry;
+import org.apache.sis.storage.rs.internal.shared.CodedResources;
+import org.apache.sis.storage.rs.internal.shared.ResampledCodedCoverage;
 
 
 /**
@@ -68,7 +71,16 @@ public final class MemoryDiscreteGlobalGridResource extends 
AbstractResource imp
 
     @Override
     public CodedCoverage read(CodedGeometry geometry, int... range) throws 
DataStoreException {
-        return coverage;
+        geometry = CodedResources.align(this, geometry);
+        if (geometry.equals(coverage.getGeometry())) {
+            return coverage;
+        }
+        //view restricted to the requested zones
+        try {
+            return new ResampledCodedCoverage(null, coverage, geometry);
+        } catch (FactoryException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
     }
 
 }
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/TiledDiscreteGlobalGridCoverageResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/TiledDiscreteGlobalGridCoverageResource.java
index 0885a019b6..20b9f285b7 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/TiledDiscreteGlobalGridCoverageResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/dggs/internal/shared/TiledDiscreteGlobalGridCoverageResource.java
@@ -30,6 +30,7 @@ import org.apache.sis.storage.dggs.DiscreteGlobalGridGeometry;
 import org.apache.sis.storage.dggs.DiscreteGlobalGridResource;
 import org.apache.sis.storage.rs.CodedCoverage;
 import org.apache.sis.storage.rs.CodedGeometry;
+import org.apache.sis.storage.rs.internal.shared.CodedResources;
 
 
 /**
@@ -71,7 +72,7 @@ public abstract class TiledDiscreteGlobalGridCoverageResource 
extends AbstractRe
         final NumberRange<Integer> availableDepths = getAvailableDepths();
 
         //convert the query to this DGGRS and depth
-        DiscreteGlobalGridGeometry dggrsQuery = 
DiscreteGlobalGridResource.toDiscreteGlobalGridGeometry(query);
+        DiscreteGlobalGridGeometry dggrsQuery = 
DiscreteGlobalGridResource.toDiscreteGlobalGridGeometry(CodedResources.align(this,
 query));
         try {
             dggrsQuery = dggrsQuery.transformTo(localDggrs, availableDepths, 
getTileRelativeDepth());
         } catch (TransformException | IncommensurableException ex) {
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedDerivation.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedDerivation.java
new file mode 100644
index 0000000000..db5de4a498
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedDerivation.java
@@ -0,0 +1,466 @@
+/*
+ * 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.storage.rs;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.measure.IncommensurableException;
+import com.google.common.geometry.S2Polygon;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.ReferenceSystem;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.coverage.grid.GridDerivation;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridRoundingMode;
+import org.apache.sis.coverage.grid.IllegalGridGeometryException;
+import org.apache.sis.geometry.DirectPosition2D;
+import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.dggs.DiscreteGlobalGrid;
+import org.apache.sis.referencing.dggs.DiscreteGlobalGridHierarchy;
+import org.apache.sis.referencing.dggs.DiscreteGlobalGridReferenceSystem;
+import org.apache.sis.referencing.dggs.Zone;
+import org.apache.sis.referencing.rs.ReferenceSystems;
+import org.apache.sis.storage.dggs.DiscreteGlobalGridGeometry;
+import org.apache.sis.storage.dggs.DiscreteGlobalGridSystems;
+import 
org.apache.sis.storage.dggs.internal.shared.GridAsDiscreteGlobalGridResource;
+import org.apache.sis.storage.rs.internal.shared.CodeTransforms;
+import org.apache.sis.storage.rs.internal.shared.s2.S2;
+import org.apache.sis.util.Utilities;
+
+
+/**
+ * Creates a new coded geometry derived from a base coded geometry with a 
different extent.
+ * {@code CodedDerivation} are created by calls to {@link 
CodedGeometry#derive()}.
+ * Properties of the desired coded geometry can be specified by calls to the 
following methods,
+ * in that order (each method is optional):
+ *
+ * <ol>
+ *   <li>{@link #rounding(GridRoundingMode)}</li>
+ *   <li>{@link #subgrid(CodedGeometry)} and/or {@link #subgrid(Envelope)}</li>
+ * </ol>
+ *
+ * Then the coded geometry is created by a call to {@link #build()}.
+ * The result is aligned with the base geometry : it uses the base reference 
system components
+ * and only contains cells of the base geometry.
+ *
+ * <p>The derivation is applied on each single component of the base reference 
system:</p>
+ * <ul>
+ *   <li>Discrete Global Grid components keep the zones which intersect the 
area of interest.</li>
+ *   <li>Regular grid components are derived using {@link 
GridGeometry#derive()}.</li>
+ *   <li>Undefined components (without extent or zones), which means any cell 
is accepted,
+ *       are replaced by the area of interest, converted to the base Discrete 
Global Grid if needed.</li>
+ *   <li>Other components, or components without a matching area of interest, 
are unchanged.</li>
+ * </ul>
+ * The number of dimensions is preserved, area of interest components not in 
the base are ignored.
+ *
+ * @author Johann Sorel (Geomatys)
+ *
+ * @see CodedGeometry#derive()
+ * @see org.apache.sis.coverage.grid.GridDerivation
+ */
+public class CodedDerivation {
+
+    /**
+     * The base coded geometry from which to derive a new coded geometry.
+     */
+    protected final CodedGeometry base;
+
+    /**
+     * Single components of the base reference system, empty if the base has 
no reference system.
+     */
+    private final List<ReferenceSystem> components;
+
+    /**
+     * Base geometry of each component.
+     */
+    private final CodedGeometry[] baseSlices;
+
+    /**
+     * Current derived geometry of each component.
+     */
+    private final CodedGeometry[] slices;
+
+    /**
+     * If the base has no reference system, the area of interest to use as 
result.
+     */
+    private CodedGeometry replacement;
+
+    /**
+     * Controls behavior of rounding from floating point values to integers on 
regular grid components.
+     */
+    private GridRoundingMode rounding = GridRoundingMode.NEAREST;
+
+    /**
+     * Whether a {@code subgrid(…)} method has been invoked.
+     */
+    private boolean subgridSet;
+
+    /**
+     * Creates a new builder for deriving a coded geometry from the specified 
base.
+     *
+     * @param  base  the base to use as a template for deriving a new coded 
geometry.
+     *
+     * @see CodedGeometry#derive()
+     */
+    protected CodedDerivation(final CodedGeometry base) {
+        this.base = Objects.requireNonNull(base);
+        final ReferenceSystem rs = base.getReferenceSystem();
+        components = (rs == null) ? List.of() : 
ReferenceSystems.getSingleComponents(rs, true);
+        baseSlices = new CodedGeometry[components.size()];
+        for (int i = 0; i < baseSlices.length; i++) {
+            baseSlices[i] = base.slice(components.get(i)).get();
+        }
+        slices = baseSlices.clone();
+    }
+
+    /**
+     * Controls behavior of rounding from floating point values to integers on 
regular grid components.
+     * If this method is never invoked, the default value is {@link 
GridRoundingMode#NEAREST}.
+     *
+     * @param  mode  the new rounding mode.
+     * @return {@code this} for method call chaining.
+     * @throws IllegalStateException if a {@code subgrid(…)} method has 
already been invoked.
+     *
+     * @see 
org.apache.sis.coverage.grid.GridDerivation#rounding(GridRoundingMode)
+     */
+    public CodedDerivation rounding(final GridRoundingMode mode) {
+        if (subgridSet) {
+            throw new IllegalStateException("Rounding must be set before 
subgrid");
+        }
+        rounding = Objects.requireNonNull(mode);
+        return this;
+    }
+
+    /**
+     * Adapts the base coded geometry for containing only the cells which 
intersect the given area of interest.
+     * Each component of the area of interest is matched to the base component 
which can be converted to it.
+     * Base components without a matching area of interest component are 
unchanged.
+     *
+     * <p>When the base and area of interest components use the same Discrete 
Global Grid,
+     * the zone hierarchy is used : a base zone is kept if it is, contains or 
is contained
+     * by a zone of the area of interest.</p>
+     *
+     * @param  areaOfInterest  the area of interest.
+     * @return {@code this} for method call chaining.
+     * @throws DisjointExtentException if the area of interest does not 
intersect the base geometry.
+     * @throws IllegalGridGeometryException if the derivation can not be 
computed.
+     */
+    public CodedDerivation subgrid(final CodedGeometry areaOfInterest) {
+        Objects.requireNonNull(areaOfInterest);
+        subgridSet = true;
+        if (components.isEmpty()) {
+            //base is undefined, any geometry is accepted
+            replacement = areaOfInterest;
+            return this;
+        }
+        final List<ReferenceSystem> aoiComponents = 
ReferenceSystems.getSingleComponents(areaOfInterest.getReferenceSystem(), true);
+        try {
+            for (int i = 0; i < slices.length; i++) {
+                final ReferenceSystem component = components.get(i);
+                final CoordinateReferenceSystem target = leaningCRS(component);
+                for (ReferenceSystem aoiComponent : aoiComponents) {
+                    if (!isConvertible(leaningCRS(aoiComponent), target)) 
continue;
+                    final CodedGeometry aoi = 
areaOfInterest.slice(aoiComponent).get();
+                    if (component instanceof 
DiscreteGlobalGridReferenceSystem) {
+                        slices[i] = subgridZones(asDggs(slices[i]), aoi);
+                    } else {
+                        slices[i] = subgridCells(component, slices[i], aoi);
+                    }
+                    break;
+                }
+            }
+        } catch (TransformException | IncommensurableException ex) {
+            throw new IllegalGridGeometryException(ex.getMessage(), ex);
+        }
+        return this;
+    }
+
+    /**
+     * Adapts the base coded geometry for containing only the cells which 
intersect the given envelope.
+     * Each component of the envelope CRS is matched to the base component 
which can be converted to it.
+     * Base components without a matching envelope component, or undefined, 
are unchanged.
+     * If the envelope has no CRS, it is assumed to be in the
+     * {@linkplain ReferenceSystems#getLeaningCRS(ReferenceSystem) leaning 
CRS} of the base geometry.
+     *
+     * @param  areaOfInterest  the area of interest.
+     * @return {@code this} for method call chaining.
+     * @throws DisjointExtentException if the area of interest does not 
intersect the base geometry.
+     * @throws IllegalGridGeometryException if the derivation can not be 
computed.
+     */
+    public CodedDerivation subgrid(Envelope areaOfInterest) {
+        Objects.requireNonNull(areaOfInterest);
+        subgridSet = true;
+        if (components.isEmpty()) return this;
+        try {
+            if (areaOfInterest.getCoordinateReferenceSystem() == null) {
+                final GeneralEnvelope env = new 
GeneralEnvelope(areaOfInterest);
+                
env.setCoordinateReferenceSystem(ReferenceSystems.getLeaningCRS(base.getReferenceSystem()));
+                areaOfInterest = env;
+            }
+            for (int i = 0; i < slices.length; i++) {
+                final ReferenceSystem component = components.get(i);
+                if (component instanceof DiscreteGlobalGridReferenceSystem) {
+                    slices[i] = subgrid(asDggs(slices[i]), areaOfInterest);
+                } else if (component instanceof CoordinateReferenceSystem crs) 
{
+                    final Optional<GridGeometry> grid = 
slices[i].isRegularGrid().filter(CodedDerivation::isComplete);
+                    if (grid.isPresent()) {
+                        final Envelope env = subEnvelope(areaOfInterest, crs);
+                        if (env != null) {
+                            slices[i] = toGeometry(component, 
grid.get().derive().rounding(rounding).subgrid(env).build());
+                        }
+                    }
+                }
+            }
+        } catch (TransformException | FactoryException ex) {
+            throw new IllegalGridGeometryException(ex.getMessage(), ex);
+        }
+        return this;
+    }
+
+    /**
+     * Builds a coded geometry with the configurations specified by the other 
methods in this class.
+     *
+     * @return the modified coded geometry. May be the {@link #base} geometry 
if no change apply.
+     */
+    public CodedGeometry build() {
+        if (replacement != null) return replacement;
+        for (int i = 0; i < slices.length; i++) {
+            if (slices[i] != baseSlices[i]) {
+                return CodedGeometry.compound(slices);
+            }
+        }
+        return base;
+    }
+
+    /**
+     * Derives a DGGRS component using a coded area of interest.
+     */
+    private static CodedGeometry subgridZones(DiscreteGlobalGridGeometry 
slice, CodedGeometry aoi)
+            throws TransformException, IncommensurableException {
+        final DiscreteGlobalGridReferenceSystem dggrs = 
slice.getReferenceSystem();
+        final List<Object> zoneIds = slice.getZoneIds();
+
+        if (aoi.getReferenceSystem() instanceof 
DiscreteGlobalGridReferenceSystem) {
+            final DiscreteGlobalGridGeometry dggsAoi = asDggs(aoi);
+            final List<Object> aoiIds = dggsAoi.getZoneIds();
+            if (aoiIds == null) return slice;
+            if (aoiIds.isEmpty()) throw new DisjointExtentException("Area of 
interest has no zones");
+            final boolean sameDggrs = 
dggrs.equals(dggsAoi.getReferenceSystem());
+            if (zoneIds == null) {
+                //base accepts any zone, use the area of interest zones
+                return sameDggrs ? dggsAoi : dggsAoi.transformTo(dggrs, null, 
null);
+            }
+            return sameDggrs ? subgrid(slice, dggsAoi) : subgrid(slice, 
dggsAoi.getEnvelope());
+        }
+
+        final Optional<GridGeometry> grid = 
aoi.isRegularGrid().filter(CodedDerivation::isComplete);
+        if (grid.isEmpty()) return slice;
+        if (zoneIds == null) {
+            //base accepts any zone, use the zones matching the grid resolution
+            return toZones(dggrs, grid.get());
+        }
+        return subgrid(slice, grid.get().getEnvelope());
+    }
+
+    /**
+     * Derives a non DGGRS component using a coded area of interest.
+     */
+    private CodedGeometry subgridCells(ReferenceSystem component, 
CodedGeometry slice, CodedGeometry aoi) {
+        final Optional<GridGeometry> grid = 
slice.isRegularGrid().filter(CodedDerivation::isComplete);
+        if (grid.isPresent()) {
+            final GridDerivation derivation = 
grid.get().derive().rounding(rounding);
+            final Optional<GridGeometry> aoiGrid = 
aoi.isRegularGrid().filter(CodedDerivation::isComplete);
+            if (aoiGrid.isPresent()) {
+                return toGeometry(component, 
derivation.subgrid(aoiGrid.get()).build());
+            }
+            final Envelope env = envelopeOf(aoi);
+            return (env != null) ? toGeometry(component, 
derivation.subgrid(env).build()) : slice;
+        }
+        if (!slice.isDefined(CodedGeometry.EXTENT | CodedGeometry.GRID_TO_RS)
+                && Utilities.equalsIgnoreMetadata(component, 
aoi.getReferenceSystem())) {
+            //base accepts any cell, use the area of interest
+            return new CodedGeometry(component, aoi.getExtent(), 
aoi.getGridToRS(), null);
+        }
+        return slice;
+    }
+
+    /**
+     * Keeps the zones of the given slice which intersect the envelope.
+     */
+    private static CodedGeometry subgrid(DiscreteGlobalGridGeometry slice, 
Envelope areaOfInterest) throws TransformException {
+        final List<Object> zoneIds = slice.getZoneIds();
+        if (zoneIds == null || zoneIds.isEmpty()) return slice;
+        final Envelope env = subEnvelope(areaOfInterest, 
CommonCRS.WGS84.normalizedGeographic());
+        if (env == null) return slice;
+
+        final DiscreteGlobalGridHierarchy hierarchy = 
slice.getReferenceSystem().getGridSystem().getHierarchy();
+        final List<Object> kept = new ArrayList<>();
+        if (env.getSpan(0) == 0 && env.getSpan(1) == 0) {
+            //envelope degenerates to a point, keep the zone which contains it
+            final DirectPosition2D dp = new 
DirectPosition2D(env.getCoordinateReferenceSystem(), env.getMinimum(0), 
env.getMinimum(1));
+            final long zid = 
hierarchy.getGrids().get(slice.getRefinementLevel()).getZone(dp).getLongIdentifier();
+            for (Object zoneId : zoneIds) {
+                if (hierarchy.getZone(zoneId).getLongIdentifier() == zid) 
kept.add(zoneId);
+            }
+        } else {
+            final S2Polygon polygon = S2.toS2Polygon(env);
+            for (Object zoneId : zoneIds) {
+                final S2Polygon geometry = 
DiscreteGlobalGridSystems.toS2Polygon(hierarchy.getZone(zoneId).getGeographicExtent());
+                if (geometry == null || polygon.intersects(geometry)) 
kept.add(zoneId);
+            }
+        }
+        return toGeometry(slice, kept);
+    }
+
+    /**
+     * Keeps the zones of the given slice which are, contain or are contained 
by a zone of the area of interest.
+     * Both geometries must use the same DGGRS.
+     */
+    private static CodedGeometry subgrid(DiscreteGlobalGridGeometry slice, 
DiscreteGlobalGridGeometry areaOfInterest) {
+        final List<Object> zoneIds = slice.getZoneIds();
+        if (zoneIds.isEmpty()) return slice;
+
+        final DiscreteGlobalGridHierarchy hierarchy = 
slice.getReferenceSystem().getGridSystem().getHierarchy();
+        final int level = slice.getRefinementLevel();
+        final int aoiLevel = areaOfInterest.getRefinementLevel();
+
+        //compare zones at the coarsest level of both geometries
+        final Set<Long> aoiZones = new HashSet<>();
+        for (Object zoneId : areaOfInterest.getZoneIds()) {
+            Zone zone = hierarchy.getZone(zoneId);
+            if (aoiLevel > level) zone = zone.getFirstParent(level);
+            aoiZones.add(zone.getLongIdentifier());
+        }
+        final List<Object> kept = new ArrayList<>();
+        for (Object zoneId : zoneIds) {
+            Zone zone = hierarchy.getZone(zoneId);
+            if (level > aoiLevel) zone = zone.getFirstParent(aoiLevel);
+            if (aoiZones.contains(zone.getLongIdentifier())) kept.add(zoneId);
+        }
+        return toGeometry(slice, kept);
+    }
+
+    /**
+     * Returns the zones intersecting the given grid, at the level matching 
the grid resolution.
+     */
+    private static CodedGeometry toZones(DiscreteGlobalGridReferenceSystem 
dggrs, GridGeometry grid)
+            throws TransformException, IncommensurableException {
+        final DiscreteGlobalGridReferenceSystem.Coder coder = 
dggrs.createCoder();
+        
coder.setPrecision(GridAsDiscreteGlobalGridResource.computeAverageResolution(grid),
 null);
+        final DiscreteGlobalGrid dgg = 
dggrs.getGridSystem().getHierarchy().getGrids().get(coder.getPrecisionLevel());
+        final List<Object> zoneIds;
+        try (Stream<Zone> zones = 
dgg.getZones(grid.getEnvelope(dggrs.getGridSystem().getCrs()))) {
+            zoneIds = zones.map(Zone::getIdentifier).toList();
+        }
+        if (zoneIds.isEmpty()) throw new DisjointExtentException("Area of 
interest does not intersect any zone");
+        return DiscreteGlobalGridGeometry.unstructured(dggrs, zoneIds, null);
+    }
+
+    private static CodedGeometry toGeometry(DiscreteGlobalGridGeometry slice, 
List<Object> kept) {
+        if (kept.isEmpty()) throw new DisjointExtentException("Area of 
interest does not intersect any zone");
+        if (kept.size() == slice.getZoneIds().size()) return slice;
+        return 
DiscreteGlobalGridGeometry.unstructured(slice.getReferenceSystem(), kept, null);
+    }
+
+    private static CodedGeometry toGeometry(ReferenceSystem component, 
GridGeometry grid) {
+        return new CodedGeometry(component, grid.getExtent(), 
CodeTransforms.toTransform(grid), null);
+    }
+
+    /**
+     * Returns the given DGGRS slice as a DiscreteGlobalGridGeometry.
+     */
+    private static DiscreteGlobalGridGeometry asDggs(CodedGeometry slice) {
+        if (slice instanceof DiscreteGlobalGridGeometry dggs) return dggs;
+        return 
DiscreteGlobalGridGeometry.unstructured((DiscreteGlobalGridReferenceSystem) 
slice.getReferenceSystem(),
+                slice.getExtent(), slice.getGridToRS(), null);
+    }
+
+    /**
+     * Returns true if the grid geometry has an extent and a grid to CRS 
transform.
+     */
+    private static boolean isComplete(GridGeometry grid) {
+        return grid.isDefined(GridGeometry.EXTENT | GridGeometry.GRID_TO_CRS);
+    }
+
+    /**
+     * Returns the envelope of the given area of interest slice, or null if it 
can not be computed.
+     */
+    private static Envelope envelopeOf(CodedGeometry aoi) {
+        if (aoi instanceof DiscreteGlobalGridGeometry dggs) {
+            final List<Object> zoneIds = dggs.getZoneIds();
+            return (zoneIds == null || zoneIds.isEmpty()) ? null : 
dggs.getEnvelope();
+        }
+        return aoi.isRegularGrid().filter((g) -> 
g.isDefined(GridGeometry.ENVELOPE)).map(GridGeometry::getEnvelope).orElse(null);
+    }
+
+    /**
+     * Extracts from the area of interest the part which can be converted to 
the given CRS.
+     *
+     * @return envelope in target CRS, or null if the area of interest has no 
matching component.
+     */
+    private static Envelope subEnvelope(Envelope areaOfInterest, 
CoordinateReferenceSystem target) throws TransformException {
+        final CoordinateReferenceSystem crs = 
areaOfInterest.getCoordinateReferenceSystem();
+        int offset = 0;
+        for (ReferenceSystem component : 
ReferenceSystems.getSingleComponents(crs, true)) {
+            final CoordinateReferenceSystem ccrs = (CoordinateReferenceSystem) 
component;
+            final int dim = ccrs.getCoordinateSystem().getDimension();
+            if (isConvertible(ccrs, target)) {
+                final GeneralEnvelope sub = 
GeneralEnvelope.castOrCopy(areaOfInterest).subEnvelope(offset, offset + 
dim).clone();
+                sub.setCoordinateReferenceSystem(ccrs);
+                return Envelopes.transform(sub, target);
+            }
+            offset += dim;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the CRS the given reference system is based on, or null if 
unknown.
+     */
+    private static CoordinateReferenceSystem leaningCRS(ReferenceSystem rs) {
+        try {
+            return ReferenceSystems.getLeaningCRS(rs);
+        } catch (FactoryException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns true if a coordinate operation exist between the two CRS.
+     */
+    private static boolean isConvertible(CoordinateReferenceSystem source, 
CoordinateReferenceSystem target) {
+        if (source == null || target == null) return false;
+        if (Utilities.equalsIgnoreMetadata(source, target)) return true;
+        try {
+            CRS.findOperation(source, target, null);
+            return true;
+        } catch (FactoryException ex) {
+            return false;
+        }
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedGeometry.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedGeometry.java
index dc9bf081d1..221cd7d88f 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedGeometry.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedGeometry.java
@@ -329,6 +329,23 @@ public class CodedGeometry {
         return Optional.empty();
     }
 
+    /**
+     * Returns an object that can be used for creating a new coded geometry 
derived from this geometry.
+     * {@code derive()} does not change the state of this {@code 
CodedGeometry} but instead creates
+     * new instances as needed. Example:
+     *
+     * {@snippet lang="java" :
+     *     CodedGeometry subgrid = 
geometry.derive().subgrid(areaOfInterest).build();
+     *     }
+     *
+     * @return an object for deriving a coded geometry from {@code this}.
+     *
+     * @see GridGeometry#derive()
+     */
+    public CodedDerivation derive() {
+        return new CodedDerivation(this);
+    }
+
     public Optional<GridGeometry> isRegularGrid() {
         if (gridToRS instanceof CodeTransforms.Grid g) {
             return Optional.of(g.getGrid());
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedResource.java
index 193101a656..1c5aeee74e 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/CodedResource.java
@@ -169,6 +169,12 @@ public interface CodedResource extends 
BandedCoverageResource {
     /**
      * Retrieve a set of DGGRS zone data.
      *
+     * <p>The given geometry does not need to match the resource geometry 
exactly.
+     * Implementations shall align it on the resource geometry, using
+     * {@code getGridGeometry().derive().subgrid(geometry).build()}.
+     * Components of the geometry which are not in the resource are ignored,
+     * components of the resource which are not in the geometry are read 
entirely.</p>
+     *
      * @param geometry to read
      * @param range bands to select
      * @return DiscreteGlobalGridCoverage, never null
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/AddDimCodedResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/AddDimCodedResource.java
index 6b9c46967b..3d78ee1ec8 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/AddDimCodedResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/AddDimCodedResource.java
@@ -97,7 +97,8 @@ public final class AddDimCodedResource extends 
AbstractResource implements Coded
 
     @Override
     public CodedCoverage read(CodedGeometry geometry, int... range) throws 
DataStoreException {
-        final CodedCoverage coverage = source.read(geometry, range);
+        //ensure the query intersects the added dimension, the source ignores 
it
+        final CodedCoverage coverage = source.read(CodedResources.align(this, 
geometry), range);
         return new AddDimCodedCoverage(coverage, slice);
     }
 
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/CodedResources.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/CodedResources.java
new file mode 100644
index 0000000000..f5e721c413
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/CodedResources.java
@@ -0,0 +1,82 @@
+/*
+ * 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.storage.rs.internal.shared;
+
+import java.util.List;
+import org.opengis.referencing.ReferenceSystem;
+import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.coverage.grid.IllegalGridGeometryException;
+import org.apache.sis.referencing.dggs.DiscreteGlobalGridReferenceSystem;
+import org.apache.sis.referencing.rs.ReferenceSystems;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.NoSuchDataException;
+import org.apache.sis.storage.rs.CodedGeometry;
+import org.apache.sis.storage.rs.CodedResource;
+
+
+/**
+ * Utility methods for coded resources implementations.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class CodedResources {
+
+    private CodedResources(){}
+
+    /**
+     * Aligns a query on the resource geometry.
+     * If the query uses a Discrete Global Grid declared in one of the
+     * {@linkplain CodedResource#getAlternateGridGeometry() alternate 
geometries},
+     * this geometry is used, otherwise the default resource geometry is used.
+     *
+     * @param resource the resource to read
+     * @param query the user query, may not match the resource geometry
+     * @return query aligned on the resource geometry
+     * @throws NoSuchDataException if the query does not intersect the 
resource geometry
+     * @throws DataStoreException if the query can not be aligned
+     */
+    public static CodedGeometry align(CodedResource resource, CodedGeometry 
query) throws DataStoreException {
+        final CodedGeometry base = selectGeometry(resource, query);
+        try {
+            return base.derive().subgrid(query).build();
+        } catch (DisjointExtentException ex) {
+            throw new NoSuchDataException(ex.getMessage(), ex);
+        } catch (IllegalGridGeometryException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * Returns the resource geometry sharing the query Discrete Global Grid, 
or the default geometry.
+     */
+    private static CodedGeometry selectGeometry(CodedResource resource, 
CodedGeometry query) throws DataStoreException {
+        final ReferenceSystem queryRs = query.getReferenceSystem();
+        if (queryRs != null) {
+            final List<ReferenceSystem> queryComponents = 
ReferenceSystems.getSingleComponents(queryRs, true);
+            for (CodedGeometry candidate : 
resource.getAlternateGridGeometry()) {
+                final ReferenceSystem rs = candidate.getReferenceSystem();
+                if (rs == null) continue;
+                for (ReferenceSystem component : 
ReferenceSystems.getSingleComponents(rs, true)) {
+                    if (component instanceof DiscreteGlobalGridReferenceSystem 
&& queryComponents.contains(component)) {
+                        return candidate;
+                    }
+                }
+            }
+        }
+        return resource.getGridGeometry();
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/CompoundCodedResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/CompoundCodedResource.java
index a12bdd8485..e72371494c 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/CompoundCodedResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/CompoundCodedResource.java
@@ -108,6 +108,7 @@ public final class CompoundCodedResource extends 
AbstractResource implements Cod
 
     @Override
     public CodedCoverage read(CodedGeometry geometry, int... range) throws 
DataStoreException {
+        geometry = CodedResources.align(this, geometry);
         try {
             final CodedCoverage[] resampled = new 
CodedCoverage[resources.size()];
             for (int i = 0; i < resampled.length; i++) {
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/GridAsCodedResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/GridAsCodedResource.java
index 901aa138df..48a1539a10 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/GridAsCodedResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/GridAsCodedResource.java
@@ -124,6 +124,7 @@ public final class GridAsCodedResource extends 
AbstractResource implements Coded
 
     @Override
     public CodedCoverage read(CodedGeometry userQuery, int... range) throws 
DataStoreException {
+        userQuery = CodedResources.align(this, userQuery);
 
         List<SampleDimension> sampleDimensions = source.getSampleDimensions();
         if (range != null && range.length != 0) {
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/MemoryCodedResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/MemoryCodedResource.java
index 55a3370458..73b46ad9ca 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/MemoryCodedResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/MemoryCodedResource.java
@@ -18,6 +18,7 @@ package org.apache.sis.storage.rs.internal.shared;
 
 import java.util.List;
 import java.util.Optional;
+import org.opengis.util.FactoryException;
 import org.opengis.util.GenericName;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.storage.AbstractResource;
@@ -60,7 +61,16 @@ public final class MemoryCodedResource extends 
AbstractResource implements Coded
 
     @Override
     public CodedCoverage read(CodedGeometry geometry, int... range) throws 
DataStoreException {
-        return coverage;
+        geometry = CodedResources.align(this, geometry);
+        if (geometry.equals(coverage.getGeometry())) {
+            return coverage;
+        }
+        //view restricted to the requested cells
+        try {
+            return new ResampledCodedCoverage(name, coverage, geometry);
+        } catch (FactoryException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
     }
 
 }
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/ReducedCodedResource.java
 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/ReducedCodedResource.java
index 6887418fbb..06e01a36e2 100644
--- 
a/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/ReducedCodedResource.java
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/main/org/apache/sis/storage/rs/internal/shared/ReducedCodedResource.java
@@ -155,6 +155,8 @@ public final class ReducedCodedResource extends 
AbstractResource implements Code
 
     @Override
     public CodedCoverage read(CodedGeometry query, int... range) throws 
DataStoreException {
+        query = CodedResources.align(this, query);
+
         //rebuild a query on all source components, inserting the fixed slices
         final CodedGeometry[] parts = new CodedGeometry[components.size()];
         for (int i = 0; i < parts.length; i++) {
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/storage/rs/CodedDerivationTest.java
 
b/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/storage/rs/CodedDerivationTest.java
new file mode 100644
index 0000000000..f190514f01
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/storage/rs/CodedDerivationTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.storage.rs;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.opengis.referencing.crs.VerticalCRS;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridOrientation;
+import org.apache.sis.coverage.grid.GridRoundingMode;
+import org.apache.sis.geometry.DirectPosition2D;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.dggs.DiscreteGlobalGridHierarchy;
+import org.apache.sis.referencing.dggs.DiscreteGlobalGridReferenceSystem;
+import org.apache.sis.referencing.dggs.Zone;
+import org.apache.sis.referencing.dggs.s2.S2Dggrs;
+import org.apache.sis.storage.dggs.DiscreteGlobalGridGeometry;
+
+// Test dependencies
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+
+/**
+ * Tests {@link CodedDerivation}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class CodedDerivationTest {
+
+    private final DiscreteGlobalGridReferenceSystem dggrs = new S2Dggrs();
+    private final DiscreteGlobalGridHierarchy hierarchy = 
dggrs.getGridSystem().getHierarchy();
+    private final VerticalCRS verticalCrs = 
CommonCRS.Vertical.MEAN_SEA_LEVEL.crs();
+
+    /**
+     * Vertical grid of 3 cells from 0 to 300 metres.
+     */
+    private GridGeometry verticalGrid() {
+        final GeneralEnvelope env = new GeneralEnvelope(verticalCrs);
+        env.setRange(0, 0, 300);
+        return new GridGeometry(new GridExtent(null, 0, 2, true), env, 
GridOrientation.HOMOTHETY);
+    }
+
+    private List<Object> zoneIds(int level) throws TransformException {
+        return 
hierarchy.getGrids().get(level).getZones().map(Zone::getIdentifier).toList();
+    }
+
+    private Zone zoneAt(int level, double lon, double lat) throws 
TransformException {
+        return hierarchy.getGrids().get(level).getZone(new 
DirectPosition2D(CommonCRS.WGS84.normalizedGeographic(), lon, lat));
+    }
+
+    /**
+     * Tests that no derivation returns the base geometry.
+     */
+    @Test
+    public void testNoChange() throws Exception {
+        final CodedGeometry base = 
DiscreteGlobalGridGeometry.unstructured(dggrs, zoneIds(1), null);
+        assertSame(base, base.derive().build());
+    }
+
+    /**
+     * Tests that rounding can not be set after subgrid.
+     */
+    @Test
+    public void testRoundingAfterSubgrid() throws Exception {
+        final CodedGeometry base = new CodedGeometry(verticalGrid());
+        final CodedDerivation derivation = base.derive().subgrid(new 
CodedGeometry(verticalGrid()));
+        assertThrows(IllegalStateException.class, () -> 
derivation.rounding(GridRoundingMode.ENCLOSING));
+    }
+
+    /**
+     * Tests subgrid with an envelope covering both the DGGRS and the vertical 
components.
+     */
+    @Test
+    public void testSubgridEnvelope() throws Exception {
+        final List<Object> zones = zoneIds(1);
+        final CodedGeometry base = CodedGeometry.compound(
+                DiscreteGlobalGridGeometry.unstructured(dggrs, zones, null),
+                new CodedGeometry(verticalGrid()));
+
+        final GeneralEnvelope aoi = new 
GeneralEnvelope(CRS.compound(CommonCRS.WGS84.normalizedGeographic(), 
verticalCrs));
+        aoi.setRange(0, 1, 10);
+        aoi.setRange(1, 1, 10);
+        aoi.setRange(2, 120, 180);
+
+        final CodedGeometry result = 
base.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(aoi).build();
+
+        final List<Object> kept = ((DiscreteGlobalGridGeometry) 
result.slice(dggrs).get()).getZoneIds();
+        assertTrue(kept.contains(zoneAt(1, 5, 5).getIdentifier()));
+        assertFalse(kept.contains(zoneAt(1, -100, -40).getIdentifier()));
+        assertTrue(zones.containsAll(kept));
+
+        final GridExtent vertical = 
result.slice(verticalCrs).get().getExtent();
+        assertEquals(1, vertical.getLow(0));
+        assertEquals(1, vertical.getHigh(0));
+    }
+
+    /**
+     * Tests subgrid with an envelope which does not intersect the vertical 
component.
+     */
+    @Test
+    public void testSubgridEnvelopeDisjoint() throws Exception {
+        final CodedGeometry base = CodedGeometry.compound(
+                DiscreteGlobalGridGeometry.unstructured(dggrs, zoneIds(1), 
null),
+                new CodedGeometry(verticalGrid()));
+
+        final GeneralEnvelope aoi = new GeneralEnvelope(verticalCrs);
+        aoi.setRange(0, 1000, 2000);
+        assertThrows(DisjointExtentException.class, () -> 
base.derive().subgrid(aoi));
+    }
+
+    /**
+     * Tests subgrid with an area of interest using the same DGGRS at a finer 
level.
+     */
+    @Test
+    public void testSubgridFinerZones() throws Exception {
+        final CodedGeometry base = 
DiscreteGlobalGridGeometry.unstructured(dggrs, zoneIds(1), null);
+        final Zone parent = zoneAt(1, 5, 5);
+        final List<Object> children = 
parent.getChildrenAtRelativeDepth(2).map(Zone::getIdentifier).toList();
+        final CodedGeometry aoi = 
DiscreteGlobalGridGeometry.unstructured(dggrs, children, null);
+
+        final DiscreteGlobalGridGeometry result = (DiscreteGlobalGridGeometry) 
base.derive().subgrid(aoi).build();
+        assertEquals(List.of(parent.getIdentifier()), new 
ArrayList<>(result.getZoneIds()));
+    }
+
+    /**
+     * Tests subgrid with an area of interest using the same DGGRS at a 
coarser level.
+     */
+    @Test
+    public void testSubgridCoarserZones() throws Exception {
+        final CodedGeometry base = 
DiscreteGlobalGridGeometry.unstructured(dggrs, zoneIds(2), null);
+        final Zone face = zoneAt(0, 5, 5);
+        final CodedGeometry aoi = 
DiscreteGlobalGridGeometry.unstructured(dggrs, List.of(face.getIdentifier()), 
null);
+
+        final DiscreteGlobalGridGeometry result = (DiscreteGlobalGridGeometry) 
base.derive().subgrid(aoi).build();
+        final List<Object> kept = result.getZoneIds();
+        assertEquals(16, kept.size());
+        for (Object zid : kept) {
+            assertEquals(face.getLongIdentifier(), 
hierarchy.getZone(zid).getFirstParent(0).getLongIdentifier());
+        }
+    }
+
+    /**
+     * Tests that a base without zones accepts the area of interest zones.
+     */
+    @Test
+    public void testSubgridUndefinedZones() throws Exception {
+        final CodedGeometry base = 
DiscreteGlobalGridGeometry.unstructured(dggrs, (List<Object>) null, null);
+        final CodedGeometry aoi = 
DiscreteGlobalGridGeometry.unstructured(dggrs, zoneIds(2), null);
+        assertSame(aoi, base.derive().subgrid(aoi).build());
+    }
+
+    /**
+     * Tests that a base without zones computes zones from a regular grid area 
of interest.
+     */
+    @Test
+    public void testSubgridUndefinedZonesFromGrid() throws Exception {
+        final CodedGeometry base = 
DiscreteGlobalGridGeometry.unstructured(dggrs, (List<Object>) null, null);
+        final GeneralEnvelope env = new 
GeneralEnvelope(CommonCRS.WGS84.normalizedGeographic());
+        env.setRange(0, 0, 10);
+        env.setRange(1, 0, 10);
+        final CodedGeometry aoi = new CodedGeometry(new GridGeometry(new 
GridExtent(10, 10), env, GridOrientation.HOMOTHETY));
+
+        final DiscreteGlobalGridGeometry result = (DiscreteGlobalGridGeometry) 
base.derive().subgrid(aoi).build();
+        final List<Object> zones = result.getZoneIds();
+        assertFalse(zones.isEmpty());
+        assertTrue(zones.contains(zoneAt(result.getRefinementLevel(), 5, 
5).getIdentifier()));
+    }
+
+    /**
+     * Tests that a component without extent accepts the area of interest 
cells.
+     */
+    @Test
+    public void testSubgridUndefinedGrid() throws Exception {
+        final CodedGeometry base = new CodedGeometry(new GridGeometry(null, 
null, null, verticalCrs));
+        final CodedGeometry aoi = new CodedGeometry(verticalGrid());
+        final CodedGeometry result = base.derive().subgrid(aoi).build();
+        assertEquals(verticalCrs, result.getReferenceSystem());
+        assertEquals(aoi.getExtent(), result.getExtent());
+        assertEquals(aoi.getGridToRS(), result.getGridToRS());
+    }
+
+    /**
+     * Tests that a base without reference system accepts any area of interest.
+     */
+    @Test
+    public void testSubgridUndefinedBase() throws Exception {
+        final CodedGeometry base = new CodedGeometry(null, null, null, null);
+        final CodedGeometry aoi = new CodedGeometry(verticalGrid());
+        assertSame(aoi, base.derive().subgrid(aoi).build());
+    }
+
+    /**
+     * Tests subgrid with an area of interest covering only the vertical 
component.
+     * The DGGRS component must be unchanged.
+     */
+    @Test
+    public void testSubgridOtherComponent() throws Exception {
+        final DiscreteGlobalGridGeometry dggs = 
DiscreteGlobalGridGeometry.unstructured(dggrs, zoneIds(1), null);
+        final CodedGeometry base = CodedGeometry.compound(dggs, new 
CodedGeometry(verticalGrid()));
+        final GridGeometry aoi = verticalGrid().derive().subgrid(new 
GridExtent(null, 2, 2, true)).build();
+
+        final CodedGeometry result = base.derive().subgrid(new 
CodedGeometry(aoi)).build();
+        assertSame(dggs, result.slice(dggrs).get());
+
+        final GridExtent vertical = 
result.slice(verticalCrs).get().getExtent();
+        assertEquals(2, vertical.getLow(0));
+        assertEquals(2, vertical.getHigh(0));
+    }
+}
diff --git 
a/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/storage/rs/internal/shared/CodedResourcesTest.java
 
b/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/storage/rs/internal/shared/CodedResourcesTest.java
new file mode 100644
index 0000000000..578d4061eb
--- /dev/null
+++ 
b/incubator/src/org.apache.sis.referencing.dggs/test/org/apache/sis/storage/rs/internal/shared/CodedResourcesTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.storage.rs.internal.shared;
+
+import java.util.List;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.GenericName;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.geometry.DirectPosition2D;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.dggs.DiscreteGlobalGridHierarchy;
+import org.apache.sis.referencing.dggs.DiscreteGlobalGridReferenceSystem;
+import org.apache.sis.referencing.dggs.Zone;
+import org.apache.sis.referencing.dggs.s2.S2Dggrs;
+import org.apache.sis.storage.NoSuchDataException;
+import org.apache.sis.storage.dggs.DiscreteGlobalGridGeometry;
+import org.apache.sis.storage.rs.CodeIterator;
+import org.apache.sis.storage.rs.CodedCoverage;
+import org.apache.sis.storage.rs.WritableCodeIterator;
+import org.apache.sis.util.iso.Names;
+
+// Test dependencies
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+
+
+/**
+ * Tests {@link CodedResources} through {@link MemoryCodedResource}.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class CodedResourcesTest {
+
+    private static final GenericName NAME = Names.createLocalName(null, null, 
"memory");
+
+    private final DiscreteGlobalGridReferenceSystem dggrs = new S2Dggrs();
+    private final DiscreteGlobalGridHierarchy hierarchy = 
dggrs.getGridSystem().getHierarchy();
+
+    private Zone zoneAt(int level, double lon, double lat) throws 
TransformException {
+        return hierarchy.getGrids().get(level).getZone(new 
DirectPosition2D(CommonCRS.WGS84.normalizedGeographic(), lon, lat));
+    }
+
+    /**
+     * Creates a resource on given zones, the cell value is the zone index.
+     */
+    private MemoryCodedResource createResource(List<Object> zones) throws 
Exception {
+        final ArrayCodedCoverage coverage = new ArrayCodedCoverage(NAME,
+                DiscreteGlobalGridGeometry.unstructured(dggrs, zones, null),
+                new SampleDimension.Builder().setName("index").build());
+        try (WritableCodeIterator iterator = 
coverage.createWritableIterator()) {
+            while (iterator.next()) {
+                iterator.setSample(0, (double) iterator.getPosition()[0]);
+            }
+        }
+        return new MemoryCodedResource(NAME, coverage);
+    }
+
+    /**
+     * Tests reading with the exact resource geometry returns the coverage.
+     */
+    @Test
+    public void testReadExact() throws Exception {
+        final List<Object> zones = 
hierarchy.getGrids().get(1).getZones().map(Zone::getIdentifier).toList();
+        final MemoryCodedResource resource = createResource(zones);
+        final CodedCoverage coverage = 
resource.read(resource.getGridGeometry());
+        assertSame(resource.getGridGeometry(), coverage.getGeometry());
+    }
+
+    /**
+     * Tests reading with a geometry which does not match the resource 
geometry,
+     * the result must be aligned on the resource zones.
+     */
+    @Test
+    public void testReadMisaligned() throws Exception {
+        final List<Object> zones = 
hierarchy.getGrids().get(1).getZones().map(Zone::getIdentifier).toList();
+        final MemoryCodedResource resource = createResource(zones);
+
+        //query a level 0 zone, the 4 level 1 children are expected
+        final Zone face = zoneAt(0, 5, 5);
+        final CodedCoverage coverage = 
resource.read(DiscreteGlobalGridGeometry.unstructured(dggrs, 
List.of(face.getIdentifier()), null));
+        final List<Object> kept = ((DiscreteGlobalGridGeometry) 
coverage.getGeometry()).getZoneIds();
+        assertEquals(4, kept.size());
+
+        final CodeIterator iterator = coverage.createIterator();
+        int count = 0;
+        while (iterator.next()) {
+            final Object zid = kept.get(iterator.getPosition()[0]);
+            assertEquals(face.getLongIdentifier(), 
hierarchy.getZone(zid).getFirstParent(0).getLongIdentifier());
+            assertEquals(zones.indexOf(zid), iterator.getSampleDouble(0), 0.0);
+            count++;
+        }
+        assertEquals(4, count);
+    }
+
+    /**
+     * Tests reading with a geometry which does not intersect the resource.
+     */
+    @Test
+    public void testReadDisjoint() throws Exception {
+        final Zone face = zoneAt(0, 5, 5);
+        final List<Object> zones = 
face.getChildrenAtRelativeDepth(1).map(Zone::getIdentifier).toList();
+        final MemoryCodedResource resource = createResource(zones);
+
+        final Zone other = zoneAt(1, 179, -60);
+        assertNotEquals(face.getLongIdentifier(), 
other.getFirstParent(0).getLongIdentifier());
+        assertThrows(NoSuchDataException.class,
+                () -> 
resource.read(DiscreteGlobalGridGeometry.unstructured(dggrs, 
List.of(other.getIdentifier()), null)));
+    }
+}

Reply via email to