This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 9686f9d864ed0cc4cb68ccfc1eb7245ab71a9e33
Merge: 7276317ed7 b81aa21f91
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Oct 28 16:16:18 2023 +0200

    Merge branch 'geoapi-3.1'

 .../sis/coverage/grid/j2d/ImageUtilities.java      |  18 +
 .../test/org/apache/sis/image/TiledImageMock.java  |  38 +-
 .../apache/sis/referencing/IdentifiedObjects.java  |  25 +-
 .../referencing/operation/matrix/MatrixSIS.java    |  36 +-
 .../org/apache/sis/storage/geotiff/DataCube.java   |   9 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java |   5 +-
 .../apache/sis/storage/geotiff/DeferredEntry.java  |   2 +
 .../org/apache/sis/storage/geotiff/GeoKeys.java    | 142 ----
 .../apache/sis/storage/geotiff/GeoTiffOption.java  |  60 ++
 .../apache/sis/storage/geotiff/GeoTiffStore.java   | 280 +++++--
 .../sis/storage/geotiff/GeoTiffStoreProvider.java  |  26 +-
 .../storage/geotiff/{GeoTIFF.java => IOBase.java}  |  60 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |  40 +-
 .../apache/sis/storage/geotiff/NativeMetadata.java |  21 +-
 .../org/apache/sis/storage/geotiff/Reader.java     | 107 ++-
 .../org/apache/sis/storage/geotiff/Writer.java     | 826 +++++++++++++++++++++
 .../geotiff/{internal => base}/Compression.java    |   2 +-
 .../sis/storage/geotiff/{ => base}/GeoCodes.java   |  33 +-
 .../apache/sis/storage/geotiff/base/GeoKeys.java   | 126 ++++
 .../geotiff/{internal => base}/Predictor.java      |   2 +-
 .../geotiff/{internal => base}/Resources.java      |  22 +-
 .../{internal => base}/Resources.properties        |   4 +
 .../geotiff/{internal => base}/Resources_en.java   |   2 +-
 .../geotiff/{internal => base}/Resources_fr.java   |   2 +-
 .../{internal => base}/Resources_fr.properties     |   4 +
 .../sis/storage/geotiff/{ => base}/Tags.java       |   8 +-
 .../apache/sis/storage/geotiff/base/UnitKey.java   | 186 +++++
 .../geotiff/{internal => base}/package-info.java   |   4 +-
 .../geotiff/inflater/CompressionChannel.java       |   2 +-
 .../sis/storage/geotiff/inflater/Inflater.java     |   9 +-
 .../apache/sis/storage/geotiff/inflater/LZW.java   |   2 +-
 .../storage/geotiff/inflater/PredictorChannel.java |   2 +-
 .../apache/sis/storage/geotiff/package-info.java   |   2 +-
 .../storage/geotiff/{ => reader}/CRSBuilder.java   | 317 ++++----
 .../geotiff/{ => reader}/GeoKeysLoader.java        |  36 +-
 .../geotiff/{ => reader}/GridGeometryBuilder.java  |  38 +-
 .../geotiff/{ => reader}/ImageMetadataBuilder.java |  46 +-
 .../storage/geotiff/{ => reader}/Localization.java |   2 +-
 .../geotiff/{ => reader}/ReversedBitsChannel.java  |  26 +-
 .../sis/storage/geotiff/{ => reader}/Type.java     |   8 +-
 .../storage/geotiff/{ => reader}/XMLMetadata.java  |  39 +-
 .../geotiff/{internal => reader}/package-info.java |  10 +-
 .../sis/storage/geotiff/writer/GeoEncoder.java     | 800 ++++++++++++++++++++
 .../storage/geotiff/writer/ReformattedImage.java   | 158 ++++
 .../sis/storage/geotiff/writer/TagValue.java       | 105 +++
 .../sis/storage/geotiff/writer/TileMatrix.java     | 212 ++++++
 .../geotiff/{internal => writer}/package-info.java |  10 +-
 .../org/apache/sis/storage/geotiff/WriterTest.java | 479 ++++++++++++
 .../{internal => base}/CompressionTest.java        |   2 +-
 .../storage/geotiff/{ => base}/GeoCodesTest.java   |   2 +-
 .../storage/geotiff/{ => base}/GeoIdentifiers.java |   2 +-
 .../storage/geotiff/{ => base}/GeoKeysTest.java    |  14 +-
 .../sis/storage/geotiff/{ => base}/TagsTest.java   |   2 +-
 .../geotiff/{ => reader}/CRSBuilderTest.java       |   2 +-
 .../sis/storage/geotiff/{ => reader}/TypeTest.java |   2 +-
 .../geotiff/{ => reader}/XMLMetadataTest.java      |   2 +-
 .../org/apache/sis/storage/gpx/StoreProvider.java  |   2 +-
 .../org.apache.sis.storage/main/module-info.java   |   4 +-
 .../main/org/apache/sis/io/stream/ChannelData.java | 285 ++++---
 .../org/apache/sis/io/stream/ChannelDataInput.java | 298 ++++++--
 .../apache/sis/io/stream/ChannelDataOutput.java    | 407 +++++++---
 .../sis/io/stream/ChannelImageInputStream.java     | 115 +--
 .../sis/io/stream/ChannelImageOutputStream.java    | 223 +++---
 .../apache/sis/io/stream/HyperRectangleReader.java |   2 +-
 .../apache/sis/io/stream/HyperRectangleWriter.java | 301 ++++++++
 .../main/org/apache/sis/io/stream/IOUtilities.java |  75 +-
 .../apache/sis/io/stream/InputStreamAdapter.java   |  58 +-
 .../main/org/apache/sis/io/stream/Markable.java    |   2 +-
 .../apache/sis/io/stream/OutputStreamAdapter.java  |  11 +-
 .../main/org/apache/sis/io/stream/Region.java      |  38 +-
 .../org/apache/sis/io/stream/UpdatableWrite.java   | 352 +++++++++
 .../sis/storage/ReadOnlyStorageException.java      |   1 +
 .../org/apache/sis/storage/StorageConnector.java   | 138 ++--
 ...ception.java => WriteOnlyStorageException.java} |  30 +-
 .../apache/sis/storage/base/MetadataBuilder.java   |   6 +-
 .../apache/sis/storage/base/MetadataFetcher.java   | 399 ++++++++++
 .../org/apache/sis/storage/base/URIDataStore.java  |  31 +-
 .../sis/storage/esri/AsciiGridStoreProvider.java   |   2 +-
 .../org/apache/sis/storage/image/FormatFinder.java |   2 +-
 .../storage/internal/WritableResourceSupport.java  |   1 -
 .../main/org/apache/sis/storage/package-info.java  |   2 +-
 .../org/apache/sis/io/stream/ByteArrayChannel.java |  49 +-
 .../sis/io/stream/ChannelDataOutputTest.java       | 406 +++++-----
 .../apache/sis/io/stream/ChannelDataTestCase.java  |  53 +-
 .../sis/io/stream/ChannelImageInputStreamTest.java |  97 ++-
 .../io/stream/ChannelImageOutputStreamTest.java    | 257 ++++---
 .../sis/io/stream/HyperRectangleWriterTest.java    | 206 +++++
 .../io/stream/MemoryCacheImageOutputStream.java    |  68 +-
 .../apache/sis/io/stream/UpdatableWriteTest.java   | 145 ++++
 .../apache/sis/storage/StorageConnectorTest.java   |  10 +-
 .../org/apache/sis/util/internal/Numerics.java     |   6 +
 .../main/org/apache/sis/util/resources/Errors.java |  10 +
 .../apache/sis/util/resources/Errors.properties    |   2 +
 .../apache/sis/util/resources/Errors_fr.properties |   2 +
 incubator/build.gradle.kts                         |   1 +
 .../org.apache.sis.storage.DataStoreProvider       |   4 +
 .../main/module-info.java                          |  54 ++
 .../storage/coveragejson/CoverageJsonStore.java    | 195 +++++
 .../coveragejson/CoverageJsonStoreProvider.java    | 104 +++
 .../sis/storage/coveragejson/CoverageResource.java | 727 ++++++++++++++++++
 .../sis/storage/coveragejson/binding/Axe.java      | 153 ++++
 .../sis/storage/coveragejson/binding/Axes.java     |  63 ++
 .../sis/storage/coveragejson/binding/Category.java |  76 ++
 .../coveragejson/binding/CategoryEncoding.java     |  82 ++
 .../sis/storage/coveragejson/binding/Coverage.java | 147 ++++
 .../coveragejson/binding/CoverageCollection.java   | 103 +++
 .../coveragejson/binding/CoverageJsonObject.java   |  58 ++
 .../storage/coveragejson/binding/Dictionary.java   |  73 ++
 .../sis/storage/coveragejson/binding/Domain.java   |  99 +++
 .../coveragejson/binding/GeographicCRS.java        |  79 ++
 .../sis/storage/coveragejson/binding/I18N.java     | 125 ++++
 .../storage/coveragejson/binding/IdentifierRS.java |  95 +++
 .../storage/coveragejson/binding/Identifiers.java  |  17 +-
 .../sis/storage/coveragejson/binding/NdArray.java  | 116 +++
 .../coveragejson/binding/ObservedProperty.java     |  94 +++
 .../storage/coveragejson/binding/Parameter.java    | 105 +++
 .../coveragejson/binding/ParameterGroup.java       |  98 +++
 .../storage/coveragejson/binding/Parameters.java   |  78 ++
 .../storage/coveragejson/binding/ProjectedCRS.java |  71 ++
 .../sis/storage/coveragejson/binding/Ranges.java   |  74 ++
 .../binding/ReferenceSystemConnection.java         |  74 ++
 .../sis/storage/coveragejson/binding/Symbol.java   |  65 ++
 .../coveragejson/binding/TargetConcept.java        |  59 ++
 .../storage/coveragejson/binding/TemporalRS.java   |  86 +++
 .../sis/storage/coveragejson/binding/TileSet.java  |  81 ++
 .../storage/coveragejson/binding/TiledNdArray.java |  90 +++
 .../sis/storage/coveragejson/binding/Unit.java     |  85 +++
 .../storage/coveragejson/binding/VerticalCRS.java  |  68 ++
 .../storage/coveragejson/binding/package-info.java |  15 +-
 .../sis/storage/coveragejson/package-info.java     |  15 +-
 .../coveragejson/CoverageJsonStoreTest.java        | 142 ++++
 .../storage/coveragejson/binding/BindingTest.java  | 236 ++++++
 .../sis/storage/coveragejson/binding/LICENSE.md    |   5 +
 .../storage/coveragejson/binding/axe_bounds.json   |  12 +
 .../storage/coveragejson/binding/axe_polygon.json  |  33 +
 .../storage/coveragejson/binding/axe_regular.json  |   5 +
 .../storage/coveragejson/binding/axe_tuples.json   |  20 +
 .../binding/coverage_vertical_profile.json         |  91 +++
 .../binding/coverage_vertical_profile_nocs.json    |  80 ++
 .../coveragejson/binding/coveragecollection.json   |  92 +++
 .../storage/coveragejson/binding/domain_grid.json  |  26 +
 .../coveragejson/binding/domain_trajectory.json    |  27 +
 .../coveragejson/binding/domaintype_grid.json      |   5 +
 .../binding/domaintype_multipoint.json             |  30 +
 .../binding/domaintype_multipointseries.json       |  31 +
 .../binding/domaintype_multipolygon.json           |  32 +
 .../binding/domaintype_multipolygonseries.json     |  32 +
 .../coveragejson/binding/domaintype_point.json     |  24 +
 .../binding/domaintype_pointseries.json            |  26 +
 .../coveragejson/binding/domaintype_polygon.json   |  29 +
 .../binding/domaintype_polygonseries.json          |  31 +
 .../coveragejson/binding/domaintype_section.json   |  31 +
 .../binding/domaintype_trajectory.json             |  30 +
 .../binding/domaintype_vertical_profile.json       |  26 +
 .../binding/geographiccrs_longlat.json             |   4 +
 .../binding/geographiccrs_longlatheight.json       |   4 +
 .../sis/storage/coveragejson/binding/ndarray.json  |  10 +
 .../binding/parameter_categoricaldata.json         |  33 +
 .../binding/parameter_continuousdata.json          |  24 +
 .../binding/parametergroup_uncertainty.json        |  13 +
 .../binding/parametergroup_vectorquantity.json     |   9 +
 .../binding/projectedcrs_britishnationalgrid.json  |   4 +
 .../binding/reference_system_connection.json       |   7 +
 .../storage/coveragejson/binding/temporalrs.json   |   4 +
 .../storage/coveragejson/binding/tiledndarray.json |  16 +
 .../coveragejson/binding/verticalcrs_navd88.json   |   4 +
 .../sis/storage/coveragejson/coverage_xyzt.json    |  58 ++
 netbeans-project/ivy.xml                           |   1 +
 netbeans-project/nbproject/project.properties      |   1 +
 settings.gradle.kts                                |   1 +
 170 files changed, 11764 insertions(+), 1571 deletions(-)

diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
index 0000000000,d02ccefe7c..8e0f417ddb
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataFetcher.java
@@@ -1,0 -1,395 +1,399 @@@
+ /*
+  * 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.base;
+ 
+ import java.util.ArrayList;
+ import java.util.Date;
+ import java.util.List;
+ import java.util.Locale;
+ import java.util.Set;
+ import java.util.function.BiPredicate;
+ import java.util.function.Function;
+ import org.opengis.util.CodeList;
+ import org.opengis.util.InternationalString;
+ import org.apache.sis.util.collection.CodeListSet;
+ import org.opengis.metadata.Metadata;
+ import org.opengis.metadata.citation.Citation;
+ import org.opengis.metadata.citation.CitationDate;
+ import org.opengis.metadata.citation.DateType;
 -import org.opengis.metadata.citation.Party;
 -import org.opengis.metadata.citation.Responsibility;
+ import org.opengis.metadata.citation.Series;
+ import org.opengis.metadata.identification.Identification;
+ import org.opengis.metadata.lineage.Lineage;
+ import org.opengis.metadata.lineage.ProcessStep;
+ import org.opengis.metadata.acquisition.AcquisitionInformation;
+ import org.opengis.metadata.acquisition.Instrument;
+ import org.opengis.metadata.acquisition.Platform;
+ import org.opengis.metadata.spatial.CellGeometry;
+ import org.opengis.metadata.spatial.Georectified;
+ import org.opengis.metadata.spatial.SpatialRepresentation;
+ import org.opengis.metadata.spatial.GridSpatialRepresentation;
+ 
++import org.opengis.metadata.citation.ResponsibleParty;
++import org.apache.sis.metadata.iso.DefaultMetadata;
++
+ 
+ /**
+  * Helper methods for fetching metadata to be written by {@code DataStore} 
implementations.
+  * This is not a general-purpose builder suitable for public API, since the 
methods provided
+  * in this class are tailored for Apache SIS data store needs.
+  * API of this class may change in any future SIS versions.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  *
+  * @param  <T>  type of temporal objects.
+  */
+ public abstract class MetadataFetcher<T> {
+     /**
+      * Title of the resource, or {@code null} if none.
+      *
+      * <p>Path: {@code metadata/identificationInfo/citation/title}</p>
+      */
+     public List<String> title;
+ 
+     /**
+      * Name of the series of which the resource is a part, or {@code null} if 
none.
+      *
+      * <p>Path: {@code metadata/identificationInfo/citation/series/name}</p>
+      */
+     public List<String> series;
+ 
+     /**
+      * Details on which pages of the publication the resource was published, 
or {@code null} if none.
+      *
+      * <p>Path: {@code metadata/identificationInfo/citation/series/page}</p>
+      */
+     public List<String> page;
+ 
+     /**
+      * Names of the authors, or {@code null} if none.
+      *
+      * <p>Path: {@code metadata/identificationInfo/citation/party/name}</p>
+      */
+     public List<String> party;
+ 
+     /**
+      * Dates when the resource has been created, or {@code null} if none.
+      * Limited to a singleton by default.
+      *
+      * <p>Path: {@code metadata/identificationInfo/citation/date}</p>
+      */
+     public List<T> creationDate;
+ 
+     /**
+      * Unique identification of the measuring instrument, or {@code null} if 
none.
+      *
+      * <p>Path: {@code 
metadata/acquisitionInformation/platform/instrument/identifier}</p>
+      */
+     public List<String> instrument;
+ 
+     /**
+      * Reference to document describing processing software, or {@code null} 
if none.
+      *
+      * <p>Path: {@code 
metadata/resourceLineage/processStep/processingInformation/softwareReference/title}</p>
+      */
+     public List<String> software;
+ 
+     /**
+      * Additional details about the processing procedure, or {@code null} if 
none.
+      *
+      * <p>Path: {@code 
metadata/resourceLineage/processStep/processingInformation/procedureDescription}</p>
+      */
+     public List<String> procedure;
+ 
+     /**
+      * General description of the transformation to a georectified grid, or 
{@code null} if none.
+      *
+      * <p>Path: {@code 
metadata/spatialRepresentationInfo/transformationDimensionDescription}</p>
+      */
+     public List<String> transformationDimension;
+ 
+     /**
+      * Whether grid data are representative of pixel areas or points, or 
{@code null} if none.
+      */
+     public Set<CellGeometry> cellGeometry;
+ 
+     /**
+      * The locale to use for converting international strings to {@link 
String} objects.
+      * May also be used for date or number formatting.
+      */
+     protected final Locale locale;
+ 
+     /**
+      * Creates an initially empty metadata fetcher.
+      *
+      * @param  locale  the locale to use for converting international strings 
to {@link String} objects.
+      */
+     public MetadataFetcher(final Locale locale) {
+         this.locale = locale;
+     }
+ 
+     /**
+      * Invokes {@code this.accept(E)} for all elements in the given 
collection.
+      * This method ignores null values (this is a paranoiac safety) and stops
+      * the iteration if an {@code accept(E)} call returns {@code true}.
+      *
+      * @param  <E>       type of elements in the method.
+      * @param  accept    the method to invoke for each element.
+      * @param  elements  the collection of elements, or {@code null} if none.
+      */
+     private <E> void forEach(final BiPredicate<MetadataFetcher<T>,E> accept, 
final Iterable<? extends E> elements) {
+         if (elements != null) {
+             for (final E info : elements) {
+                 if (info != null && accept.test(this, info)) break;
+             }
+         }
+     }
+ 
+     /**
+      * Fetches some properties from the given metadata object.
+      * The default implementation iterates over the {@link Identification}, 
{@link Lineage} and
+      * {@link AcquisitionInformation} objects, filters null values (this is a 
paranoiac safety),
+      * then delegate to the corresponding {@code accept(…)} method.
+      *
+      * @param  info  the metadata, or {@code null} if none.
+      */
+     public void accept(final Metadata info) {
+         if (info != null) {
+             forEach(MetadataFetcher::accept, info.getIdentificationInfo());
 -            forEach(MetadataFetcher::accept, info.getResourceLineages());
+             forEach(MetadataFetcher::accept, 
info.getAcquisitionInformation());
+             forEach(MetadataFetcher::accept, 
info.getSpatialRepresentationInfo());
++            if (info instanceof DefaultMetadata) {
++                forEach(MetadataFetcher::accept, ((DefaultMetadata) 
info).getResourceLineages());
++            }
+         }
+     }
+ 
+     /**
+      * Fetches some properties from the given identification object.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the identification object (not null).
+      * @return whether to stop iteration after the given object.
+      */
+     protected boolean accept(final Identification info) {
+         final Citation citation = info.getCitation();
+         if (citation == null) {
+             return false;
+         }
+         title = addString(title, citation.getTitle());
+         final Series e = citation.getSeries();
+         if (e != null) {
+             series = addString(series, e.getName());
+             page   = addString(page,   e.getPage());
+         }
+         forEach(MetadataFetcher::accept, 
citation.getCitedResponsibleParties());
+         forEach(MetadataFetcher::accept, citation.getDates());
+         return title != null;
+     }
+ 
+     /**
+      * Fetches some properties from the given responsible party.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the responsible party (not null).
+      * @return whether to stop iteration after the given object.
+      */
 -    protected boolean accept(final Responsibility info) {
 -        party = addStrings(party, info.getParties(), Party::getName);
++    protected boolean accept(final ResponsibleParty info) {
++        party = addString(party, info.getIndividualName());
++        party = addString(party, info.getOrganisationName());
+         return false;
+     }
+ 
+     /**
+      * Fetches some properties from the given resource citation date.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the resource citation date (not null).
+      * @return whether to stop iteration after the given object.
+      */
+     protected boolean accept(final CitationDate info) {
+         if (creationDate == null) {
+             if (DateType.CREATION.equals(info.getDateType())) {
+                 creationDate = List.of(convertDate(info.getDate()));
+             } else {
+                 return false;       // Search another date.
+             }
+         }
+         return true;
+     }
+ 
+     /**
+      * Fetches some properties from the given lineage object.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the lineage object (not null).
+      * @return whether to stop iteration after the given object.
+      */
+     protected boolean accept(final Lineage info) {
+         forEach(MetadataFetcher::accept, info.getProcessSteps());
+         return false;
+     }
+ 
+     /**
+      * Fetches some properties from the given process step.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the process step object (not null).
+      * @return whether to stop iteration after the given object.
+      */
+     protected boolean accept(final ProcessStep info) {
+         final var processing = info.getProcessingInformation();
+         if (processing != null) {
+             software  = addStrings(software,  
processing.getSoftwareReferences(), Citation::getTitle);
+             procedure = addString (procedure, 
processing.getProcedureDescription());
+         }
+         return false;
+     }
+ 
+     /**
+      * Fetches some properties from the given acquisition object.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the acquisition object (not null).
+      * @return whether to stop iteration after the given object.
+      */
+     protected boolean accept(final AcquisitionInformation info) {
+         forEach(MetadataFetcher::accept, info.getPlatforms());
+         return false;
+     }
+ 
+     /**
+      * Fetches some properties from the given platform object.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the platform object (not null).
+      * @return whether to stop iteration after the given object.
+      */
+     protected boolean accept(final Platform info) {
+         forEach(MetadataFetcher::accept, info.getInstruments());
+         return false;
+     }
+ 
+     /**
+      * Fetches some properties from the given instrument object.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the instrument object (not null).
+      * @return whether to stop iteration after the given object.
+      */
+     protected boolean accept(final Instrument info) {
+         final var id = info.getIdentifier();
+         if (id != null) {
+             instrument = addString(instrument, id.getCode());
+         }
+         return false;
+     }
+ 
+     /**
+      * Fetches some properties from the given spatial representation object.
+      * Subclasses can override if they need to fetch more details.
+      *
+      * @param  info  the spatial representation object (not null).
+      * @return whether to stop iteration after the given object.
+      */
+     protected boolean accept(final SpatialRepresentation info) {
+         if (info instanceof GridSpatialRepresentation) {
+             addCode(CellGeometry.class, cellGeometry, 
((GridSpatialRepresentation) info).getCellGeometry());
+             if (info instanceof Georectified) {
+                 addString(transformationDimension, ((Georectified) 
info).getTransformationDimensionDescription());
+             }
+         }
+         return false;
+     }
+ 
+     /**
+      * Adds all international strings in the given collection.
+      *
+      * @param  <E>     type of elements in the collection.
+      * @param  target  where to add strings, or {@code null} if not yet 
created.
+      * @param  source  elements from which to get international string, or 
{@code null} if none.
+      * @param  getter  method to invoke on each element for getting the 
international string.
+      * @return the collection where strings were added.
+      */
+     private <E> List<String> addStrings(List<String> target, final Iterable<? 
extends E> source,
+                                         final Function<E,InternationalString> 
getter)
+     {
+         if (source != null) {
+             for (final E e : source) {
+                 if (e != null) {
+                     target = addString(target, getter.apply(e));
+                 }
+             }
+         }
+         return target;
+     }
+ 
+     /**
+      * Adds the given international string in the given collection.
+      *
+      * @param  target  where to add the string, or {@code null} if not yet 
created.
+      * @param  value   the international string to add, or {@code null} if 
none.
+      * @return the collection where the string was added.
+      */
+     private List<String> addString(List<String> target, final 
InternationalString value) {
+         if (value != null) {
+             target = addString(target, value.toString(locale));
+         }
+         return target;
+     }
+ 
+     /**
+      * Adds the given string in the given collection.
+      *
+      * @param  target  where to add the string, or {@code null} if not yet 
created.
+      * @param  value   the string to add, or {@code null} if none.
+      * @return the collection where the string was added.
+      */
+     private static List<String> addString(List<String> target, String value) {
+         if (value != null && !(value = value.trim()).isEmpty()) {
+             if (target == null) {
+                 target = new ArrayList<>(2);        // We will usually have 
only one element.
+             }
+             target.add(value);
+         }
+         return target;
+     }
+ 
+     /**
+      * Adds the given code in the given collection.
+      *
+      * @param  <E>     compile-time value of {@code type} argument.
+      * @param  type    type of code list elements.
+      * @param  target  collection where to add the code.
+      * @param  value   the code to add.
+      * @return the collection where to code was added.
+      */
+     private static <E extends CodeList<E>> Set<E> addCode(final Class<E> 
type, Set<E> target, final E value) {
+         if (value != null) {
+             if (target == null) {
+                 target = new CodeListSet<>(type);
+             }
+             target.add(value);
+         }
+         return target;
+     }
+ 
+     /**
+      * Converts the given date into the object to store.
+      * The {@code <T>} type may be for example {@code <String>}
+      * with a string representation specified by the format implemented by 
the store.
+      *
+      * @param  date  the date to convert.
+      * @return subclass-dependent object representing the given date.
+      */
+     protected abstract T convertDate(final Date date);
+ }
diff --cc 
incubator/src/org.apache.sis.storage.coveragejson/main/org/apache/sis/storage/coveragejson/CoverageResource.java
index 0000000000,305c924a72..2891698e2e
mode 000000,100644..100644
--- 
a/incubator/src/org.apache.sis.storage.coveragejson/main/org/apache/sis/storage/coveragejson/CoverageResource.java
+++ 
b/incubator/src/org.apache.sis.storage.coveragejson/main/org/apache/sis/storage/coveragejson/CoverageResource.java
@@@ -1,0 -1,723 +1,727 @@@
+ /*
+  * 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.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;
+ 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;
+ import javax.measure.Unit;
+ import org.apache.sis.coverage.SampleDimension;
+ import org.apache.sis.coverage.grid.BufferedGridCoverage;
+ import org.apache.sis.coverage.grid.DisjointExtentException;
+ 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.storage.coveragejson.binding.Axe;
+ import org.apache.sis.storage.coveragejson.binding.Axes;
+ import org.apache.sis.storage.coveragejson.binding.Category;
+ import org.apache.sis.storage.coveragejson.binding.CategoryEncoding;
+ import org.apache.sis.storage.coveragejson.binding.Coverage;
+ import org.apache.sis.storage.coveragejson.binding.CoverageJsonObject;
+ import org.apache.sis.storage.coveragejson.binding.Domain;
+ import org.apache.sis.storage.coveragejson.binding.GeographicCRS;
+ import org.apache.sis.storage.coveragejson.binding.IdentifierRS;
+ import org.apache.sis.storage.coveragejson.binding.NdArray;
+ import org.apache.sis.storage.coveragejson.binding.ObservedProperty;
+ import org.apache.sis.storage.coveragejson.binding.Parameter;
+ import org.apache.sis.storage.coveragejson.binding.Parameters;
+ import org.apache.sis.storage.coveragejson.binding.ProjectedCRS;
+ import org.apache.sis.storage.coveragejson.binding.Ranges;
+ import org.apache.sis.storage.coveragejson.binding.ReferenceSystemConnection;
+ import org.apache.sis.storage.coveragejson.binding.TemporalRS;
+ import org.apache.sis.storage.coveragejson.binding.VerticalCRS;
+ import org.apache.sis.measure.NumberRange;
+ 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;
+ 
++import org.apache.sis.image.SequenceType;
++
+ 
+ /**
+  *
+  * @author Johann Sorel (Geomatys)
+  */
+ final class CoverageResource extends AbstractGridCoverageResource {
+ 
+     private static final DateTimeFormatter YEAR = new 
DateTimeFormatterBuilder()
+                 .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                 .toFormatter();
+ 
+ 
+     private static final DateTimeFormatter YEAR_MONTH = new 
DateTimeFormatterBuilder()
+                 .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                 .appendLiteral('-')
+                 .appendValue(ChronoField.MONTH_OF_YEAR, 1)
+                 .toFormatter();
+ 
+     private static final DateTimeFormatter YEAR_MONTH_DAY = new 
DateTimeFormatterBuilder()
+                 .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                 .appendLiteral('-')
+                 .appendValue(ChronoField.MONTH_OF_YEAR, 1)
+                 .appendLiteral('-')
+                 .appendValue(ChronoField.DAY_OF_MONTH, 1)
+                 .toFormatter();
+ 
+     private static final DateTimeFormatter DATE_TIME = 
DateTimeFormatter.ISO_DATE_TIME;
+ 
+     private final CoverageJsonStore store;
+     private final Coverage binding;
+ 
+     private final GridGeometry gridGeometry;
+     private final List<SampleDimension> sampleDimensions;
+     private final Map<String,double[]> datas;
+ 
+     public CoverageResource(CoverageJsonStore store, Coverage binding) throws 
DataStoreException {
+         super(null);
+         this.store = store;
+         this.binding = binding;
+ 
+         //rebuild grid geometry
+         try {
+             gridGeometry = jsonToGridGeometry(binding.domain);
+         } catch (FactoryException ex) {
+             throw new DataStoreException("Failed to create GridGeometry from 
JSON Domain", ex);
+         }
+         //rebuild sample dimensions
+         sampleDimensions = new ArrayList<>();
+         for (Entry<String,Parameter> entry : 
binding.parameters.any.entrySet()) {
+             final SampleDimension sd = jsonToSampleDimension(entry.getKey(), 
entry.getValue());
+             sampleDimensions.add(sd);
+         }
+         if (binding.parameterGroups != null) {
+             throw new UnsupportedOperationException("Parameter groups not 
supported yet.");
+         }
+         //read datas
+         datas = new HashMap<>();
+         for (Entry<String,NdArray> entry : binding.ranges.any.entrySet()) {
+             datas.put(entry.getKey(), jsonToDataBuffer(entry.getValue()));
+         }
+     }
+ 
+     /**
+      * Return the JSON coverage binding.
+      */
+     Coverage getBinding() {
+         return binding;
+     }
+ 
+     @Override
+     public GridGeometry getGridGeometry() throws DataStoreException {
+         return gridGeometry;
+     }
+ 
+     @Override
+     public List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
+         return Collections.unmodifiableList(sampleDimensions);
+     }
+ 
+     @Override
+     public GridCoverage read(GridGeometry domain, int... ranges) throws 
DataStoreException {
+ 
+         final GridGeometry intersection;
+         if (domain != null) {
+             try {
+                 intersection = 
gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain).build();
+             } catch (DisjointExtentException ex) {
+                 throw new NoSuchDataException(ex);
+             }
+         } else {
+             intersection = gridGeometry;
+         }
+ 
+         final double[][] rawDatas;
+         final List<SampleDimension> selected;
+         if (ranges == null || ranges.length == 0) {
+             selected = sampleDimensions;
+             rawDatas = new double[sampleDimensions.size()][0];
+             for (int i = 0; i < rawDatas.length; i++) {
+                 rawDatas[i] = 
datas.get(sampleDimensions.get(i).getName().toString());
+             }
+ 
+         } else {
+             selected = new ArrayList<>();
+             rawDatas = new double[ranges.length][0];
+             for (int i = 0; i < rawDatas.length; i++) {
+                 final SampleDimension sd = sampleDimensions.get(ranges[i]);
+                 selected.add(sd);
+                 rawDatas[i] = datas.get(sd.getName().toString());
+             }
+         }
+ 
+         final DataBuffer buffer = new DataBufferDouble(rawDatas, 
rawDatas[0].length);
+         return new BufferedGridCoverage(intersection, selected, buffer);
+     }
+ 
+     /**
+      * Transform JSON domain to GridGeometry.
+      */
+     private static GridGeometry jsonToGridGeometry(Domain domain) throws 
DataStoreException, FactoryException {
+ 
+         if (Domain.DOMAINTYPE_GRID.equalsIgnoreCase(domain.domainType)) {
+ 
+             //build coordinate system
+             final List<ReferenceSystemConnection> referencing = 
domain.referencing;
+             final List<String> axeNames = new ArrayList<>();
+             final List<CoordinateReferenceSystem> crss = new ArrayList<>();
+             if (referencing != null && !referencing.isEmpty()) {
+                 for (ReferenceSystemConnection rsc : referencing) {
+                     axeNames.addAll(rsc.coordinates);
+                     final CoordinateReferenceSystem crs = 
jsonToCoordinateReferenceSystem(rsc.system);
+                     if (crs.getCoordinateSystem().getDimension() != 
rsc.coordinates.size()) {
+                         throw new DataStoreException("Declared CRS " + 
rsc.system.toString() + " do not match coordinates length");
+                     }
+                     crss.add(crs);
+                 }
+             } else {
+                 throw new DataStoreException("Coverage domain must be 
defined, Coverage as part of CoverageCollection not supported yet.");
+             }
+ 
+             //build extent
+             final int dimension = axeNames.size();
+             final Axes axes = domain.axes;
+             final GridGeometry[] axeGrids = new GridGeometry[dimension];
+ 
+             //check if axes declared on crs are ordered in the same way as 
the grid extent.
+             final int[] reorder = new int[dimension];
+             boolean inOrder = true;
+ 
+             for (int i = 0; i < dimension; i++) {
+                 final String axeName = axeNames.get(i);
+                 final Axe axe;
+                 final int realIdx;
+                 switch (axeName) {
+                     case "x" :
+                         if (axes.x == null) throw new DataStoreException("X 
axe is undefined");
+                         axe = axes.x;
+                         realIdx = 0;
+                         reorder[i] = realIdx;
+                         inOrder &= (i == realIdx);
+                         break;
+                     case "y" :
+                         if (axes.y == null) throw new DataStoreException("Y 
axe is undefined");
+                         axe = axes.y;
+                         realIdx = 1;
+                         reorder[i] = realIdx;
+                         inOrder &= (i == realIdx);
+                         break;
+                     case "z" :
+                         if (axes.z == null) throw new DataStoreException("Z 
axe is undefined");
+                         axe = axes.z;
+                         realIdx = 2;
+                         reorder[i] = realIdx;
+                         inOrder &= (i == realIdx);
+                         break;
+                     case "t" :
+                         if (axes.t == null) throw new DataStoreException("T 
axe is undefined");
+                         axe = axes.t;
+                         realIdx = reorder.length == 3 ? 2 : 3;
+                         reorder[i] = realIdx;
+                         inOrder &= (i == realIdx);
+                         break;
+                     default: throw new DataStoreException("Unexpected axe 
name :" + axeName);
+                 }
+                 axeGrids[realIdx] = jsonAxeToGridGeometry(axeName, axe);
+             }
+ 
+             final DimensionNameType[] dnt = new DimensionNameType[dimension];
+             final long[] lower = new long[dimension];
+             final long[] upper = new long[dimension]; //inclusive
+             MathTransform gridToCrs = null;
+             for (int i = 0; i < dimension; i++) {
+                 dnt[i] = axeGrids[i].getExtent().getAxisType(0).get();
+                 upper[i] = axeGrids[i].getExtent().getHigh(0);
+                 if (gridToCrs == null) {
+                     gridToCrs = 
axeGrids[i].getGridToCRS(PixelInCell.CELL_CENTER);
+                 } else {
+                     gridToCrs = MathTransforms.compound(gridToCrs, 
axeGrids[i].getGridToCRS(PixelInCell.CELL_CENTER));
+                 }
+             }
+ 
+ 
+             if (!inOrder) {
+                 final MatrixSIS m = Matrices.createZero(dimension+1, 
dimension+1);
+                 for (int i = 0; i < dimension; i++) {
+                     m.setElement(i, reorder[i], 1.0);
+                 }
+                 m.setElement(dimension, dimension, 1.0);
+                 final MathTransform reorderTrs = MathTransforms.linear(m);
+                 gridToCrs = MathTransforms.concatenate(reorderTrs, gridToCrs);
+             }
+ 
+ 
+             final CoordinateReferenceSystem crs = 
CRS.compound(crss.toArray(CoordinateReferenceSystem[]::new));
+             final GridExtent extent = new GridExtent(dnt, lower, upper, true);
+             return new GridGeometry(extent, PixelInCell.CELL_CENTER, 
gridToCrs, crs);
+ 
+         } else {
+             throw new DataStoreException("Unsupported domain type " + 
domain.domainType);
+         }
+     }
+ 
+     /**
+      * Transform JSON axe to 1D GridGeometry without CRS.
+      */
+     private static GridGeometry jsonAxeToGridGeometry(String axeName, Axe 
axe) throws DataStoreException {
+ 
+         if (axe.dataType == null || 
Axe.DATATYPE_PRIMITIVE.equals(axe.dataType)) {
+ 
+         } else if (Axe.DATATYPE_TUPLE.equals(axe.dataType) ) {
+             throw new UnsupportedOperationException("Tuple axe data type not 
supported yet.");
+         } else if (Axe.DATATYPE_POLYGON.equals(axe.dataType) ) {
+             throw new UnsupportedOperationException("Polygon axe data type 
not supported yet.");
+         } else {
+             throw new DataStoreException("Unexpected axe data type :" + 
axe.dataType);
+         }
+ 
+         //rebuild axe transform
+         final MathTransform1D axeTrs;
+         final int size;
+         if (axe.values != null) {
+             final double[] values = new double[axe.values.size()];
+             for (int i = 0; i < values.length; i++) {
+                 values[i] = asDouble(axe.values.get(i));
+             }
+             size = values.length;
+             axeTrs = MathTransforms.interpolate(null, values);
+         } else if (axe.start != null) {
+             size = axe.num;
+             if (axe.num == 1) {
+                 axeTrs = (MathTransform1D) MathTransforms.linear(1.0, 
axe.start);
+             } else {
+                 final double step = (axe.stop - axe.start) / (axe.num -1);
+                 axeTrs = (MathTransform1D) MathTransforms.linear(step, 
axe.start);
+             }
+         } else {
+             throw new DataStoreException("Axe must have values or star/stop 
values");
+         }
+ 
+         final GridExtent extent = new GridExtent(new 
DimensionNameType[]{DimensionNameType.valueOf(axeName)}, new long[]{0}, new 
long[]{size-1}, true);
+         return new GridGeometry(extent, PixelInCell.CELL_CENTER, axeTrs, 
null);
+     }
+ 
+     /**
+      * Transform JSON system object to CoordinateReferenceSystem.
+      */
+     private static CoordinateReferenceSystem 
jsonToCoordinateReferenceSystem(CoverageJsonObject obj) throws FactoryException 
{
+         if (obj instanceof GeographicCRS) {
+             final GeographicCRS jcrs = (GeographicCRS) obj;
+             if (jcrs.id != null) {
+                 if 
(jcrs.id.equals("http://www.opengis.net/def/crs/EPSG/0/4979";)) {
+                     return CommonCRS.WGS84.geographic3D();
+                 }
+                 return CRS.forCode(jcrs.id);
+             } else {
+                 /* Spec 9.5.1.1
+                 Note that sometimes (e.g. for numerical model data) the exact 
CRS
+                 may not be known or may be undefined. In this case the "id" 
may
+                 be omitted, but the "type" still indicates that this is a 
geographic CRS.
+                 Therefore clients can still use geodetic longitude, geodetic 
latitude
+                 (and maybe height) axes, even if they cannot accurately 
georeference the information.
+                 */
+                 return CommonCRS.WGS84.normalizedGeographic();
+             }
+         } else if (obj instanceof ProjectedCRS) {
+             final ProjectedCRS jcrs = (ProjectedCRS) obj;
+             throw new UnsupportedOperationException("ProjectedCRS not 
supported yet");
+ 
+         } else if (obj instanceof VerticalCRS) {
+             final VerticalCRS jcrs = (VerticalCRS) obj;
+             throw new UnsupportedOperationException("VerticalCRS not 
supported yet");
+ 
+         } else if (obj instanceof TemporalRS) {
+             final TemporalRS jcrs = (TemporalRS) obj;
+             if (jcrs.timeScale != null) {
+                 throw new UnsupportedOperationException("TemporalRS timeScale 
not supported yet");
+             }
+             if ("Gregorian".equalsIgnoreCase(jcrs.calendar)) {
+                 return CommonCRS.Temporal.JAVA.crs();
+             } else {
+                 throw new UnsupportedOperationException(jcrs.calendar + 
"calendar not supported yet");
+             }
+ 
+         } else if (obj instanceof IdentifierRS) {
+             final IdentifierRS jcrs = (IdentifierRS) obj;
+             throw new UnsupportedOperationException("IdentifierRS not 
supported yet");
+ 
+         } else {
+             throw new UnsupportedOperationException("Unsupported system " + 
String.valueOf(obj));
+         }
+     }
+ 
+     /**
+      * Transform JSON parameter to SampleDimension.
+      */
+     private static SampleDimension jsonToSampleDimension(String name, 
Parameter parameter) {
+         final SampleDimension.Builder builder = new SampleDimension.Builder();
+ 
+         builder.setName(name);
+ 
+ //        if (parameter.id != null) {
+ //            builder.setName(parameter.id);
+ //        } else if (parameter.label != null) {
+ //            builder.setName(parameter.label);
+ //        } else if (parameter.description != null) {
+ //            builder.setName(parameter.description);
+ //        }
+ 
+         Unit unit = jsonToUnit(parameter.unit);
+ 
+         //TODO categories
+         //parameter.categoryEncoding;
+         //parameter.observedProperty;
+ 
+         return builder.build();
+     }
+ 
+     /**
+      * Transform JSON unit to SIS Unit.
+      */
+     private static Unit 
jsonToUnit(org.apache.sis.storage.coveragejson.binding.Unit unit) {
+         if (unit == null) return Units.UNITY;
+ 
+         if (unit.symbol instanceof String) {
+             return Units.valueOf(unit.symbol.toString());
+         }
+         return Units.UNITY;
+     }
+ 
+     /**
+      * Transform JSON NdArray to number array.
+      */
+     private static double[] jsonToDataBuffer(NdArray array) throws 
DataStoreException {
+         //TODO more work on checking axes order
+         double[] values = new double[array.values.size()];
+         for (int i = 0; i < values.length; i++) {
+             values[i] = asDouble(array.values.get(i));
+         }
+ 
+         return values;
+     }
+ 
+     private static double asDouble(Object cdt) throws DataStoreException {
+         if (cdt == null) {
+             return Double.NaN;
+         } else if (cdt instanceof String) {
+             final Instant instant = parseDataTime(String.valueOf(cdt));
+             return instant.toEpochMilli();
+         } else if (cdt instanceof Number) {
+             return ((Number) cdt).doubleValue();
+         } else {
+             throw new DataStoreException("Unexpected value : " + cdt);
+         }
+     }
+ 
+     /**
+      * If the calendar is based on years, months, days, then the referenced
+      * values SHOULD use one of the following ISO8601-based lexical 
representations:
+      * YYYY
+      * ±XYYYY (where X stands for extra year digits)
+      * YYYY-MM
+      * YYYY-MM-DD
+      * YYYY-MM-DDTHH:MM:SS[.F]Z where Z is either “Z” or a time scale offset 
+|-HH:MM
+      *
+      * If calendar dates with reduced precision are used in a lexical
+      * representation (e.g. "2016"), then a client SHOULD interpret those 
dates
+      * in that reduced precision.
+      */
+     private static Instant parseDataTime(String str) throws 
DataStoreException {
+ 
+         for (DateTimeFormatter dtf : Arrays.asList(YEAR,YEAR_MONTH, 
YEAR_MONTH_DAY, DATE_TIME)) {
+             try {
+                 TemporalAccessor accesser = dtf.parse(str);
+                 return Instant.from(accesser);
+             } catch (DateTimeParseException ex) {
+                 //do nothing
+             }
+         }
+         throw new DataStoreException("Unable to parse date : " + str);
+     }
+ 
+     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 long[] gridLow = new long[dimension];
++        for (int i=0; i<dimension; i++) {
++            gridLow[i] = extent.getLow(i);
++        }
+         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 int gridIdx2 = gridToCrsIndex.get(crsIdx+1);
+                 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);
+                 binding.axes.x = buildAxe(gridLow[gridIdx], 
gridSize[gridIdx], scales[gridIdx], offsets[gridIdx], false);
+                 binding.axes.y = buildAxe(gridLow[gridIdx2], 
gridSize[gridIdx2], scales[gridIdx2], offsets[gridIdx2], false);
+ 
+                 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 int gridIdx2 = gridToCrsIndex.get(crsIdx+1);
+                 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);
+                 binding.axes.x = buildAxe(gridLow[gridIdx], 
gridSize[gridIdx], scales[gridIdx], offsets[gridIdx], false);
+                 binding.axes.y = buildAxe(gridLow[gridIdx2], 
gridSize[gridIdx2], scales[gridIdx2], offsets[gridIdx2], false);
+ 
+                 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;
+ 
+         final List<org.apache.sis.coverage.Category> categories = 
sd.getCategories();
+         if (categories != null && !categories.isEmpty()) {
+ 
+             final ObservedProperty obs = new ObservedProperty();
+             obs.id = name;
+             obs.categories = new ArrayList<>();
+             binding.observedProperty = obs;
+ 
+             final CategoryEncoding catEnc = new CategoryEncoding();
+             binding.categoryEncoding = catEnc;
+             for (org.apache.sis.coverage.Category cat : sd.getCategories()) {
+                 final Category catb = new Category();
+                 catb.id = cat.getName().toString();
+                 obs.categories.add(catb);
+ 
+                 final NumberRange<?> range = cat.getSampleRange();
+                 final double min = range.getMinDouble();
+                 final double max = range.getMaxDouble(false);
+                 final List<Integer> values = new ArrayList<>();
+                 for (double i = min; i < max; i++) {
+                     values.add((int) i);
+                 }
+                 catEnc.any.put(catb.id, values);
+             }
+         }
+ 
+ 
+         //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 {
+         String urn = IdentifiedObjects.lookupURN(crs, null);
+         if (urn != null && !urn.isBlank()) return urn;
+ 
+         final Integer code = IdentifiedObjects.lookupEPSG(crs);
+         if (code == null) {
+             if (crs instanceof org.opengis.referencing.crs.GeographicCRS) {
+                 /* Spec 9.5.1.1
+                 Note that sometimes (e.g. for numerical model data) the exact 
CRS
+                 may not be known or may be undefined. In this case the "id" 
may
+                 be omitted, but the "type" still indicates that this is a 
geographic CRS.
+                 Therefore clients can still use geodetic longitude, geodetic 
latitude
+                 (and maybe height) axes, even if they cannot accurately 
georeference the information.
+                 */
+                 return null;
+             }
+             throw new DataStoreException("Could not find EPSG code for CRS " 
+ crs);
+         }
+         return "http://www.opengis.net/def/crs/EPSG/0/"; + code;
+     }
+ }
diff --cc netbeans-project/ivy.xml
index d89626bbf0,44be15410c..6dfb805b1a
--- a/netbeans-project/ivy.xml
+++ b/netbeans-project/ivy.xml
@@@ -11,10 -11,9 +11,11 @@@
  <ivy-module version="2.0">
      <info organisation="org.apache" module="sis"/>
      <dependencies defaultconf="default">
 +        <dependency org="org.opengis"            name="geoapi"                
  rev="3.0.2"/>
 +        <dependency org="org.opengis"            name="geoapi-conformance"    
  rev="3.0.2"/>
          <dependency org="javax.measure"          name="unit-api"              
  rev="2.1.3"/>
          <dependency org="org.glassfish.jaxb"     name="jaxb-runtime"          
  rev="4.0.3"/>
+         <dependency org="org.eclipse"            name="yasson"                
  rev="3.0.3"/>
          <dependency org="com.esri.geometry"      name="esri-geometry-api"     
  rev="2.2.4"/>
          <dependency org="org.locationtech.jts"   name="jts-core"              
  rev="1.19.0"/>
          <dependency org="org.postgresql"         name="postgresql"            
  rev="42.6.0"/>

Reply via email to