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 <johann.so...@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); + } + } + }