/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2007-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; import java.awt.Point; import java.util.Set; import java.util.EnumSet; import java.util.List; import java.util.Locale; import java.util.logging.LogRecord; import java.awt.Rectangle; import java.awt.image.IndexColorModel; import javax.imageio.IIOParam; import javax.imageio.ImageReader; import javax.imageio.ImageReadParam; import org.opengis.referencing.cs.AxisDirection; import org.geotoolkit.resources.Errors; import org.apache.sis.util.resources.IndexedResourceBundle; import org.geotoolkit.image.io.metadata.SampleDomain; import org.geotoolkit.internal.image.io.Warnings; import org.apache.sis.internal.util.UnmodifiableArrayList; import org.apache.sis.util.Classes; import static org.apache.sis.util.collection.Containers.isNullOrEmpty; import org.geotoolkit.image.palette.PaletteFactory; /** * Default parameters for {@link SpatialImageReader}. This class extends the standard * {@link ImageReadParam} class with the following additional capabilities: * <p> * <ul> * <li><p>Specify the plane to read in datasets having more than 2 dimensions, as for example * in {@linkplain org.geotoolkit.image.io.plugin.NetcdfImageReader NetCDF} files.</p></li> * * <li><p>Specify the name of a {@linkplain Palette color palette}. This is useful when * reading an image from a file that doesn't contain such information, for example * {@linkplain org.geotoolkit.image.io.plugin.AsciiGridReader ASCII grid}.</p></li> * * <li><p>For images having more than one band where the bands are <strong>not</strong> color * components, specify which band to use with the {@linkplain IndexColorModel Index * Color Model}. For example an image may contain <cite>Sea Surface Temperature</cite> (SST) * measurements in the first band, and an estimation of the measurement errors in the second * band. Users may want to read both bands for computation purpose, while applying a color * palette using only the values in the first band.</p></li> * </ul> * * {@section Handling more than two dimensions} * Some file formats like NetCDF can store dataset having more than two dimensions. * Geotk handles the supplemental dimensions with the following policies by default: * * <ol> * <li><p>The two first dimensions - typically named (<var>x</var>, <var>y</var>) - are * assigned to the (<var>columns</var>, <var>rows</var>) pixel indices.</p></li> * * <li><p>An additional dimension can optionally be assigned to band indices. This is typically the * altitude (<var>z</var>) in a dataset having the (<var>x</var>, <var>y</var>, <var>z</var>, * <var>t</var>) dimensions, but can be customized. The actual dimension assigned to band * indices is returned by {@link DimensionSlice#findDimensionIndex(Iterable)}. See * {@link MultidimensionalImageStore} for more information.</p></li> * * <li><p>An additional dimension can optionally be assigned to image index. This is typically * the time (<var>t</var>) in a dataset having the (<var>x</var>, <var>y</var>, <var>z</var>, * <var>t</var>) dimensions, but can be customized. See * {@link MultidimensionalImageStore} for more information.</p></li> * * <li><p>Only one slice of every supplemental dimensions can be read. By default the data at index * 0 are loaded, but different indices can be selected (see {@link DimensionSlice}). The actual * index used is the value returned by {@link #getSliceIndex(Object[])}.</p></li> * </ol> * * @author Martin Desruisseaux (Geomatys) * @version 3.20 * * @since 3.05 (derived from 2.4) * @module */ public class SpatialImageReadParam extends ImageReadParam implements WarningProducer { /** * The name of the default color palette to apply when none was explicitly specified. * The default palette is {@value}. * * @see #getPaletteName() * @see #setPaletteName(String) */ public static final String DEFAULT_PALETTE_NAME = "grayscale"; /** * The set of {@link DimensionSlice} instances, which contains also the * implementation of public API exposed in {@code SpatialImageReadParam}. * * @since 3.08 */ private DimensionSet dimensionSlices; /** * The name of the color palette. */ private String paletteName; /** * The factory for creating a palette from the name. */ private PaletteFactory paletteFactory; /** * The band to display. */ private int visibleBand; /** * The range of valid values and the fill values for each band to be read, * or {@code null} if unspecified. * * @since 3.12 */ private List<SampleDomain> sampleDomains; /** * The kind of sample conversions which are allowed, or {@code null} if none. */ private Set<SampleConversionType> allowedConversions; /** * The image reader for which this {@code SpatialImageReadParam} instance * has been created, or {@code null} if unknown. * * @since 3.15 */ protected final ImageReader reader; /** * Creates a new, initially empty, set of parameters. * * @param reader The reader for which this parameter block is created, or {@code null}. */ public SpatialImageReadParam(final ImageReader reader) { this.reader = reader; } /** * Returns the resources for formatting error messages. */ private IndexedResourceBundle getErrorResources() { return Errors.getResources(getLocale()); } /** * Returns {@code true} if the parameters contain at least one {@link DimensionSlice}. * Invoking this method is equivalent to testing * <code>!{@linkplain #getDimensionSlices()}.isEmpty()</code>. * * @return If the parameters contain at least one dimension slice. * * @since 3.21 */ public boolean hasDimensionSlices() { return (dimensionSlices != null) && !dimensionSlices.isEmpty(); } /** * Creates a new handler for selecting a slice of the (<var>x</var>, <var>y</var>) plane to * read. This is relevant only for <var>n</var>-dimensional dataset where <var>n</var>>2. * The caller should invoke one or many {@link DimensionSlice#addDimensionId(int) * addDimensionId(...)} methods for specifying the dimension, and invoke * {@link DimensionSlice#setSliceIndex(int)} for specifying the index of the * <cite>slice point</cite> (WMS 2.0 terminology). * <p> * A new handler can be created for each supplemental dimension above the two (<var>x</var>, * <var>y</var>) dimensions and the bands handled by Java2D. If no slice is specified for a * supplemental dimension, then the default is the slice at index 0. * * @return A new handler for specifying the index of the slice to read in a supplemental * dimension. * * @since 3.08 */ public DimensionSlice newDimensionSlice() { if (dimensionSlices == null) { dimensionSlices = new DimensionSet(this); } return new DimensionSlice(dimensionSlices); } /** * Returns all {@code DimensionSlice} instances known to this parameters block. They are * the instances created by {@link #newDimensionSlice()} and for which at least one * {@linkplain DimensionSlice#addDimensionId(String[]) identifier has been added}. * <p> * The returned collection is <cite>live</cite>: if {@linkplain #newDimensionSlice() new * dimension slices} are created, they will appear dynamically in the returned set. * * @return The dimensions registered in this parameters, or an empty set if none. * * @since 3.08 */ @SuppressWarnings({"unchecked","rawtypes"}) public Set<DimensionSlice> getDimensionSlices() { if (dimensionSlices == null) { dimensionSlices = new DimensionSet(this); } return (Set) dimensionSlices; } /** * Returns the dimension slice identified by at least one of the given identifiers. This is * relevant mostly for <var>n</var>-dimensional dataset where <var>n</var>>2. 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, for example: * * {@preformat java * DimensionSlice slice = getDimensionSlice("time", AxisDirection.FUTURE); * } * * If different slices are found for the given identifiers, then a * {@linkplain SpatialImageReader#warningOccurred warning is emitted} * and this method returns the first slice matching the given identifiers. * If no slice is found, {@code null} is returned. * * @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. * * @since 3.08 */ public DimensionSlice getDimensionSlice(final Object... dimensionIds) { return (dimensionSlices != null) ? (DimensionSlice) dimensionSlices.getDimensionSlice( SpatialImageReadParam.class, dimensionIds) : null; } /** * 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(dimensionIds); * return (slice != null) ? slice.getSliceIndex() : 0; * } * * This method is relevant mostly for <var>n</var>-dimensional dataset where <var>n</var>>2. * 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, for example: * * {@preformat java * int index = getSliceIndex("time", AxisDirection.FUTURE); * } * * If different indices are found for the given identifiers, then a * {@linkplain SpatialImageReader#warningOccurred warning is emitted} * and this method returns the index for the first slice matching the given identifiers. * If no index is found, 0 (which is the default index value) is returned. * * @param dimensionIds {@link Integer}, {@link String} or {@link AxisDirection} * that identify the dimension for which the index is desired. * @return The index set in the first slice found for the given dimension identifiers, * or 0 if none. * * @see DimensionSlice#getSliceIndex() * * @since 3.08 */ public int getSliceIndex(final Object... dimensionIds) { return (dimensionSlices != null) ? dimensionSlices.getSliceIndex( SpatialImageReadParam.class, dimensionIds) : 0; } /** * Ensures that the specified band number is valid. */ private void ensureValidBand(final int band) throws IllegalArgumentException { if (band < 0) { throw new IllegalArgumentException(getErrorResources().getString( Errors.Keys.IllegalBandNumber_1, band)); } } /** * Returns the band to display in the target image. In theory, images backed by * {@linkplain java.awt.image.IndexColorModel index color model} should have only * one band. But sometime we want to load additional bands as numerical data, in * order to perform computations. In such case, we need to specify which band in * the destination image will be used as an index for displaying the colors. The * default value is 0. * * @return The band to display in the target image. */ public int getVisibleBand() { return visibleBand; } /** * Sets the band to make visible in the destination image. * * @param visibleBand The band to make visible. * @throws IllegalArgumentException if the specified band index is invalid. */ public void setVisibleBand(final int visibleBand) throws IllegalArgumentException { ensureValidBand(visibleBand); this.visibleBand = visibleBand; } /** * Returns a name of the color palette, or a {@linkplain #DEFAULT_PALETTE_NAME default name} * if none were explicitly specified. */ final String getNonNullPaletteName() { final String palette = getPaletteName(); return (palette != null) ? palette : DEFAULT_PALETTE_NAME; } /** * Returns the name of the color palette to apply when creating an * {@linkplain IndexColorModel index color model}. * This is the name specified by the last call to {@link #setPaletteName(String)}. * <p> * For a table of available palette names in the default Geotk installation, * see the {@link PaletteFactory} class javadoc. * * @return The name of the color palette to apply, or {@code null} if none. * * @see SpatialImageReader#hasColors(int) */ public String getPaletteName() { return paletteName; } /** * Sets the color palette as one of the {@linkplain PaletteFactory#getAvailableNames available * names} provided by the {@linkplain PaletteFactory#getDefault default palette factory}. This * name will be given by the {@link SpatialImageReader} default implementation to the * {@linkplain PaletteFactory#getDefault default palette factory} for creating a * {@linkplain javax.imageio.ImageTypeSpecifier image type specifier}. * <p> * <b>Note:</b> This method is useful with image formats that don't store any color information * in the file. If the image format provides its own color palette (as in PNG of JPEG formats), * then the palette name given to this method may be ignored. The * {@link SpatialImageReader#hasColors(int)} method can be invoked in order to check if the * image file provides its own color palette. * <p> * For a table of available palette names in the default Geotk installation, * see the {@link PaletteFactory} class javadoc. * * @param palette The name of the color palette to apply. * * @see SpatialImageReader#hasColors(int) * @see PaletteFactory#getAvailableNames() */ public void setPaletteName(final String palette) { this.paletteName = palette; } /** * Returns the palette factory to use for creating a color model from the * {@linkplain #getPaletteName() palette name}. This method is invoked by * the {@link SpatialImageReader#getImageType(int, ImageReadParam, SampleConverter[]) * SpatialImageReader.getImageType} method after it has determined the range of sample * values from the {@linkplain SpatialImageReader#getImageMetadata(int) image metadata}. * The returned factory may be used in various way, but the following pseudo-code can be * considered typical: * * {@preformat java * public ImageTypeSpecifier getImageType(int imageIndex, ImageReadParam param, ...) { * SpatialMetadata md = getImageMetadata(imageIndex); * // ... process metadata * return param.getPaletteFactory().getPalette(param.getPaletteName(), * lower, upper, size, numBands, visibleBand); * } * } * * @return The palette factory that the {@link SpatialImageReader#getImageType(int, * ImageReadParam, SampleConverter[]) SpatialImageReader.getImageType} method * shall use for creating a color model from the {@linkplain #getPaletteName() * palette name}, or {@code null} for the {@linkplain PaletteFactory#getDefault() * default factory}. * * @since 3.11 */ public PaletteFactory getPaletteFactory() { return paletteFactory; } /** * Sets the palette factory to use for creating a color model from the * {@linkplain #getPaletteName() palette name}. * * @param factory The new factory, or {@code null} for the * {@linkplain PaletteFactory#getDefault() default factory}. * * @since 3.11 */ public void setPaletteFactory(final PaletteFactory factory) { paletteFactory = factory; } /** * Returns the range of valid values together with the fill values, or {@code null} if * unspecified. If non-null, then {@link SpatialImageReader} will use this information * for creating the image color model as documented in the {@link #getPaletteFactory()} * method. * <p> * This property is typically {@code null}, either because the color model is specified * by the image format, or because the range of valid values is extracted from the * {@linkplain SpatialImageReader#getImageMetadata(int) image metadata}. * <p> * If non-null, then the size of this list shall be equals to the number of * {@linkplain #getSourceBands() source bands}. The {@code SampleDomain} at * index <var>i</var> is for the band at index {@code sourceBands[i]} in the * stream. * * @return The range of valid values and the fill values for each band to be read, * or {@code null} if unspecified. * * @since 3.12 */ public List<SampleDomain> getSampleDomains() { return sampleDomains; } /** * Sets the range of valid values and fill values to use for creating a color model. It is * typically not necessary to set this property, since those information are extracted from * the {@linkplain SpatialImageReader#getImageMetadata(int) image metadata} by default. * However it may be useful to set the range and the fill values explicitly if the image * to be read is known to have missing or incomplete metadata. This is the case for example * of NetCDF files not conform to the <a href="http://www.cfconventions.org">CF conventions</a>. * <p> * If this method is invoked with a non-null value, then the size of the given list * shall be equals to the number of {@linkplain #getSourceBands() source bands}. The * {@code SampleDomain} at index <var>i</var> is for the band at index {@code sourceBands[i]} * in the stream. The range and fill values in the given {@code SampleDomain}s while have * precedence over any value declared in the image metadata. * * @param domains The range of valid values and the fill values for each band to be read, * or {@code null} for letting the reader infers them from the image metadata. * * @since 3.12 */ public void setSampleDomains(List<SampleDomain> domains) { if (domains != null && !(domains instanceof UnmodifiableArrayList<?>)) { domains = UnmodifiableArrayList.wrap(domains.toArray(new SampleDomain[domains.size()])); } sampleDomains = domains; } /** * Returns {@code true} if the given kind of sample conversions is allowed. By default, newly * constructed {@code SpatialImageReadParam} instances return {@code false} for any given type * (i.e. {@link SpatialImageReader} will make its best effort for storing the sample values * with no change). However more efficient storage can be achieved if some changes are allowed * on the sample values. See {@link #setSampleConversionAllowed setSampleConversionAllowed} for examples. * * @param type The kind of conversion. * @return Whatever the given kind of conversion is allowed. * * @since 3.11 */ public boolean isSampleConversionAllowed(final SampleConversionType type) { return (allowedConversions != null) && allowedConversions.contains(type); } /** * Sets whatever the given kind of sample conversions is allowed. By default, the {@code false} * value is assigned to all conversion types (i.e. {@link SpatialImageReader} will make its best * effort for storing the sample values with no change). However more efficient storage can be * achieved if some changes are allowed on the sample values, for example * {@linkplain SampleConversionType#SHIFT_SIGNED_INTEGERS adding an offset to signed integers} * in order to ensure that all values are positive. * * @param type The kind of conversion. * @param allowed Whatever the given kind of conversion is allowed. * * @since 3.11 */ public void setSampleConversionAllowed(final SampleConversionType type, final boolean allowed) { if (allowed) { if (allowedConversions == null) { allowedConversions = EnumSet.noneOf(SampleConversionType.class); } allowedConversions.add(type); } else if (allowedConversions != null) { allowedConversions.remove(type); } } /** * Returns the locale used for formatting error messages, or {@code null} if none. * The default implementation returns the locale used by the {@link ImageReader} * given at construction time, or {@code null} if none. */ @Override public Locale getLocale() { return (reader != null) ? reader.getLocale() : null; } /** * Invoked when a warning occurred. The default implementation * {@linkplain SpatialImageReader#warningOccurred forwards the warning to the image reader} * given at construction time if possible, or logs the warning otherwise. * * @since 3.08 */ @Override public boolean warningOccurred(final LogRecord record) { return Warnings.log(reader, record); } /** * Builds the first part of the string representation of the given parameters. * The closing bracket is missing from the buffer, in order to allow callers * to add more elements. */ static StringBuilder toStringBegining(final IIOParam param) { final Rectangle sourceRegion = param.getSourceRegion(); final Point destinationOffset = param.getDestinationOffset(); final int sourceXSubsampling = param.getSourceXSubsampling(); final int sourceYSubsampling = param.getSourceYSubsampling(); final int[] sourceBands = param.getSourceBands(); final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(param)); buffer.append('['); if (sourceRegion != null) { buffer.append("sourceRegion=(").append(sourceRegion.x).append(',').append(sourceRegion.y) .append(" : ").append(sourceRegion.width).append(',').append(sourceRegion.height) .append("), "); } if (sourceXSubsampling != 1 || sourceYSubsampling != 1) { buffer.append("sourceSubsampling=(").append(sourceXSubsampling).append(','). append(sourceYSubsampling).append("), "); } if (sourceBands != null) { buffer.append("sourceBands={"); for (int i=0; i<sourceBands.length; i++) { if (i != 0) { buffer.append(','); } buffer.append(sourceBands[i]); } buffer.append("}, "); } if (destinationOffset != null && (destinationOffset.x != 0 || destinationOffset.y != 0)) { buffer.append("destinationOffset=(").append(destinationOffset.x) .append(',').append(destinationOffset.y).append("), "); } return buffer; } /** * Returns a string representation of this block of parameters. The default implementation * formats the {@linkplain #sourceRegion source region}, subsampling values, * {@linkplain #sourceBands source bands}, {@linkplain #destinationOffset destination offset} * and the color palette on a single line, completed by the list of * {@linkplain DimensionSlice dimension slices} (if any) on the next lines. */ @Override public String toString() { final StringBuilder buffer = toStringBegining(this); if (paletteName != null) { buffer.append("palette=\"").append(paletteName).append('"'); } return toStringEnd(buffer, dimensionSlices); } /** * Completes the string representation with the list of dimension slices. If the last character * in the given buffer is a space, then this method removes the two last characters on the * assumption that they are {@code ", "}. Then the closing {@code ']'} character is appended. */ static String toStringEnd(final StringBuilder buffer, final Set<DimensionIdentification> identifiers) { final int length = buffer.length(); if (buffer.charAt(length - 1) == ' ') { buffer.setLength(length - 2); } buffer.append(']'); if (!isNullOrEmpty(identifiers)) { int last = 0; for (final DimensionIdentification slice : identifiers) { last = buffer.append("\n\u00A0\u00A0\u251C\u2500\u00A0").length(); buffer.append(slice); } if (last != 0) { buffer.append('\n').setCharAt(last - 3, '\u2514'); } } return buffer.toString(); } }