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