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"/>