/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2012, Open Source Geospatial Foundation (OSGeo) * (C) 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.metadata.netcdf; import java.util.Set; import java.util.HashSet; import java.util.List; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Date; import java.util.logging.Level; import java.io.IOException; import java.net.URI; import javax.measure.Unit; import javax.measure.UnitConverter; import javax.measure.IncommensurableException; import ucar.nc2.NetcdfFileWriteable; import ucar.nc2.units.DateFormatter; import org.opengis.metadata.Metadata; import org.opengis.metadata.Identifier; import org.opengis.metadata.extent.*; import org.opengis.metadata.spatial.*; import org.opengis.metadata.content.*; import org.opengis.metadata.citation.*; import org.opengis.metadata.constraint.*; import org.opengis.metadata.identification.*; import org.opengis.metadata.lineage.Lineage; import org.opengis.metadata.quality.DataQuality; import org.opengis.referencing.crs.SingleCRS; import org.opengis.util.InternationalString; import org.opengis.util.ControlledVocabulary; import org.apache.sis.measure.Units; import org.geotoolkit.util.Utilities; import org.apache.sis.util.ArgumentChecks; import org.geotoolkit.image.io.WarningProducer; import org.apache.sis.util.iso.Types; import org.geotoolkit.internal.image.io.Warnings; import org.geotoolkit.resources.Errors; import org.geotoolkit.util.Strings; import org.apache.sis.internal.referencing.ReferencingUtilities; import org.apache.sis.util.ArraysExt; /** * Mapping from ISO 19115-2 metadata to NetCDF metadata. The {@link String} constants declared in * the {@linkplain NetcdfMetadata parent class} are the name of attributes to be written by this * class. * * {@section Multi-occurrences} * <p>Multi-occurrences is allowed only for the following attribute values:</p> * <ul> * <li>{@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#LICENSE}, * formatted as a multi-lines string.</li> * <li>{@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#ACCESS_CONSTRAINT}, * formatted as a comma-separated string.</li> * <li>{@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#HISTORY}, * formatted as a multi-lines string.</li> * <li>{@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#KEYWORDS}, * formatted as a comma-separated list. However only the keywords belonging to the * first vocabulary found will be formatted. The vocabulary name will be stored in * the {@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#VOCABULARY} attribute.</li> * <li>{@link #LATITUDE LATITUDE}, {@link #LONGITUDE LONGITUDE}, {@link #VERTICAL VERTICAL} and * {@link #TIME TIME} groups of attributes, as the union of all extents using compatible units * of measurement. If some extents use incompatible units, then only values compatible with * the first unit of measurement found are retained.</li> * </ul> * <p>For every attributes not in the above list, only the first occurrence will be written in the NetCDF file. * For example if the ISO-19115 metadata defines many {@linkplain Citation#getIdentifiers() identifiers}, then * only the first one will be stored in the {@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#IDENTIFIER} * attribute.</p> * * {@section Known limitations} * <p>The current implementation does not set the * {@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#STANDARD_NAME} and * {@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#STANDARD_NAME_VOCABULARY} attributes. This is because both * {@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#STANDARD_NAME} and * {@value org.geotoolkit.metadata.netcdf.NetcdfMetadata#KEYWORDS} attributes take their values from a * {@link Keywords} object having {@link KeywordType#THEME}, * so we don't have a way to differentiate them at this stage.</p> * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @since 3.20 * @module */ public class NetcdfMetadataWriter extends NetcdfMetadata { /** * Number of dimensions in the {@link #spatioTemporalExtent} array. */ private static final int NUM_DIMENSIONS = 4; /** * Where to write the spatio-temporal extent. The length of this array * shall be equals to the {@link #NUM_DIMENSIONS} value. */ private static final Dimension[] DIMENSIONS = { LONGITUDE, LATITUDE, VERTICAL, TIME }; /** * The NetCDF file where to write ISO metadata. * This file is set at construction time. * <p> * This {@code NetcdfMetadataReader} class does <strong>not</strong> close this file. * Closing this file after usage is the user responsibility. */ protected final NetcdfFileWriteable file; /** * The set of attribute values already defined by this writer. * This is used in order to avoid overwriting a value written in a previous pass. */ private final Set<String> defined; /** * The values to write in the {@value #KEYWORDS} attribute. * This is created only when first needed and will be formatted as a comma-separated list. */ private Set<String> keywords; /** * The values to write in the {@value #LICENSE} attribute. * This is created only when first needed and will be formatted as a multi-lines string. */ private Set<String> licenses; /** * The values to write in the {@value #ACCESS_CONSTRAINT} attribute. * This is created only when first needed and will be formatted as a comma-separated list. */ private Set<String> restrictions; /** * The vocabulary of the {@linkplain #keywords}, or {@code null} if not yet determined. * This field is used both for providing the content of the {@value #VOCABULARY} attribute, * and for filtering the keywords in order to retain only the ones using the same vocabulary. */ private String vocabulary; /** * The union of all geographic, vertical and temporal extents, and their resolution. * The element in this array are as below, in that order: * * {@preformat text * xmin, ymin, zmin, tmin, xres, yres, zres, tres, xmax, ymax, zmax, tmax * } * * The values at a given dimension are considered uninitialized if the the minimal value * is greater than the maximal one. * * @see #NUM_DIMENSIONS * @see #addExtent(int, double, double) */ private final double[] spatioTemporalExtent; /** * The vertical and temporal units, or {@code null} if unknown. As a special case, we * use {@link Units#UNITY} when a minimal and maximal values where specified without units. * * @see #getUnit(SingleCRS) */ private Unit<?> verticalUnit, temporalUnit; /** * The name of the next attribute value to set in a call to {@link #setAttribute(String)}. */ private transient String attributeName; /** * The object to use for formatting date, created when first needed. */ private transient DateFormatter dateFormatter; /** * Creates a new <cite>ISO to NetCDF</cite> mapper for the given file. * * @param file The NetCDF file where to write metadata. * @param owner Typically the {@link org.geotoolkit.image.io.SpatialImageWriter} instance * using this encoder, or {@code null}. */ public NetcdfMetadataWriter(final NetcdfFileWriteable file, final WarningProducer owner) { super(owner); ArgumentChecks.ensureNonNull("file", file); this.file = file; defined = new HashSet<>(); spatioTemporalExtent = new double[3*NUM_DIMENSIONS]; Arrays.fill(spatioTemporalExtent, 0*NUM_DIMENSIONS, 2*NUM_DIMENSIONS, Double.POSITIVE_INFINITY); Arrays.fill(spatioTemporalExtent, 2*NUM_DIMENSIONS, 3*NUM_DIMENSIONS, Double.NEGATIVE_INFINITY); } /** * Reports a warning. * * @param method The method in which the warning occurred. * @param exception The exception to log. */ private void warning(final String method, final Exception exception) { Warnings.log(this, Level.WARNING, NetcdfMetadataWriter.class, method, exception); } /** * Adds the given element in the given set, if non-null. If the given set is null, * then a new set will be created and returned. */ private static <E> Set<E> addTo(Set<E> set, final E element) { if (element != null) { if (set == null) { set = new LinkedHashSet<>(); } set.add(element); } return set; } /** * Returns the given collection if non-null, or an empty set otherwise. */ private static <E> Collection<E> nonNull(Collection<E> collection) { if (collection == null) { collection = Collections.emptySet(); } return collection; } /** * Returns the value of the given number, or {@link Double#NaN} if the number is null. */ private static double valueOf(final Double value) { return (value != null) ? value.doubleValue() : Double.NaN; } /** * Returns the units of measurement of the given CRS, or {@code Units#UNITY} if unspecified. * * @see #verticalUnit * @see #temporalUnit */ private static Unit<?> getUnit(final SingleCRS crs) { if (crs != null) { final Unit<?> unit = ReferencingUtilities.getUnit(crs.getCoordinateSystem()); if (unit != null) { return unit; } } return Units.UNITY; } /** * Formats the given units as a NetCDF unit. This method handles a few units in a special way * in order to match the NetCDF conventions. For example the {@linkplain Units#DEGREE * angular degrees} are formatted as {@code "degrees"} instead than {@code "°"}. * * @param unit The unit to format, or {@code null}. * @return A string representation of the given units, * or {@code null} if the given unit was null. */ private static String toString(final Unit<?> unit) { if (unit == null || unit.equals(Units.UNITY)) { return null; } if (unit.equals(Units.DEGREE)) { return "degrees"; } return unit.toString(); } /** * Returns the first non-null localized text in the given collection. */ private String toString(final Collection<? extends InternationalString> elements) { for (final InternationalString element : nonNull(elements)) { final String text = toString(element); if (text != null) { return text; } } return null; } /** * Returns a string representation of the given text if non-null and non-empty, * or {@code null} otherwise. */ private String toString(final InternationalString text) { if (text != null) { String s = text.toString(getLocale()); if (s != null && !((s = s.trim()).isEmpty())) { return s; } } return null; } /** * Adds the given ({@linkplain #attributeName}, <var>value</var>) pair to the global attributes. * If the given value is {@code null}, then this method does nothing and returns {@code false}. * * @param value The attribute value to add, or {@code null} if none. * @return {@code true} if the value has been added, or {@code false} otherwise. * @throws IOException If an I/O operation was required and failed. */ private boolean setAttribute(final InternationalString value) throws IOException { return (value != null) && setAttribute(value.toString(getLocale())); } /** * Adds the ({@linkplain #attributeName}, <var>title</var>) pair to the global attributes. * If the given value is {@code null}, then this method does nothing and returns {@code false}. * * @param value The attribute value to add, or {@code null} if none. * @return {@code true} if the value has been added, or {@code false} otherwise. * @throws IOException If an I/O operation was required and failed. */ private boolean setAttribute(final Citation value) throws IOException { return (value != null) && setAttribute(value.getTitle()); } /** * Adds the given ({@linkplain #attributeName}, <var>code</var>) pair to the global attributes. * If the given identifier is {@code null}, then this method does nothing and returns {@code false}. * * @param value The attribute value to add, or {@code null} if none. * @return {@code true} if the value has been added, or {@code false} otherwise. * @throws IOException If an I/O operation was required and failed. */ private boolean setAttribute(final Identifier identifier) throws IOException { return (identifier != null) && setAttribute(identifier.getCode()); } /** * Adds the given ({@linkplain #attributeName}, <var>code</var>) pair to the global attributes. * If the given code is {@code null}, then this method does nothing and returns {@code false}. * * @param value The attribute value to add, or {@code null} if none. * @return {@code true} if the value has been added, or {@code false} otherwise. * @throws IOException If an I/O operation was required and failed. */ private boolean setAttribute(final ControlledVocabulary code) throws IOException { return (code != null) && setAttribute(Types.getCodeName(code)); } /** * Adds the ({@linkplain #attributeName}, <var>date</var>) pair to the global attributes. * If the given value is {@code null}, then this method does nothing and returns {@code false}. * * @param value The attribute value to add, or {@code null} if none. * @return {@code true} if the value has been added, or {@code false} otherwise. * @throws IOException If an I/O operation was required and failed. */ private boolean setAttribute(final Date date) throws IOException { if (date == null) { return false; } if (dateFormatter == null) { dateFormatter = new DateFormatter(); } return setAttribute(dateFormatter.toDateTimeString(date)); } /** * Adds the given ({@linkplain #attributeName}, <var>value</var>) pair to the global attributes. * If the protected {@linkplain #setAttribute(String, String)} method returns {@code false}, * then this method does nothing. * * @param value The attribute value to add, or {@code null} if none. * @return {@code true} if the value has been added, or {@code false} otherwise. * @throws IOException If an I/O operation was required and failed. */ private boolean setAttribute(final String value) throws IOException { final String key = attributeName; if (!setAttribute(key, value)) { return false; } if (!defined.add(key)) { // Must be 'key' even if subclass used a different attribute name. throw new IllegalStateException(Errors.format(Errors.Keys.ValueAlreadyDefined_1, key)); } return true; } /** * Adds the given (<var>key</var>, <var>value</var>) pair to the global attributes. * If the given value is {@code null} or {@linkplain String#isEmpty() empty} (ignoring * leading and trailing spaces), then this method does nothing and returns {@code false}. * <p> * This method is invoked for every non-numerical attributes to be defined in the NetCDF file. * Subclasses can override this method if they want to alter the values, store it under a * different key, or skip this value. * * @param key The key of the attribute to add. * @param value The attribute value to add, or {@code null} if none. * @return {@code true} if the value has been added, or {@code false} otherwise. * @throws IOException If an I/O operation was required and failed. */ protected boolean setAttribute(final String key, String value) throws IOException { if (value == null || (value = value.trim()).isEmpty()) { return false; } file.addGlobalAttribute(key, value); return true; } /** * Adds the given (<var>key</var>, <var>value</var>) pair to the global attributes. * If the given value is {@code NaN} or infinite, then this method does nothing and * returns {@code false}. * <p> * This method is invoked for every numerical attributes to be defined in the NetCDF file. * Subclasses can override this method if they want to alter the values, store it under a * different key, or skip this value. * * @param key The key of the attribute to add. * @param value The attribute value to add, or {@code NaN} if none. * @return {@code true} if the value has been added, or {@code false} otherwise. * @throws IOException If an I/O operation was required and failed. */ protected boolean setAttribute(final String key, final double value) throws IOException { if (Double.isNaN(value) || Double.isInfinite(value)) { return false; } file.addGlobalAttribute(key, value); return true; } /** * Returns {@code true} if an attribute value is already defined for the given key. * This method sets the {@link #attributeName} field to the given key, so one of the private * {@code setAttribute(...)} methods can be invoked right after for setting the actual value. * <p> * As a special case, this method returns {@code true} if the given key is null. This should * cause {@code NetcdfMetadataWriter} to skip the ISO 19115 metadata that are not associated * to NetCDF attributes, which are indicated by null values in the {@link Responsible} object * for instance. */ private boolean isDefined(final String key) { attributeName = key; return (key == null) || defined.contains(key); } /** * Writes the given creator, contributor or publisher in the given set of attributes. * * @param author The responsible party to write. * @param role The set of attribute where to write the responsible party. * @throws IOException If an I/O operation was required and failed. */ private void write(final ResponsibleParty author, final Responsible role) throws IOException { if (author == null) { return; } if (!isDefined(role.NAME)) { setAttribute(author.getIndividualName()); } if (!isDefined(role.INSTITUTION)) { setAttribute(author.getOrganisationName()); } final Contact contact = author.getContactInfo(); if (contact != null) { if (!isDefined(role.URL)) { final OnlineResource resource = contact.getOnlineResource(); if (resource != null) { final URI linkage = resource.getLinkage(); if (linkage != null) { setAttribute(linkage.toString()); } } } if (!isDefined(role.EMAIL)) { final Address address = contact.getAddress(); if (address != null) { for (final String mail : nonNull(address.getElectronicMailAddresses())) { if (setAttribute(mail)) { break; } } } } } if (!isDefined(role.ROLE)) { setAttribute(author.getRole()); } } /** * Writes the {@value #IDENTIFIER}, {@value #TITLE}, {@value #SUMMARY}, {@value #DATE_CREATED}, * {@value #DATA_TYPE} and more attributes from the given identification info. * <p> * This method also build the set of {@linkplain #keywords} and {@linkplain #licenses} * information, but does not write their content. The caller is responsible for writing * the content of the above-cited fields after this method call. * * @param info The identification info to write, or {@code null} if none. * @param fileIdentifier The file identifier (used only as a fallback), or {@code null}. * @throws IOException If an I/O operation was required and failed. */ private void write(final Identification info, String fileIdentifier) throws IOException { if (info == null) { return; } final boolean isData = (info instanceof DataIdentification); final Citation citation = info.getCitation(); if (!isDefined(IDENTIFIER)) { if (citation != null) { for (final Identifier id : nonNull(citation.getIdentifiers())) { if (setAttribute(id)) { // Unconditionally set the naming authority // in order to be consistent with the code. attributeName = NAMING_AUTHORITY; setAttribute(id.getAuthority()); fileIdentifier = null; break; } } } if (fileIdentifier != null) { setAttribute(fileIdentifier); } } if (!isDefined(TITLE)) { setAttribute(citation); } if (!isDefined(SUMMARY)) { setAttribute(info.getAbstract()); } if (!isDefined(PURPOSE)) { setAttribute(info.getPurpose()); } if (!isDefined(COMMENT) && isData) { setAttribute(((DataIdentification) info).getSupplementalInformation()); } /* * There is 3 types of keyword which are recognized by this code. * All other keyword types are silently ignored. * * 1) The first keyword of type "project" is stored in the "project" attribute. * This attribute will appear below the above title, summary and comments ones. * * 2) The first keyword of type "dataCenter" is saved in the 'dataCenter' variable * in order to be used as a fallback later if no publisher were found. * * 3) All keywords of type "theme" are saved in a 'keywords' set, for later processing. * The caller will need to write the "keywords" attribute himself, preferably right * after the identification info in order to keep the keywords close to the topic * category. */ String dataCenter = null; for (final Keywords kset : nonNull(info.getDescriptiveKeywords())) { final KeywordType type = kset.getType(); if (type != null) { if (type.equals(KeywordType.THEME)) { final Citation vk = kset.getThesaurusName(); if (vk != null) { final String title = toString(vk.getTitle()); if (title != null) { if (vocabulary == null) { vocabulary = title; } else if (!title.equalsIgnoreCase(vocabulary)) { continue; } } } for (final InternationalString keyword : kset.getKeywords()) { keywords = addTo(keywords, toString(keyword)); } } else { final String[] names = type.names(); if (!isDefined(PROJECT) && ArraysExt.containsIgnoreCase(names, "project")) { setAttribute(toString(kset.getKeywords())); } if (dataCenter == null && ArraysExt.containsIgnoreCase(names, "dataCenter")) { dataCenter = toString(kset.getKeywords()); } } } } /* * Write the author information and data creation/revision/publishing dates. * If an author role is unspecified, then it will be assumed to be the creator * unless a creator has already been found, in which case the unspecified role * will be assumed to be a contributor. */ if (citation != null) { if (!isDefined(REFERENCES)) { for (final InternationalString details : nonNull(citation.getOtherCitationDetails())) { if (setAttribute(details)) break; // Write only the first citation details. } } final Collection<? extends Responsibility> authors = nonNull(citation.getCitedResponsibleParties()); if (!authors.isEmpty()) { boolean foundCreator = false; final List<ResponsibleParty> deferred = new ArrayList<>(authors.size()); for (final Responsibility author : authors) { if (author != null) { if (Role.ORIGINATOR.equals(author.getRole())) { write((ResponsibleParty) author, CREATOR); foundCreator = true; } else { deferred.add((ResponsibleParty) author); } } } for (final ResponsibleParty author : deferred) { write(author, Role.PUBLISHER.equals(author.getRole()) ? PUBLISHER : foundCreator ? CONTRIBUTOR : CREATOR); } } if (!isDefined(PUBLISHER.NAME)) { setAttribute(dataCenter); // Possible fallback extracted from the keywords. } if (!isDefined(ACKNOWLEDGMENT)) { Collection<? extends InternationalString> credits = nonNull(info.getCredits()); if (credits.size() >= 2) { credits = new LinkedHashSet<>(credits); // Avoid duplicated values. } setAttribute(Strings.toString(credits, "\n")); } int undefined = 0; if (!isDefined(DATE_CREATED)) undefined |= 1; if (!isDefined(DATE_MODIFIED)) undefined |= 2; if (!isDefined(DATE_ISSUED)) undefined |= 4; if (undefined != 0) { nextDate: for (final CitationDate date : nonNull(citation.getDates())) { final DateType type = date.getDateType(); for (int flag=1; ; flag <<= 1) { if ((undefined & flag) != 0) { final DateType forType; switch (flag) { case 1: forType=DateType.CREATION; attributeName=DATE_CREATED; break; case 2: forType=DateType.REVISION; attributeName=DATE_MODIFIED; break; case 4: forType=DateType.PUBLICATION; attributeName=DATE_ISSUED; break; default: continue nextDate; } if (forType.equals(type)) { if (setAttribute(date.getDate())) { if ((undefined &= ~flag) == 0) { break nextDate; // Continuing the loop would be useless. } } break; // No need to compare the others DateType. } } } } } } /* * Write data type and topic category last in order to keep them close to the keywords, * which should be written by the caller soon after this write(Identification) method call. */ if (isData) { final DataIdentification dataInfo = (DataIdentification) info; if (!isDefined(DATA_TYPE)) { for (final SpatialRepresentationType type : nonNull(dataInfo.getSpatialRepresentationTypes())) { if (setAttribute(type)) break; // Write only the first type. } } if (!isDefined(TOPIC_CATEGORY)) { for (final TopicCategory topic : nonNull(dataInfo.getTopicCategories())) { if (setAttribute(topic)) break; // Write only the first topic. } } } /* * Following code store license information, but do not write them yet. * They will be written by the caller. */ for (final Constraints constraint : nonNull(info.getResourceConstraints())) { for (final InternationalString c : nonNull(constraint.getUseLimitations())) { licenses = addTo(licenses, toString(c)); } if (constraint instanceof LegalConstraints) { for (final Restriction r : nonNull(((LegalConstraints) constraint).getAccessConstraints())) { restrictions = addTo(restrictions, Types.getCodeName(r)); } } } } /** * Computes the values of the NetCDF attributes for the given extent information. * This method does not write bounding box information immediately, but instead stores the * information in the {@link #spatioTemporalExtent} field. It is caller responsibility to * write that field after this method invocation. * * @param content The extent information to write, or {@code null}. * @throws IOException If an I/O operation was required and failed. */ private void addExtent(final Extent extent) throws IOException { if (extent == null) { return; } boolean hasIdentifier = isDefined(GEOGRAPHIC_IDENTIFIER); for (final GeographicExtent element : nonNull(extent.getGeographicElements())) { if (!hasIdentifier && (element instanceof GeographicDescription)) { hasIdentifier = setAttribute(((GeographicDescription) element).getGeographicIdentifier()); } if (element instanceof GeographicBoundingBox) { final GeographicBoundingBox bbox = (GeographicBoundingBox) element; if (!Boolean.FALSE.equals(bbox.getInclusion())) { addExtent(null, 0, bbox.getWestBoundLongitude(), bbox.getEastBoundLongitude(), Units.DEGREE); addExtent(null, 1, bbox.getSouthBoundLatitude(), bbox.getNorthBoundLatitude(), Units.DEGREE); } } } for (final VerticalExtent element : extent.getVerticalElements()) { verticalUnit = addExtent(verticalUnit, 2, valueOf(element.getMinimumValue()), valueOf(element.getMaximumValue()), getUnit(element.getVerticalCRS())); } for (final TemporalExtent element : extent.getTemporalElements()) { temporalUnit = addExtent(temporalUnit, 3, Double.NaN, // TODO Double.NaN, // TODO Units.UNITY); // TODO } } /** * Adds the given minimum and maximum values for the extent at the given dimension. * This method does nothing if the unit of measurement is incompatible with the one * of previous calls. * * @param oldUnit The unit of measurement of previous calls, or {@code null} if none. * @param dimension The dimension to set, from 0 inclusive to {@value #NUM_DIMENSIONS} exclusive. * @param min The minimal value, or {@code NaN} if unknown. * @param max The minimal value, or {@code NaN} if unknown. * @param unit The unit of measurement, or {@link Units#UNITY} if unknown. * @return The unit of measurement to retain. */ private Unit<?> addExtent(Unit<?> oldUnit, int dimension, double min, double max, final Unit<?> unit) { if (oldUnit == null) { oldUnit = unit; } else try { final UnitConverter c = unit.getConverterToAny(oldUnit); min = c.convert(min); max = c.convert(max); } catch (IncommensurableException e) { warning("addExtent", e); return oldUnit; } double value = spatioTemporalExtent[dimension]; if (min < value) value = min; if (max < value) value = max; // Paranoiac check, but should not happen. spatioTemporalExtent[dimension] = value; value = spatioTemporalExtent[dimension += 2*NUM_DIMENSIONS]; if (max > value) value = max; if (min > value) value = min; // Paranoiac check, but should not happen. spatioTemporalExtent[dimension] = value; return oldUnit; } /** * Computes the values of the NetCDF attributes for the given spatial information. * This method does not write resolution information immediately, but instead stores the * information in the {@link #spatioTemporalExtent} field. It is caller responsibility to * write that field after this method invocation. * * @param dimension The dimension information to write, or {@code null}. * @throws IOException If an I/O operation was required and failed. */ private void write(final SpatialRepresentation spatial) throws IOException { if (spatial instanceof GridSpatialRepresentation) { for (final org.opengis.metadata.spatial.Dimension dimension : nonNull(((GridSpatialRepresentation) spatial).getAxisDimensionProperties())) { final DimensionNameType type = dimension.getDimensionName(); if (type != null) for (int i=0; i<NUM_DIMENSIONS; i++) { if (type.equals(DIMENSIONS[i].DEFAULT_NAME_TYPE)) { final Double resolution = dimension.getResolution(); if (resolution != null) { final double value = resolution; if (value > 0 && value < spatioTemporalExtent[i + NUM_DIMENSIONS]) { spatioTemporalExtent[i + NUM_DIMENSIONS] = value; } } break; } } } } } /** * Writes NetCDF attribute values for the given metadata object. * If this method is invoked more than once, then subsequent invocations will add * the values of new attributes but will not alter the old attributes, unless the * values can be appended (e.g. in a comma-separated list). * * @param metadata The metadata object to write, or {@code null}. * @throws IOException If an error occurred while writing the attribute values. */ public void write(final Metadata metadata) throws IOException { if (metadata == null) { return; } final String fileIdentifier = metadata.getFileIdentifier(); for (final Identification info : nonNull(metadata.getIdentificationInfo())) { write(info, fileIdentifier); } /* * Unconditionally write the "keywords" attribute, overwriting the old attribute if any. * This is okay because we accumulated the keywords in a class field, so the previous * attribute values are not lost. Similar argument applies to the legal information. */ if (setAttribute(KEYWORDS, Strings.toString(keywords, ", "))) { setAttribute(VOCABULARY, vocabulary); // Must be consistent with the keywords. } if (!isDefined(PROCESSING_LEVEL)) { for (final ContentInformation content : nonNull(metadata.getContentInfo())) { if (content instanceof ImageDescription) { if (setAttribute(((ImageDescription) content).getProcessingLevelCode())) break; } } } /* * Computes, then write the geographic extent and resolution. */ for (final Identification info : nonNull(metadata.getIdentificationInfo())) { if (info instanceof DataIdentification) { for (final Extent extent : nonNull(((DataIdentification) info).getExtents())) { addExtent(extent); } } } for (final SpatialRepresentation spatial : nonNull(metadata.getSpatialRepresentationInfo())) { write(spatial); } for (int i=0; i<NUM_DIMENSIONS; i++) { final Dimension dim = DIMENSIONS[i]; setAttribute(dim.MINIMUM, spatioTemporalExtent[i]); setAttribute(dim.MAXIMUM, spatioTemporalExtent[i + 2*NUM_DIMENSIONS]); setAttribute(dim.RESOLUTION, spatioTemporalExtent[i + NUM_DIMENSIONS]); final Unit<?> unit; switch (i) { default: continue; case 2: unit = verticalUnit; break; case 3: unit = temporalUnit; break; } setAttribute(dim.UNITS, toString(unit)); } /* * Unconditionally write the legal information. This is okay because we accumulated * those information in class fields, so the previous attribute values are not lost. */ setAttribute(LICENSE, Strings.toString(licenses, "\n")); setAttribute(ACCESS_CONSTRAINT, Strings.toString(restrictions, ", ")); /* * Write history-related information last. */ if (!isDefined(METADATA_CREATION)) { setAttribute(metadata.getDateStamp()); } if (!isDefined(HISTORY)) { final StringBuilder history = new StringBuilder(80); for (final DataQuality quality : nonNull(metadata.getDataQualityInfo())) { final Lineage lineage = quality.getLineage(); if (lineage != null) { if (history.length() != 0) { history.append('\n'); } final String s = toString(lineage.getStatement()); if (s != null) { history.append(s); } } } if (history.length() == 0) { history.append("Created by Geotoolkit.org version ").append(Utilities.VERSION); } setAttribute(history.toString()); } } }