This is an automated email from the ASF dual-hosted git repository. amanin pushed a commit to branch feat/resource-processor in repository https://gitbox.apache.org/repos/asf/sis.git
commit 211c9e7af62c33bff49a9ec6d65475a9bbc69f11 Author: Alexis Manin <alexis.ma...@geomatys.com> AuthorDate: Mon Dec 5 18:09:20 2022 +0100 feat(Feature+Storage): add a dimension selection grid coverage --- .../coverage/grid/DimensionSelectionCoverage.java | 20 +++ .../sis/coverage/grid/GridCoverageProcessor.java | 49 +++++++ .../coverage/grid/GridDimensionSelection.java | 148 +++++++++++++++++++++ .../sis/storage/DimensionSelectionResource.java | 46 +++++++ .../org/apache/sis/storage/ResourceProcessor.java | 54 ++++++++ .../apache/sis/storage/ResourceProcessorTest.java | 47 +++++++ 6 files changed, 364 insertions(+) diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionSelectionCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionSelectionCoverage.java new file mode 100644 index 0000000000..6292b7d95d --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/DimensionSelectionCoverage.java @@ -0,0 +1,20 @@ +package org.apache.sis.coverage.grid; + +import java.awt.image.RenderedImage; +import org.apache.sis.internal.coverage.grid.GridDimensionSelection; +import org.opengis.coverage.CannotEvaluateException; + +class DimensionSelectionCoverage extends DerivedGridCoverage { + private final GridDimensionSelection.Specification spec; + + DimensionSelectionCoverage(GridCoverage source, GridDimensionSelection.Specification spec) { + super(source, spec.getReducedGridGeometry()); + this.spec = spec; + } + + @Override + public RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException { + if (sliceExtent == null) return source.render(null); + else return source.render(spec.reverse(sliceExtent)); + } +} diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java index 9e207ab329..6c3b4f664d 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java @@ -26,6 +26,8 @@ import java.awt.Rectangle; import java.awt.image.ColorModel; import java.awt.image.RenderedImage; import javax.measure.Quantity; +import org.apache.sis.internal.coverage.grid.GridDimensionSelection; +import org.opengis.referencing.operation.MathTransform; import org.opengis.util.FactoryException; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.crs.CoordinateReferenceSystem; @@ -504,6 +506,53 @@ public class GridCoverageProcessor implements Cloneable { return resample(source, new GridGeometry(null, PixelInCell.CELL_CENTER, null, target)); } + /** + * Remove all "flat" grid dimensions from input data. Flat dimensions are dimensions with a single grid cell. + * @param source The coverage we want to reduce to lower dimension. + * @return Either input coverage if no dimension can be removed, or a view of the coverage with less dimensions. + */ + public GridCoverage squeeze(GridCoverage source) { + return GridDimensionSelection.squeeze(source.getGridGeometry()) + .<GridCoverage>map(spec -> new DimensionSelectionCoverage(source, spec)) + .orElse(source); + } + + /** + * Create a coverage containing only specified dimensions. + * + * Constraints: + * <ul> + * <li>Removed dimensions must have only one degree of liberty.</li> + * <li>Output dimension order is the same as in source coverage, whatever order axes are given as input.</li> + * <li>If input dataset contains dimensions that are not separable, but only part of them are selected, this code will fail.</li> + * </ul> + * + * @param source The coverage to reduce to lower dimension. + * @param gridAxesToPreserve Index of each grid dimension to maintain in result. Must contain at least one element. + */ + public GridCoverage selectDimensions(GridCoverage source, int... gridAxesToPreserve) { + final GridDimensionSelection.Specification spec = GridDimensionSelection.preserve(source.getGridGeometry(), gridAxesToPreserve); + return new DimensionSelectionCoverage(source, spec); + } + + /** + * Create a coverage trimmed from specified <em>grid</em> dimensions. + * + * Constraints: + * <ul> + * <li>Removed dimensions must have only one degree of liberty.</li> + * <li>Output dimension order is the same as in source coverage.</li> + * <li>If input dataset contains dimensions that are not separable, but only part of them are selected for removal, this code will fail.</li> + * </ul> + * + * @param source Dataset to reduce. + * @param gridAxesToRemove Index of each grid dimension to strip from result. Must contain at least one element. + */ + public GridCoverage removeDimensions(GridCoverage source, int... gridAxesToRemove) { + final GridDimensionSelection.Specification spec = GridDimensionSelection.remove(source.getGridGeometry(), gridAxesToRemove); + return new DimensionSelectionCoverage(source, spec); + } + /** * Invoked when an ignorable exception occurred. * diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/grid/GridDimensionSelection.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/grid/GridDimensionSelection.java new file mode 100644 index 0000000000..00f3f5fe84 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/grid/GridDimensionSelection.java @@ -0,0 +1,148 @@ +package org.apache.sis.internal.coverage.grid; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.IntStream; +import org.apache.sis.coverage.grid.GridExtent; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.referencing.operation.matrix.Matrices; +import org.apache.sis.referencing.operation.matrix.MatrixSIS; +import org.apache.sis.referencing.operation.transform.LinearTransform; +import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.Static; +import org.apache.sis.util.Utilities; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.NoninvertibleTransformException; + +import static org.apache.sis.util.ArgumentChecks.ensureNonNull; +import static org.opengis.referencing.datum.PixelInCell.CELL_CENTER; + +/** + * Provide utility methods to reduce/select dimensions of a grid-geometry, and provide an object holding information + * needed to travel between source to reduced space. + */ +public final class GridDimensionSelection extends Static { + private GridDimensionSelection() {} + + public static Optional<Specification> squeeze(GridGeometry source) { + final int[] axesToPreserve = findUnsqueezableDimensions(source); + if (axesToPreserve.length == source.getExtent().getDimension()) return Optional.empty(); + else if (axesToPreserve.length == 0) throw new IllegalArgumentException("All input grid dimensions are squeezable. Squeezing it would degenerate to a 0 dimension grid."); + else return Optional.of(preserve(source, axesToPreserve)); + } + + public static Specification remove(GridGeometry source, int... gridAxesToRemove) { + return preserve(source, reverse(source, gridAxesToRemove)); + } + + public static Specification preserve(GridGeometry source, int... gridAxesToPreserve) { + ensureNonNull("Source", source); + final GridExtent extent = source.getExtent(); + ArgumentChecks.ensureNonEmpty("Grid axes to preserve", gridAxesToPreserve, 0, extent.getDimension(), true); + Arrays.sort(gridAxesToPreserve); + final GridGeometry reducedGeom = source.selectDimensions(gridAxesToPreserve); + + final int sourceDim = extent.getDimension(); + final int targetDim = gridAxesToPreserve.length; + int newSpaceIdx = 0; + final MatrixSIS mat = Matrices.create(sourceDim + 1, targetDim + 1, new double[Math.multiplyExact(sourceDim + 1, targetDim + 1)]); + mat.setElement(sourceDim, targetDim, 1.0); + for (int row = 0 ; row < sourceDim ; row++) { + if (Arrays.binarySearch(gridAxesToPreserve, row) >= 0) { + mat.setElement(row, newSpaceIdx++, 1.0); + } else { + mat.setElement(row, targetDim, extent.getLow(row)); + } + } + final LinearTransform reducedToOrigin = MathTransforms.linear(mat); + return new Specification(reducedGeom, gridAxesToPreserve, reducedToOrigin, source); + } + + public static class Specification { + private final GridGeometry reducedGridGeometry; + private final int[] gridAxesToPreserve; + private final LinearTransform rollbackAxes; + private final GridGeometry sourceGeometry; + + public Specification(GridGeometry reducedGridGeometry, int[] gridAxesToPreserve, LinearTransform rollbackAxes, GridGeometry sourceGeometry) { + this.reducedGridGeometry = reducedGridGeometry; + this.gridAxesToPreserve = gridAxesToPreserve; + this.rollbackAxes = rollbackAxes; + this.sourceGeometry = sourceGeometry; + } + + public GridGeometry getReducedGridGeometry() { + return reducedGridGeometry; + } + + public int[] getGridAxesToPreserve() { + return gridAxesToPreserve.clone(); + } + + public LinearTransform getRollbackAxes() { + return rollbackAxes; + } + + public GridGeometry getSourceGeometry() { + return sourceGeometry; + } + + public GridExtent reverse(GridExtent extent) { + final GridExtent sourceExtent = sourceGeometry.getExtent(); + final long[] newLow = sourceExtent.getLow().getCoordinateValues(); + final long[] newHigh = sourceExtent.getHigh().getCoordinateValues(); + for (int i = 0 ; i < gridAxesToPreserve.length ; i++) { + int j = gridAxesToPreserve[i]; + newLow[j] = extent.getLow(i); + newHigh[j] = extent.getHigh(i); + } + return new GridExtent(null, newLow, newHigh, true); + } + + public GridGeometry reverse(GridGeometry domain) throws NoninvertibleTransformException { + if (domain.isDefined(GridGeometry.CRS) && !Utilities.equalsIgnoreMetadata(reducedGridGeometry.getCoordinateReferenceSystem(), domain.getCoordinateReferenceSystem())) { + throw new IllegalArgumentException("Input geometry CRS must match this specification CRS"); + } + + final MathTransform inflatedGridToCrs; + if (domain.isDefined(GridGeometry.GRID_TO_CRS)) { + inflatedGridToCrs = null; + } else if (Utilities.equalsIgnoreMetadata(domain.getGridToCRS(CELL_CENTER), reducedGridGeometry.getGridToCRS(CELL_CENTER))) { + inflatedGridToCrs = sourceGeometry.getGridToCRS(CELL_CENTER); + } else { + final MathTransform reducedToSource = MathTransforms.concatenate( + reducedGridGeometry.getGridToCRS(CELL_CENTER).inverse(), + rollbackAxes, + sourceGeometry.getGridToCRS(CELL_CENTER) + ); + + inflatedGridToCrs = MathTransforms.concatenate( + rollbackAxes.inverse(), + domain.getGridToCRS(CELL_CENTER), + reducedToSource + ); + } + + final CoordinateReferenceSystem inflatedCrs = sourceGeometry.isDefined(GridGeometry.CRS) ? sourceGeometry.getCoordinateReferenceSystem() : null; + return new GridGeometry(reverse(domain.getExtent()), CELL_CENTER, inflatedGridToCrs, inflatedCrs); + } + } + + private static int[] findUnsqueezableDimensions(GridGeometry sourceGeom) { + final GridExtent extent = sourceGeom.getExtent(); + return IntStream.range(0, extent.getDimension()) + .filter(i -> extent.getSize(i) > 1) + .toArray(); + } + + private static int[] reverse(GridGeometry source, int[] axes) { + final int[] sorted = axes.clone(); + Arrays.sort(sorted); + + return IntStream.range(0, source.getExtent().getDimension()) + .filter(i -> Arrays.binarySearch(sorted, i) < 0) + .toArray(); + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/DimensionSelectionResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/DimensionSelectionResource.java new file mode 100644 index 0000000000..d94691849c --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/DimensionSelectionResource.java @@ -0,0 +1,46 @@ +package org.apache.sis.storage; + +import org.apache.sis.coverage.grid.GridCoverage; +import org.apache.sis.coverage.grid.GridCoverageProcessor; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.internal.coverage.grid.GridDimensionSelection; +import org.apache.sis.internal.storage.DerivedGridCoverageResource; +import org.apache.sis.util.collection.BackingStoreException; +import org.opengis.referencing.operation.NoninvertibleTransformException; +import org.opengis.util.GenericName; + +import static org.apache.sis.util.ArgumentChecks.ensureNonNull; + +class DimensionSelectionResource extends DerivedGridCoverageResource { + + private final GridCoverageProcessor processor; + private final GridDimensionSelection.Specification spec; + + protected DimensionSelectionResource(GenericName name, GridCoverageResource source, GridDimensionSelection.Specification spec, GridCoverageProcessor processor) { + super(name, source); + ensureNonNull("Specification", spec); + this.spec = spec; + this.processor = processor; + } + + @Override + public GridGeometry getGridGeometry() throws DataStoreException { + return spec.getReducedGridGeometry(); + } + + @Override + public GridCoverage read(GridGeometry domain, int... ranges) throws DataStoreException { + if (domain == null) domain = spec.getSourceGeometry(); + else { + domain = spec.getReducedGridGeometry().derive().subgrid(domain).build(); + try { + domain = spec.reverse(domain); + } catch (NoninvertibleTransformException e) { + throw new BackingStoreException("Cannot determine source geometry from reduced one", e); + } + } + + final GridCoverage sourceData = source.read(domain, ranges); + return processor.selectDimensions(sourceData, spec.getGridAxesToPreserve()); + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/ResourceProcessor.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/ResourceProcessor.java index bce74739e5..23a9a2219e 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/ResourceProcessor.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/ResourceProcessor.java @@ -33,6 +33,7 @@ import org.apache.sis.coverage.grid.GridRoundingMode; import org.apache.sis.coverage.grid.IncompleteGridGeometryException; import org.apache.sis.image.DataType; import org.apache.sis.image.ImageProcessor; +import org.apache.sis.internal.coverage.grid.GridDimensionSelection; import org.apache.sis.internal.storage.ConvertedCoverageResource; import org.apache.sis.internal.system.Loggers; import org.apache.sis.measure.NumberRange; @@ -171,6 +172,59 @@ public class ResourceProcessor implements Cloneable { return new BandAggregateGridResource(name, selections, userColors); } + /** + * Remove all "flat" grid dimensions from input data. Flat dimensions are dimensions with a single grid cell. + * @param resultName A name to affect to output coverage resource. If null, result will not have any identifier. + * @param source The coverage we want to reduce to lower dimension. + * @return Either input coverage if no dimension can be removed, or a view of the coverage with fewer dimensions. + * @see GridCoverageProcessor#squeeze(GridCoverage) + */ + public GridCoverageResource squeeze(GenericName resultName, GridCoverageResource source) throws DataStoreException { + return GridDimensionSelection.squeeze(source.getGridGeometry()) + .<GridCoverageResource>map(spec -> new DimensionSelectionResource(resultName, source, spec, processor)) + .orElse(source); + } + + /** + * Create a coverage containing only specified dimensions. + * + * Constraints: + * <ul> + * <li>Removed dimensions must have only one degree of liberty.</li> + * <li>Output dimension order is the same as in source coverage, whatever order axes are given as input.</li> + * <li>If input dataset contains dimensions that are not separable, but only part of them are selected, this code will fail.</li> + * </ul> + * + * @param resultName A name to affect to output coverage resource. If null, result will not have any identifier. + * @param source The coverage to reduce to lower dimension. + * @param gridAxesToPreserve Index of each grid dimension to maintain in result. Must contain at least one element. + * @see GridCoverageProcessor#selectDimensions(GridCoverage, int...) + */ + public GridCoverageResource selectDimensions(GenericName resultName, GridCoverageResource source, int... gridAxesToPreserve) throws DataStoreException { + final GridDimensionSelection.Specification spec = GridDimensionSelection.preserve(source.getGridGeometry(), gridAxesToPreserve); + return new DimensionSelectionResource(resultName, source, spec, processor); + } + + /** + * Create a coverage trimmed from specified <em>grid</em> dimensions. + * + * Constraints: + * <ul> + * <li>Removed dimensions must have only one degree of liberty.</li> + * <li>Output dimension order is the same as in source coverage.</li> + * <li>If input dataset contains dimensions that are not separable, but only part of them are selected for removal, this code will fail.</li> + * </ul> + * + * @param resultName A name to affect to output coverage resource. If null, result will not have any identifier. + * @param source Dataset to reduce. + * @param gridAxesToRemove Index of each grid dimension to strip from result. Must contain at least one element. + * @see GridCoverageProcessor#removeDimensions(GridCoverage, int...) + */ + public GridCoverageResource removeDimensions(GenericName resultName, GridCoverageResource source, int... gridAxesToRemove) throws DataStoreException { + final GridDimensionSelection.Specification spec = GridDimensionSelection.remove(source.getGridGeometry(), gridAxesToRemove); + return new DimensionSelectionResource(resultName, source, spec, processor); + } + private static Optional<GeographicBoundingBox> searchGeographicExtent(GridCoverageResource source) throws DataStoreException { final Optional<GeographicBoundingBox> bbox = source.getMetadata().getIdentificationInfo().stream() .flatMap(it -> it.getExtents().stream()) diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java index 59b4a4ef9d..95cb05aac3 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java @@ -15,12 +15,14 @@ import org.apache.sis.coverage.grid.GridCoverageProcessor; 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.geometry.GeneralEnvelope; import org.apache.sis.image.ImageProcessor; import org.apache.sis.image.Interpolation; import org.apache.sis.internal.referencing.j2d.AffineTransform2D; import org.apache.sis.internal.storage.MemoryGridResource; import org.apache.sis.measure.Units; import org.apache.sis.referencing.crs.HardCodedCRS; +import org.apache.sis.test.Assert; import org.apache.sis.test.TestCase; import org.apache.sis.util.iso.Names; import org.junit.Test; @@ -174,6 +176,42 @@ public class ResourceProcessorTest extends TestCase { ); } + @Test + public void testDimensionSelection() throws Exception { + final GridExtent extent4d = new GridExtent(null, new long[4], new long[]{2, 2, 1, 1}, false); + final GeneralEnvelope env4d = new GeneralEnvelope(HardCodedCRS.WGS84_4D); + env4d.setEnvelope(0, 1, 2, 3, 4, 5, 6, 7); + final GridGeometry domain4d = new GridGeometry(extent4d, env4d, GridOrientation.HOMOTHETY); + final GridCoverageResource data4d = grid1234(domain4d); + + final GridCoverageResource squeezed = nearestInterpol().squeeze(null, data4d); + final GridGeometry squeezedDomain = squeezed.getGridGeometry(); + assertEquals("Only 2 dimensions should remain", 2, squeezedDomain.getDimension()); + Assert.assertEqualsIgnoreMetadata(HardCodedCRS.WGS84, squeezedDomain.getCoordinateReferenceSystem()); + GridCoverage loaded = squeezed.read(null); + assertEquals(squeezedDomain, loaded.getGridGeometry()); + int[] values = loaded.render(null).getData().getPixels(0, 0, 2, 2, (int[]) null); + assertArrayEquals(new int[] { 1, 2, 3, 4 }, values); + + final GridCoverageResource selected = nearestInterpol().selectDimensions(null, data4d, 0, 1, 3); + final GridGeometry selectedDomain = selected.getGridGeometry(); + assertEquals("Only 3 dimensions should remain", 3, selectedDomain.getDimension()); + Assert.assertEqualsIgnoreMetadata(HardCodedCRS.WGS84_WITH_TIME, selectedDomain.getCoordinateReferenceSystem()); + loaded = selected.read(null); + assertEquals(selectedDomain, loaded.getGridGeometry()); + values = loaded.render(null).getData().getPixels(0, 0, 2, 2, (int[]) null); + assertArrayEquals(new int[] { 1, 2, 3, 4 }, values); + + final GridCoverageResource removed = nearestInterpol().removeDimensions(null, data4d, 3); + final GridGeometry removedDomain = removed.getGridGeometry(); + assertEquals("Only 3 dimensions should remain", 3, removedDomain.getDimension()); + Assert.assertEqualsIgnoreMetadata(HardCodedCRS.WGS84_3D, removedDomain.getCoordinateReferenceSystem()); + loaded = removed.read(null); + assertEquals(removedDomain, loaded.getGridGeometry()); + values = loaded.render(null).getData().getPixels(0, 0, 2, 2, (int[]) null); + assertArrayEquals(new int[] { 1, 2, 3, 4 }, values); + } + private static GridCoverageResource singleValuePerBand(int... bandValues) { GridGeometry domain = new GridGeometry(new GridExtent(2, 2), PixelInCell.CELL_CENTER, identity(2), HardCodedCRS.WGS84); final List<SampleDimension> samples = IntStream.of(bandValues) @@ -195,6 +233,15 @@ public class ResourceProcessorTest extends TestCase { */ private static GridCoverageResource grid1234() { GridGeometry domain = new GridGeometry(new GridExtent(2, 2), PixelInCell.CELL_CENTER, identity(2), HardCodedCRS.WGS84); + return grid1234(domain); + } + + /** + * Same as {@link #grid1234()}, but allow to override output domain, mostly to allow additional flat dimensions. + * + * @param domain A 2D+ domain whose x and y axes (rendering axes) are 2 cells each. + */ + private static GridCoverageResource grid1234(GridGeometry domain) { SampleDimension band = new SampleDimension.Builder() .setBackground(0) .addQuantitative("1-based row-major order pixel number", 1, 5, 1, 0, Units.UNITY)