/*
* 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.awt.Point;
import java.awt.Rectangle;
import javax.imageio.IIOParam;
import javax.imageio.ImageWriteParam;
/**
* Tuple of a <cite>dimension identifier</cite> and <cite>index</cite> in that dimension for a slice
* to read or write in a data file. This class is relevant mostly for <var>n</var>-dimensional datasets
* where <var>n</var>>2. Each {@code DimensionSlice} instance applies to only one dimension;
* if the indices of a slice need to be specified for more than one dimension, then many instances
* of {@code DimensionSlice} will be required.
*
* {@note The <code>DimensionSlice</code> name is used in the WCS 2.0 specification for the same
* purpose. The semantic of attributes are similar but not identical: the <code>getDimensionIds()</code>
* method in this class is equivalent to the <code>dimension</code> attribute in WCS 2.0, and the
* <code>getSliceIndex()</code> method is close to the <code>slicePoint</code> attribute. The main
* differences compared to WCS 2.0 are:
* <p>
* <ul>
* <li>The dimension can be identified by an index type, by an axis direction or by the name
* type as used in WCS 2.0.</li>
* <li>The dimension can be identified by more than one identifier, with conflicts trigging
* a warning.</li>
* <li>The slice point is an index offset in the discrete coverage, rather than a metric
* value in the continuous dimension.</li>
* </ul>}
*
* This class refers always to the indices in the file, which can be either the source
* or the destination:
* <p>
* <ul>
* <li>When used with {@link SpatialImageReadParam}, this class contains the index of the
* section to read from the file (the <cite>source region</cite>).</li>
* <li>When used with {@link SpatialImageWriteParam}, this class contains the index of the
* section to write in the file (the <cite>destination offset</cite>).</li>
* </ul>
* <p>
* In addition to the index, {@code DimensionSlice} also specifies the dimension on which the
* index applies. See the {@link DimensionIdentification} javadoc for more information about
* how dimensions are identified.
*
* {@section Example 1: Setting the indices in the time and depth dimensions}
* Instances of {@code DimensionSlice} can be created and used as below:
*
* {@preformat java
* SpatialImageReadParam parameters = imageReader.getDefaultReadParam();
*
* DimensionSlice timeSlice = parameters.newDimensionSlice();
* timeSlice.addDimensionId("time");
* timeSlice.setSliceIndex(25);
*
* DimensionSlice depthSlice = parameters.newDimensionSlice();
* depthSlice.addDimensionId("depth");
* depthSlice.setSliceIndex(40);
*
* // Read the (x,y) plane at time[25] and depth[40]
* BufferedImage image = imageReader.read(0, parameters);
* }
*
* {@section Example 2: Setting the indices in the third dimension}
* In a 4-D dataset having (<var>x</var>, <var>y</var>, <var>z</var>, <var>t</var>) dimensions
* when the time is known to be the dimension at index 3 (0-based numbering), read the
* (<var>x</var>, <var>y</var>) plane at time <var>t</var><sub>25</sub>:
*
* {@preformat java
* DimensionSlice timeSlice = parameters.newDimensionSlice();
* timeSlice.addDimensionId(3);
* timeSlice.setSliceIndex(25);
* }
*
* If no {@code setSliceIndex(int)} method is invoked, then the default value is 0. This means that
* for the above-cited 4-D dataset, only the image at the first time (<var>t</var><sub>0</sub>)
* is selected by default. See <cite>Handling more than two dimensions</cite> in
* {@link SpatialImageReadParam} javadoc for more details.
*
* {@section Example 3: Setting the index of the vertical axis}
* Read the (<var>x</var>, <var>y</var>) plane at elevation <var>z</var><sub>25</sub>,
* where the index of the <var>z</var> dimension is unknown. We don't even known if the
* <var>z</var> dimension is actually a height or a depth. We can identify the dimension
* by its name, but it works only with file formats that provide support for named dimensions
* (like NetCDF). So we also identify the dimension by axis directions, which should works
* for any format:
*
* {@preformat java
* DimensionSlice elevationSlice = parameters.newDimensionSlice();
* elevationSlice.addDimensionId("height", "depth");
* elevationSlice.addDimensionId(AxisDirection.UP, AxisDirection.DOWN);
* elevationSlice.setSliceIndex(25);
* }
*
* If there is ambiguity (for example if both a dimension named {@code "height"} and an other
* dimension named {@code "depth"} exist), then a {@linkplain SpatialImageReader#warningOccurred
* warning will be emitted} at reading time and the index 25 will be set to the dimension named
* {@code "height"} because that name has been specified first.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.15
*
* @see SpatialImageReadParam
* @see MultidimensionalImageStore
*
* @since 3.08
* @module
*/
public class DimensionSlice extends DimensionIdentification {
/**
* The standard Java API used for selecting the slice to read or write in a particular
* dimension. The region to read or write in a hyper-cube can be specified in up to 4
* dimensions in the following ways:
*
* <ul>
* <li><p>When reading:</p></li>
* <ol>
* <li>Specifying the {@linkplain Rectangle#x x} ordinate of the
* {@linkplain IIOParam#getSourceRegion() source region}.</li>
* <li>Specifying the {@linkplain Rectangle#y y} ordinate of the
* {@linkplain IIOParam#getSourceRegion() source region}.</li>
* <li>Specifying the the {@linkplain IIOParam#getSourceBands() source bands}.</li>
* <li>Specifying the image index.</li>
* </ol>
* <li><p>When writing:</p></li>
* <ol>
* <li>Specifying the {@linkplain Point#x x} ordinate of the
* {@linkplain IIOParam#getDestinationOffset() destination offset}.</li>
* <li>Specifying the {@linkplain Point#y y} ordinate of the
* {@linkplain IIOParam#getDestinationOffset() destination offset}.</li>
* <li>Specifying the image index.</li>
* </ol>
* </ul>
* <p>
* Supplemental dimensions if any can not be specified by an API from the standard Java library.
* {@link DimensionSlice} instances shall be created for those supplemental dimensions.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.08
*
* @see DimensionSlice
* @see MultidimensionalImageStore
* @see IllegalImageDimensionException
*
* @since 3.08
* @module
*/
public static enum API {
/**
* The region to read/write along a dimension is specified by the <var>x</var> ordinate.
* <p>
* <ul>
* <li>On reading, this is the ({@linkplain Rectangle#x x}, {@linkplain Rectangle#width width})
* attributes of the {@linkplain IIOParam#getSourceRegion() source region}.</li>
* <li>On writing, this is the {@linkplain Point#x x} attribute of the
* {@linkplain IIOParam#getDestinationOffset() destination offset}.</li>
* </ul>
*/
COLUMNS,
/**
* The region to read/write along a dimension is specified by the <var>y</var> ordinate.
* <p>
* <ul>
* <li>On reading, this is the ({@linkplain Rectangle#y y}, {@linkplain Rectangle#height height})
* attributes of the {@linkplain IIOParam#getSourceRegion() source region}.</li>
* <li>On writing, this is the {@linkplain Point#y y} attribute of the
* {@linkplain IIOParam#getDestinationOffset() destination offset}.</li>
* </ul>
*/
ROWS,
/**
* The region to read along a dimension is specified by the
* {@linkplain IIOParam#getSourceBands() source bands}.
*/
BANDS,
/**
* The region to read/write along a dimension is specified by the image index. Note that
* this parameter needs to be given directly to the {@link javax.imageio.ImageReader} or
* {@link javax.imageio.ImageWriter} instead than the {@link IIOParam} object.
*/
IMAGES,
/**
* Indicates that no standard Java API match the dimension.
*/
NONE;
/**
* All valid API except {@link #NONE}. The index of each element in the
* array shall be equals to the ordinal value.
*/
static final API[] VALIDS = new API[] {
COLUMNS, ROWS, BANDS, IMAGES
};
}
/**
* The index of the region to read along the dimension that this
* {@code DimensionSlice} object represents.
*/
private int index;
/**
* Creates a new {@code DimensionSlice} instance. This constructor is not public in order
* to ensure that the given collection contains only {@code DimensionSlice} instances, not
* mixed with {@link DimensionIdentification}. In addition, the {@link DimensionSet#owner}
* shall be a {@link SpatialImageReadParam} or {@link SpatialImageWriteParam} instance, as
* required by {@link #getParameters()}.
*
* @param owner The collection that created this object.
*/
DimensionSlice(final DimensionSet owner) {
super(owner);
}
/**
* Creates a new instance initialized to the same values than the given instance.
* This copy constructor provides a way to substitute the instances created by
* {@link SpatialImageReadParam#newDimensionSlice()} by custom instances overriding
* some methods, as in the example below:
*
* {@preformat java
* class MyParameters extends SpatialImageReadParam {
* MyParameters(ImageReader reader) {
* super(reader);
* }
*
* public DimensionSlice newDimensionSlice() {
* return new MySelection(super.newDimensionSlice());
* }
* }
*
* class MySelection extends DimensionSlice {
* MySelection(DimensionSlice original) {
* super(original);
* }
*
* // Override some methods here.
* }
* }
*
* @param original The instance to copy.
*/
protected DimensionSlice(final DimensionSlice original) {
super(original);
this.index = original.index;
}
/**
* Returns the parameter which created this {@code DimensionSlice} instance.
*/
private IIOParam getParameters() {
return (IIOParam) owner.owner;
}
/**
* Returns the standard Java API that can be used for selecting a region along the
* dimension represented by this object. The default value is {@link API#NONE NONE}.
*
* @return The standard Java API for selecting a region along the dimension.
*
* @see SpatialImageReadParam#getDimensionSliceForAPI(DimensionSlice.API)
*/
private API getAPI() {
final IIOParam param = getParameters();
Object candidate = null;
if (param instanceof SpatialImageReadParam) {
candidate = ((SpatialImageReadParam) param).reader;
} else if (param instanceof SpatialImageWriteParam) {
candidate = ((SpatialImageWriteParam) param).writer;
}
if (candidate instanceof MultidimensionalImageStore) {
return ((MultidimensionalImageStore) candidate).getAPIForDimension(getDimensionIds());
}
return API.NONE;
}
/**
* Returns the index of the section to read or write along the dimension
* represented by this object. This method applies the following rules:
* <p>
* <ul>
* <li>For {@link SpatialImageReadParam}:<ul>
* <li>If the API is {@link API#COLUMNS COLUMNS} or {@link API#ROWS ROWS}, then this method
* invokes {@link IIOParam#getSourceRegion()} and returns the {@link Rectangle#x x} or
* {@link Rectangle#y y} attribute respectively, or 0 if the source region is not set.</li>
* <li>Otherwise if the API is {@link API#BANDS BANDS}, then this method invokes
* {@link IIOParam#getSourceBands()} and returns the index of the first band,
* or 0 if the source bands are not set.</li>
* <li>Otherwise this method returns the value set by the last call to
* {@link #setSliceIndex(int)}.</li>
* </ul></li>
* <li>For {@link SpatialImageWriteParam}:<ul>
* <li>If the API is {@link API#COLUMNS COLUMNS} or {@link API#ROWS ROWS}, then this method
* invokes {@link IIOParam#getDestinationOffset()} and returns the {@link Point#x x} or
* {@link Point#y y} attribute respectively, or 0 if the offset is not set.</li>
* <li>Otherwise this method returns the value set by the last call to
* {@link #setSliceIndex(int)}.</li>
* </ul></li>
* </ul>
*
* {@note This method could have been named <code>getFirstIndex()</code> because it returns
* the index of the <em>first</em> element to read (often the lower index, but not always).
* However there would be no <code>getLastIndex()</code> method, because the default values
* to return when the source region or source bands are unspecified depend on information known
* only to the <code>ImageReader</code> when the input is set. This is probably not a major
* issue since the main purpose of this method is to get the index in extra dimensions where
* no standard Java API is available.}
*
* @return The index of the first element to read/write in the dimension represented by this
* object.
*
* @see SpatialImageReadParam#getSliceIndex(Object[])
*/
@SuppressWarnings("fallthrough")
public int getSliceIndex() {
final boolean isY;
switch (getAPI()) {
case COLUMNS: {
isY = false;
break;
}
case ROWS: {
isY = true;
break;
}
case BANDS: {
final IIOParam parameters = getParameters();
if (!(parameters instanceof ImageWriteParam)) {
final int[] sourceBands = parameters.getSourceBands();
return (sourceBands != null && sourceBands.length != 0) ? sourceBands[0] : 0;
}
// Fall through
}
default: {
return index;
}
}
/*
* COLUMNS and ROWS cases.
*/
final IIOParam parameters = getParameters();
if (parameters instanceof ImageWriteParam) {
final Point offset = parameters.getDestinationOffset();
if (offset != null) {
return isY ? offset.y : offset.x;
}
} else {
final Rectangle region = parameters.getSourceRegion();
if (region != null) {
return isY ? region.y : region.x;
}
}
return 0;
}
/**
* Sets the index of the region to read along the dimension represented by this object.
* This method applies the following rules:
* <p>
* <ul>
* <li>For {@link SpatialImageReadParam}:<ul>
* <li>If the API is {@link API#COLUMNS COLUMNS} or {@link API#ROWS ROWS}, then this method
* invokes {@link IIOParam#setSourceRegion(Rectangle)} with a {@link Rectangle#x x} or
* {@link Rectangle#y y} attribute set to the given index, and the corresponding width
* or height attribute set to 1.</li>
* <li>Otherwise if the API is {@link API#BANDS BANDS}, then this method invokes
* {@link IIOParam#setSourceBands(int[])} with the given index.</li>
* <li>Otherwise this method stores the given index.</li>
* </ul></li>
* <li>For {@link SpatialImageWriteParam}:<ul>
* <li>If the API is {@link API#COLUMNS COLUMNS} or {@link API#ROWS ROWS}, then this method
* invokes {@link IIOParam#setDestinationOffset(Point)} with a {@link Point#x x} or
* {@link Point#y y} attribute set to the given index.</li>
* <li>Otherwise this method stores the given index.</li>
* </ul></li>
* </ul>
*
* @param index The slice point to read/write in the dimension represented by this object.
*
* @see SpatialImageReadParam#getSliceIndex(Object[])
*/
@SuppressWarnings("fallthrough")
public void setSliceIndex(final int index) {
final boolean isY;
switch (getAPI()) {
case COLUMNS: {
isY = false;
break;
}
case ROWS: {
isY = true;
break;
}
case BANDS: {
final IIOParam parameters = getParameters();
if (!(parameters instanceof ImageWriteParam)) {
parameters.setSourceBands(new int[] {index});
return;
}
// Fall through
}
default: {
this.index = index;
return;
}
}
/*
* COLUMNS and ROWS cases.
*/
final IIOParam parameters = getParameters();
if (parameters instanceof ImageWriteParam) {
final Point offset = parameters.getDestinationOffset();
if (isY) {
offset.y = index;
} else {
offset.x = index;
}
parameters.setDestinationOffset(offset);
} else {
Rectangle region = parameters.getSourceRegion();
if (region == null) {
region = new Rectangle(1,1);
}
if (isY) {
region.y = index;
region.height = 1;
} else {
region.x = index;
region.width = 1;
}
parameters.setSourceRegion(region);
}
}
/**
* Returns a string representation of this object. The default implementation
* formats on a single line the class name, the list of dimension identifiers,
* the {@linkplain #getSliceIndex() index} and the {@linkplain #getAPI() API} (if any).
*
* @see SpatialImageReadParam#toString()
*/
@Override
public String toString() {
final StringBuilder buffer = toStringBuilder().append("}, sliceIndex=").append(getSliceIndex());
final API api = getAPI();
if (api != API.NONE) {
buffer.append(", API=").append(api.name());
}
return buffer.append(']').toString();
}
}