/*
* 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.internal.image.io;
import java.util.Locale;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import javax.imageio.ImageWriter;
import javax.imageio.IIOImage;
import javax.imageio.IIOParam;
import javax.imageio.ImageWriteParam;
import javax.imageio.metadata.IIOMetadata;
import org.geotoolkit.image.internal.ImageUtilities;
import org.opengis.util.InternationalString;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.coverage.grid.GridGeometry;
import org.opengis.coverage.grid.RectifiedGrid;
import org.opengis.metadata.spatial.Georectified;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.operation.MathTransform;
import org.geotoolkit.image.io.ImageMetadataException;
import org.geotoolkit.image.io.metadata.MetadataHelper;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.metadata.iso.spatial.PixelTranslation;
import org.geotoolkit.referencing.cs.PredefinedCS;
import org.geotoolkit.resources.Errors;
import static org.geotoolkit.image.io.MultidimensionalImageStore.*;
/**
* A helper class for processing the {@link IIOImage} and {@link IIOParam} information before to
* write an image.
* <p>
* This class is defined in the NetCDF module because the NetCDF writer is currently the only
* one to use it. However we may move it to an other module if it happen to be useful for other
* writers too.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @since 3.20
* @module
*/
public class IIOImageHelper {
/**
* The data type, as one of the {@link DataBuffer} constants.
*/
public final int dataType;
/**
* The number of bands in the source image.
* Shall be equals or greater than the length of the {@link #sourceBands} array.
*/
private final int numBands;
/**
* {@code true} if the {@code IIOParam} were {@code null}.
*/
public final boolean isDefaultParameters;
/**
* The smallest rectangle that fully encompass the region to read from the source (never
* {@code null}). This rectangle is computed as the intersection of the user-supplied region
* (if any) and the image bounds. If a sub-sampling has been specified, it will be taken in
* account in order to ensure that the first and last row and column are included in the set
* of pixels coordinates to iterate.
*
* @see #getSourceRegionCenter()
*/
public final Rectangle sourceRegion;
/**
* The bands to read, or {@code null} for reading all bands.
* The length of this array shall be equals or less than {@link #numBands}.
*/
public final int[] sourceBands;
/**
* The sub-sampling, or 1 if none.
*
* @see #hasSubsampling()
*/
public final int sourceXSubsampling, sourceYSubsampling;
/**
* The locale for formatting locale-sensitive data (never {@code null}).
*/
public final Locale locale;
// ---- The above variables were working on standard image parameters only. ----------------
// ---- The remaining fields below work on spatial metadata. ----------------
/**
* The spatial metadata about the image to be written, or {@code null} if none.
*/
public final SpatialMetadata metadata;
/**
* The coordinate reference system of the image to write, or {@code null} if not yet computed.
* Note that the CRS may have more than 2 dimensions.
*
* @see #getCoordinateReferenceSystem()
*/
private CoordinateReferenceSystem crs;
/**
* The coordinate system of the image to write, or {@code null} if not yet computed.
* Note that the CS may have more than 2 dimensions.
*
* @see #getCoordinateSystem()
*/
private CoordinateSystem coordinateSystem;
/**
* {@code true} if already checked for {@link RectifiedGrid} information in the
* {@linkplain #metadata}. If {@code true}, then the value of {@link #gridToCRS}
* and {@link #gridDomain} are considered final, even if they are null.
*/
private boolean isGridGeometryComputed;
/**
* The conversion from grid to CRS coordinates, or {@code null} if not yet computed.
* This transform is <strong>not</strong> adjusted for user parameters (sub-region,
* sub-sampling).
*/
private MathTransform gridToCRS;
/**
* The lower and upper bounds of the grid, or {@code null} if not yet computed.
* This extent is <strong>not</strong> adjusted for user parameters (sub-region,
* sub-sampling).
*/
private GridEnvelope gridDomain;
/**
* Coordinates in the center of the image to write, or {@code null} if not yet computed.
*/
private double[] gridCenter;
/**
* Creates a helper for the specified image and parameters.
*
* @param writer The writer which is preparing the image to write, or {@code null} if unknown.
* @param image The image or raster to be read or written.
* @param param The parameters that control the writing process, or {@code null} if none.
*/
public IIOImageHelper(final ImageWriter writer, final IIOImage image, final IIOParam param) {
/*
* This code is a duplication of the SpatialImageWriter.createRectIter(...) method,
* except that we unconditionally compute the intersection with the image bounds.
*/
final Rectangle bounds;
if (image.hasRaster()) {
final Raster raster = image.getRaster();
bounds = raster.getBounds(); // Needs to be a clone.
numBands = raster.getNumBands();
dataType = raster.getSampleModel().getDataType();
} else {
final RenderedImage raster = image.getRenderedImage();
final SampleModel model = raster.getSampleModel();
bounds = ImageUtilities.getBounds(raster);
numBands = model.getNumBands();
dataType = model.getDataType();
}
/*
* Examines the parameters for subsampling in lines, columns and bands. If a subsampling
* is specified, the source region will be translated by the subsampling offset (if any).
*/
isDefaultParameters = (param == null);
if (param != null) {
Rectangle region = param.getSourceRegion();
sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = param.getSourceYSubsampling();
if (region == null) {
region = bounds;
} else {
region = region.intersection(bounds);
}
if (hasSubsampling()) {
final int xOffset = param.getSubsamplingXOffset();
final int yOffset = param.getSubsamplingYOffset();
region.x += xOffset;
region.y += yOffset;
region.width -= xOffset;
region.height -= yOffset;
// Fits to the smallest bounding box, which is
// required by SubsampledRectIter implementation.
region.width -= (region.width - 1) % sourceXSubsampling;
region.height -= (region.height - 1) % sourceYSubsampling;
}
sourceRegion = region;
sourceBands = param.getSourceBands();
} else {
sourceRegion = bounds;
sourceBands = null;
sourceXSubsampling = 1;
sourceYSubsampling = 1;
}
/*
* Gets the locale from the parameter if possible, or from the image writer otherwise.
*/
Locale locale = null;
if (param instanceof ImageWriteParam) {
locale = ((ImageWriteParam) param).getLocale();
}
if (locale == null) {
locale = writer.getLocale();
if (locale == null) {
locale = Locale.getDefault();
}
}
this.locale = locale;
/*
* The code below this point were working on standard image parameters only.
* The code below this point work on Geotk-specific metadata.
*/
final IIOMetadata md = image.getMetadata();
metadata = (md == null || md instanceof SpatialMetadata) ? (SpatialMetadata) md :
new SpatialMetadata(false, writer, md);
}
/**
* Returns {@code true} if iteration over the pixel values will perform sub-sampling.
*
* @return {@code true} if the parameters given at construction time specify a
* sub-sampling, either horizontal, vertical or both.
*/
public final boolean hasSubsampling() {
return sourceXSubsampling != 1 || sourceYSubsampling != 1;
}
/**
* Returns the number of source bands to use. This number may be equals or less
* than the actual number of bands in the source image.
*
* @return The number of source bands.
*/
public final int getNumSourceBands() {
return (sourceBands != null) ? sourceBands.length : numBands;
}
/**
* Returns the source band at the given index.
*
* @param i The index in the range 0 inclusive to {@link #getNumSourceBands()} exclusive.
* @return The source bands at the given index.
*/
public final int getSourceBand(final int i) {
return (sourceBands != null) ? sourceBands[i] : i;
}
/**
* Returns the coordinate reference system of the image to write, or {@code null} if none.
* Note that the CRS may have more or less than 2 dimensions; this method does not perform
* any dimensionality check.
*
* @return The image coordinate reference system, or {@code null}.
* @throws ImageMetadataException If an error occurred while fetching the coordinate reference system.
*/
public final CoordinateReferenceSystem getCoordinateReferenceSystem() throws ImageMetadataException {
if (crs == null && metadata != null) {
crs = metadata.getInstanceForType(CoordinateReferenceSystem.class);
}
return crs;
}
/**
* Returns the coordinate system of the image to write.
* Note that the CS may have more than 2 dimensions.
* <p>
* This method never returns null - if no coordinate system can be inferred from the
* {@linkplain #metadata}, then the default is {@link DefaultCartesianCS#GRID}.
*
* @return The image coordinate system (never {@code null}).
* @throws ImageMetadataException If an error occurred while computing the coordinate system.
*/
public final CoordinateSystem getCoordinateSystem() throws ImageMetadataException {
CoordinateSystem cs = coordinateSystem;
if (cs == null) {
final CoordinateReferenceSystem crs = getCoordinateReferenceSystem();
if (crs != null) {
cs = crs.getCoordinateSystem();
}
if (cs == null) {
cs = PredefinedCS.GRID;
} else {
final int dim = cs.getDimension();
if (dim < 2) {
throw new ImageMetadataException(Errors.format(Errors.Keys.IllegalCsDimension_1, dim));
}
}
coordinateSystem = cs; // Store ony on success.
}
return cs;
}
/**
* Computes the {@link #gridToCRS}, {@link #gridDomain} and {@link #gridCenter} fields. Any of
* those fields may still be {@code null} after this method call if they couldn't be computed.
*
* @throws ImageMetadataException If an error occurred while computing the grid geometry.
*/
private void computeGridGeometry() throws ImageMetadataException {
/*
* Maybe the coordinate system contains itself the information we are looking for.
* This happen for example if the CS has been created by NetcdfImageReader.
*/
final CoordinateSystem cs = getCoordinateSystem();
if (cs instanceof GridGeometry) {
final GridGeometry gridGeometry = (GridGeometry) cs;
gridToCRS = gridGeometry.getGridToCRS();
gridDomain = gridGeometry.getExtent();
} else if (metadata != null) {
final RectifiedGrid domain = metadata.getInstanceForType(RectifiedGrid.class);
if (domain != null) {
gridDomain = domain.getExtent();
gridToCRS = MetadataHelper.INSTANCE.getGridToCRS(domain);
final Georectified rectified = metadata.getInstanceForType(Georectified.class);
if (rectified != null) {
final PixelOrientation orientation = rectified.getPointInPixel();
if (orientation != null) {
gridToCRS = PixelTranslation.translate(gridToCRS, orientation,
PixelOrientation.CENTER, X_DIMENSION, Y_DIMENSION);
}
}
}
}
isGridGeometryComputed = true;
}
/**
* Returns the extent of grid coordinates, or {@code null} if none. This grid envelope is
* <strong>not</strong> adjusted for user parameters (sub-region, sub-sampling).
*
* @return The grid envelope, or {@code null} if none.
* @throws ImageMetadataException If an error occurred while computing the grid geometry.
*/
public GridEnvelope getGridDomain() throws ImageMetadataException {
if (!isGridGeometryComputed) {
computeGridGeometry();
}
return gridDomain;
}
/**
* Returns the conversion from grid to CRS coordinates, or {@code null} if none.
* This transform is <strong>not</strong> adjusted for user parameters (sub-region, sub-sampling).
*
* @return The conversion from grid to CRS coordinates, or {@code null} if none.
* @throws ImageMetadataException If an error occurred while computing the grid geometry.
*/
public MathTransform getGridToCRS() throws ImageMetadataException {
if (!isGridGeometryComputed) {
computeGridGeometry();
}
return gridToCRS;
}
/**
* Returns the center of the source region. This method returns an array of length 2 or greater
* since the grid extent may have more than 2 dimensions. Note that this method returns a direct
* reference to the internal array; do not modify.
* <p>
* This method does not make any adjustment for sub-sampling. If the returned array needs to be
* transformed using a <cite>grid to CRS</cite> transform, use a transform <strong>without</strong>
* adjustment for sub-sampling.
*
* @return The center of the source region in grid coordinates units,
* as a direct reference to the internal array.
* @throws ImageMetadataException If an error occurred while computing the grid geometry.
*/
public final double[] getSourceRegionCenter() throws ImageMetadataException {
if (gridCenter == null) {
if (!isGridGeometryComputed) {
computeGridGeometry();
}
gridCenter = new double[(gridDomain != null) ? gridDomain.getDimension() : coordinateSystem.getDimension()];
for (int i=0; i<gridCenter.length; i++) {
final int low, span;
switch (i) {
case X_DIMENSION: low=sourceRegion.x; span=sourceRegion.width; break;
case Y_DIMENSION: low=sourceRegion.y; span=sourceRegion.height; break;
default: {
if (gridDomain == null) {
continue; // Let the gridCenter[i] ordinate to zero.
}
low = gridDomain.getLow(i);
span = gridDomain.getSpan(i);
break;
}
}
gridCenter[i] = low + 0.5*span;
}
}
return gridCenter;
}
/**
* Returns the given international string as a string in the current {@linkplain #locale}.
*
* @param text The international string, or {@code null}.
* @return The given text as a string, or {@code null} if the given text was null.
*/
public final String toString(final InternationalString text) {
return (text != null) ? text.toString(locale) : null;
}
}