This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new a18611b Initial support of `GEO_METADATA` and `GDAL_METADATA` tags when reading GeoTIFF image. a18611b is described below commit a18611b19198329b8f19a3a77310484c44f6b2ac Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Jan 7 06:34:39 2022 +0100 Initial support of `GEO_METADATA` and `GDAL_METADATA` tags when reading GeoTIFF image. --- .../src/test/java/org/apache/sis/test/Assert.java | 2 +- .../sis/storage/geotiff/ImageFileDirectory.java | 12 +- .../org/apache/sis/storage/geotiff/Reader.java | 9 +- .../apache/sis/storage/geotiff/XMLMetadata.java | 195 ++++++++++++++++++++- .../sis/storage/geotiff/XMLMetadataTest.java | 106 +++++++++++ .../apache/sis/test/suite/GeoTiffTestSuite.java | 3 +- .../sis/internal/storage/MetadataBuilder.java | 183 +++++++++---------- 7 files changed, 410 insertions(+), 100 deletions(-) diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java b/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java index 53c3482..9a1b6d9 100644 --- a/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java +++ b/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java @@ -130,7 +130,7 @@ public strictfp class Assert extends org.opengis.test.Assert { /** * Asserts that two strings are equal, ignoring the differences in EOL characters. - * The comparisons is performed one a line-by-line basis. For each line, trailing + * The comparisons are performed on a line-by-line basis. For each line, trailing * spaces (but not leading spaces) are ignored. * * @param expected the expected string. diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java index 3e7a3f2..ba9bd62 100644 --- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java +++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java @@ -526,9 +526,7 @@ final class ImageFileDirectory extends DataCube { * @throws UnsupportedOperationException if the given type is {@link Type#UNDEFINED}. * @throws DataStoreException if a logical error is found or an unsupported TIFF feature is used. */ - Object addEntry(final short tag, final Type type, final long count) - throws IOException, ParseException, DataStoreException - { + Object addEntry(final short tag, final Type type, final long count) throws Exception { switch (tag) { //////////////////////////////////////////////////////////////////////////////////////////////// @@ -1116,10 +1114,16 @@ final class ImageFileDirectory extends DataCube { //////////////////////////////////////////////////////////////////////////////////////////////// //// //// - //// Extensions defined by GDAL. //// + //// Extensions defined by DGIWG or GDAL. //// //// //// //////////////////////////////////////////////////////////////////////////////////////////////// + case Tags.GEO_METADATA: + case Tags.GDAL_METADATA: { + final XMLMetadata parser = new XMLMetadata(reader, type, count, tag == Tags.GDAL_METADATA); + parser.appendTo(metadata); + break; + } case Tags.GDAL_NODATA: { noData = type.readDouble(input(), count); break; diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java index 770836f..3af73e0 100644 --- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java +++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java @@ -24,7 +24,6 @@ import java.util.HashSet; import java.util.Iterator; import java.io.IOException; import java.nio.ByteOrder; -import java.text.ParseException; import org.opengis.util.NameFactory; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.storage.DataStoreException; @@ -289,7 +288,9 @@ final class Reader extends GeoTIFF { * without value (count = 0) - in principle illegal but we make this reader tolerant. */ error = dir.addEntry(tag, type, count); - } catch (ParseException | RuntimeException e) { + } catch (IOException | DataStoreException e) { + throw e; + } catch (Exception e) { error = e; } if (error != null) { @@ -361,7 +362,9 @@ final class Reader extends GeoTIFF { Object error; try { error = entry.owner.addEntry(entry.tag, entry.type, entry.count); - } catch (ParseException | RuntimeException e) { + } catch (IOException | DataStoreException e) { + throw e; + } catch (Exception e) { error = e; } if (error != null) { diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/XMLMetadata.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/XMLMetadata.java index 8e7465d..299163f 100644 --- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/XMLMetadata.java +++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/XMLMetadata.java @@ -16,13 +16,18 @@ */ package org.apache.sis.storage.geotiff; +import java.util.Locale; import java.util.Iterator; +import java.util.Collections; import java.util.StringJoiner; +import java.util.logging.Filter; +import java.util.logging.LogRecord; import java.io.IOException; import java.io.StringReader; import java.io.ByteArrayInputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.Instant; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamException; @@ -30,10 +35,19 @@ import javax.xml.stream.events.XMLEvent; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.Characters; import javax.xml.stream.events.StartElement; +import javax.xml.transform.stax.StAXSource; +import javax.xml.bind.JAXBException; +import javax.xml.namespace.QName; +import org.apache.sis.internal.util.StandardDateFormat; +import org.apache.sis.internal.storage.MetadataBuilder; +import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.util.collection.TreeTable; import org.apache.sis.util.collection.DefaultTreeTable; import org.apache.sis.util.collection.TableColumn; import org.apache.sis.util.resources.Errors; +import org.apache.sis.xml.XML; + +import static org.apache.sis.internal.util.TemporalUtilities.toDate; /** @@ -57,7 +71,12 @@ import org.apache.sis.util.resources.Errors; * @since 1.2 * @module */ -final class XMLMetadata { +final class XMLMetadata implements Filter { + /** + * The {@value} string, used in GDAL metadata. + */ + private static final String ITEM = "Item", NAME = "name"; + /** * The bytes to decode as an XML document. * DGIWG specification mandates UTF-8 encoding. @@ -76,6 +95,11 @@ final class XMLMetadata { private String currentElement; /** + * Where to report non-fatal warnings. + */ + private final StoreListeners listeners; + + /** * {@code true} if the XML is GDAL metadata. Example: * * {@preformat xml @@ -88,6 +112,15 @@ final class XMLMetadata { private final boolean isGDAL; /** + * Creates a new instance with the given XML. Used for testing purposes. + */ + XMLMetadata(final String xml, final boolean isGDAL) { + this.isGDAL = isGDAL; + string = xml; + listeners = null; + } + + /** * Creates new metadata which will decode the given vector of bytes. * * @param reader the TIFF reader. @@ -97,6 +130,7 @@ final class XMLMetadata { */ XMLMetadata(final Reader reader, final Type type, final long count, final boolean isGDAL) throws IOException { this.isGDAL = isGDAL; + listeners = reader.store.listeners(); switch (type) { case ASCII: { final String[] cs = type.readString(reader.input, count, reader.store.encoding); @@ -196,6 +230,7 @@ final class XMLMetadata { * * @param source the XML document to represent as a tree table. * @param target where to append this root node. + * @param name name to assign to this root node. * @return {@code true} on success, or {@code false} if the XML document could not be decoded. */ Root(final XMLMetadata source, final DefaultTreeTable.Node parent, final String name) { @@ -239,14 +274,14 @@ final class XMLMetadata { final String previous = currentElement; currentElement = element.getName().getLocalPart(); node.setValue(Root.NAME, currentElement); - final boolean isItem = isGDAL && currentElement.equals("Item"); + final boolean isItem = isGDAL && currentElement.equals(ITEM); final Iterator<Attribute> attributes = element.getAttributes(); while (attributes.hasNext()) { final Attribute attribute = attributes.next(); if (attribute.isSpecified()) { final String name = attribute.getName().getLocalPart(); final String value = attribute.getValue(); - if (isItem && name.equals("name")) { + if (isItem && name.equals(NAME)) { /* * GDAL metadata does not really use of XML schema. * Instead, it is a collection of lines like below: @@ -286,4 +321,158 @@ final class XMLMetadata { } currentElement = previous; } + + /** + * Appends the content of this object to the given metadata builder. + * + * @param metadata the builder where to append the content of this {@code XMLMetadata}. + * @throws XMLStreamException if an error occurred while parsing the XML. + * @throws JAXBException if an error occurred while parsing the XML. + */ + public void appendTo(final MetadataBuilder metadata) throws XMLStreamException, JAXBException { + final XMLEventReader reader = toXML(); + if (reader != null) { + if (isGDAL) { + /* + * We expect a list of XML elements as below: + * + * <Item name="acquisitionEndDate">2016-09-08T15:53:00+05:00</Item> + * + * Those items should be children of <GDALMetadata> node. That node should be the root node, + * but current implementation searches <GDALMetadata> recursively if not found at the root. + */ + final Parser parser = new Parser(reader, metadata); + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isStartElement()) { + parser.root(event.asStartElement()); + } + } + parser.flush(); + } else { + /* + * Parse as an ISO 19115 document and get the content as a `Metadata` object. + * Some other types are accepted as well (e.g. `IdentificationInfo`). + * The `mergeMetadata` method applies heuristic rules for adding components. + */ + metadata.mergeMetadata(XML.unmarshal(new StAXSource(reader), + Collections.singletonMap(XML.WARNING_FILTER, this)), listeners.getLocale()); + } + reader.close(); // No need to close the underlying input stream. + } + } + + /** + * Parser of GDAL metadata. + */ + private static final class Parser { + /** The XML reader from which to get XML elements. */ + private final XMLEventReader reader; + + /** A qualified name with the {@value #NAME} local part, used for searching attributes. */ + private final QName name; + + /** A value increased for each level of nested {@code <Item>} element. */ + private int depth; + + /** Where to write metadata. */ + private final MetadataBuilder metadata; + + /** Temporary storage for metadata values that need a little processing. */ + private Instant startTime, endTime; + + /** + * Creates a new reader. + * + * @param reader the source of XML elements. + * @param metadata the target of metadata elements. + */ + Parser(final XMLEventReader reader, final MetadataBuilder metadata) { + this.reader = reader; + this.metadata = metadata; + name = new QName(NAME); + } + + /** + * Parses a {@code <GDALMetadata>} element and its children. After this method returns, + * the reader is positioned after the closing {@code </GDALMetadata>} tag. + * + * @param start the {@code <GDALMetadata>} element. + */ + void root(final StartElement start) throws XMLStreamException { + final boolean parse = start.getName().getLocalPart().equals("GDALMetadata"); + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isStartElement()) { + if (parse) { + item(event.asStartElement()); // Parse <Item> elements. + } else { + root(event.asStartElement()); // Search a nested <GDALMetadata>. + } + } else if (event.isEndElement()) { + break; + } + } + } + + /** + * Parses a {@code <Item>} element and its children. After this method returns, + * the reader is positioned after the closing {@code </Item>} tag. + * + * @param start the {@code <Item>} element. + */ + private void item(final StartElement start) throws XMLStreamException { + String attribute = null; + if (depth == 0 && start.getName().getLocalPart().equals(ITEM)) { + final Attribute a = start.getAttributeByName(name); + if (a != null) attribute = a.getValue(); + } + final StringJoiner buffer = new StringJoiner(""); + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + if (event.isEndElement()) { + break; + } else if (event.isCharacters()) { + buffer.add(event.asCharacters().getData()); + } else if (event.isStartElement()) { + depth++; + item(event.asStartElement()); + depth--; + } + } + if (attribute != null) { + if (Character.isUpperCase(attribute.codePointAt(0))) { + attribute = attribute.toLowerCase(Locale.US); + } + final String content = buffer.toString(); + if (!content.isEmpty()) { + switch (attribute) { + case "acquisitionStartDate": startTime = StandardDateFormat.parseInstantUTC(content); break; + case "acquisitionEndDate": endTime = StandardDateFormat.parseInstantUTC(content); break; + case "title": metadata.addTitle(content); break; + } + } + } + } + + /** + * Writes to {@link MetadataBuilder} all information that were pending parsing completion. + */ + void flush() { + try { + metadata.addTemporalExtent(toDate(startTime), toDate(endTime)); + } catch (UnsupportedOperationException e) { + // Ignore (this operation requires "sis-temporal" module). + } + } + } + + /** + * Invoked when a non-fatal warning occurs during the parsing of XML document. + */ + @Override + public boolean isLoggable(final LogRecord warning) { + listeners.warning(warning); + return false; + } } diff --git a/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/XMLMetadataTest.java b/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/XMLMetadataTest.java new file mode 100644 index 0000000..cb21eba --- /dev/null +++ b/storage/sis-geotiff/src/test/java/org/apache/sis/storage/geotiff/XMLMetadataTest.java @@ -0,0 +1,106 @@ +/* + * 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.geotiff; + +import org.apache.sis.metadata.iso.DefaultMetadata; +import org.apache.sis.internal.storage.MetadataBuilder; +import org.apache.sis.util.collection.DefaultTreeTable; +import org.apache.sis.util.collection.TableColumn; +import org.apache.sis.test.TestCase; +import org.junit.Test; + +import static org.apache.sis.test.Assert.*; + + +/** + * Tests the {@link XMLMetadata} enumeration. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +public final strictfp class XMLMetadataTest extends TestCase { + /** + * A GDAL metadata. + */ + private static final String GDAL_METADATA = + "<GDALMetadata>\n" + + " <Item name=\"TITLE\">My image</Item>\n" + + " <Item name=\"SCALE\" sample=\"-3\">0.015</Item>" + + " <Item name=\"acquisitionStartDate\">2018-02-28T04:48:00</Item>\n" + + " <Item name=\"acquisitionEndDate\">2018-02-28T03:04:00</Item>\n" + + " <Foo>bar</Foo>\n" + + "</GDALMetadata>\n"; + + /** + * Tests parsing of GDAL metadata and formatting as a tree table. + * THe XML document is like below: + * + * {@preformat xml + * <GDALMetadata> + * <Item name="SCALE" sample="-3">0.015</Item> + * <Item name="acquisitionStartDate">2018-02-28T04:48:00</Item> + * <Item name="acquisitionEndDate">2018-02-28T03:04:00</Item> + * </GDALMetadata> + * } + * + * The tree table output is expected to <em>not</em> contains the "Item" word, + * because this is redundancy repeated for all nodes. Instead "Item" should be + * replaced by the value of the "name" attribute. + */ + @Test + public void testTreeGDAL() { + XMLMetadata xml = new XMLMetadata(GDAL_METADATA, true); + assertSame(GDAL_METADATA, xml.toString()); + assertFalse(xml.isEmpty()); + DefaultTreeTable table = new DefaultTreeTable(TableColumn.NAME, TableColumn.VALUE); + DefaultTreeTable.Node root = (DefaultTreeTable.Node ) table.getRoot(); + DefaultTreeTable.Node node = new XMLMetadata.Root(xml, root, "Test"); + assertEquals("Test", node.getValue(TableColumn.NAME)); + root.setValue(TableColumn.NAME, "Root"); + assertMultilinesEquals( + "Root\n" + + " └─Test\n" + + " └─GDALMetadata\n" + + " ├─TITLE…………………………………………… My image\n" + + " ├─SCALE…………………………………………… 0.015\n" + + " │ └─sample……………………………… -3\n" + + " ├─acquisitionStartDate…… 2018-02-28T04:48:00\n" + + " ├─acquisitionEndDate………… 2018-02-28T03:04:00\n" + + " └─Foo………………………………………………… bar\n", + table.toString()); + } + + /** + * Tests parsing GDAL metadata and conversion to ISO 19115 metadata. + * + * @throws Exception if an error occurred during XML parsing. + */ + @Test + public void testMetadataGDAL() throws Exception { + XMLMetadata xml = new XMLMetadata(GDAL_METADATA, true); + MetadataBuilder builder = new MetadataBuilder(); + xml.appendTo(builder); + DefaultMetadata metadata = builder.build(false); + assertMultilinesEquals( + "Metadata\n" + + " └─Identification info\n" + + " └─Citation………………… My image\n", + metadata.toString()); + } +} diff --git a/storage/sis-geotiff/src/test/java/org/apache/sis/test/suite/GeoTiffTestSuite.java b/storage/sis-geotiff/src/test/java/org/apache/sis/test/suite/GeoTiffTestSuite.java index 69f2ba6..7540ccb 100644 --- a/storage/sis-geotiff/src/test/java/org/apache/sis/test/suite/GeoTiffTestSuite.java +++ b/storage/sis-geotiff/src/test/java/org/apache/sis/test/suite/GeoTiffTestSuite.java @@ -25,7 +25,7 @@ import org.junit.BeforeClass; * All tests from the {@code sis-geotiff} module, in rough dependency order. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * @since 0.8 * @module */ @@ -36,6 +36,7 @@ import org.junit.BeforeClass; org.apache.sis.storage.geotiff.TypeTest.class, org.apache.sis.storage.geotiff.GeoKeysTest.class, org.apache.sis.storage.geotiff.CRSBuilderTest.class, + org.apache.sis.storage.geotiff.XMLMetadataTest.class, org.apache.sis.storage.geotiff.SelfConsistencyTest.class }) public final strictfp class GeoTiffTestSuite extends TestSuite { diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java index a969c23..5e498bf 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.net.URI; import java.nio.charset.Charset; +import java.time.Instant; import javax.measure.Unit; import javax.measure.quantity.Length; import org.opengis.util.MemberName; @@ -37,41 +38,18 @@ import org.opengis.util.GenericName; import org.opengis.util.InternationalString; import org.opengis.geometry.Envelope; import org.opengis.geometry.Geometry; -import org.opengis.metadata.Metadata; -import org.opengis.metadata.Identifier; -import org.opengis.metadata.citation.Role; -import org.opengis.metadata.citation.DateType; -import org.opengis.metadata.citation.Citation; -import org.opengis.metadata.citation.CitationDate; -import org.opengis.metadata.citation.OnLineFunction; -import org.opengis.metadata.citation.OnlineResource; -import org.opengis.metadata.identification.Identification; -import org.opengis.metadata.extent.Extent; -import org.opengis.metadata.spatial.GCP; -import org.opengis.metadata.spatial.Dimension; -import org.opengis.metadata.spatial.DimensionNameType; -import org.opengis.metadata.spatial.CellGeometry; -import org.opengis.metadata.spatial.PixelOrientation; -import org.opengis.metadata.spatial.GeolocationInformation; -import org.opengis.metadata.spatial.SpatialRepresentation; -import org.opengis.metadata.spatial.SpatialRepresentationType; -import org.opengis.metadata.constraint.Constraints; -import org.opengis.metadata.constraint.Restriction; -import org.opengis.metadata.content.ContentInformation; -import org.opengis.metadata.content.CoverageContentType; -import org.opengis.metadata.content.TransferFunctionType; -import org.opengis.metadata.maintenance.ScopeCode; -import org.opengis.metadata.acquisition.Context; -import org.opengis.metadata.acquisition.OperationType; -import org.opengis.metadata.identification.Progress; -import org.opengis.metadata.identification.KeywordType; -import org.opengis.metadata.identification.Resolution; -import org.opengis.metadata.identification.TopicCategory; -import org.opengis.metadata.distribution.Distribution; -import org.opengis.metadata.distribution.Distributor; -import org.opengis.metadata.distribution.Format; -import org.opengis.metadata.lineage.Lineage; +import org.opengis.metadata.*; +import org.opengis.metadata.acquisition.*; +import org.opengis.metadata.citation.*; +import org.opengis.metadata.constraint.*; +import org.opengis.metadata.content.*; +import org.opengis.metadata.distribution.*; +import org.opengis.metadata.extent.*; +import org.opengis.metadata.identification.*; +import org.opengis.metadata.lineage.*; +import org.opengis.metadata.maintenance.*; import org.opengis.metadata.quality.Element; +import org.opengis.metadata.spatial.*; import org.opengis.geometry.DirectPosition; import org.opengis.referencing.ReferenceSystem; import org.opengis.referencing.cs.CoordinateSystem; @@ -81,62 +59,24 @@ import org.opengis.referencing.operation.TransformException; import org.apache.sis.internal.util.CollectionsExt; import org.apache.sis.internal.simple.SimpleDuration; import org.apache.sis.geometry.AbstractEnvelope; -import org.apache.sis.metadata.iso.DefaultMetadata; -import org.apache.sis.metadata.iso.DefaultIdentifier; -import org.apache.sis.metadata.iso.DefaultMetadataScope; -import org.apache.sis.metadata.iso.extent.DefaultExtent; -import org.apache.sis.metadata.iso.extent.DefaultVerticalExtent; -import org.apache.sis.metadata.iso.extent.DefaultTemporalExtent; -import org.apache.sis.metadata.iso.extent.DefaultBoundingPolygon; -import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; -import org.apache.sis.metadata.iso.extent.DefaultGeographicDescription; -import org.apache.sis.metadata.iso.spatial.DefaultGridSpatialRepresentation; -import org.apache.sis.metadata.iso.spatial.DefaultDimension; -import org.apache.sis.metadata.iso.spatial.DefaultGeorectified; -import org.apache.sis.metadata.iso.spatial.DefaultGeoreferenceable; -import org.apache.sis.metadata.iso.spatial.DefaultGCPCollection; -import org.apache.sis.metadata.iso.spatial.DefaultGCP; -import org.apache.sis.metadata.iso.content.DefaultAttributeGroup; -import org.apache.sis.metadata.iso.content.DefaultSampleDimension; -import org.apache.sis.metadata.iso.content.DefaultBand; -import org.apache.sis.metadata.iso.content.DefaultCoverageDescription; -import org.apache.sis.metadata.iso.content.DefaultFeatureCatalogueDescription; -import org.apache.sis.metadata.iso.content.DefaultRangeElementDescription; -import org.apache.sis.metadata.iso.content.DefaultImageDescription; -import org.apache.sis.metadata.iso.content.DefaultFeatureTypeInfo; -import org.apache.sis.metadata.iso.citation.Citations; -import org.apache.sis.metadata.iso.citation.AbstractParty; -import org.apache.sis.metadata.iso.citation.DefaultSeries; -import org.apache.sis.metadata.iso.citation.DefaultCitation; -import org.apache.sis.metadata.iso.citation.DefaultCitationDate; -import org.apache.sis.metadata.iso.citation.DefaultResponsibility; -import org.apache.sis.metadata.iso.citation.DefaultIndividual; -import org.apache.sis.metadata.iso.citation.DefaultOrganisation; -import org.apache.sis.metadata.iso.citation.DefaultOnlineResource; -import org.apache.sis.metadata.iso.constraint.DefaultLegalConstraints; -import org.apache.sis.metadata.iso.identification.DefaultKeywords; -import org.apache.sis.metadata.iso.identification.DefaultResolution; -import org.apache.sis.metadata.iso.identification.DefaultDataIdentification; -import org.apache.sis.metadata.iso.distribution.DefaultFormat; -import org.apache.sis.metadata.iso.distribution.DefaultDistributor; -import org.apache.sis.metadata.iso.distribution.DefaultDistribution; -import org.apache.sis.metadata.iso.acquisition.DefaultAcquisitionInformation; -import org.apache.sis.metadata.iso.acquisition.DefaultEvent; -import org.apache.sis.metadata.iso.acquisition.DefaultInstrument; -import org.apache.sis.metadata.iso.acquisition.DefaultOperation; -import org.apache.sis.metadata.iso.acquisition.DefaultPlatform; -import org.apache.sis.metadata.iso.acquisition.DefaultRequirement; -import org.apache.sis.metadata.iso.lineage.DefaultLineage; -import org.apache.sis.metadata.iso.lineage.DefaultProcessStep; -import org.apache.sis.metadata.iso.lineage.DefaultProcessing; -import org.apache.sis.metadata.iso.lineage.DefaultSource; -import org.apache.sis.metadata.iso.maintenance.DefaultScope; -import org.apache.sis.metadata.iso.maintenance.DefaultScopeDescription; +import org.apache.sis.metadata.ModifiableMetadata; +import org.apache.sis.metadata.iso.*; +import org.apache.sis.metadata.iso.acquisition.*; +import org.apache.sis.metadata.iso.citation.*; +import org.apache.sis.metadata.iso.constraint.*; +import org.apache.sis.metadata.iso.content.*; +import org.apache.sis.metadata.iso.distribution.*; +import org.apache.sis.metadata.iso.extent.*; +import org.apache.sis.metadata.iso.identification.*; +import org.apache.sis.metadata.iso.lineage.*; +import org.apache.sis.metadata.iso.maintenance.*; +import org.apache.sis.metadata.iso.spatial.*; import org.apache.sis.metadata.sql.MetadataStoreException; import org.apache.sis.metadata.sql.MetadataSource; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.coverage.SampleDimension; +import org.apache.sis.internal.metadata.Merger; import org.apache.sis.internal.util.Numerics; import org.apache.sis.internal.util.Strings; import org.apache.sis.util.resources.Vocabulary; @@ -152,8 +92,6 @@ import static org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_D // Branch-dependent imports import org.opengis.temporal.Duration; import org.opengis.feature.FeatureType; -import org.opengis.metadata.acquisition.AcquisitionInformation; -import org.opengis.metadata.citation.Responsibility; /** @@ -166,7 +104,7 @@ import org.opengis.metadata.citation.Responsibility; * @author Rémi Maréchal (Geomatys) * @author Thi Phuong Hao Nguyen (VNSC) * @author Alexis Manin (Geomatys) - * @version 1.1 + * @version 1.2 * @since 0.8 * @module */ @@ -2770,6 +2708,32 @@ parse: for (int i = 0; i < length;) { } /** + * Adds an event that describe the range of time at which data were acquired. + * Current implementation computes the average of given instants. + * Storage location is: + * + * <ul> + * <li>{@code metadata/acquisitionInformation/operation/significantEvent/time}</li> + * </ul> + * + * @param startTime start time, or {@code null} if unknown. + * @param endTime end time, or {@code null} if unknown. + */ + public final void addAcquisitionTime(final Instant startTime, final Instant endTime) { + final Date time; + if (startTime == null) { + if (endTime == null) return; + time = Date.from(endTime); + } else if (endTime == null) { + time = Date.from(startTime); + } else { + // Divide by 2 before to add in order to avoid overflow. + time = new Date((startTime.toEpochMilli() >> 1) + (endTime.toEpochMilli() >> 1)); + } + addAcquisitionTime(time); + } + + /** * Adds the identifier of the operation used to acquire the dataset. * Examples: "GHRSST", "NOAA CDR", "NASA EOS", "JPSS", "GOES-R". * Storage location is: @@ -3240,6 +3204,49 @@ parse: for (int i = 0; i < length;) { } /** + * Merge the given metadata into the metadata created by this builder. + * The given source should be an instance of {@link Metadata}, + * but some types of metadata components are accepted as well. + * + * @param source the source metadata to merge. Will never be modified. + * @param locale the locale to use for error message in exceptions. + * @return {@code true} if the given source has been merged, + * or {@code false} if its type is not managed by this builder. + * @throws RuntimeException if the merge failed (may be {@link IllegalArgumentException}, + * {@link ClassCastException}, {@link org.apache.sis.metadata.InvalidMetadataException}…) + * + * @see Merger + */ + public boolean mergeMetadata(final Object source, final Locale locale) { + final ModifiableMetadata target; + if (source instanceof Metadata) target = metadata(); + else if (source instanceof DataIdentification) target = identification(); + else if (source instanceof Citation) target = citation(); + else if (source instanceof Series) target = series(); + else if (source instanceof Responsibility) target = responsibility(); + else if (source instanceof Party) target = party(); + else if (source instanceof LegalConstraints) target = constraints(); + else if (source instanceof Extent) target = extent(); + else if (source instanceof AcquisitionInformation) target = acquisition(); + else if (source instanceof Platform) target = platform(); + else if (source instanceof FeatureCatalogueDescription) target = featureDescription(); + else if (source instanceof CoverageDescription) target = coverageDescription(); + else if (source instanceof AttributeGroup) target = attributeGroup(); + else if (source instanceof SampleDimension) target = sampleDimension(); + else if (source instanceof GridSpatialRepresentation) target = gridRepresentation(); + else if (source instanceof GCPCollection) target = groundControlPoints(); + else if (source instanceof Distribution) target = distribution(); + else if (source instanceof Format) target = format(); + else if (source instanceof Lineage) target = lineage(); + else if (source instanceof ProcessStep) target = processStep(); + else if (source instanceof Processing) target = processing(); + else return false; + final Merger merger = new Merger(locale); + merger.copy(source, target); + return true; + } + + /** * Returns the metadata (optionally as an unmodifiable object), or {@code null} if none. * If {@code freeze} is {@code true}, then the returned metadata instance can not be modified. *