/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.image.io.metadata;
import java.util.Locale;
import java.util.Arrays;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.Collections;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataFormat;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.Metadata;
import org.opengis.metadata.acquisition.AcquisitionInformation;
import org.opengis.metadata.acquisition.EnvironmentalRecord;
import org.opengis.metadata.acquisition.Instrument;
import org.opengis.metadata.acquisition.Platform;
import org.opengis.metadata.content.Band;
import org.opengis.metadata.content.ImageDescription;
import org.opengis.metadata.content.RangeElementDescription;
import org.opengis.metadata.identification.DataIdentification;
import org.opengis.metadata.identification.Keywords;
import org.opengis.metadata.identification.Resolution;
import org.opengis.metadata.extent.Extent;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.metadata.extent.VerticalExtent;
import org.opengis.metadata.spatial.Georectified;
import org.opengis.metadata.quality.DataQuality;
import org.opengis.metadata.ExtendedElementInformation;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.operation.Conversion;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.coverage.grid.RectifiedGrid;
import org.opengis.util.InternationalString;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.metadata.KeyNamePolicy;
import org.apache.sis.metadata.MetadataStandard;
import org.geotoolkit.referencing.cs.*;
import org.geotoolkit.referencing.crs.*;
import org.geotoolkit.gui.swing.tree.Trees;
import org.geotoolkit.gui.swing.tree.TreeTableNode;
import org.geotoolkit.internal.image.io.DataTypes;
import org.geotoolkit.resources.Errors;
import org.apache.sis.referencing.CommonCRS;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* Describes the structure of {@linkplain SpatialMetadata spatial metadata}.
* The default {@linkplain #getStreamInstance(String) stream} and {@link #getImageInstance(String)
* image} formats are inferred from a subset of the GeoAPI metadata interfaces, especially
* {@link Metadata} and {@link ImageDescription}. Consequently those instances can be considered
* as profiles of ISO 19115-2, with a few minor departures:
* <p>
* <ul>
* <li>The {@link Band} interface defined by ISO 19115-2 is used only when the values are
* measurements of wavelengths in the electromagnetic spectrum, as specified in the ISO
* specification. Otherwise the {@link SampleDimension} interface (which is very similar)
* is used.</li>
* </ul>
* <p>
* <a name="default-formats">The tree structures are show below</a>. As a general rule, the name of
* <em>elements</em> start with a upper case letter while the name of <em>attributes</em> start with
* a lower case letter. The valid types of attributes values are <a href="package-summary.html#accessor-types">listed here</a>.
* For browsing these trees in an applet together with additional information, see the
* <a href="http://www.geotoolkit.org/demos/geotk-simples/applet/IIOMetadataPanel.html">IIOMetadataPanel applet</a>.
*
<blockquote><table border="1" cellpadding="12">
<tr bgcolor="lightblue"><th>Stream metadata</th><th>Image metadata</th></tr>
<tr><td nowrap valign="top" width="50%">
<pre>geotk-coverageio_3.07
├───<b>DiscoveryMetadata</b> : {@linkplain DataIdentification}
│ ├───citation
│ ├───abstract
│ ├───purpose
│ ├───credits
│ ├───status
│ ├───<b>DescriptiveKeywords</b> : {@linkplain Keywords}[]
│ │ └───DescriptiveKeywordsEntry
│ │ ├───keywords
│ │ ├───thesaurusName
│ │ └───type
│ ├───<b>SpatialResolution</b> : {@linkplain Resolution}
│ │ ├───distance
│ │ └───EquivalentScale
│ │ └───denominator
│ ├───topicCategories
│ ├───environmentDescription
│ ├───<b>Extent</b> : {@linkplain Extent}
│ │ ├───description
│ │ ├───<b>GeographicElement</b> : {@linkplain GeographicBoundingBox}
│ │ │ ├───inclusion
│ │ │ ├───westBoundLongitude
│ │ │ ├───eastBoundLongitude
│ │ │ ├───southBoundLatitude
│ │ │ └───northBoundLatitude
│ │ └───<b>VerticalElement</b> : {@linkplain VerticalExtent}
│ │ ├───minimumValue
│ │ ├───maximumValue
│ │ └───verticalCRS
│ └───supplementalInformation
├───<b>AcquisitionMetadata</b> : {@linkplain AcquisitionInformation}
│ ├───<b>EnvironmentalConditions</b> : {@linkplain EnvironmentalRecord}
│ │ ├───averageAirTemperature
│ │ ├───maxRelativeHumidity
│ │ ├───maxAltitude
│ │ └───meteorologicalConditions
│ └───<b>Platform</b> : {@linkplain Platform}
│ ├───citation
│ ├───identifier
│ ├───description
│ └───Instruments
│ └───<b>Instrument</b> : {@linkplain Instrument}
│ ├───citation
│ ├───Identifier : {@linkplain Identifier}
│ │ ├───code
│ │ └───authority
│ ├───type
│ └───description
└───<b>QualityMetadata</b> : {@linkplain DataQuality}
└───<b>Report</b> : {@linkplain Element}
├───namesOfMeasure
├───measureIdentification
├───measureDescription
├───evaluationMethodType
├───evaluationMethodDescription
├───evaluationProcedure
└───date</pre>
</td><td nowrap valign="top" width="50%">
<pre>geotk-coverageio_3.07
├───<b>ImageDescription</b> : {@linkplain ImageDescription}
│ ├───contentType
│ ├───illuminationElevationAngle
│ ├───illuminationAzimuthAngle
│ ├───imagingCondition
│ ├───ImageQualityCode : {@linkplain Identifier}
│ │ ├───code
│ │ └───authority
│ ├───cloudCoverPercentage
│ ├───ProcessingLevelCode : {@linkplain Identifier}
│ │ ├───code
│ │ └───authority
│ ├───compressionGenerationQuantity
│ ├───triangulationIndicator
│ ├───radiometricCalibrationDataAvailable
│ ├───cameraCalibrationInformationAvailable
│ ├───filmDistortionInformationAvailable
│ ├───lensDistortionInformationAvailable
│ ├───<b>Dimensions</b> : {@linkplain SampleDimension}[]
│ │ └───Dimension
│ │ ├───descriptor
│ │ ├───sequenceIdentifier
│ │ ├───validSampleValues
│ │ ├───fillSampleValues
│ │ ├───minValue
│ │ ├───maxValue
│ │ ├───units
│ │ ├───peakResponse
│ │ ├───bitsPerValue
│ │ ├───toneGradation
│ │ ├───scaleFactor
│ │ ├───offset
│ │ ├───bandBoundaryDefinition
│ │ ├───nominalSpatialResolution
│ │ ├───transferFunctionType
│ │ ├───transmittedPolarization
│ │ └───detectedPolarization
│ └───<b>RangeElementDescriptions</b> : {@linkplain RangeElementDescription}
│ └───RangeElementDescription
│ ├───name
│ ├───definition
│ └───rangeElements
├───<b>SpatialRepresentation</b> : {@linkplain Georectified}
│ ├───numberOfDimensions
│ ├───cellGeometry
│ ├───centerPoint
│ └───pointInPixel
└───<b>RectifiedGridDomain</b> : {@linkplain RectifiedGrid}
├───<b>Limits</b> : {@linkplain GridEnvelope}
│ ├───low
│ └───high
├───origin
├───<b>OffsetVectors</b>
│ └───OffsetVector
│ └───values
└───<b>CoordinateReferenceSystem</b> : {@linkplain CoordinateReferenceSystem}
├───name
├───type
├───<b>CoordinateSystem</b> : {@linkplain CoordinateSystem}
│ ├───name
│ ├───type
│ ├───dimension
│ └───Axes
│ └───<b>CoordinateSystemAxis</b> : {@linkplain CoordinateSystemAxis}
│ ├───name
│ ├───direction
│ ├───minimumValue
│ ├───maximumValue
│ ├───rangeMeaning
│ └───unit
├───<b>Datum</b> : {@linkplain Datum}
│ ├───name
│ ├───type
│ ├───<b>Ellipsoid</b> : {@linkplain Ellipsoid}
│ │ ├───name
│ │ ├───axisAbbrev
│ │ ├───axisUnit
│ │ ├───semiMajorAxis
│ │ ├───semiMinorAxis
│ │ └───inverseFlattening
│ └───<b>PrimeMeridian</b> : {@linkplain PrimeMeridian}
│ ├───name
│ ├───greenwichLongitude
│ └───angularUnit
└───<b>Conversion</b> : {@linkplain Conversion}
├───name
├───method
└───Parameters : {@linkplain ParameterValueGroup}
└───ParameterValue : {@linkplain ParameterValue}
├───name
└───value</pre>
</tr></table></blockquote>
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @see SpatialMetadata
*
* @since 3.04 (derived from 2.4)
* @module
*/
public class SpatialMetadataFormat extends IIOMetadataFormatImpl {
/**
* The metadata format name, which is {@value}. The {@link javax.imageio.metadata} package
* description requires that we provide a version number as part of the format name. The
* version number provided in this constant is set to the last Geotk version when this
* format has been modified, and may change in any future version.
*
* @since 3.20 (derived from 2.4)
*/
public static final String GEOTK_FORMAT_NAME = "geotk-coverageio_3.07";
/**
* The ISO-19115 format name, which is {@value}. This metadata format is big and supported
* only by a few plugins like {@link org.geotoolkit.image.io.plugin.NetcdfImageReader}.
* For applications that don't need to full verbosity of ISO 19115, consider using the
* {@linkplain #getStreamInstance(String) stream metadata instance} identified by the
* {@value #GEOTK_FORMAT_NAME} name instead.
*
* {@note The 3.0 version number is the GeoAPI version that define the format used here.}
*
* @since 3.20
*/
public static final String ISO_FORMAT_NAME = "ISO-19115_3.0";
/**
* The policy for the names of the nodes to be inferred from the ISO objects.
* We use JavaBeans names instead of UML identifiers in order to get the plural
* form for collections.
*/
static final KeyNamePolicy NAME_POLICY = KeyNamePolicy.JAVABEANS_PROPERTY;
/**
* Holder for the default instance for <cite>ISO 19115</cite> metadata format.
* Applies the <cite>Initialization-on-demand holder</cite> idiom, because the
* ISO metadata format is very large and only occasionally used.
*/
private static final class ISO {
private ISO() {}
/** The ISO-19115 instance for <cite>stream</cite> metadata format. */
static final SpatialMetadataFormat INSTANCE;
static {
final SpatialMetadataFormatBuilder builder = new SpatialMetadataFormatBuilder(ISO_FORMAT_NAME);
builder.addTreeForISO19115(null);
INSTANCE = builder.build();
}
}
/**
* Holder for the default instances of Geotk metadata format.
* Applies the <cite>Initialization-on-demand holder</cite> idiom,
* because the builder class is relatively large.
*/
private static final class Geotk {
private Geotk() {}
/** The default instance for <cite>stream</cite> metadata format. */
static final SpatialMetadataFormat STREAM;
static {
SpatialMetadataFormatBuilder builder = new SpatialMetadataFormatBuilder(GEOTK_FORMAT_NAME);
builder.addTreeForStream(null);
STREAM = builder.build();
}
/** The default instance for <cite>image</cite> metadata format. */
static final SpatialMetadataFormat IMAGE;
static {
final SpatialMetadataFormatBuilder builder = new SpatialMetadataFormatBuilder(GEOTK_FORMAT_NAME);
builder.addTreeForImage(null);
builder.addTreeForCRS("RectifiedGridDomain");
IMAGE = builder.build();
}
}
/**
* Returns the <cite>stream</cite> metadata format for the given name. This is the metadata
* format that apply to a file as a whole, which may contain more than one image. The
* tree structure is documented in the <a href="#default-formats">class javadoc</a>.
*
* @param name The {@value #GEOTK_FORMAT_NAME} or {@value #ISO_FORMAT_NAME} constant.
* @return The stream metadata format for the given name.
* @throws IllegalArgumentException If the given name is not one of the supported constants.
*
* @since 3.20
*/
public static SpatialMetadataFormat getStreamInstance(final String name) {
if (name.equalsIgnoreCase(GEOTK_FORMAT_NAME)) return Geotk.STREAM;
if (name.equalsIgnoreCase(ISO_FORMAT_NAME)) return ISO.INSTANCE;
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgument_2, "name", name));
}
/**
* Returns the <cite>image</cite> metadata format for the given name.
* This is the metadata format that apply to a particular image in a file.
* The tree structure is documented in the <a href="#default-formats">class javadoc</a>.
*
* @param name The {@value #GEOTK_FORMAT_NAME} constant.
* @return The image metadata format for the given name.
* @throws IllegalArgumentException If the given name is not one of the supported constants.
*
* @since 3.20
*/
public static SpatialMetadataFormat getImageInstance(final String name) {
if (name.equalsIgnoreCase(GEOTK_FORMAT_NAME)) return Geotk.IMAGE;
// More formats may be added later (e.g. GML in JPEG2000).
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgument_2, "name", name));
}
/**
* The metadata standards represented by each node. The most common standard is
* {@link MetadataStandard#ISO_19115 ISO_19115}. This information is used for
* {@linkplain #getDescription(String, String, Locale)} implementation.
*/
private final Map<String,MetadataStandard> standards = new HashMap<>();
/**
* The mapping from method names to attribute or child element names for a given element.
*/
private final Map<String, Map<String,String>> namesMapping = new HashMap<>();
/**
* The last value returned by {@link #getDescription(String, String, Locale)}, cached on
* the assumption that the description of different attributes of the same element are
* likely to be asked a few consecutive time.
*/
private transient volatile MetadataDescriptions descriptions;
/**
* Creates an initially empty format. Subclasses shall invoke the various
* {@code addFoo(...)} methods defined in this class or parent class for
* adding new elements and attributes.
*
* @param rootName the name of the root element.
*/
protected SpatialMetadataFormat(final String rootName) {
super(rootName, CHILD_POLICY_SOME);
}
/**
* Adds a new element type to this metadata document format with a
* child policy of {@link #CHILD_POLICY_REPEAT CHILD_POLICY_REPEAT}.
* <p>
* This method is defined mostly in order to give access to protected methods
* from {@link SpatialMetadataFormatBuilder}.
*
* @param <T> The compile-time type of the {@code type} argument.
* @param standard The standard from which the new element is derived, or {@code null}.
* @param type The legal class of the object value, or {@code null}.
* @param elementName The name of the new element.
* @param parentName The name of the element that will be the parent of the new element.
* @param childPolicy One of the {@code CHILD_POLICY_*} constants indicating the child policy
* of the new element.
* @param minOccurrence The minimum number of children of the node.
* @param maxOccurrence The maximum number of children of the node.
*/
@SuppressWarnings("fallthrough")
final <T> void addElement(final MetadataStandard standard, final Class<T> type,
final String elementName, final String parentName, int childPolicy,
final int minOccurrence, final int maxOccurrence)
{
switch (maxOccurrence) {
case 0: childPolicy = CHILD_POLICY_EMPTY; // Fallthrough
case 1: addElement(elementName, parentName, childPolicy); break;
default: addElement(elementName, parentName, minOccurrence, maxOccurrence); break;
}
if (standard != null) {
standards.put(elementName, standard);
}
if (type != null) {
addObjectValue(elementName, type);
addCustomAttributes(elementName, type);
}
}
/**
* Adds a reference to an existing child element. This method is defined here only in
* order to give access to the protected method from {@link SpatialMetadataFormatBuilder}.
* This method is defined mostly in order to give access to a protected method from
* {@link SpatialMetadataFormatBuilder}.
*/
final void addExistingElement(final String elementName, final String parentName) {
addChildElement(elementName, parentName);
}
/**
* Allows an {@code Object} reference of a given class type to be stored in nodes implementing
* the named element. This method delegates to one of the {@link #addObjectValue(String, Class,
* boolean, Object) addObjectValue} methods defined in the super-class. The current
* implementation is as below:
*
* {@preformat java
* addObjectValue(elementName, classType, mandatory, getDefaultValue(classType));
* }
*
* @param <T> The compile-time type of {@code classType}.
* @param elementName The name of the element for which to add an object value.
* @param classType The legal class type of the object value.
*/
final <T> void addObjectValue(final String elementName, final Class<T> classType) {
addObjectValue(elementName, classType, false, getDefaultValue(classType));
}
/**
* Adds a wrapper for a list of attributes. This is usually not needed, since attributes
* can be declared with the {@link #VALUE_LIST VALUE_LIST} type. However in the case of
* two-dimensional arrays (or lists of lists), the second dimension needs to be represented
* by a wrapper element. The main use case if the list of offset vectors in a
* {@linkplain RectifiedGrid rectified grid domain}, which can be represented as below:
*
* {@preformat text
* RectifiedGridDomain : RectifiedGrid
* └───OffsetVectors : List<double[]>
* └───OffsetVector : double[]
* └───values
* }
*
* In the above example, the name of the {@code RectifiedGridDomain}, {@code OffsetVectors}
* and {@code OffsetVector} nodes shall be specified by the {@code parentName},
* {@code elementName} and {@code componentName} arguments respectively. The creation of
* the {@code values} attribute is caller responsibility.
*
* @see #addElement(String, String, int, int)
* @see #addElement(String, String, int)
* @see #addObjectValue(String, Class, int, int)
*/
final void addListWrapper(final MetadataStandard standard,
final String parentName, final String elementName, final String componentName,
final Class<?> componentType, final int minOccurrence, final int maxOccurrence)
{
addElement(elementName, parentName, minOccurrence, maxOccurrence);
standards.put(elementName, standard);
// The repeated element with no child, only a single attribute.
addElement(componentName, elementName, CHILD_POLICY_EMPTY);
addObjectValue(componentName, componentType, 0, Integer.MAX_VALUE);
standards.put(componentName, standard);
}
/**
* Adds an attribute for a code list or an enumeration. The attribute has no
* default value and its type is {@link #DATATYPE_STRING DATATYPE_STRING}.
* <p>
* This method is defined mostly in order to give access to protected methods
* from {@link SpatialMetadataFormatBuilder}.
*
* @param elementName The name of the node where to add the attribute.
* @param attributeName The name of the attribute to add in the given element.
* @param mandatory {@code true} if the attribute is mandatory, or {@code false} for optional.
* @param codes The enumeration of valid attribute values.
*
* @see #addAttribute(String, String, int, boolean, String, List)
*/
final void addEnumeration(final String elementName, final String attributeName,
final boolean mandatory, final String... codes)
{
addAttribute(elementName, attributeName, DATATYPE_STRING, mandatory, null, Arrays.asList(codes));
}
/**
* Adds a new attribute of the given type. This method delegates to one of the {@code addAttribute}
* methods defined in the super-class. The choice of method and parameters is performed according
* the following rules (non-exhaustive list):
* <p>
* <ul>
* <li>The attribute is declared mandatory if {@code minOccurrence} is different than zero.</li>
* <li>The value type is set to {@link #VALUE_RANGE} if the {@code range} parameter is non-null.</li>
* <li>The value type is set to {@link #VALUE_LIST} if the {@code maxOccurrence} parameter is different than 1.</li>
* <li>The value type is set to {@link #VALUE_ARBITRARY} otherwise (except for boolean values).</li>
* <p>
* This method is defined mostly in order to give access to protected methods
* from {@link SpatialMetadataFormatBuilder}.
*
* @param elementName The name of the node where to add the attribute.
* @param attributeName The name of the attribute to add in the given element.
* @param dataType The type of the attribute to add.
* @param minOccurrence the smallest legal number of list items (typically 0 or 1).
* @param maxOccurrence the largest legal number of list items (typically 1 or more).
* @param range The range of valid values, or {@code null} if none.
*
* @see #addBooleanAttribute(String, String, boolean, boolean)
* @see #addAttribute(String, String, int, boolean, int, int)
* @see #addAttribute(String, String, int, boolean, String, List)
* @see #addAttribute(String, String, int, boolean, String, String, String, boolean, boolean)
*/
final void addAttribute(final String elementName, final String attributeName,
int dataType, final int minOccurrence, final int maxOccurrence, final NumberRange<?> range)
{
if (dataType == IIOMetadataFormat.DATATYPE_BOOLEAN) {
/*
* Boolean ⇒ Attribute VALUE_ENUMERATION
*
* A default value (false) is provided only if the attribute is
* not mandatory (minOccurrence == 0), otherwise we will require
* the user to specify a value explicitly.
*/
addBooleanAttribute(elementName, attributeName, minOccurrence == 0, false);
} else if (range != null) {
/*
* Number ⇒ Attribute VALUE_RANGE[_?_INCLUSIVE]
*/
addAttribute(elementName, attributeName, dataType, minOccurrence != 0, null,
toString(range.getMinValue()), toString(range.getMaxValue()),
range.isMinIncluded(), range.isMaxIncluded());
} else if (maxOccurrence == 1) {
/*
* Object ⇒ Attribute VALUE_ARBITRARY
*/
addAttribute(elementName, attributeName, dataType, minOccurrence != 0, null);
} else {
/*
* Object ⇒ Attribute VALUE_LIST
*/
addAttribute(elementName, attributeName, dataType, minOccurrence != 0, minOccurrence, maxOccurrence);
}
}
/**
* Invoked when a metadata element is about to be added to the tree. This is a hook for adding
* custom attributes which may not be in the UML or excluded. The main attributes of interest
* are {@code "name"} and {@code "type"} for referencing objects.
*/
final void addCustomAttributes(final String elementName, final Class<?> type) {
if (IdentifiedObject.class.isAssignableFrom(type)) {
addAttribute(elementName, "name", DATATYPE_STRING, true, null);
}
if (CoordinateSystemAxis.class.isAssignableFrom(type)) {
addAttribute(elementName, "axisAbbrev", DATATYPE_STRING, true, null);
}
final List<String> types;
if (CoordinateReferenceSystem.class.isAssignableFrom(type)) {
types = DataTypes.CRS_TYPES;
} else if (CoordinateSystem.class.isAssignableFrom(type)) {
types = DataTypes.CS_TYPES;
} else if (Datum.class.isAssignableFrom(type)) {
types = DataTypes.DATUM_TYPES;
} else {
return;
}
addAttribute(elementName, "type", DATATYPE_STRING, true, null, types);
}
/**
* Remembers the name of child element or attribute for the given method.
* This information is required by {@link MetadataProxy}.
*
* @param parentName The name of the parent element.
* @param The name of the method which is mapped to an element/attribute in the parent element.
* @param elementName The name of the element/attribute which represents the method value.
* @throws IllegalArgumentException If the parent element doesn't exist, or if the given method
* is already defined for the given parent.
*/
final void mapName(final String parentName, final String methodName, final String elementName)
throws IllegalArgumentException
{
// Actually 'methodName' is the only parameter which has not been verified
// by the caller, but we nevertheless verify all parameters as a safety.
ensureNonNull("parentName", parentName);
ensureNonNull("methodName", methodName);
ensureNonNull("elementName", elementName);
Map<String, String> map = namesMapping.get(parentName);
if (map == null) {
map = new HashMap<>();
namesMapping.put(parentName, map);
}
final String old = map.put(methodName, elementName);
if (old != null && !old.equals(elementName)) {
map.put(methodName, old); // Preserve the previous value.
throw new IllegalArgumentException(Errors.format(
Errors.Keys.DuplicatedValuesForKey_1, methodName));
}
}
/**
* Makes the first character an upper-case letter. This is used for element names,
* which typically starts with an upper-case letter in Image I/O metadata.
*
* @param elementName The element name, or {@code null}.
* @return The given name with the first character converted to an upper-case letter,
* or {@code null} if the given argument was null.
*
* @since 3.06
*/
static String toElementName(String elementName) {
if (elementName != null && !(elementName = elementName.trim()).isEmpty()) {
final char c = elementName.charAt(0);
final char u = Character.toUpperCase(c);
if (c != u) {
final StringBuilder buffer = new StringBuilder(elementName);
buffer.setCharAt(0, u);
elementName = buffer.toString();
}
}
return elementName;
}
/**
* Returns a string representation of the given value, or
* {@code null} if that value is null (unbounded range).
*/
private static String toString(final Comparable<?> value) {
return (value != null) ? value.toString() : null;
}
/**
* Removes an attribute from a previously defined element. If no attribute with the given
* name was present in the given element, nothing happens and no exception is thrown.
*
* @param elementName The name of the element.
* @param attributeName The name of the attribute being removed.
*/
@Override
protected void removeAttribute(final String elementName, final String attributeName) {
// This method is overriden only in order to allow SpatialMetadataFormatBuilder to access it.
super.removeAttribute(elementName, attributeName);
}
/**
* Removes an element from the format. If no element with the given
* name was present, nothing happens and no exception is thrown.
*
* @param elementName the name of the element to be removed.
*/
@Override
protected void removeElement(final String elementName) {
super.removeElement(elementName);
standards .remove(elementName);
namesMapping.remove(elementName);
}
/**
* Returns {@code true} if the element (and the subtree below it) is allowed to appear
* in a metadata document for an image of the given type. The default implementation
* always returns {@code true}.
*/
@Override
public boolean canNodeAppear(final String elementName, final ImageTypeSpecifier imageType) {
return true;
}
/**
* Returns the element which is the parent of the named element, or {@code null} if none.
* For example if this metadata format is the {@linkplain #getStreamInstance(String) stream}
* instance, then:
* <p>
* <ul>
* <li>The path to {@code "GeographicElement"} is {@code "DiscoveryMetadata/Extent/GeographicElement"}.</li>
* <li>The parent of {@code "GeographicElement"} returned by this method is {@code "Extent"}.</li>
* </ul>
*
* {@note An element may have more than one parent, since the same element can be copied under
* many nodes using <code>addChildElement(...)</code>. In such case, this method returns
* only the first path. Such cases do not occur with the Geotk formats identified by
* <code>GEOTK_FORMAT_NAME</code> in this class, but occur with the more complex ISO-19115
* format.}
*
* @param elementName The element for which the parent is desired.
* @return The parent of the given element, or {@code null}.
*
* @see #getElementPath(String)
*
* @since 3.06
*/
public String getElementParent(final String elementName) {
ensureNonNull("elementName", elementName);
return getElementParent(getRootName(), elementName, null);
}
/**
* Returns the element which is the parent of the named element, or {@code null} if none.
* <p>
* <b>Note:</b> Current implementation is somewhat inefficient. We could maintain a map of
* parents when new elements are added, but {@link IIOMetadataFormatImpl} already maintains
* such map - I'm not sure why they do no provide API for getting that info. This API could
* have been implemented as:
*
* {@preformat java
* public String[] getElementParents(String elementName) {
* List<String> parents = getElement(elementName).parentList;
* return parents.toString(new String[parents.size()]);
* }
* }
*
* @param root The root element from which to starts the scan.
* @param elementName The element for which the parent is desired.
* @param path If non-null, a buffer where to append the path before the node.
* @return The parent of the given element, or {@code null}.
*/
private String getElementParent(final String root, final String elementName, final StringBuilder path) {
final String[] childs = getChildNames(root);
if (childs != null) {
for (final String child : childs) {
if (child.equals(elementName)) {
return root;
}
}
// Do recursive call only after we checked every childs at the root. If a name
// appears twice (it should not), we will favor the one at the lowest depth.
for (final String child : childs) {
final String candidate = getElementParent(child, elementName, path);
if (candidate != null) {
if (path != null) {
path.insert(0, '/').insert(0, child);
}
return candidate;
}
}
}
return null;
}
/**
* Returns the path to the named element, or {@code null} if none. For example if this
* metadata format is the {@linkplain #getStreamInstance stream} instance, then the path to the
* {@code "GeographicElement"} is {@code "DiscoveryMetadata/Extent/GeographicElement"}.
*
* {@note An element may have more than one path, since the same element can be copied under
* many nodes using <code>addChildElement(...)</code>. In such case, this method returns
* only the first path. Such cases do not occur with the Geotk formats identified by
* <code>GEOTK_FORMAT_NAME</code> in this class, but occur with the more complex ISO-19115
* format.}
*
* @param elementName The element for which the path is desired.
* @return The path to the given element, or {@code null}.
*
* @see #getElementParent(String)
*
* @since 3.06
*/
public String getElementPath(final String elementName) {
ensureNonNull("elementName", elementName);
final StringBuilder path = new StringBuilder(64);
final String parent = getElementParent(getRootName(), elementName, path);
if (parent != null) {
// The parent is already in the path at this point.
return path.append(elementName).toString();
}
return null;
}
/**
* Returns the metadata standard implemented by the element of the given name.
* If the given element does not implement a standard (which may happen if the
* element has not been added by {@link SpatialMetadataFormatBuilder} method),
* then this method returns {@code null}.
*
* @param elementName The element for which the standard is desired.
* @return The standard implemented by the given element, or {@code null}.
*
* @since 3.06
*/
public MetadataStandard getElementStandard(final String elementName) {
return standards.get(elementName);
}
/**
* Returns the mapping from method names to element/attribute names, or {@code null} if this
* mapping is unknown. Keys are method names, and values are the attribute name as determined
* by {@link SpatialMetadataFormat#NAME_POLICY}.
* <p>
* This method returns a direct reference to the internal map.
* <strong>Do not modify the map content!</strong>
*/
final Map<String, String> getElementNames(final String elementName) {
return namesMapping.get(elementName);
}
/**
* Returns a description of the named element, or {@code null}. The description will be
* localized for the supplied locale if possible.
* <p>
* The default implementation first queries the
* {@linkplain MetadataStandard#asDescriptionMap description map} associated with the
* {@linkplain #getElementStandard metadata standard}. If no description is found, then the
* {@linkplain IIOMetadataFormatImpl#getElementDescription super-class implementation}
* is used.
*
* @param elementName The name of the element.
* @param locale The Locale for which localization will be attempted, or null.
* @return The attribute description.
*
* @since 3.05
*/
@Override
public String getElementDescription(final String elementName, final Locale locale) {
ensureNonNull("elementName", elementName);
String description = getDescription(elementName, null, locale);
if (description == null) {
description = super.getElementDescription(elementName, locale);
}
return description;
}
/**
* Returns a description of the named attribute, or {@code null}. The description will be
* localized for the supplied locale if possible.
* <p>
* The default implementation first queries the
* {@linkplain MetadataStandard#asDescriptionMap description map} associated with the
* {@linkplain #getElementStandard metadata standard}. If no description is found, then the
* {@linkplain IIOMetadataFormatImpl#getAttributeDescription super-class implementation}
* is used.
*
* @param elementName The name of the element.
* @param attrName The name of the attribute.
* @param locale The Locale for which localization will be attempted, or null.
* @return The attribute description.
*
* @since 3.05
*/
@Override
public String getAttributeDescription(final String elementName, final String attrName, final Locale locale) {
ensureNonNull("elementName", elementName);
ensureNonNull("attrName", attrName);
String description = getDescription(elementName, attrName, locale);
if (description == null) {
description = super.getAttributeDescription(elementName, attrName, locale);
}
return description;
}
/**
* Returns the description of the given attribute of the given element, in the given locale.
* If the attribute is null, then this method assumes that the caller want the description
* of the element itself. If there is no description available, returns {@code null}.
*
* @param elementName The name of the element in which to search for attributes.
* @param attrName The name of the attribute for which the descriptions is desired, or {@code null}.
* @param locale The locale of the descriptions, or {@code null} for the default.
* @return The requested description, or {@code null} if none.
*/
private String getDescription(String elementName, String attrName, Locale locale) {
if (locale == null) {
locale = Locale.getDefault(Locale.Category.DISPLAY);
}
if (attrName == null) {
attrName = elementName;
elementName = getElementParent(elementName);
if (elementName == null) {
return null;
}
}
MetadataDescriptions candidate = descriptions;
if (candidate == null || !locale.equals(candidate.locale) || !elementName.equals(candidate.elementName)) {
Class<?> type = null;
try {
type = getObjectClass(elementName);
} catch (IllegalArgumentException e) {
// The given element does not allow the storage of objects.
// We will set the description map to an empty map.
}
Map<String, ExtendedElementInformation> desc = Collections.emptyMap();
if (type != null) {
final MetadataStandard standard = getElementStandard(elementName);
if (standard != null) try {
desc = standard.asInformationMap(type, NAME_POLICY);
} catch (ClassCastException e) {
// The element type is not an instance of the expected standard.
// We will set the description map to an empty map.
}
}
candidate = new MetadataDescriptions(desc, elementName, locale);
descriptions = candidate;
}
final ExtendedElementInformation info = candidate.descriptions.get(attrName);
if (info != null) {
final InternationalString definition = info.getDefinition();
if (definition != null) {
return definition.toString(locale);
}
}
return null;
}
/**
* Returns the default value for an object reference of the given type. This method is
* invoked automatically by {@link SpatialMetadataFormatBuilder} for determining the value
* of the {@code defaultValue} argument in the call to the {@link #addObjectValue(String,
* Class, boolean, Object) addObjectValue} method.
* <p>
* This method is also invoked by {@link ReferencingBuilder#getDefault(Class)}, which does not
* rely on {@link IIOMetadataFormat#getObjectDefaultValue(String)} because the default value of
* some referencing objects depends on the type of the enclosing element. For example the default
* coordinate system shall be ellipsoidal for a geographic CRS and Cartesian for a projected
* CRS.
* <p>
* The default implementation returns a value determined from the table below.
* Subclasses can override this method for providing different default values.
* <p>
* <table border="1" cellspacing="0">
* <tr bgcolor="lightblue">
* <th>Type</th>
* <th>Default value</th>
* </tr><tr>
* <td> {@link PrimeMeridian} </td>
* <td> {@link DefaultPrimeMeridian#GREENWICH} </td>
* </tr><tr>
* <td> {@link Ellipsoid} </td>
* <td> {@link DefaultEllipsoid#WGS84} </td>
* </tr><tr>
* <td> {@link GeodeticDatum} </td>
* <td> {@link DefaultGeodeticDatum#WGS84} </td>
* </tr><tr>
* <td> {@link VerticalDatum} </td>
* <td> {@link DefaultVerticalDatum#GEOIDAL} </td>
* </tr><tr>
* <td> {@link EngineeringDatum} </td>
* <td> {@link DefaultEngineeringDatum#UNKNOWN} </td>
* </tr><tr>
* <td> {@link EllipsoidalCS} </td>
* <td> {@link DefaultEllipsoidalCS#GEODETIC_2D} </td>
* </tr><tr>
* <td> {@link CartesianCS} </td>
* <td> {@link DefaultCartesianCS#GENERIC_2D} </td>
* </tr><tr>
* <td> {@link GeographicCRS} </td>
* <td> {@link DefaultGeographicCRS#WGS84} </td>
* </tr><tr>
* <td> {@link GeocentricCRS} </td>
* <td> {@link DefaultGeocentricCRS#CARTESIAN} </td>
* </tr><tr>
* <td> All other type </td>
* <td> {@code null} </td>
* </tr>
* </table>
*
* @param <T> The compile-time type of {@code classType}.
* @param type The class type of the object for which to get a default value.
* @return The default value for an object of the given type, or {@code null} if none.
*
* @see ReferencingBuilder#getDefault(Class)
* @see #getObjectDefaultValue(String)
*
* @since 3.08
*/
public <T> T getDefaultValue(final Class<T> type) {
final IdentifiedObject object;
if (PrimeMeridian.class.isAssignableFrom(type)) {
object = CommonCRS.WGS84.primeMeridian();
} else if (Ellipsoid.class.isAssignableFrom(type)) {
object = CommonCRS.WGS84.ellipsoid();
} else if (GeodeticDatum.class.isAssignableFrom(type)) {
object = CommonCRS.WGS84.datum();
} else if (VerticalDatum.class.isAssignableFrom(type)) {
object = CommonCRS.Vertical.MEAN_SEA_LEVEL.datum();
} else if (EngineeringDatum.class.isAssignableFrom(type)) {
object = PredefinedCRS.CARTESIAN_2D.getDatum(); // Unknown datum.
} else if (EllipsoidalCS.class.isAssignableFrom(type)) {
object = CommonCRS.defaultGeographic().getCoordinateSystem();
} else if (CartesianCS.class.isAssignableFrom(type)) {
object = PredefinedCS.CARTESIAN_2D;
} else if (GeographicCRS.class.isAssignableFrom(type)) {
object = CommonCRS.WGS84.normalizedGeographic();
} else if (GeocentricCRS.class.isAssignableFrom(type)) {
object = CommonCRS.WGS84.geocentric();
} else {
object = null;
}
return type.cast(object);
}
/**
* Returns a <cite>tree table</cite> representation of this metadata standard.
* This convenience method delegates the work to {@link MetadataTreeTable}.
*
* @param locale The locale for which localization will be attempted, or {@code null}.
* @return A tree representation of this metadata standard.
*/
public TreeTableNode toTreeTable(final Locale locale) {
final MetadataTreeTable tree = new MetadataTreeTable(this);
if (locale != null) {
tree.setLocale(locale);
}
return tree.getRootNode();
}
/**
* Returns a string representation of this format.
* The default implementation formats this object as a tree.
*/
@Override
public String toString() {
return Trees.toString(toTreeTable(null));
}
}