/* * 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.Iterator; import java.util.Collections; import java.util.AbstractSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.NoSuchElementException; import java.util.Locale; import java.util.logging.LogRecord; import javax.imageio.IIOParam; import org.opengis.referencing.cs.AxisDirection; import org.geotoolkit.resources.Errors; import org.apache.sis.util.Localized; import org.apache.sis.util.NullArgumentException; import org.geotoolkit.util.collection.XCollections; import org.geotoolkit.internal.image.io.Warnings; import static org.geotoolkit.image.io.DimensionSlice.API; import static org.apache.sis.util.collection.Containers.isNullOrEmpty; import static org.geotoolkit.util.collection.XCollections.unmodifiableOrCopy; /** * The set of {@link DimensionIdentification} instances managed by a given * {@link MultidimensionalImageStore} instance. This class is provided for * {@code MultidimensionalImageStore} implementations and usually don't need * to be accessed directly by users. * <p> * The code snippet below gives an example of {@code ImageReader} implementation * using a {@code DimensionSet}: * * {@preformat java * public class MyReader extends SpatialImageReader implements MultidimensionalImageStore { * private final DimensionSet dimensionsForAPI; * * public SpatialImageReader(Spi provider) { * super(provider); * dimensionsForAPI = new DimensionSet(this); * } * * public DimensionIdentification getDimensionForAPI(DimensionSlice.API api) { * return dimensionsForAPI.getOrCreate(api); * } * * public DimensionSlice.API getAPIForDimension(Object... identifiers) { * return dimensionsForAPI.getAPI(identifiers); * } * * public Set<DimensionSlice.API> getAPIForDimensions() { * return dimensionsForAPI.getAPIs(); * } * * public BufferedImage read(int imageIndex, ImageReadParam param) { * DimensionIdentification bandsDimension = dimensionsForAPI.get(DimensionSlice.API.BANDS); * if (bandsDimension != null) { * Collection<?> propertiesOfAxes = ...; // This is plugin-specific. * int index = bandsDimension.findDimensionIndex(propertiesOfAxes); * if (index >= 0) { * // We have found the dimension index of bands. * } * } * // Continue the process of reading the image... * } * } * } * * {@code DimensionSet} is not modifiable through the {@link Set} interface, since it doesn't * support the {@link Set#add add} method. However new elements can be created by calls to the * {@link #getOrCreate(DimensionSlice.API)} method. * * @author Martin Desruisseaux (Geomatys) * @version 3.15 * * @see MultidimensionalImageStore * @see DimensionIdentification * * @since 3.15 (derived from 3.08) * @module */ public class DimensionSet extends AbstractSet<DimensionIdentification> implements WarningProducer { /** * The {@link MultidimensionalImageStore} or {@link IIOParam} instance that created this * set, or {@code null} if none. This is used for implementation of {@link #getLocale()}. * The {@link DimensionSlice} class also uses this reference for fetching (indirectly) * information from the image store. */ final Object owner; /** * The identifiers assigned to {@link DimensionIdentification} instances. Keys are * identifiers as {@link Integer} (dimension index), {@link String} (dimension names) * or {@link AxisDirection} (dimension directions). Many keys can be defined for the * same dimension value. * <p> * This map is created only when first needed. */ private Map<Object,DimensionIdentification> identifiersMap; /** * For <var>n</var>-dimensional images, the standard Java API to use for setting the index. * Will be created only when first needed. The length of its internal array shall be equals * to the length of the {@link DimensionSlice.API#VALIDS} array. */ APIs apiMapping; /** * The dimensions set, or {@code null} if not yet computed. */ private transient Set<DimensionIdentification> dimensions; /** * Creates a new {@code DimensionSet} instance for the given image reader or writer. * * @param store The image reader or writer for which this instance is created, or {@code null}. */ public DimensionSet(final MultidimensionalImageStore store) { owner = store; } /** * Creates a new {@code DimensionSet} instance for the given parameters. This constructor * is not public because {@link SpatialImageReadParam} and {@link SpatialImageWriteParam} * leverage this class in an opportunist but undocumented way. * * @param owner The parameters that created this object. */ DimensionSet(final IIOParam parameters) { owner = parameters; } /** * Clears any setting in this object. After this method call, the state of this * object is than same than after construction. */ @Override public void clear() { XCollections.clear(identifiersMap); dimensions = null; } /** * Returns the identifiers assigned to {@link DimensionIdentification} instances. * This map should be considered read-only; callers are not allowed to change anything. * <p> * Keys are identifiers as {@link Integer} (dimension index), {@link String} * (dimension names) or {@link AxisDirection} (dimension directions). Many * keys can be defined for the same dimension. * * @return The identifiers mapping, or en empty map if none. */ final Map<Object,DimensionIdentification> identifiersMap() { return (identifiersMap != null) ? identifiersMap : Collections.<Object,DimensionIdentification>emptyMap(); } /** * Returns {@code true} if this set doesn't contains any dimension. */ @Override public boolean isEmpty() { return isNullOrEmpty(identifiersMap); } /** * Returns the number of dimensions contained in this set. */ @Override public int size() { return dimensions().size(); } /** * Returns an iterator over all dimensions contained in this set. */ @Override public Iterator<DimensionIdentification> iterator() { return dimensions().iterator(); } /** * Returns {@code true} if this set contains the given {@link DimensionIdentification} instance. * * @param value The {@link DimensionIdentification} to test for inclusion. * @return {@code true} if this set contains the given dimension. */ @Override public boolean contains(final Object value) { return dimensions().contains(value); } /** * Returns all dimensions contained in this set. The returned set is unmodifiable * in order to ensure that {@link #iterator()} does not support element removal. * A new set will need to be created if the {@link #identifiersMap} change. */ private Set<DimensionIdentification> dimensions() { if (dimensions == null) { if (identifiersMap != null) { dimensions = unmodifiableOrCopy(new LinkedHashSet<>(identifiersMap.values())); } else { dimensions = Collections.emptySet(); } } return dimensions; } /** * Returns the dimension which has been assigned to the given API, or {@code null} if none. * <p> * This method is typically invoked by {@link SpatialImageReader} implementations together * with {@link DimensionIdentification#findDimensionIndex(Iterable)} in order to locate the * index of the dimension to read as bands. See <cite>Assigning a third dimension to bands</cite> * in the {@link MultidimensionalImageStore} class javadoc. * * @param api The API for which to test if a dimension slice has been assigned. * @return The dimension slice assigned to the given API, or {@code null} if none. */ public DimensionIdentification get(final API api) { if (api == null) { throw new NullArgumentException(Warnings.message(this, Errors.Keys.NullArgument_1, "api")); } if (api != API.NONE && apiMapping != null) { return apiMapping.dimensions[api.ordinal()]; } return null; } /** * Returns the dimension assigned to the given API. If a dimension has been previously created * for the given API, it is returned. Otherwise a new dimension is created and returned. * <p> * In order to check if a dimension exists without creating a new one, use the * {@link #get(DimensionSlice.API)} method instead. * * @param api The API for which to return a dimension. * @return The dimension assigned to the given API. */ public DimensionIdentification getOrCreate(final API api) { DimensionIdentification dimension = get(api); if (dimension == null) { dimension = new DimensionIdentification(this, api); } return dimension; } /** * Returns the API assigned to the given dimension identifiers. If more than one dimension * is found for the given identifiers, then a {@linkplain #warningOccurred warning is emitted} * and this method returns the first dimension matching the given identifiers. If no dimension * is found, {@code null} is returned. * * @param identifiers The identifiers of the dimension to query. * @return The API assigned to the given dimension, or {@link API#NONE} if none. */ public API getAPI(Object... identifiers) { if (apiMapping != null) { final DimensionIdentification dimension = find(DimensionSet.class, "getAPI", identifiers); if (dimension != null) { final DimensionIdentification[] dimensions = apiMapping.dimensions; for (int i=dimensions.length; --i>=0;) { if (dimensions[i] == dimension) { return API.VALIDS[i]; } } } } return API.NONE; } /** * Returns the set of APIs for which at least one dimension has identifiers. This * is typically an empty set until the following code is invoked at least once: * * {@preformat java * getOrCreate(...).addDimensionId(...); * } * * @return The API for which at least one dimension has identifiers. */ public Set<API> getAPIs() { if (apiMapping == null) { apiMapping = new APIs(); } return apiMapping; } /** * Returns the dimension mapped to Image I/O API. */ final DimensionIdentification[] apiMapping() { if (apiMapping == null) { apiMapping = new APIs(); } return apiMapping.dimensions; } /** * The set of API for which at least one dimension has been assigned an identifier. */ private static final class APIs extends AbstractSet<API> { /** The standard Java API to use for setting the index. */ final DimensionIdentification[] dimensions; /** Creates a new initially empty set. */ APIs() { dimensions = new DimensionIdentification[DimensionSlice.API.VALIDS.length]; } /** Returns the number of elements in this set. */ @Override public int size() { int count = 0; for (final Iterator<API> it=iterator(); it.hasNext();) { count++; } return count; } /** Checks if this set contains the given API. */ @Override public boolean contains(final Object api) { if (api instanceof DimensionSlice.API) { final DimensionIdentification dimension = dimensions[((DimensionSlice.API) api).ordinal()]; if (dimension != null) { return dimension.hasDimensionIds(); } } return false; } /** Returns an iterator over the elements. */ @Override public Iterator<API> iterator() { return new Iter(dimensions); } } /** * The iterator over {@link APIs} elements. */ private static final class Iter implements Iterator<API> { /** A direct reference to the {@link APIs#apiMapping} array. */ private final DimensionIdentification[] dimensions; /** Index of the next element to scan. */ private int index; /** The next element to return, or {@code null} if none. */ private API next; /** Creates a new iterator. */ Iter(final DimensionIdentification[] apiMapping) { this.dimensions = apiMapping; search(); } /** Searches for the next element. */ private void search() { while (index < dimensions.length) { final int i = index++; final DimensionIdentification dimension = dimensions[i]; if (dimension != null && dimension.hasDimensionIds()) { next = DimensionSlice.API.VALIDS[i]; return; // Found the next element. } } next = null; // No next element found. } /** Returns {@code true} if there is more elements to iterator. */ @Override public boolean hasNext() { return (next != null); } /** Returns the next element in this iteration. */ @Override public API next() { final API element = next; if (element == null) { throw new NoSuchElementException(); } search(); return element; } /** Unsupported since the set is not modifiable. */ @Override public void remove() { throw new UnsupportedOperationException(); } } /** * Returns the locale used for formatting error messages, or {@code null} if none. * The default implementation delegates to the store given at construction time, * if it implements the {@link Localized} interface. */ @Override public Locale getLocale() { return (owner instanceof Localized) ? ((Localized) owner).getLocale() : null; } /** * Invoked when a warning occurred. The default implementation forwards the warning to * the store given at construction time if possible, or logs the warning otherwise. */ @Override public boolean warningOccurred(final LogRecord record) { return Warnings.log(owner, record); } /** * Adds an identifier for the dimension represented by the given {@code DimensionIdentification}. * * @param dimension The dimension for which to add identifiers. * @param identifier The identifier to add. * @throws IllegalArgumentException If the given identifier is already assigned * to an other {@code DimensionIdentification} instance. */ final void addDimensionId(final DimensionIdentification dimension, final Object identifier) throws IllegalArgumentException { if (identifiersMap == null) { identifiersMap = new LinkedHashMap<>(); } final DimensionIdentification old = identifiersMap.put(identifier, dimension); if (old != null && !old.equals(dimension)) { identifiersMap.put(identifier, old); // Restore the previous value. throw new IllegalArgumentException(Errors.getResources(getLocale()) .getString(Errors.Keys.ValueAlreadyDefined_1, identifier)); } dimensions = null; // Will need to be recomputed. } /** * Removes identifiers for the given dimension. The {@code identifiers} argument can contain * the identifiers given to any {@code addDimensionId(...)} method. Unknown identifiers are * silently ignored. * * @param dimension The dimension from which to remove identifiers. * @param identifiers The identifiers to remove. */ final void removeDimensionId(final DimensionIdentification dimension, final Object[] identifiers) { if (identifiersMap != null) { for (final Object identifier : identifiers) { final DimensionIdentification old = identifiersMap.remove(identifier); if (old != null && !old.equals(dimension)) { identifiersMap.put(identifier, old); // Restore the previous state. } } dimensions = null; // Will need to be recomputed. } } /** * Returns the index of the slice in the dimension identified by at least one of the given * identifiers. This method is equivalent to the code below, except that a warning is emitted * only if index values are ambiguous: * * {@preformat java * DimensionSlice slice = getDimensionSlice(callerClass, dimensionIds); * return (slice != null) ? slice.getSliceIndex() : 0; * } * * This method is used for {@link SpatialImageReadParam} and {@link SpatialImageWriteParam} * implementations only. In this case, all {@link #identifiersMap} values shall be instances * of {@link DimensionSlice}. * * @param callerClass The class which is invoking this method, used for logging purpose. * @param dimensionIds {@link Integer}, {@link String} or {@link AxisDirection} * that identify the dimension for which the slice is desired. * @return The index set in the first slice found for the given dimension identifiers, * or 0 if none. */ final int getSliceIndex(final Class<? extends WarningProducer> callerClass, final Object[] dimensionIds) { if (identifiersMap != null) { Map<Integer,Object> found = null; for (final Object id : dimensionIds) { final DimensionSlice source = (DimensionSlice) identifiersMap.get(id); if (source != null) { final Integer index = source.getSliceIndex(); if (found == null) { found = new LinkedHashMap<>(4); } final Object old = found.put(index, id); if (old != null) { found.put(index, old); // Keep the old value. } } } final Integer index = first(found, this, callerClass, "getSliceIndex"); if (index != null) { return index; } } return 0; } /** * Returns the dimension slice identified by at least one of the given identifiers. The * dimension can be identified by a zero-based index as an {@link Integer}, a dimension * name as a {@link String}, or an axis direction as an {@link AxisDirection}. More than * one identifier can be specified in order to increase the chance to get the index. * <p> * This method is used for {@link SpatialImageReadParam} and {@link SpatialImageWriteParam} * implementations only. In this case, all {@link #identifiersMap} values shall be instances * of {@link DimensionSlice}. * * @param callerClass The class which is invoking this method, used for logging purpose. * @param dimensionIds {@link Integer}, {@link String} or {@link AxisDirection} * that identify the dimension for which the slice is desired. * @return The first slice found for the given dimension identifiers, or {@code null} if none. */ final DimensionIdentification getDimensionSlice( final Class<? extends WarningProducer> callerClass, final Object[] dimensionIds) { return find(callerClass, "getDimensionSlice", dimensionIds); } /** * Returns the dimension identified by at least one of the given identifier. * * @param caller The object which is invoking this method (for logging purpose). * @param callerClass The class which is invoking this method (for logging purpose). * @param callerMethod The method which is invoking this method (for logging purpose). * @param dimensionIds The integers, strings or axis directions identifying a dimension. * @return The first dimension found for the given identifiers, or {@code null} if none. */ private DimensionIdentification find( final Class<? extends WarningProducer> callerClass, final String callerMethod, final Object[] dimensionIds) { if (identifiersMap != null) { Map<DimensionIdentification,Object> found = null; for (final Object id : dimensionIds) { final DimensionIdentification slice = identifiersMap.get(id); if (slice != null) { if (found == null) { found = new LinkedHashMap<>(4); } final Object old = found.put(slice, id); if (old != null) { found.put(slice, old); // Keep the old value. } } } final DimensionIdentification slice = first(found, this, callerClass, callerMethod); if (slice != null) { return slice; } } return null; } /** * Returns the first key in the given map. If the map has more than one entry, * a warning is emitted. If the map is empty, {@code null} is returned. This * method is used for determining the {@link DimensionIdentification} instance to use * after we have iterated over the properties of all axes in a coordinate system. * * @param <T> Either {@link Integer} or {@link API}. * @param found The map from which to extract the first key. * @param caller The instance which is invoking this method. * @param callerClass The class which is invoking this method, used for logging purpose. * @param methodName The method which is invoking this method, used for logging purpose. * @return The first key in the given map, or {@code null} if none. */ static <T> T first(final Map<T,?> found, final WarningProducer caller, final Class<? extends WarningProducer> callerClass, final String methodName) { if (found != null) { final int size = found.size(); if (size != 0) { /* * At least one (source, property) pair has been found. We will return the * index. However if we found more than one pair, we have an ambiguity. In * the later case, we will log a warning before to return the first index. */ if (size > 1) { final StringBuilder buffer = new StringBuilder(); for (final Object value : found.values()) { if (buffer.length() != 0) { buffer.append(" | "); } buffer.append(value); } String message = Warnings.message(caller, Errors.Keys.AmbiguousValue_1, buffer); buffer.setLength(0); buffer.append(message); for (final T source : found.keySet()) { if (buffer.length() != 0) { buffer.append(','); } buffer.append(' ').append(source); } message = buffer.toString(); Warnings.log(caller, null, callerClass, methodName, message); } return found.keySet().iterator().next(); } } return null; } }