/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2010-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2010-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; import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Locale; import java.util.logging.LogRecord; import org.opengis.util.CodeList; import org.opengis.referencing.cs.AxisDirection; import org.apache.sis.util.ArraysExt; import org.geotoolkit.resources.Errors; import org.apache.sis.util.resources.IndexedResourceBundle; import org.apache.sis.util.NullArgumentException; import org.apache.sis.util.Classes; import org.geotoolkit.internal.image.io.Warnings; import static org.geotoolkit.image.io.DimensionSlice.API; /** * Identifies a domain dimension by its index, its name or its axis direction. * This class is relevant mostly for <var>n</var>-dimensional datasets where <var>n</var>>2. * The dimension can be identified in any of the following ways: * * <ul> * <li><p>As a zero-based index using an {@link Integer}. This is the most straightforward approach * when the set of dimensions is known.</p> * * <blockquote><font size="-1"><b>Example:</b> If the dimensions are known to be (<var>x</var>, * <var>y</var>, <var>z</var>, <var>t</var>), then the <var>t</var> dimension can be identified * by the index 3.</font></blockquote></li> * * <li><p>As a dimension name using a {@link String}. This is a better approach than indexes when * dimensions have known names, because it is insensitive to dimension order and whatever other * dimensions exist or not.</p> * * <blockquote><font size="-1"><b>Example:</b> If the list of dimensions can be either * ({@code "longitude"}, {@code "latitude"}, {@code "depth"}, {@code "time"}) or * ({@code "latitude"}, {@code "longitude"}, {@code "time"}), then the index of the time * dimension can be either 3 or 2. It is better to identify the time dimension by its * name: {@code "time"}.</font></blockquote></li> * * <li><p>As a direction using an {@link AxisDirection}. This provides similar benefit to using * a named dimension, but can work without knowledge of the actual name. It can also be used * with file formats that don't support named dimensions.</p></li> * </ul> * * More than one identifier can be used in order to increase the chance of finding the dimension. * For example in order to fetch the <var>z</var> dimension, it may be necessary to specify two * names: {@code "height"} and {@code "depth"}. In case of ambiguity, a {@linkplain #warningOccurred * warning will be emitted} to any registered listeners at image reading or writing time. * <p> * See the {@link DimensionSlice} javadoc for usage examples. * * @author Martin Desruisseaux (Geomatys) * @version 3.15 * * @see MultidimensionalImageStore * @see IllegalImageDimensionException * * @since 3.15 * @module */ public class DimensionIdentification implements WarningProducer { /** * The collection that created this object. */ final DimensionSet owner; /** * Creates a new {@code DimensionIdentification} instance for the given API. This * constructor is protected for subclasses usage only. The public API for fetching * new instances is the {@link DimensionSet#getOrCreate(DimensionSlice.API)} method. * * @param owner The collection that created this object. * @param api The API to assign to this dimension. * @throws IllegalArgumentException If an other dimension is already assigned to the given API. */ protected DimensionIdentification(final DimensionSet owner, final API api) throws IllegalArgumentException { if (owner == null) { throw new NullArgumentException(Errors.format(Errors.Keys.NullArgument_1, "owner")); } this.owner = owner; if (api != API.NONE) { final int ordinal = api.ordinal(); final DimensionIdentification[] apiMapping = owner.apiMapping(); if (apiMapping[ordinal] != null) { throw new IllegalArgumentException(getErrorResources() .getString(Errors.Keys.ValueAlreadyDefined_1, api)); } apiMapping[ordinal] = this; } } /** * Creates a new instance which is not assigned to any API. * This constructor is for {@link DimensionSlice} creation. * * @param owner The collection that created this object. */ DimensionIdentification(final DimensionSet owner) { this.owner = owner; } /** * Creates a new instance initialized to the same values than the given instance. * This method is not public on intend because it is unsafe ({@code original.owner} * could be incompatible). * * @param original The instance to copy. */ DimensionIdentification(final DimensionIdentification original) { this.owner = original.owner; } /** * Returns the resources for formatting error messages. */ private IndexedResourceBundle getErrorResources() { return Errors.getResources(getLocale()); } /** * Adds one or many identifiers for the dimension represented by this object. * * @param argName The argument name, used for producing an error message if needed. * @param identifiers The identifiers to add. * @throws IllegalArgumentException If an identifier is already * assigned to an other {@code DimensionSlice} instance. */ private void addDimensionId(final String argName, final Object[] identifiers) throws IllegalArgumentException { for (int i=0; i<identifiers.length; i++) { final Object identifier = identifiers[i]; if (identifier == null) { throw new NullArgumentException(getErrorResources().getString( Errors.Keys.NullArgument_1, argName + '[' + i + ']')); } owner.addDimensionId(this, identifier); } } /** * Declares the index for the dimension represented by this object. For example in a 4-D * dataset having (<var>x</var>, <var>y</var>, <var>z</var>, <var>t</var>) dimensions where * the time dimension is known to be the dimension #3 (0-based numbering), users may want to * read the (<var>x</var>, <var>y</var>) plane at time <var>t</var><sub>25</sub>. This can be * done by invoking: * * {@preformat java * addDimensionId(3); * setSliceIndex(25); * } * * {@note The <code>setSliceIndex(int)</code> method is available only if this object is * actually an instance of <code>DimensionSlice</code>.} * * @param index The index of the dimension. Must be non-negative. * @throws IllegalArgumentException If the given dimension index is negative * or already assigned to an other {@code DimensionSlice} instance. */ public void addDimensionId(final int index) throws IllegalArgumentException { if (index < 0) { throw new IllegalArgumentException(getErrorResources().getString( Errors.Keys.IllegalArgument_2, "index", index)); } owner.addDimensionId(this, index); } /** * Adds an identifier for the dimension represented by this object. The dimension is identified * by name, which works only with file formats that provide support for named dimensions (e.g. * NetCDF). For example in a dataset having either (<var>x</var>, <var>y</var>, <var>z</var>, * <var>t</var>) or (<var>x</var>, <var>y</var>, <var>t</var>) dimensions, users may want to * read the (<var>x</var>, <var>y</var>) plane at time <var>t</var><sub>25</sub> without knowing * if the time dimension is the third or the fourth one. This can be done by invoking: * * {@preformat java * addDimensionId("time"); * setSliceIndex(25); * } * * {@note The <code>setSliceIndex(int)</code> method is available only if this object is * actually an instance of <code>DimensionSlice</code>.} * * More than one name can be specified if they should be considered as possible identifiers * for the same dimension. For example in order to set the index for the <var>z</var> dimension, * it may be necessary to specify both the {@code "height"} and {@code "depth"} names. * * @param names The names of the dimension. * @throws IllegalArgumentException If a name is already assigned to an * other {@code DimensionSlice} instance. */ public void addDimensionId(final String... names) throws IllegalArgumentException { addDimensionId("names", names); } /** * Adds an identifier for the dimension represented by this object. The dimension is identified * by axis direction. For example in a dataset having either (<var>x</var>, <var>y</var>, * <var>z</var>, <var>t</var>) or (<var>x</var>, <var>y</var>, <var>t</var>) dimensions, users * may want to read the (<var>x</var>, <var>y</var>) plane at time <var>t</var><sub>25</sub> * without knowing if the time dimension is the third or the fourth one. This can be done by * invoking: * * {@preformat java * addDimensionId(AxisDirection.FUTURE); * setSliceIndex(25); * } * * {@note The <code>setSliceIndex(int)</code> method is available only if this object is * actually an instance of <code>DimensionSlice</code>.} * * More than one direction can be specified if they should be considered as possible identifiers * for the same dimension. For example in order to set the index for the <var>z</var> dimension, * it may be necessary to specify both the {@link AxisDirection#UP UP} and * {@link AxisDirection#DOWN DOWN} directions. * * @param axes The axis directions of the dimension. * @throws IllegalArgumentException If a name is already assigned to an * other {@code DimensionSlice} instance. */ public void addDimensionId(final AxisDirection... axes) throws IllegalArgumentException { addDimensionId("axes", axes); } /** * Removes identifiers for the dimension represented by this object. The {@code identifiers} * argument can contain the identifiers given to any {@code addDimensionId(...)} method. * Unknown identifiers are silently ignored. * * @param identifiers The identifiers to remove. */ public void removeDimensionId(final Object... identifiers) { owner.removeDimensionId(this, identifiers); } /** * Returns all identifiers for this dimension. The array returned by this method contains * the arguments given to any {@code addDimensionId(...)} method call on this instance. * * @return All identifiers for this dimension. */ public Object[] getDimensionIds() { final Map<Object,DimensionIdentification> identifiersMap = owner.identifiersMap(); final Object[] identifiers = new Object[identifiersMap.size()]; int count = 0; for (final Map.Entry<Object,DimensionIdentification> entry : identifiersMap.entrySet()) { if (equals(entry.getValue())) { identifiers[count++] = entry.getKey(); } } return ArraysExt.resize(identifiers, count); } /** * Returns {@code true} if this dimension contains at least one identifier. Invoking * this method is equivalent to the code below, but is potentially more efficient: * * {@preformat java * boolean hasDimensionIds = (getDimensionIds().length != 0); * } * * @return {@code true} if this dimension contains at least one identifier. */ public boolean hasDimensionIds() { return owner.identifiersMap().values().contains(this); } /** * Returns the index of this dimension in a data file. This method is invoked by some * {@link SpatialImageReader} or {@link SpatialImageWriter} subclasses when a dataset * is about to be read from a file, or written to a file. The caller needs to know the * set of dimensions that exist in the file dataset, which may not be identical to the set * of dimensions declared in {@link SpatialImageReadParam} or {@link SpatialImageWriteParam}. * <p> * The default implementation makes the following choice: * * <ol> * <li><p>If {@link #addDimensionId(int)} has been invoked, then the value specified to * that method is returned regardless the {@code properties} argument value.</p></li> * <li><p>Otherwise if an other {@link #addDimensionId(String[]) addDimensionId(...)} method * has been invoked and the {@code properties} argument is non-null, then this method * iterates over the given properties. The iteration must return exactly one element * for each dimension, in order. If an element is equals to a value specified to a * {@code addDimensionId(...)} method, then the position of that element in the * {@code properties} iteration is returned.</p></li> * <li><p>Otherwise this method returns -1.</p></li> * </ol> * * If more than one dimension match, then a {@linkplain SpatialImageReader#warningOccurred * warning is emitted} and this method returns the index of the first property matching an * identifier. * * @param properties Contains one property (the dimension name as a {@link String} or the axis * direction as an {@link AxisDirection}) for each dimension in the dataset being read * or written. The iteration order shall be the order of dimensions in the dataset. This * argument can be {@code null} if no such properties are available. * @return The index of the dimension, or -1 if none. */ public int findDimensionIndex(final Iterable<?> properties) { /* * Get all identifiers for the slice. If an explicit dimension * index is found in the process, it will be returned immediately. */ Set<Object> identifiers = null; for (final Map.Entry<Object,DimensionIdentification> entry : owner.identifiersMap().entrySet()) { if (equals(entry.getValue())) { final Object key = entry.getKey(); if (key instanceof Integer) { return (Integer) key; } if (identifiers == null) { identifiers = new HashSet<>(8); } identifiers.add(key); } } /* * No explicit dimension found. Now searches for an element from the * given iterator which would be one of the declared identifiers. */ if (properties != null && identifiers != null) { Map<Integer,Object> found = null; int position = 0; for (Object property : properties) { /* * Undocumented (for now) feature: if we have Map.Entry<?,Integer>, * the value will be the dimension. This allow us to pass more than * one property per dimension. */ if (property instanceof Map.Entry<?,?>) { final Map.Entry<?,?> entry = (Map.Entry<?,?>) property; property = entry.getKey(); position = (Integer) entry.getValue(); } if (identifiers.contains(property)) { if (found == null) { found = new LinkedHashMap<>(4); } final Object old = found.put(position, property); if (old != null) { found.put(position, old); // Keep the first value. } } position++; } final Integer dimension = DimensionSet.first(found, this, DimensionIdentification.class, "findDimensionIndex"); if (dimension != null) { return dimension; } } return -1; } /** * Returns the locale used for formatting error messages, or {@code null} if none. */ @Override public Locale getLocale() { return owner.getLocale(); } /** * Invoked when a warning occurred. The default implementation * {@linkplain SpatialImageReader#warningOccurred forwards the warning to the image reader} or * {@linkplain SpatialImageWriter#warningOccurred writer} if possible, or logs the warning * otherwise. */ @Override public boolean warningOccurred(final LogRecord record) { return Warnings.log(this, record); } /** * Returns a string representation of this object. The default implementation * formats on a single line the class name and the list of dimension identifiers. * * @see SpatialImageReadParam#toString() */ @Override public String toString() { return toStringBuilder().append("}]").toString(); } /** * Partial implementation of the {@link #toString()} method, to be leveraged by * the {@link DimensionSlice} subclass. */ final StringBuilder toStringBuilder() { final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this)).append("[id={"); boolean addSeparator = false; for (final Map.Entry<Object,DimensionIdentification> entry : owner.identifiersMap().entrySet()) { if (entry.getValue() == this) { Object key = entry.getKey(); final boolean addQuotes = (key instanceof CharSequence); if (key instanceof CodeList<?>) { key = ((CodeList<?>) key).name(); } if (addSeparator) { buffer.append(", "); } if (addQuotes) { buffer.append('"'); } buffer.append(key); if (addQuotes) { buffer.append('"'); } addSeparator = true; } } return buffer; } }