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