You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by js...@apache.org on 2023/05/19 09:38:58 UTC
[sis] branch feat/coverage-json updated: feat(CoverageJson): add basic support for coverage json writing
This is an automated email from the ASF dual-hosted git repository.
jsorel pushed a commit to branch feat/coverage-json
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/feat/coverage-json by this push:
new 1e1677302c feat(CoverageJson): add basic support for coverage json writing
1e1677302c is described below
commit 1e1677302cee9c66a78631768a3dbaa7fb565a32
Author: jsorel <jo...@geomatys.com>
AuthorDate: Fri May 19 11:38:25 2023 +0200
feat(CoverageJson): add basic support for coverage json writing
---
.../internal/coveragejson/CoverageJsonStore.java | 76 +++++++-
.../internal/coveragejson/CoverageResource.java | 206 +++++++++++++++++++++
.../coveragejson/CoverageJsonStoreTest.java | 67 ++++++-
3 files changed, 343 insertions(+), 6 deletions(-)
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java
index 8a41c2d7bd..e0fada32a8 100644
--- a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java
@@ -18,9 +18,12 @@ package org.apache.sis.internal.coveragejson;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
import java.io.BufferedInputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -28,6 +31,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.internal.coveragejson.binding.Coverage;
import org.apache.sis.internal.coveragejson.binding.CoverageCollection;
import org.apache.sis.internal.coveragejson.binding.CoverageJsonObject;
@@ -36,6 +40,8 @@ import org.apache.sis.internal.storage.URIDataStore;
import org.apache.sis.internal.storage.io.IOUtilities;
import org.apache.sis.storage.DataStore;
import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.NoSuchDataException;
import org.apache.sis.storage.Resource;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.WritableAggregate;
@@ -99,6 +105,7 @@ public class CoverageJsonStore extends DataStore implements WritableAggregate {
components.add(new CoverageResource(this, coverage));
} else if (obj instanceof CoverageCollection) {
+ final CoverageCollection col = (CoverageCollection) obj;
throw new UnsupportedOperationException("Coverage collection not supported yet.");
}
@@ -111,15 +118,74 @@ public class CoverageJsonStore extends DataStore implements WritableAggregate {
return Collections.unmodifiableList(components);
}
-
@Override
- public Resource add(Resource resource) throws DataStoreException {
- throw new UnsupportedOperationException("Not supported yet.");
+ public synchronized Resource add(Resource resource) throws DataStoreException {
+ //ensure file is parsed
+ components();
+
+ if (resource instanceof GridCoverageResource) {
+ final GridCoverageResource gcr = (GridCoverageResource) resource;
+ final GridCoverage coverage = gcr.read(null);
+ final Coverage binding = CoverageResource.gridCoverageToBinding(coverage);
+ final CoverageResource jcr = new CoverageResource(this, binding);
+ components.add(jcr);
+ save();
+ return jcr;
+ }
+
+ throw new DataStoreException("Only GridCoverage resource are supported");
}
@Override
- public void remove(Resource resource) throws DataStoreException {
- throw new UnsupportedOperationException("Not supported yet.");
+ public synchronized void remove(Resource resource) throws DataStoreException {
+ //ensure file is parsed
+ components();
+
+ for (int i = 0, n = components.size(); i < n ;i++) {
+ if (components.get(i) == resource) {
+ components.remove(i);
+ save();
+ return;
+ }
+ }
+ throw new NoSuchDataException();
+ }
+
+ private synchronized void save() throws DataStoreException {
+ //ensure file is parsed
+ components();
+
+ final int size = components.size();
+ final String json;
+ if (size == 1) {
+ //single coverage
+ final CoverageResource res = (CoverageResource) components.get(0);
+
+ try (Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withFormatting(true))) {
+ json = jsonb.toJson(res.getBinding());
+ } catch (Exception ex) {
+ throw new DataStoreException("Failed to create coverage json binding", ex);
+ }
+ } else {
+ final CoverageCollection col = new CoverageCollection();
+ col.coverages = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ final CoverageResource res = (CoverageResource) components.get(i);
+ col.coverages.add(res.getBinding());
+ }
+
+ try (Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withFormatting(true))) {
+ json = jsonb.toJson(col);
+ } catch (Exception ex) {
+ throw new DataStoreException("Failed to create coverage collection json binding", ex);
+ }
+ }
+
+ try {
+ Files.write(path, json.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException ex) {
+ throw new DataStoreException("Failed to save coverage-json", ex);
+ }
}
@Override
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java
index 50f7e9d56c..d1041721c7 100644
--- a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java
@@ -18,6 +18,7 @@ package org.apache.sis.internal.coveragejson;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferDouble;
+import java.awt.image.RenderedImage;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
@@ -25,10 +26,12 @@ import java.time.format.DateTimeParseException;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
+import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -40,6 +43,7 @@ import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.GridRoundingMode;
+import org.apache.sis.image.PixelIterator;
import org.apache.sis.internal.coveragejson.binding.Axe;
import org.apache.sis.internal.coveragejson.binding.Axes;
import org.apache.sis.internal.coveragejson.binding.Coverage;
@@ -49,24 +53,30 @@ import org.apache.sis.internal.coveragejson.binding.GeographicCRS;
import org.apache.sis.internal.coveragejson.binding.IdentifierRS;
import org.apache.sis.internal.coveragejson.binding.NdArray;
import org.apache.sis.internal.coveragejson.binding.Parameter;
+import org.apache.sis.internal.coveragejson.binding.Parameters;
import org.apache.sis.internal.coveragejson.binding.ProjectedCRS;
+import org.apache.sis.internal.coveragejson.binding.Ranges;
import org.apache.sis.internal.coveragejson.binding.ReferenceSystemConnection;
import org.apache.sis.internal.coveragejson.binding.TemporalRS;
import org.apache.sis.internal.coveragejson.binding.VerticalCRS;
import org.apache.sis.measure.Units;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.IdentifiedObjects;
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.storage.AbstractGridCoverageResource;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.NoSuchDataException;
+import org.opengis.coverage.grid.SequenceType;
import org.opengis.metadata.spatial.DimensionNameType;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.referencing.operation.Matrix;
import org.opengis.util.FactoryException;
/**
@@ -130,6 +140,13 @@ final class CoverageResource extends AbstractGridCoverageResource {
}
}
+ /**
+ * Return the JSON coverage binding.
+ */
+ Coverage getBinding() {
+ return binding;
+ }
+
@Override
public GridGeometry getGridGeometry() throws DataStoreException {
return gridGeometry;
@@ -454,4 +471,193 @@ final class CoverageResource extends AbstractGridCoverageResource {
}
throw new DataStoreException("Unable to parse date : " + str);
}
+
+ public static Coverage gridCoverageToBinding(GridCoverage coverage) throws DataStoreException {
+ final Coverage binding = new Coverage();
+
+ try {
+ //build domain
+ binding.domain = gridGeometryToJson(coverage.getGridGeometry());
+ } catch (FactoryException ex) {
+ throw new DataStoreException(ex.getMessage(), ex);
+ }
+
+ //build parameters
+ binding.parameters = new Parameters();
+ for (SampleDimension sd : coverage.getSampleDimensions()) {
+ final Entry<String, Parameter> entry = sampleDimensionToJson(sd);
+ binding.parameters.setAnyProperty(entry.getKey(), entry.getValue());
+ }
+
+ //build datas
+ binding.ranges = new Ranges();
+ binding.ranges.any.putAll(imageToJson(coverage, new ArrayList(binding.parameters.any.keySet())));
+
+ return binding;
+ }
+
+ private static Domain gridGeometryToJson(GridGeometry gridGeometry) throws DataStoreException, FactoryException {
+ final Domain binding = new Domain();
+ binding.domainType = Domain.DOMAINTYPE_GRID;
+ binding.referencing = new ArrayList<>();
+ binding.axes = new Axes();
+
+ final GridExtent extent = gridGeometry.getExtent();
+ final MathTransform gridToCrs = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER);
+ final int dimension = gridGeometry.getDimension();
+
+ final long[] gridLow = extent.getLow().getCoordinateValues();
+ final int[] gridSize = new int[dimension];
+ final List<Integer> gridToCrsIndex = new ArrayList<>(dimension);
+ final double[] scales = new double[dimension];
+ final double[] offsets = new double[dimension];
+ if (gridToCrs instanceof LinearTransform) {
+ final Matrix matrix = ((LinearTransform)gridToCrs).getMatrix();
+ search:
+ for (int i = 0; i < dimension; i++) {
+ //find scale column
+ for (int c = 0; c < dimension; c++) {
+ double d = matrix.getElement(i, c);
+ if (d != 0.0) {
+ gridToCrsIndex.add(c);
+ gridSize[i] = Math.toIntExact(extent.getSize(i));
+ scales[i] = d;
+ offsets[i] = matrix.getElement(i, dimension);
+ }
+ continue search;
+ }
+ throw new DataStoreException("An axe in the Grid to CRS transform has no scale value");
+ }
+ } else {
+ //todo handle cases of compound transforms, would allow us to handle no linear 1D axes.
+ throw new DataStoreException("Coveragejson only support linear grid to CRS transform without rotation or shearing");
+ }
+
+ final CoordinateReferenceSystem crs = gridGeometry.getCoordinateReferenceSystem();
+ int crsIdx = 0;
+ for (CoordinateReferenceSystem scrs : CRS.getSingleComponents(crs)) {
+ final int gridIdx = gridToCrsIndex.get(crsIdx);
+ //coverage-json expect us to order x/y in longitude/latitude order
+ if (scrs instanceof org.opengis.referencing.crs.GeographicCRS) {
+ final org.opengis.referencing.crs.GeographicCRS gcrs = (org.opengis.referencing.crs.GeographicCRS) scrs;
+ final GeographicCRS grs = new GeographicCRS();
+ grs.id = toURI(gcrs);
+ final ReferenceSystemConnection rsc = new ReferenceSystemConnection();
+ rsc.coordinates = Arrays.asList("x", "y");
+ rsc.system = grs;
+ binding.referencing.add(rsc);
+
+ crsIdx +=2;
+ } else if (scrs instanceof org.opengis.referencing.crs.ProjectedCRS) {
+ final org.opengis.referencing.crs.ProjectedCRS pcrs = (org.opengis.referencing.crs.ProjectedCRS) scrs;
+ final ProjectedCRS grs = new ProjectedCRS();
+ grs.id = toURI(pcrs);
+ final ReferenceSystemConnection rsc = new ReferenceSystemConnection();
+ rsc.coordinates = Arrays.asList("x", "y");
+ rsc.system = grs;
+ binding.referencing.add(rsc);
+
+
+ crsIdx +=2;
+ } else if (scrs instanceof org.opengis.referencing.crs.VerticalCRS) {
+ final org.opengis.referencing.crs.VerticalCRS vcrs = (org.opengis.referencing.crs.VerticalCRS) scrs;
+
+ if (CommonCRS.Vertical.ELLIPSOIDAL.crs().equals(vcrs)) {
+ final VerticalCRS vrs = new VerticalCRS();
+ final ReferenceSystemConnection rsc = new ReferenceSystemConnection();
+ rsc.coordinates = Arrays.asList("z");
+ rsc.system = vrs;
+ binding.referencing.add(rsc);
+ binding.axes.z = buildAxe(gridLow[gridIdx], gridSize[gridIdx], scales[gridIdx], offsets[gridIdx], false);
+ } else {
+ throw new DataStoreException("A temporal reference system could not be mapped to CoverageJSON\n" + scrs.toString());
+ }
+
+ crsIdx++;
+ } else if (scrs instanceof org.opengis.referencing.crs.TemporalCRS) {
+ final org.opengis.referencing.crs.TemporalCRS tcrs = (org.opengis.referencing.crs.TemporalCRS) scrs;
+
+ if (CommonCRS.Temporal.JAVA.crs().equals(tcrs)) {
+ final TemporalRS trs = new TemporalRS();
+ trs.calendar = "Gregorian";
+ final ReferenceSystemConnection rsc = new ReferenceSystemConnection();
+ rsc.coordinates = Arrays.asList("t");
+ rsc.system = trs;
+ binding.referencing.add(rsc);
+ binding.axes.t = buildAxe(gridLow[gridIdx], gridSize[gridIdx], scales[gridIdx], offsets[gridIdx], true);
+ } else {
+ throw new DataStoreException("A temporal reference system could not be mapped to CoverageJSON\n" + scrs.toString());
+ }
+
+ crsIdx++;
+ } else {
+ throw new DataStoreException("A coordinate reference system could not be mapped to CoverageJSON\n" + scrs.toString());
+ }
+ }
+
+ return binding;
+ }
+
+ private static Entry<String,Parameter> sampleDimensionToJson(SampleDimension sd) {
+ final Parameter binding = new Parameter();
+ final String name = sd.getName().toString();
+ binding.id = name;
+ //TODO convert categories, units,... we might need a database of observed properties
+ return new AbstractMap.SimpleImmutableEntry<>(name, binding);
+ }
+
+ private static Map<String,NdArray> imageToJson(GridCoverage coverage, List<String> properties) throws DataStoreException {
+ if (coverage.getGridGeometry().getDimension() != 2) {
+ throw new DataStoreException("Only Grid coverage 2D supported as this time");
+ }
+
+ final RenderedImage image = coverage.render(null);
+ final PixelIterator ite = new PixelIterator.Builder().setIteratorOrder(SequenceType.LINEAR).create(image);
+ final int width = image.getWidth();
+ final int height = image.getHeight();
+
+ final int nbSample = properties.size();
+ final double[] pixel = new double[nbSample];
+ final NdArray[] arrays = new NdArray[nbSample];
+ final Map<String,NdArray> map = new LinkedHashMap<>();
+ for (int i = 0; i < nbSample; i++) {
+ arrays[i] = new NdArray();
+ arrays[i].dataType = NdArray.DATATYPE_FLOAT;
+ arrays[i].shape = new int[]{width, height};
+ arrays[i].axisNames = new String[]{"y","x"};
+ arrays[i].values = new ArrayList<>();
+ map.put(properties.get(i), arrays[i]);
+ }
+
+ while (ite.next()) {
+ ite.getPixel(pixel);
+ for (int i = 0; i < nbSample; i++) {
+ arrays[i].values.add(pixel[i]);
+ }
+ }
+
+ return map;
+ }
+
+ private static Axe buildAxe(long gridLow, int gridSize, double scale, double offset, boolean asDate) {
+ final Axe axe = new Axe();
+ if (asDate) {
+ axe.values = new ArrayList<>(gridSize);
+ for (int i = 0; i < gridSize; i++) {
+ Instant ofEpochMilli = Instant.ofEpochMilli((long) ((gridLow+i)*scale + offset));
+ axe.values.add(DATE_TIME.format(ofEpochMilli));
+ }
+ } else {
+ axe.num = gridSize;
+ axe.start = gridLow*scale + offset;
+ axe.stop = (gridLow+gridSize-1) * scale + offset;
+ }
+ return axe;
+ }
+
+ private static String toURI(CoordinateReferenceSystem crs) throws FactoryException, DataStoreException {
+ final Integer code = IdentifiedObjects.lookupEPSG(crs);
+ if (code == null) throw new DataStoreException("Could not find EPSG code for CRS " + crs);
+ return "http://www.opengis.net/def/crs/EPSG/0/" + code;
+ }
}
diff --git a/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java
index 23fe71e7fd..34ae3c7939 100644
--- a/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java
+++ b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java
@@ -17,17 +17,28 @@
package org.apache.sis.internal.coveragejson;
import jakarta.json.bind.JsonbBuilder;
+import java.awt.image.BufferedImage;
import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridCoverageBuilder;
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.internal.storage.MemoryGridResource;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.storage.Aggregate;
import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.GridCoverageResource;
import org.apache.sis.storage.Resource;
import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.WritableAggregate;
import org.apache.sis.test.TestCase;
import org.eclipse.yasson.internal.JsonBindingBuilder;
import org.junit.Assert;
@@ -44,7 +55,7 @@ public class CoverageJsonStoreTest extends TestCase {
* Test coverage example from https://covjson.org/playground/.
*/
@Test
- public void testCoverageXYZT() throws Exception {
+ public void testReadCoverageXYZT() throws Exception {
try (final DataStore store = new CoverageJsonStoreProvider().open(new StorageConnector(CoverageJsonStoreTest.class.getResource("coverage_xyzt.json")))) {
@@ -78,9 +89,63 @@ public class CoverageJsonStoreTest extends TestCase {
{ //test data
GridCoverage coverage = gcr.read(null);
Raster data = coverage.render(null).getData();
+ Assert.assertEquals(0.5, data.getSampleDouble(0, 0, 0), 0.0);
+ Assert.assertEquals(0.6, data.getSampleDouble(1, 0, 0), 0.0);
+ Assert.assertEquals(0.4, data.getSampleDouble(2, 0, 0), 0.0);
+ Assert.assertEquals(0.6, data.getSampleDouble(0, 1, 0), 0.0);
+ Assert.assertEquals(0.2, data.getSampleDouble(1, 1, 0), 0.0);
+ Assert.assertEquals(Double.NaN, data.getSampleDouble(2, 1, 0), 0.0);
}
}
}
+ /**
+ * Test writing most simple 2D Grid coverage.
+ */
+ @Test
+ public void testWriteCoverageXY() throws IOException, DataStoreException {
+
+ final Path tempPath = Files.createTempFile("test", ".covjson");
+ Files.delete(tempPath);
+
+ try (final DataStore store = new CoverageJsonStoreProvider().open(new StorageConnector(tempPath))) {
+
+ //test grid coverage resource exist
+ Assert.assertTrue(store instanceof WritableAggregate);
+ final WritableAggregate aggregate = (WritableAggregate) store;
+ Assert.assertEquals(0, aggregate.components().size());
+
+ //write a grid coverage
+ final GridGeometry grid = new GridGeometry(new GridExtent(4,2), CRS.getDomainOfValidity(CommonCRS.WGS84.normalizedGeographic()), GridOrientation.REFLECTION_Y);
+ final BufferedImage image = new BufferedImage(4, 2, BufferedImage.TYPE_BYTE_GRAY);
+ final WritableRaster raster = image.getRaster();
+ raster.setSample(0, 0, 0, 1);
+ raster.setSample(1, 0, 0, 2);
+ raster.setSample(2, 0, 0, 3);
+ raster.setSample(3, 0, 0, 4);
+ raster.setSample(0, 1, 0, 5);
+ raster.setSample(1, 1, 0, 6);
+ raster.setSample(2, 1, 0, 7);
+ raster.setSample(3, 1, 0, 8);
+
+
+ final GridCoverageBuilder gcb = new GridCoverageBuilder();
+ gcb.setDomain(grid);
+ gcb.setValues(image);
+ final GridCoverage coverage = gcb.build();
+
+ final GridCoverageResource gcr = new MemoryGridResource(null, coverage, null);
+
+ aggregate.add(gcr);
+
+
+ String json = Files.readString(tempPath, StandardCharsets.UTF_8);
+ System.out.println(json);
+
+ } finally {
+ Files.deleteIfExists(tempPath);
+ }
+ }
+
}