You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by am...@apache.org on 2022/11/28 13:47:58 UTC
[sis] 03/03: feat(Storage): add GridCoverageResource resampling capability
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 5a6d0d24d06a7f8a57bdc6b586a07202f0685d5b
Author: Alexis Manin <al...@geomatys.com>
AuthorDate: Mon Nov 28 13:15:36 2022 +0100
feat(Storage): add GridCoverageResource resampling capability
---
.../storage/DerivedGridCoverageResource.java | 72 ++++++++++++++++
.../sis/storage/ResampledGridCoverageResource.java | 88 +++++++++++++++++++
.../org/apache/sis/storage/ResourceProcessor.java | 94 +++++++++++++++++++++
.../apache/sis/storage/ResourceProcessorTest.java | 98 ++++++++++++++++++++++
.../apache/sis/test/suite/StorageTestSuite.java | 3 +-
5 files changed, 354 insertions(+), 1 deletion(-)
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/DerivedGridCoverageResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/DerivedGridCoverageResource.java
new file mode 100644
index 0000000000..d5a4f718cf
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/DerivedGridCoverageResource.java
@@ -0,0 +1,72 @@
+package org.apache.sis.internal.storage;
+
+import java.util.List;
+import java.util.Optional;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.RasterLoadingStrategy;
+import org.apache.sis.storage.event.StoreEvent;
+import org.apache.sis.storage.event.StoreListener;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Metadata;
+import org.opengis.util.GenericName;
+
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+
+public abstract class DerivedGridCoverageResource implements GridCoverageResource {
+
+ protected final GenericName name;
+ protected final GridCoverageResource source;
+
+ protected DerivedGridCoverageResource(GenericName name, GridCoverageResource source) {
+ this.name = name;
+ ensureNonNull("Source", source);
+ this.source = source;
+ }
+
+ @Override
+ public Optional<Envelope> getEnvelope() throws DataStoreException { return source.getEnvelope(); }
+
+ @Override
+ public GridGeometry getGridGeometry() throws DataStoreException { return source.getGridGeometry(); }
+
+ @Override
+ public List<SampleDimension> getSampleDimensions() throws DataStoreException { return source.getSampleDimensions(); }
+
+ @Override
+ public RasterLoadingStrategy getLoadingStrategy() throws DataStoreException { return source.getLoadingStrategy(); }
+
+ @Override
+ public boolean setLoadingStrategy(RasterLoadingStrategy strategy) throws DataStoreException { return source.setLoadingStrategy(strategy); }
+
+ @Override
+ public Optional<GenericName> getIdentifier() throws DataStoreException { return Optional.ofNullable(name); }
+
+ @Override
+ public Metadata getMetadata() throws DataStoreException {
+ final MetadataBuilder builder = new MetadataBuilder();
+ builder.addSpatialRepresentation(null, getGridGeometry(), false);
+ builder.addSource(source.getMetadata());
+ return builder.buildAndFreeze();
+ }
+
+ @Override
+ public <T extends StoreEvent> void addListener(Class<T> eventType, StoreListener<? super T> listener) {
+ /*
+ * TODO: for now, consider it a no-op. Plugging directly into source might be a bad idea.
+ * 1. We do not know in advance what modifications are done over the source.
+ * Therefore, we do not know how events should be amended to reflect the resource derivation.
+ * For now, make derived resource not listenable by default.
+ * 2. We do not know if the same listener is already registered on source, and simply passing the listener
+ * to the source might cause redondant work.
+ * Each implementation is free to implement it as it see fit.
+ */
+ }
+
+ @Override
+ public <T extends StoreEvent> void removeListener(Class<T> eventType, StoreListener<? super T> listener) {
+ // See addListener to know why it is a no-op by default.
+ }
+}
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/ResampledGridCoverageResource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/ResampledGridCoverageResource.java
new file mode 100644
index 0000000000..86bef731c2
--- /dev/null
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/ResampledGridCoverageResource.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+
+import java.util.Optional;
+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.storage.DerivedGridCoverageResource;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.GenericName;
+
+/**
+ * A decoration over a resource to resample it on a specified {@link #outputGeometry grid geometry} when a {@link #read(GridGeometry, int...)} is triggered.
+ *
+ * @see ResourceProcessor#resample(GridCoverageResource, CoordinateReferenceSystem, GenericName)
+ * @see ResourceProcessor#resample(GridCoverageResource, GridGeometry, GenericName)
+ *
+ * @author Alexis Manin (Geomatys)
+ */
+final class ResampledGridCoverageResource extends DerivedGridCoverageResource {
+
+ private final GridCoverageProcessor processor;
+ private final GridGeometry outputGeometry;
+
+ ResampledGridCoverageResource(GridCoverageResource source, GridGeometry outputGeometry, GenericName name, GridCoverageProcessor processor) {
+ super(name, source);
+ this.processor = processor;
+ this.outputGeometry = outputGeometry;
+ }
+
+ @Override
+ public Optional<Envelope> getEnvelope() {
+ return outputGeometry.isDefined(GridGeometry.ENVELOPE)
+ ? Optional.of(outputGeometry.getEnvelope())
+ : Optional.empty();
+ }
+
+ @Override
+ public GridGeometry getGridGeometry() { return outputGeometry; }
+
+ @Override
+ public GridCoverageResource subset(Query query) throws DataStoreException {
+ if (query instanceof CoverageQuery) {
+ CoverageQuery cq = (CoverageQuery) query;
+ GridGeometry selection = cq.getSelection();
+ if (selection != null) {
+ selection = outputGeometry.derive().subgrid(selection).build();
+ final GridCoverageResource updatedResample = new ResampledGridCoverageResource(source, selection, null, processor);
+ cq = cq.clone();
+ cq.setSelection((GridGeometry) null);
+ return updatedResample.subset(cq);
+ }
+ }
+
+ return super.subset(query);
+ }
+
+ @Override
+ public GridCoverage read(GridGeometry domain, int... ranges) throws DataStoreException {
+ domain = domain == null
+ ? outputGeometry
+ : outputGeometry.derive().subgrid(domain).build();
+
+ GridCoverage rawRead = source.read(domain, ranges);
+ try {
+ return processor.resample(rawRead, domain);
+ } catch (TransformException e) {
+ throw new DataStoreException("Cannot adapt source to resampling domain", e);
+ }
+ }
+}
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 250e4de68a..3a0b745988 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
@@ -18,15 +18,34 @@ package org.apache.sis.storage;
import java.awt.image.ColorModel;
import java.awt.image.RenderedImage;
+import java.util.Optional;
import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import org.apache.sis.coverage.SampleDimension;
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.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.storage.ConvertedCoverageResource;
+import org.apache.sis.internal.system.Loggers;
import org.apache.sis.measure.NumberRange;
+import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.opengis.metadata.extent.GeographicBoundingBox;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+import org.opengis.util.GenericName;
+
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* A predefined set of operations on resources as convenience methods.
@@ -87,4 +106,79 @@ public class ResourceProcessor implements Cloneable {
{
return new ConvertedCoverageResource(source, converters, sampleDimensionModifier);
}
+
+ /**
+ * Wraps a given resource with a resample operator, to ensure it fits a provided grid geometry.
+ * Neither resampling nor read is triggered immediately. Instead, a virtual resource is returned.
+ * It will launch read then resample operations upon call to {@link GridCoverageResource#read(GridGeometry, int...)}.
+ *
+ * @param source Resource to resample. Must not be null.
+ * @param target Grid geometry to use for output/resampled resource. Must not be null.
+ * @param targetName An optional name for returned resource. If null, output {@link Resource#getIdentifier() resource identifier} will not be present.
+ * @return A resource decorating provided one. It triggers a {@link GridCoverageProcessor#resample(GridCoverage, GridGeometry) resampling operation} upon reads. Never null.
+ */
+ public GridCoverageResource resample(final GridCoverageResource source, final GridGeometry target, GenericName targetName) {
+ return new ResampledGridCoverageResource(source, target, targetName, processor);
+ }
+
+ /**
+ * Reprojects provided resource. Note that:
+ * <ul>
+ * <li>Provided resource metadata and grid geometry will be immediately fetched</li>
+ * <li>Resampling will be postponed until a call to {@link GridCoverageResource#read(GridGeometry, int...)}</li>
+ * </ul>
+ * @return Either the input resource if no reprojection is needed for conversion to target CRS. Otherwise, a virtual dataset performing resample on read.
+ * @throws DataStoreException If input resource metadata or grid geometry cannot be acquired.
+ * @throws FactoryException If referencing database is not reachable, or if it is not possible to find any valid operation from input resource system to provided CRS.
+ * @throws TransformException If an error occurs while transforming input resource geometry to the target CRS.
+ * @throws IncompleteGridGeometryException If input resource geometry does not provide enough information to build a resampling pipeline (i.e. No CRS or no envelope).
+ */
+ public GridCoverageResource resample(final GridCoverageResource source, final CoordinateReferenceSystem target, GenericName targetName) throws DataStoreException, FactoryException, TransformException {
+ ensureNonNull("Source", source);
+ ensureNonNull("Target CRS", target);
+ final GridGeometry sourceGeom = source.getGridGeometry();
+ final CoordinateReferenceSystem sourceCrs = sourceGeom.getCoordinateReferenceSystem();
+
+ final GridGeometry reprojected;
+ if (sourceGeom.isDefined(GridGeometry.GRID_TO_CRS + GridGeometry.EXTENT)) {
+ final CoordinateOperation op = CRS.findOperation(sourceCrs, target, searchGeographicExtent(source).orElse(null));
+ if (op.getMathTransform() == null || op.getMathTransform().isIdentity()) return source;
+ reprojected = new GridGeometry(sourceGeom.getExtent(), PixelInCell.CELL_CENTER,
+ MathTransforms.concatenate(sourceGeom.getGridToCRS(PixelInCell.CELL_CENTER), op.getMathTransform()), target);
+ } else if (sourceGeom.isDefined(GridGeometry.ENVELOPE)) {
+ reprojected = new GridGeometry(null, null, sourceGeom.getEnvelope(target), GridRoundingMode.ENCLOSING);
+ } else throw new IncompleteGridGeometryException("Cannot reproject a grid coverage resource whose geometry defines neither an envelope nor a conversion for grid to CRS");
+
+ return new ResampledGridCoverageResource(source, reprojected, targetName, processor);
+ }
+
+ private static Optional<GeographicBoundingBox> searchGeographicExtent(GridCoverageResource source) throws DataStoreException {
+ final Optional<GeographicBoundingBox> bbox = source.getMetadata().getIdentificationInfo().stream()
+ .flatMap(it -> it.getExtents().stream())
+ .flatMap(it -> it.getGeographicElements().stream())
+ .filter(GeographicBoundingBox.class::isInstance)
+ .map(it -> (GeographicBoundingBox) it)
+ .reduce(ResourceProcessor::union);
+
+ if (bbox.isPresent()) return bbox;
+
+ return source.getEnvelope()
+ .map(it -> {
+ DefaultGeographicBoundingBox g = new DefaultGeographicBoundingBox();
+ try {
+ g.setBounds(it);
+ } catch (TransformException e) {
+ Logger.getLogger(Loggers.COORDINATE_OPERATION)
+ .log(Level.FINE, "Cannot extract geographic extent from source resource", e);
+ return null;
+ }
+ return g;
+ });
+ }
+
+ private static GeographicBoundingBox union(GeographicBoundingBox g1, GeographicBoundingBox g2) {
+ final DefaultGeographicBoundingBox union = new DefaultGeographicBoundingBox(g1);
+ union.add(g2);
+ return union;
+ }
}
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
new file mode 100644
index 0000000000..0f0211418e
--- /dev/null
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/ResourceProcessorTest.java
@@ -0,0 +1,98 @@
+package org.apache.sis.storage;
+
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferInt;
+import java.awt.image.RenderedImage;
+import java.util.Collections;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.BufferedGridCoverage;
+import org.apache.sis.coverage.grid.GridCoverage;
+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.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.TestCase;
+import org.apache.sis.util.iso.Names;
+import org.junit.Test;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.util.GenericName;
+import org.opengis.util.LocalName;
+
+import static org.apache.sis.referencing.operation.transform.MathTransforms.identity;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class ResourceProcessorTest extends TestCase {
+
+ /**
+ * Verify that resampling is activated as ordered when inverting CRS axes.
+ *
+ * Note: the test assertion is implementation specific. We assume that due to the trivial transform in play, the
+ * resample will only modify the conversion from grid to space, without changing associated image data.
+ */
+ @Test
+ public void resampleByCrs() throws Exception {
+ final LocalName name = Names.createLocalName(null, null, "resample-by-crs");
+ final GridCoverageResource resampled = nearestInterpol().resample(grid1234(), HardCodedCRS.WGS84_LATITUDE_FIRST, name);
+ GenericName queriedName = resampled.getIdentifier().orElseThrow(() -> new AssertionError("No name defined, but one was provided"));
+ assertEquals("resampled resource name", name, queriedName);
+ final GridCoverage read = resampled.read(null);
+ assertEquals(new AffineTransform2D(0, 1, 1, 0, 0, 0), read.getGridGeometry().getGridToCRS(PixelInCell.CELL_CENTER));
+ final RenderedImage rendered = read.render(null);
+ assertEquals("Resample dimensions: width", 2, rendered.getWidth());
+ assertEquals("Resample dimensions: height", 2, rendered.getHeight());
+
+ final int[] values = rendered.getData().getPixels(0, 0, 2, 2, (int[]) null);
+ assertArrayEquals(new int[] { 1, 2, 3, 4 }, values);
+ }
+
+ /**
+ * Force a simple x2 upsampling to ensure that resample is well activated.
+ */
+ @Test
+ public void resampleByGridGeometry() throws Exception {
+ final GridCoverageResource source = grid1234();
+ final GridGeometry sourceGG = source.getGridGeometry();
+ final GridGeometry upsampledGeom = new GridGeometry(new GridExtent(4, 4), sourceGG.getEnvelope(), GridOrientation.HOMOTHETY);
+ final GridCoverageResource resampled = nearestInterpol().resample(source, upsampledGeom, null);
+ resampled.getIdentifier().ifPresent(name -> fail("Name should be null, but a value was returned: "+name));
+ final RenderedImage rendered = resampled.read(null).render(null);
+ assertEquals("Resample dimensions: width", 4, rendered.getWidth());
+ assertEquals("Resample dimensions: height", 4, rendered.getHeight());
+
+ final int[] values = rendered.getData().getPixels(0, 0, 4, 4, (int[]) null);
+ assertArrayEquals(new int[] {
+ 1, 1, 2, 2,
+ 1, 1, 2, 2,
+ 3, 3, 4, 4,
+ 3, 3, 4, 4,
+ }, values);
+ }
+
+ /**
+ * Create a trivial 2D grid coverage of dimension 2x2. It uses an identity transform for grid to space conversion,
+ * and a common WGS84 coordinate reference system, with longitude first.
+ */
+ private static GridCoverageResource grid1234() {
+ GridGeometry domain = new GridGeometry(new GridExtent(2, 2), PixelInCell.CELL_CENTER, identity(2), HardCodedCRS.WGS84);
+ SampleDimension band = new SampleDimension.Builder()
+ .setBackground(0)
+ .addQuantitative("1-based row-major order pixel number", 1, 4, 1, 0, Units.UNITY)
+ .build();
+ DataBuffer values = new DataBufferInt(new int[] {1, 2, 3, 4}, 4);
+ return new MemoryGridResource(null, new BufferedGridCoverage(domain, Collections.singletonList(band), values));
+ }
+
+ private static ResourceProcessor nearestInterpol() {
+ final ImageProcessor imp = new ImageProcessor();
+ imp.setInterpolation(Interpolation.NEAREST);
+ return new ResourceProcessor(new GridCoverageProcessor(imp));
+ }
+}
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
index af2d2b5554..0c84a6090a 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
@@ -68,7 +68,8 @@ import org.junit.BeforeClass;
org.apache.sis.internal.storage.folder.StoreTest.class,
org.apache.sis.storage.aggregate.JoinFeatureSetTest.class,
org.apache.sis.storage.aggregate.ConcatenatedFeatureSetTest.class,
- org.apache.sis.storage.DataStoresTest.class
+ org.apache.sis.storage.DataStoresTest.class,
+ org.apache.sis.storage.ResourceProcessorTest.class
})
public final strictfp class StorageTestSuite extends TestSuite {
/**