/* * 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.Map; import java.util.HashMap; import java.util.Collections; import java.util.NoSuchElementException; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataFormat; import javax.measure.quantity.Angle; import javax.measure.quantity.Length; import javax.measure.Unit; import org.opengis.parameter.*; import org.opengis.referencing.*; import org.opengis.referencing.crs.*; import org.opengis.referencing.cs.*; import org.opengis.referencing.datum.*; import org.opengis.referencing.operation.*; import org.opengis.metadata.Identifier; import org.opengis.util.FactoryException; import org.geotoolkit.referencing.CRS; import org.apache.sis.metadata.iso.ImmutableIdentifier; import org.geotoolkit.referencing.factory.ReferencingFactoryContainer; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Loggings; import org.geotoolkit.resources.Vocabulary; import org.apache.sis.util.resources.IndexedResourceBundle; import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.util.iso.Types; import org.geotoolkit.internal.image.io.DataTypes; import org.apache.sis.util.CharSequences; import org.apache.sis.util.NullArgumentException; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.iso.DefaultNameSpace; import org.geotoolkit.lang.Builder; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.referencing.operation.DefaultConversion; import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME; /** * Builds referencing objects from an {@link IIOMetadata} object. This class uses a * {@link MetadataNodeAccessor} for reading and writing the attribute values in the * {@link IIOMetadata} object given at construction time. By default, this class * uses an accessor for the {@code "RectifiedGridDomain/CoordinateReferenceSystem"} * node of the {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} * format. However a different accessor can be given to the constructor. * * {@note This class exists because we do not use the reflection mechanism like what we do for * ISO 19115-2 metadata. Dedicated code is needed because the mapping between Image I/O * metadata and the referencing objects is more indirect. For example the kind of object * to create depends on the value of the <code>"type"</code> attribute.} * * The main methods in this class are {@link #build()} for reading, and * {@link #setCoordinateReferenceSystem(CoordinateReferenceSystem) setCoordinateReferenceSystem(...)} * for writing. The other getter methods are provided mostly as hooks that subclasses can override. * The table below summarizes them: * <p> * <table border="1" cellspacing="0"> * <tr bgcolor="lightblue"> * <th nowrap> Class </th> * <th nowrap> Getter </th> * <th nowrap> Setter </th> * </tr><tr> * <td> {@link CoordinateReferenceSystem} </td> * <td> {@link #build()} </td> * <td> </td> * </tr><tr> * <td> {@link CoordinateReferenceSystem} </td> * <td> {@link #getCoordinateReferenceSystem(Class)} </td> * <td> {@link #setCoordinateReferenceSystem(CoordinateReferenceSystem)} </td> * </tr><tr> * <td> {@link CoordinateSystem} </td> * <td> {@link #getCoordinateSystem(Class)} </td> * <td> {@link #setCoordinateSystem(CoordinateSystem)} </td> * </tr><tr> * <td> {@link Datum} </td> * <td> {@link #getDatum(Class)} </td> * <td> {@link #setDatum(Datum)} </td> * </tr><tr> * <td> {@link Ellipsoid} </td> * <td> {@link #getEllipsoid(MetadataNodeParser)} </td> * <td> </td> * </tr><tr> * <td> {@link PrimeMeridian} </td> * <td> {@link #getPrimeMeridian(MetadataNodeParser)} </td> * <td> </td> * </tr> * </table> * * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @since 3.08 (derived from 3.07) * @module */ public class ReferencingBuilder extends Builder<CoordinateReferenceSystem> { /** * The default path to the CRS node. */ static final String PATH = "RectifiedGridDomain/CoordinateReferenceSystem"; /** * Small tolerance factor for comparisons of floating point numbers. */ private static final double EPS = 1E-10; /** * {@code true} if the elements that are equal to the default value should be omitted. * This apply to write operations only. */ private static final boolean OMMIT_DEFAULTS = true; /** * The factories to use for creating the referencing objects. * Will be created only when first needed. */ private transient ReferencingFactoryContainer factories; /** * The metadata accessor for the {@code "CoordinateReferenceSystem"} node. * Must be an instance of {@link MetadataNodeAccessor} if setter methods will be invoked. */ private final MetadataNodeParser accessor; /** * {@code true} if this helper should not {@linkplain MetadataNodeParser#getUserObject() get} * or {@linkplain MetadataNodeAccessor#setUserObject(Object) set} the user object property. * The default value is {@code false}. */ private boolean ignoreUserObject; /** * Creates a new metadata helper for the given metadata. * The new {@code ReferencingBuilder} can be used for read and write operations. * * @param metadata The Image I/O metadata. An instance of the {@link SpatialMetadata} * sub-class is recommended, but not mandatory. * @throws NoSuchElementException If the underlying {@code IIOMetadata} *  {@linkplain IIOMetadata#isReadOnly() is read only} and doesn't * contains a node for the element to fetch. */ public ReferencingBuilder(final IIOMetadata metadata) throws NoSuchElementException { this(new MetadataNodeAccessor(metadata, GEOTK_FORMAT_NAME, PATH, null)); } /** * Creates a new metadata helper using the given accessor. Accessors for child elements * will be derived from the given accessor. Subclasses can control the name of child * elements by overriding the {@link #createNodeReader createNodeReader} and * {@link #createNodeWriter createNodeWriter} methods. * * @param accessor The accessor to the Coordinate Reference System node. Must be an instance * of {@link MetadataNodeAccessor} if setter methods will be invoked. */ public ReferencingBuilder(final MetadataNodeParser accessor) { this.accessor = accessor; } /** * Returns the factories to use for creating the referencing objects. */ private ReferencingFactoryContainer factories() { if (factories == null) { factories = ReferencingFactoryContainer.instance(null); } return factories; } /** * Returns the user object of the given class, or {@code null} if none. * * @since 3.09 */ private <T extends IdentifiedObject> T getUserObject(final MetadataNodeParser accessor, final Class<T> type) { return (!ignoreUserObject) ? accessor.getUserObject(type) : null; } /** * Sets the user object in the given accessor, if this operation is supported. * * @since 3.09 */ private void setUserObject(final MetadataNodeAccessor accessor, final IdentifiedObject object) { if (!ignoreUserObject) try { accessor.setUserObject(object); } catch (UnsupportedOperationException e) { // The underlying node is not an instance of IIOMetadataNode. // Ignore without warning, since this operation is optional. Logging.recoverableException(MetadataNodeAccessor.LOGGER, ReferencingBuilder.class, "setUserObject", e); } } /** * Returns {@code true} if this helper class should not * {@linkplain MetadataNodeParser#getUserObject() get} or * {@linkplain MetadataNodeAccessor#setUserObject(Object) set} the <cite>User Object</cite> * node property. The default value is {@code false}, in which case: * <p> * <ul> * <li>Every call to a setter method in this {@code ReferencingBuilder} class will * {@linkplain MetadataNodeAccessor#setUserObject(Object) set the user object} to * the given value, if possible.</li> * <li>Every call to a getter method in this {@code ReferencingBuilder} class will * {@linkplain MetadataNodeParser#getUserObject() get the user object} and return * it if it exist.</li> * </ul> * <p> * If this method returns {@code true}, then the above steps are skipped. This implies * that every call to a getter method will create a new object from the values declared * in node attributes. * * @return {@code true} if user objects should be ignored. * * @see javax.imageio.metadata.IIOMetadataNode#getUserObject() * @see MetadataNodeParser#getUserObject() * * @since 3.09 */ public boolean getIgnoreUserObject() { return ignoreUserObject; } /** * Sets whatever this helper class is allowed to * {@linkplain MetadataNodeParser#getUserObject() get} or * {@linkplain MetadataNodeAccessor#setUserObject(Object) set} the <cite>User Object</cite> * node property. See {@link #getIgnoreUserObject()} for more information. * * @param ignore {@code true} if user objects should be ignored. * * @since 3.09 */ public void setIgnoreUserObject(final boolean ignore) { ignoreUserObject = ignore; } /** * Returns the coordinate reference system, or {@code null} if it can not be created. * This method delegates to {@link #getCoordinateReferenceSystem(Class)} and catch the * exception. If an exception has been thrown, the exception is * {@linkplain MetadataNodeParser#warningOccurred logged} and this method returns {@code null}. * * @return The CRS, or {@code null} if none. * * @since 3.20 (derived from 3.07) */ @Override public CoordinateReferenceSystem build() { Exception failure; try { return getCoordinateReferenceSystem(CoordinateReferenceSystem.class); } catch (FactoryException | NoSuchElementException | // Throws by MetadataNodeParser if an element is absents and IIOMetadata is read only. NullArgumentException e) // Throws by 'isNonNull' (in this class) if a mandatory element is absents. { failure = e; } accessor.warning(null, getClass(), "build", failure); return null; } /** * Gets the coordinate reference system. If no CRS is explicitly defined, then a * {@linkplain MetadataNodeParser#warningOccurred warning is logged} and a * {@linkplain #getDefault(Class) default CRS} is returned, which is {@code null} * in the default implementation. * * @param <T> The compile-time type of {@code baseType}. * @param baseType The expected CRS type. * @return The coordinate reference system, or {@code null} if the CRS * can not be parsed and there is no default value. * @throws FactoryException If the coordinate reference system can not be created. */ public <T extends CoordinateReferenceSystem> T getCoordinateReferenceSystem(final Class<T> baseType) throws FactoryException { final T userObject = getUserObject(accessor, baseType); if (userObject != null) { return userObject; } if (!accessor.isEmpty()) { final Class<? extends CoordinateReferenceSystem> type = getInterface("getCoordinateReferenceSystem", baseType, accessor); if (type != null) { final Map<String,?> properties = getName(accessor); final CRSFactory factory = factories().getCRSFactory(); if (GeographicCRS.class.isAssignableFrom(type)) { return baseType.cast(factory.createGeographicCRS(properties, getDatum(GeodeticDatum.class), getCoordinateSystem(EllipsoidalCS.class))); } else if (ProjectedCRS.class.isAssignableFrom(type)) { final GeographicCRS baseCRS = factory.createGeographicCRS( Collections.singletonMap(GeographicCRS.NAME_KEY, untitled(accessor)), getDatum(GeodeticDatum.class), CommonCRS.defaultGeographic().getCoordinateSystem()); final CartesianCS derivedCS = getCoordinateSystem(CartesianCS.class); return baseType.cast(factory.createProjectedCRS(properties, baseCRS, getConversionFromBase(baseCRS, derivedCS), derivedCS)); } else { // TODO: test for other types of CRS here (VerticalCRS, etc.) warning("getCoordinateReferenceSystem", Errors.Keys.UnknownType_1, type); } } } return getDefault("getCoordinateReferenceSystem", accessor, baseType); } /** * Returns the defining conversion from the base geographic CRS to the projected CRS. * If no coordinate system is explicitly defined, then a * {@linkplain MetadataNodeParser#warningOccurred warning is logged} and a * {@linkplain #getDefault(Class) default conversion} is returned. * * @return The conversion from geographic to projected CRS, or {@code null}. * @throws FactoryException If the conversion can not be created. */ private Conversion getConversionFromBase(final CoordinateReferenceSystem baseCRS, final CoordinateSystem derivedCS) throws FactoryException { final MetadataNodeParser cvAccessor = createNodeReader(accessor, "Conversion", null); final String method = cvAccessor.getAttribute("method"); if (isNonNull("getBaseToCRS", "method", method)) { final Map<String,?> properties = getName(cvAccessor); final MathTransformFactory factory = factories().getMathTransformFactory(); final ParameterValueGroup parameters = factory.getDefaultParameters(method); try { final MetadataNodeParser paramAccessor = createNodeReader(cvAccessor, "Parameters", "ParameterValue"); final int numParam = paramAccessor.childCount(); for (int i=0; i<numParam; i++) { paramAccessor.selectChild(i); final String name = paramAccessor.getAttribute("name"); if (isNonNull("getBaseToCRS", "name", name)) { final Double value = paramAccessor.getAttributeAsDouble("value"); if (isNonNull("getBaseToCRS", "value", value)) try { parameters.parameter(name).setValue(value.doubleValue()); } catch (IllegalArgumentException e) { paramAccessor.warning(null, getClass(), "getBaseToCRS", e); } } } } catch (NoSuchElementException e) { // May happen if there is no "Parameters" node, for // example because all parameters have their default value. accessor.warning(null, getClass(), "getConversionFromBase", e); } final MathTransform tr = factory.createBaseToDerived(baseCRS, parameters, derivedCS); return new DefaultConversion(properties, factory.getLastMethodUsed(), tr, parameters); } return getDefault("getBaseToCRS", cvAccessor, Conversion.class); } /** * Sets the coordinate reference system to the given value. * * @param crs The coordinate reference system. * * @todo The base CRS is not yet declared for the {@code DerivedCRS} case. */ public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs) { final MetadataNodeAccessor accessor; try { accessor = (MetadataNodeAccessor) this.accessor; } catch (ClassCastException e) { // We catch the ClassCastException rather than performing an instanceof check in order // to help the developer to see which instance was expected in the "caused by" part. throw new UnsupportedOperationException(this.accessor.getErrorResources() .getString(Errors.Keys.UnmodifiableMetadata), e); } setName(crs, accessor); accessor.setAttribute("type", DataTypes.getType(crs)); final Datum datum = CRS.getDatum(crs); if (datum != null) { setDatum(datum); } final CoordinateSystem cs = crs.getCoordinateSystem(); if (cs != null) { setCoordinateSystem(cs); } /* * For ProjectedCRS, the baseCRS is implicitly a GeographicCRS with the same datum. * For other kind of DerivedCRS, we need to declare the baseCRS (TODO). */ if (crs instanceof GeneralDerivedCRS) { final Conversion conversion = ((GeneralDerivedCRS) crs).getConversionFromBase(); final MetadataNodeAccessor opAccessor = createNodeWriter(accessor, "Conversion", null); setName(conversion, opAccessor); setName(conversion.getMethod(), false, opAccessor, "method"); addParameter(new MetadataNodeAccessor[] {opAccessor, null}, conversion.getParameterValues(), CRS.getEllipsoid(crs)); } setUserObject(accessor, crs); } /** * Adds the given parameter value using the given accessor. If the parameter value is actually * a {@link ParameterValueGroup}, then its child are added recursively. * <p> * In order to keep the metadata simpler, this method omits some parameters that are equal * to the default value. In order to reduce the risk of error, we omits a parameter only if * its default value is 0, or 1 in the particular case of the scale factor. * * @param accessors An array of length 2 where the first element is the accessor for the * {@link Conversion} element. The second element will be created by this method * when first needed, in order to create a {@code "Parameters"} element only if * there is at least one parameter to write. * @param param The parameter or group of parameters to add. * @param ellipsoid The ellipsoid defined in the datum, or {@code null} if none. */ private void addParameter(final MetadataNodeAccessor[] accessors, final GeneralParameterValue param, final Ellipsoid ellipsoid) { if (param instanceof ParameterValueGroup) { for (final GeneralParameterValue p : ((ParameterValueGroup) param).values()) { addParameter(accessors, p, ellipsoid); } } if (param instanceof ParameterValue<?>) { final ParameterValue<?> pv = (ParameterValue<?>) param; final Object value = pv.getValue(); if (value != null) { final ParameterDescriptor<?> descriptor = pv.getDescriptor(); final String name = descriptor.getName().getCode().trim(); if (value instanceof Number) { /* * Check if we should skip this value (see the method javadoc for more details). * Note that the omission of values equal to the default values can be disabled, * but not the omission of ellipsoid value. This is for consistency with WKT * formatting. */ final double numericValue = ((Number) value).doubleValue(); if (ellipsoid != null) { if (name.equalsIgnoreCase("semi_major")) { if (equals(numericValue, ellipsoid.getSemiMajorAxis())) { return; } } else if (name.equalsIgnoreCase("semi_minor")) { if (equals(numericValue, ellipsoid.getSemiMinorAxis())) { return; } } } if (OMMIT_DEFAULTS) { final Object defaultValue = descriptor.getDefaultValue(); if (defaultValue instanceof Number) { final double df = ((Number) defaultValue).doubleValue(); if (equals(numericValue, df)) { if (df == (name.equalsIgnoreCase("scale_factor") ? 1 : 0)) { return; } } } } } MetadataNodeAccessor accessor = accessors[1]; if (accessor == null) { accessor = createNodeWriter(accessors[0], "Parameters", "ParameterValue"); accessors[1] = accessor; } accessor.selectChild(accessor.appendChild()); accessor.setAttribute("name", name); accessor.setAttribute("value", value.toString()); } } } /** * Gets the coordinate system. If no coordinate system is explicitly defined, then a * {@linkplain MetadataNodeParser#warningOccurred warning is logged} and a * {@linkplain #getDefault(Class) default coordinate system} is returned, * which is {@link org.geotoolkit.referencing.cs.DefaultEllipsoidalCS#GEODETIC_2D}, * {@link org.geotoolkit.referencing.cs.DefaultCartesianCS#GENERIC_2D} or {@code null} * (depending on the {@code baseType} argument) in the default implementation. * * @param <T> The compile-time type of {@code baseType}. * @param baseType The expected coordinate system type. * @return The coordinate system, or {@code null} if the coordinate system * can not be parsed and there is no default value. * @throws FactoryException If the coordinate system can not be created. */ @SuppressWarnings("fallthrough") public <T extends CoordinateSystem> T getCoordinateSystem(final Class<T> baseType) throws FactoryException { final MetadataNodeParser csAccessor = createNodeReader(accessor, "CoordinateSystem", null); final T userObject = getUserObject(csAccessor, baseType); if (userObject != null) { return userObject; } final Class<? extends CoordinateSystem> type = getInterface("getCoordinateSystem", baseType, csAccessor); if (type != null) { final Map<String,?> properties = getName(csAccessor); final Integer dimension = csAccessor.getAttributeAsInteger("dimension"); final MetadataNodeParser axesAccessor = createNodeReader(csAccessor, "Axes", "CoordinateSystemAxis"); final int numAxes = axesAccessor.childCount(); if (dimension != null && dimension != numAxes) { warning("getCoordinateSystem", Errors.Keys.MismatchedDimension_3, new Object[] {"Axes", numAxes, dimension}); } final CoordinateSystemAxis[] axes = new CoordinateSystemAxis[numAxes]; final CSFactory factory = factories().getCSFactory(); for (int i=0; i<numAxes; i++) { axesAccessor.selectChild(i); final Map<String,?> axesProperties = getName(axesAccessor); String abbreviation = axesAccessor.getAttribute("axisAbbrev"); final AxisDirection direction = axesAccessor.getAttributeAsCode("direction", AxisDirection.class); if (!isNonNull("getCoordinateSystem", "direction", direction)) { return null; } if (abbreviation == null) { /* * If no abbreviation has been explicitly specified, use the first letter of the * name. Note that if non-null, the name is guaranteed to have a length greater * than 0 has of MetadataNodeParser.getAttribute(String) method implementation. */ abbreviation = axesProperties.get(IdentifiedObject.NAME_KEY).toString(); if (abbreviation == null) { abbreviation = Types.getCodeName(direction); } if (abbreviation != null) { abbreviation = CharSequences.camelCaseToAcronym(abbreviation).toString(); } } final Unit unit = axesAccessor.getAttributeAsUnit("unit", null); if (!isNonNull("getCoordinateSystem", "unit", unit)) { return null; } axes[i] = factory.createCoordinateSystemAxis(axesProperties, abbreviation, direction, unit); } /* * At this point, we have created the set of axes. * Now create the coordinate system. */ Boolean isEllipsoidal = null; if (EllipsoidalCS.class.isAssignableFrom(type)) { isEllipsoidal = Boolean.TRUE; } else if (CartesianCS.class.isAssignableFrom(type)) { isEllipsoidal = Boolean.FALSE; } if (isEllipsoidal != null) { switch (numAxes) { case 0: // Fall through case 1: { warning("getCoordinateSystem", Errors.Keys.NotTwoDimensional_1, numAxes); break; } case 2: { return baseType.cast(isEllipsoidal ? factory.createEllipsoidalCS(properties, axes[0], axes[1]) : factory.createCartesianCS (properties, axes[0], axes[1])); } default: { warning("getCoordinateSystem", Errors.Keys.UnexpectedDimensionForCs_1, type); // Fall through } case 3: { return baseType.cast(isEllipsoidal ? factory.createEllipsoidalCS(properties, axes[0], axes[1], axes[2]) : factory.createCartesianCS (properties, axes[0], axes[1], axes[2])); } } } else { // TODO: test for other types of CS here (VerticalCS, etc.) warning("getCoordinateSystem", Errors.Keys.UnknownType_1, type); } } return getDefault("getCoordinateSystem", csAccessor, baseType); } /** * Sets the coordinate system to the given value. * * @param cs The coordinate system, or {@code null} if none. */ public void setCoordinateSystem(final CoordinateSystem cs) { final MetadataNodeAccessor csAccessor = createNodeWriter(accessor, "CoordinateSystem", null); setName(cs, csAccessor); csAccessor.setAttribute("type", DataTypes.getType(cs)); final int dimension = cs.getDimension(); csAccessor.setAttribute("dimension", dimension); final MetadataNodeAccessor axes = createNodeWriter(csAccessor, "Axes", "CoordinateSystemAxis"); for (int i=0; i<dimension; i++) { final CoordinateSystemAxis axis = cs.getAxis(i); axes.selectChild(axes.appendChild()); setName(axis, axes); final String abbreviation = axis.getAbbreviation(); if (!abbreviation.equals(axis.getName().getCode())) { axes.setAttribute("axisAbbrev", abbreviation); } axes.setAttribute("direction", axis.getDirection()); boolean hasRangeMeaning = false; double value = axis.getMinimumValue(); if (value > Double.NEGATIVE_INFINITY) { axes.setAttribute("minimumValue", value); hasRangeMeaning = true; } value = axis.getMaximumValue(); if (value < Double.POSITIVE_INFINITY) { axes.setAttribute("maximumValue", value); hasRangeMeaning = true; } if (hasRangeMeaning) { axes.setAttribute("rangeMeaning", axis.getRangeMeaning()); } axes.setAttribute("unit", axis.getUnit()); setUserObject(axes, axis); } setUserObject(csAccessor, cs); } /** * Gets the datum. If no datum is explicitly defined, then a * {@linkplain MetadataNodeParser#warningOccurred warning is logged} and a * {@linkplain #getDefault(Class) default datum} is returned. * * @param <T> The compile-time type of {@code baseType}. * @param baseType The expected datum type. * @return The datum, or {@code null} if the datum can not be parsed * and there is no default value. * @throws FactoryException If the datum can not be created. * * @todo {@code VerticalDatum}, {@code TemporalDatum} and {@code ImageDatum} are not yet * implemented. */ public <T extends Datum> T getDatum(final Class<T> baseType) throws FactoryException { final MetadataNodeParser datumAccessor = createNodeReader(accessor, "Datum", null); final T userObject = getUserObject(datumAccessor, baseType); if (userObject != null) { return userObject; } final Class<? extends Datum> type = getInterface("getDatum", baseType, datumAccessor); if (type != null) { final Map<String,?> properties = getName(datumAccessor); final DatumFactory factory = factories().getDatumFactory(); if (GeodeticDatum.class.isAssignableFrom(type)) { final Ellipsoid ellipsoid = getEllipsoid(datumAccessor); final PrimeMeridian pm = getPrimeMeridian(datumAccessor); return baseType.cast(factory.createGeodeticDatum(properties, ellipsoid, pm)); } else if (EngineeringDatum.class.isAssignableFrom(type)) { return baseType.cast(factory.createEngineeringDatum(properties)); } else { warning("getDatum", Errors.Keys.UnknownType_1, type); } } return getDefault("getDatum", datumAccessor, baseType); } /** * Sets the datum to the given value. * * @param datum The datum, or {@code null} if none. */ public void setDatum(final Datum datum) { final MetadataNodeAccessor datumAccessor = createNodeWriter(accessor, "Datum", null); setName(datum, datumAccessor); datumAccessor.setAttribute("type", DataTypes.getType(datum)); if (datum instanceof GeodeticDatum) { final GeodeticDatum gd = (GeodeticDatum) datum; final Ellipsoid ellipsoid = gd.getEllipsoid(); if (ellipsoid != null) { final MetadataNodeAccessor child = createNodeWriter(datumAccessor, "Ellipsoid", null); setName(ellipsoid, child); child.setAttribute("axisUnit", ellipsoid.getAxisUnit()); child.setAttribute("semiMajorAxis", ellipsoid.getSemiMajorAxis()); if (ellipsoid.isIvfDefinitive()) { child.setAttribute("inverseFlattening", ellipsoid.getInverseFlattening()); } else { child.setAttribute("semiMinorAxis", ellipsoid.getSemiMinorAxis()); } setUserObject(child, ellipsoid); } final PrimeMeridian pm = gd.getPrimeMeridian(); if (pm != null) { final MetadataNodeAccessor child = createNodeWriter(datumAccessor, "PrimeMeridian", null); setName(pm, child); child.setAttribute("greenwichLongitude", pm.getGreenwichLongitude()); child.setAttribute("angularUnit", pm.getAngularUnit()); setUserObject(child, pm); } } setUserObject(datumAccessor, datum); } /** * Gets the ellipsoid. If no ellipsoid is explicitly defined, then a * {@linkplain MetadataNodeParser#warningOccurred warning is logged} and a * {@linkplain #getDefault(Class) default ellipsoid} is returned. * * @param datumAccessor The accessor of the datum enclosing the ellipsoid. * @return The ellipsoid, or {@code null} if the ellipsoid can not be parsed * and there is no default value. * @throws FactoryException If the ellipsoid can not be created. */ protected Ellipsoid getEllipsoid(final MetadataNodeParser datumAccessor) throws FactoryException { final MetadataNodeParser ellipsoidAccessor = createNodeReader(datumAccessor, "Ellipsoid", null); final Ellipsoid userObject = getUserObject(ellipsoidAccessor, Ellipsoid.class); if (userObject != null) { return userObject; } final Unit<Length> unit = ellipsoidAccessor.getAttributeAsUnit("axisUnit", Length.class); if (isNonNull("getEllipsoid", "axisUnit", unit)) { final Map<String,?> properties = getName(ellipsoidAccessor); final Double semiMajor = ellipsoidAccessor.getAttributeAsDouble("semiMajorAxis"); if (isNonNull("getEllipsoid", "semiMajorAxis", semiMajor)) { final DatumFactory factory = factories().getDatumFactory(); final Double semiMinor = ellipsoidAccessor.getAttributeAsDouble("semiMinorAxis"); if (semiMinor != null) { return factory.createEllipsoid(properties, semiMajor, semiMinor, unit); } final Double ivf = ellipsoidAccessor.getAttributeAsDouble("inverseFlattening"); if (isNonNull("getEllipsoid", "inverseFlattening", ivf)) { return factory.createFlattenedSphere(properties, semiMajor, ivf, unit); } } } return getDefault("getEllipsoid", ellipsoidAccessor, Ellipsoid.class); } /** * Gets the prime meridian. If no prime meridian is explicitly defined, then a * {@linkplain MetadataNodeParser#warningOccurred warning is logged} and a * {@linkplain #getDefault(Class) default prime meridian} is returned. * * @param datumAccessor The accessor of the datum enclosing the prime meridian. * @return The prime meridian, or {@code null} if the prime meridian can not be * parsed and there is no default value. * @throws FactoryException If the prime meridian can not be created. */ protected PrimeMeridian getPrimeMeridian(final MetadataNodeParser datumAccessor) throws FactoryException { final MetadataNodeParser pmAccessor = createNodeReader(datumAccessor, "PrimeMeridian", null); final PrimeMeridian userObject = getUserObject(pmAccessor, PrimeMeridian.class); if (userObject != null) { return userObject; } final Double greenwich = pmAccessor.getAttributeAsDouble("greenwichLongitude"); if (isNonNull("getEllipsoid", "greenwichLongitude", greenwich)) { final Map<String,?> properties = getName(pmAccessor); final Unit<Angle> unit = pmAccessor.getAttributeAsUnit("angularUnit", Angle.class); if (isNonNull("getPrimeMeridian", "angularUnit", unit)) { final DatumFactory factory = factories().getDatumFactory(); return factory.createPrimeMeridian(properties, greenwich, unit); } } return getDefault("getPrimeMeridian", pmAccessor, PrimeMeridian.class); } /** * Returns a default object of the given class. This method is invoked automatically * when the object was not explicitly defined in the metadata, or can not be parsed. * <p> * The default implementation delegates to {@link SpatialMetadataFormat#getDefaultValue(Class)} * for every types except {@link CoordinateReferenceSystem}. The later method is preferred to * {@link IIOMetadataFormat#getObjectDefaultValue(String)} because the default value may depend * on the {@code "type"} attribute in the enclosing element. For example if the CRS type is * {@code "geographic"}, then the default coordinate system shall be a {@link EllipsoidalCS}. * But if the CRS type is {@code "projected"} instead, then the default coordinate system shall * rather be a {@link CartesianCS}. * <p> * Subclasses can override this method if they want to provide different default values. * * @param <T> The compile-time type of the {@code type} argument. * @param type The type of the object to be returned. * @return The default object of the given type, or {@code null} if none. * @throws FactoryException If the default object can not be created. * * @see SpatialMetadataFormat#getDefaultValue(Class) * @see IIOMetadataFormat#getObjectDefaultValue(String) */ protected <T extends IdentifiedObject> T getDefault(final Class<T> type) throws FactoryException { if (!CoordinateReferenceSystem.class.isAssignableFrom(type)) { if (accessor.format instanceof SpatialMetadataFormat) { return ((SpatialMetadataFormat) accessor.format).getDefaultValue(type); } } return null; } /** * Returns a default object of the given class. This method logs a warning telling that the * returned object is used as a fallback. The default implementation delegates to the first * of the following methods which return a non-null default value: * <p> * <ul> * <li>{@link SpatialMetadataFormat#getDefaultValue(Class)}</li> * <li>{@link IIOMetadataFormat#getObjectDefaultValue(String)}</li> * </ul> */ private <T extends IdentifiedObject> T getDefault(final String method, final MetadataNodeParser accessor, final Class<T> type) throws FactoryException { T object = getDefault(type); if (object == null) { object = type.cast(accessor.format.getObjectDefaultValue(accessor.name())); } if (object != null) { warning(method, Loggings.getResources(accessor.getLocale()), Loggings.Keys.UsingFallback_1, object.getName()); } return object; } /** * Creates a read-only accessor for a child element. This method is invoked automatically when * a new accessor needs to be created for a child element, for example the {@code "Datum"} * element inside the {@code "CoordinateReferenceSystem"} element. * <p> * The default implementation is as below: * * {@preformat java * return new MetadataNodeAccessor(parent, path, childPath); * } * * Subclasses can override this method in order to create different accessors, * for example in order to use different names for the child elements. * * @param parent The accessor for which the {@code path} is relative. * @param path The path to the node of interest. * @param childPath The path to the child elements, or {@code null} if none. * @return The accessor to use. * * @see MetadataNodeParser#MetadataNodeParser(MetadataNodeParser, String, String) * * @since 3.20 */ protected MetadataNodeParser createNodeReader(final MetadataNodeParser parent, final String path, final String childPath) { return new MetadataNodeParser(parent, path, childPath); } /** * Creates the read/write accessor for a child element. This method is invoked automatically * when a new accessor needs to be created for a child element, for example the {@code "Datum"} * element inside the {@code "CoordinateReferenceSystem"} element. * <p> * The default implementation is as below: * * {@preformat java * return new MetadataNodeAccessor(parent, path, childPath); * } * * Subclasses can override this method in order to create different accessors, * for example in order to use different names for the child elements. * * @param parent The accessor for which the {@code path} is relative. * @param path The path to the node of interest. * @param childPath The path to the child elements, or {@code null} if none. * @return The accessor to use. * * @see MetadataNodeAccessor#MetadataNodeAccessor(MetadataNodeParser, String, String) * * @since 3.20 */ protected MetadataNodeAccessor createNodeWriter(final MetadataNodeParser parent, final String path, final String childPath) { return new MetadataNodeAccessor(parent, path, childPath); } /** * Returns the interface for the {@code "type"} attribute of the given accessor, or * {@code baseType} if unknown. In the later case, a warning message will be emitted. * * @param <T> The compile-time type of the {@code baseType} argument. * @param method The name of the method invoking this one (for logging purpose only). * @param baseType {@link CoordinateReferenceSystem}, {@link CoordinateSystem} or {@link Datum}. * @param accessor The accessor from which to get the value of the {@code "type"} attribute. * @return The value of the {@code "type"} attribute as an interface, or {@code baseType}. */ private <T extends IdentifiedObject> Class<? extends T> getInterface(final String method, final Class<T> baseType, final MetadataNodeParser accessor) { final String type = accessor.getAttribute("type"); if (type == null) { /* * If the type was not specified, log a warning only if the type was not * already known anyway. It may be known because some types of CRS accepts * only one specific type of datum or coordinate system. */ if (baseType.equals(Datum.class) || baseType.equals(CoordinateSystem.class)) { warning(method, Errors.Keys.NoParameterValue_1, "type"); } } else try { // Following line may throw a ClassCastException (as of method contract). final Class<? extends T> classe = DataTypes.getInterface(baseType, type); if (classe != null) { return classe; } warning(method, Errors.Keys.UnknownType_1, type); } catch (ClassCastException e) { warning(method, Errors.Keys.IllegalClass_2, new Object[] {type, baseType}); } return baseType; } /** * Gets the {@code "name"} attribute from the given accessor. * If this attribute is not found, then a default name is generated. * * @param accessor The accessor to use for getting the name attribute. * @return A map containing the name attribute. */ private static Map<String,Object> getName(final MetadataNodeParser accessor) { String name = accessor.getAttribute("name"); if (name == null) { name = untitled(accessor); } else { final int s = name.indexOf(DefaultNameSpace.DEFAULT_SEPARATOR); if (s >= 0) { final String authority = name.substring(0, s).trim(); name = name.substring(s + 1).trim(); if (name.isEmpty()) { name = authority; } else if (!authority.isEmpty()) { final Map<String,Object> properties = new HashMap<>(6); properties.put(Identifier.CODESPACE_KEY, authority); properties.put(Identifier.CODE_KEY, name); final Identifier id = new ImmutableIdentifier(properties); return Collections.<String,Object>singletonMap(IdentifiedObject.NAME_KEY, id); } } } return Collections.<String,Object>singletonMap(IdentifiedObject.NAME_KEY, name); } /** * Sets the {@code "name"} attribute for the given object. * * @param object The object from which to fetch the name. * @param accessor The accessor to use for setting the name attribute. */ private static void setName(final IdentifiedObject object, final MetadataNodeAccessor accessor) { setName(object, true, accessor, "name"); } /** * Same as {@link #setName}, but uses the given attribute name instead than {@code "name"}. * * @param object The object from which to fetch the name. * @param scoped {@code true} if the name should contains the authority prefix. * @param accessor The accessor to use for setting the name attribute. * @param attribute The attribute name ({@code "name"} by default). */ private static void setName(final IdentifiedObject object, final boolean scoped, final MetadataNodeAccessor accessor, final String attribute) { final Identifier id = object.getName(); if (id != null) { String name = id.getCode(); if (scoped) { final String authority = Citations.getIdentifier(id.getAuthority()); if (authority != null) { name = authority + DefaultNameSpace.DEFAULT_SEPARATOR + name; } } accessor.setAttribute(attribute, name); } } /** * Returns {@code "untitled"} in the locale of the given accessor. */ private static String untitled(final MetadataNodeParser accessor) { return Vocabulary.getResources(accessor.getLocale()).getString(Vocabulary.Keys.Untitled); } /** * Returns {@code true} if the given value is equals to the expected one, * accepting a tolerance interval. */ private static boolean equals(final double actual, final double expected) { return Math.abs(actual - expected) <= Math.abs(expected)*EPS; } /** * Returns {@code true} if the given object is non-null. * Otherwise emmits a warning and returns {@code false}. */ private boolean isNonNull(final String method, final String attribute, final Object value) { if (value != null) { return true; } warning(method, Errors.Keys.NoParameterValue_1, attribute); return false; } /** * Convenience method for logging a warning using the error resource bundle. */ private void warning(final String method, final short key, final Object value) { warning(method, Errors.getResources(accessor.getLocale()), key, value); } /** * Convenience method for logging a warning using the given resource bundle. */ private void warning(final String method, final IndexedResourceBundle resources, final short key, final Object value) { accessor.warning(getClass(), method, resources, key, value); } }