/* * 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.image.io.plugin; import java.util.List; import java.util.Arrays; import java.io.IOException; import javax.imageio.IIOImage; import javax.imageio.IIOParam; import javax.imageio.IIOException; import javax.imageio.ImageWriter; import java.awt.image.DataBuffer; import javax.media.jai.iterator.RectIter; import ucar.ma2.Array; import ucar.ma2.DataType; import ucar.ma2.InvalidRangeException; import ucar.nc2.Variable; import ucar.nc2.Dimension; import ucar.nc2.Attribute; import ucar.nc2.NetcdfFileWriteable; import ucar.nc2.iosp.netcdf3.N3iosp; import org.opengis.metadata.content.ImageDescription; import org.opengis.metadata.content.RangeDimension; import org.apache.sis.util.ArraysExt; import org.geotoolkit.image.io.DimensionSlice; import org.geotoolkit.image.io.ImageMetadataException; import org.geotoolkit.image.io.metadata.SampleDimension; import org.geotoolkit.internal.image.io.IIOImageHelper; import org.geotoolkit.resources.Errors; import static ucar.nc2.constants.CDM.*; import static org.apache.sis.math.MathFunctions.divisors; import static org.apache.sis.internal.util.CollectionsExt.toList; import static org.geotoolkit.internal.image.io.NetcdfVariable.*; import static org.geotoolkit.image.io.MultidimensionalImageStore.*; /** * Holds the information about an image to be written in a NetCDF file. * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @version 3.21 * * @since 3.20 * @module */ final class NetcdfImage extends IIOImageHelper { /** * Approximative size (in number of primitive elements) of the buffer to use when writing * NetCDF variable values. The size of the buffer actually used may be different, either * smaller or larger than this size. */ private static final int BUFFER_SIZE = 4096; /** * The dimensions associated with the NetCDF image. * The array length is the number of coordinate system axis. */ private final NetcdfDimension[] dimensions; /** * The dimension associated to bands, or -1 if none. If no dimension is associated * to bands, then each bands will be written as a separated variable. */ private final int bandDimension; /** * The variables where to write the pixel values. The length of this array must be equals * to either 1, or to the number of source bands in the image to write. More specifically, * the {@linkplain #dimensions} and {@linkplain #variables} must be related by exactly one, * and only one, of the following rules: * <p> * <ul> * <li>If {@link #bandDimension} is positive, then {@code variables} contains only one * element. The bands are typically the third dimension in the variable (the actual * dimension is identified by {@code bandDimension}).</p></li> * * <li>If {@link #bandDimension} is negative, then {@code variables} contains one or more * elements. The length of this array is the number of bands.</li> * </ul> * <p> * Every elements in the array shall have the same shape. For each element, the * {@linkplain Variable#getRank() rank} is equals to the {@link #dimensions} array length. * For each <var>i</var> in the range [0…rank-1], the {@code variable.getShape()[i]} value * shall be equals to {@code dimensions[i].getLength()}. */ private final Variable[] variables; /** * The iterator to use for writing the image sample values. */ private final RectIter iterator; /** * Creates a new object for the given image to write. * <p> * This method stores a reference to the given iterator, but will not use it immediately. * The actual iteration will begin when the {@link #write(NetcdfFileWriteable)} method * will be invoked. The iteration is deferred because the {@link NetcdfFileWriteable} * needs to be switched from "define mode" to write mode before we can actually write * the pixel values. We can not switch the mode here because the caller may want to add * more variables before writing sample values. * <p> * The iterator must take the source bands, source region and source sub-sampling * in account. This is done automatically if the iterator has been created by * {@link SpatialImageWriter#createRectIter(IIOImage, ImageWriteParam)}. * * @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. * @param allDimensions All dimensions created by previous invocations of this constructor. * The constructor will add new dimensions in this array if needed. * @param iter The iterator to use for extracting the image sample values. */ NetcdfImage(final ImageWriter writer, final IIOImage image, final IIOParam parameters, final List<NetcdfDimension> allDimensions, final RectIter iter) throws IIOException { super(writer, image, parameters); int bandDimension = -1; int numDimensions = getCoordinateSystem().getDimension(); if (numDimensions == 2 && getNumSourceBands() > 1) { numDimensions = 3; // Use the bands as the third dimension. } dimensions = new NetcdfDimension[numDimensions]; for (int i=0; i<numDimensions; i++) { NetcdfDimension dimension = new NetcdfDimension(this, i, i == Y_DIMENSION); final int existing = allDimensions.indexOf(dimension); if (existing >= 0) { dimension = allDimensions.get(existing); } else { allDimensions.add(dimension); } dimensions[i] = dimension; if (dimension.api == DimensionSlice.API.BANDS) { if (bandDimension >= 0) { throw new IIOException(Errors.format(Errors.Keys.DuplicatedValue_1, DimensionSlice.API.BANDS)); } bandDimension = i; } } this.bandDimension = bandDimension; variables = new Variable[bandDimension >= 0 ? 1 : getNumSourceBands()]; iterator = iter; } /** * Returns the range dimensions specified in the metadata, or {@code null} if none. */ private List<? extends RangeDimension> getRangeDimensions() { if (metadata != null) { final ImageDescription description = metadata.getInstanceForType(ImageDescription.class); if (description != null) { return toList(description.getDimensions()); } } return null; } /** * Adds the variables to the given NetCDF file. The NetCDF file must be in "define mode". * This method does not write the sample values. In order to compute the sample values, * invoke {@link #setSampleValues(RectIter)} after this method call. * * @param file The NetCDF file where to add the variables. * @throws ImageMetadataException If an error occurred while creating the variables. */ @SuppressWarnings("fallthrough") final void createVariables(final NetcdfFileWriteable file) throws ImageMetadataException { /* * Get the UCAR variable data type, and whatever the data are signed or unsigned. * The fact that we compute those two information together explain why this code * is not declared in a separated method, like what we did for * org.geotoolkit.internal.image.io.NetcdfVariable#getRawDataType(VariableIF). */ final DataType type; boolean unsigned = false; switch (dataType) { case DataBuffer.TYPE_BYTE: type = DataType.BYTE; unsigned=true; break; case DataBuffer.TYPE_USHORT: unsigned = true; // Fallthrough case DataBuffer.TYPE_SHORT: type = DataType.SHORT; break; case DataBuffer.TYPE_INT: type = DataType.INT; break; case DataBuffer.TYPE_FLOAT: type = DataType.FLOAT; break; case DataBuffer.TYPE_DOUBLE: type = DataType.DOUBLE; break; default: throw new ImageMetadataException(Errors.format(Errors.Keys.UnsupportedDataType_1, dataType)); } /* * Get the NetCDF dimensions to be given to Variable constructor. * NetCDF dimensions need to be declared in reverse order. */ final Dimension[] ncDimensions = new Dimension[dimensions.length]; for (int i=0; i<ncDimensions.length; i++) { ncDimensions[ncDimensions.length - (i+1)] = dimensions[i].getDimension(); } /* * Get the metadata which will be shared for all variables. */ final List<? extends RangeDimension> ranges = getRangeDimensions(); final int numRanges = (ranges != null) ? ranges.size() : 0; for (int i=0; i<variables.length; i++) { final int band = getSourceBand(i); final RangeDimension range = (band < numRanges) ? ranges.get(band) : null; /* * Get the variable name from the metadata if possible, otherwise generate name * from a default pattern. The "band_1", "band_2", etc. pattern is used by GDAL. */ String longName = null; String name = (range != null) ? toString(range.getDescriptor()) : null; if (name == null) { name = (variables.length != 1) ? "band_" + (i+1) : "data"; } else if (!N3iosp.isValidNetcdf3ObjectName(name)) { longName = name; name = N3iosp.createValidNetcdf3ObjectName(name); } /* * Creation of the NetCDF variable happen here. * No data are written at this stage. */ final Variable var = file.addVariable(name, type, ncDimensions); if (longName != null) { var.addAttribute(new Attribute(LONG_NAME, longName)); } if (unsigned) { var.addAttribute(new Attribute(UNSIGNED, "true")); } if (range instanceof SampleDimension) { final SampleDimension sd = (SampleDimension) range; double[] fillValues = sd.getFillSampleValues(); if (fillValues != null && fillValues.length == 0) { fillValues = null; // Must be null before call to ArraysExt.resize } // Special processing for the scale and offset factors, since we need to // erase both of them if the transfer function is an identity transform. Double scale = sd.getScaleFactor(); Double offset = sd.getOffset(); if ((scale == null || scale == 1.0) && (offset == null || offset == 0.0)) { scale = null; offset = null; } for (int k=0; k<=6; k++) { final String atn; final Object value; switch (k) { case 0: atn = VALID_MIN; value = sd.getMinValue(); break; case 1: atn = VALID_MAX; value = sd.getMaxValue(); break; case 2: atn = UNITS; value = sd.getUnits(); break; case 3: atn = ADD_OFFSET; value = offset; break; case 4: atn = SCALE_FACTOR; value = scale; break; case 5: atn = MISSING_VALUE; value = fillValues; break; case 6: atn = FILL_VALUE; value = ArraysExt.resize(fillValues, 1); break; default: throw new AssertionError(k); } if (value != null) { // The numeric values shall be stored in attributes of the same type than // the variable. We will let the UCAR array do the conversion for us. final Attribute attr; if (value instanceof Number) { final Array array = Array.factory(type, new int[] {1}); array.setDouble(0, ((Number) value).doubleValue()); attr = new Attribute(atn, array); } else if (value instanceof double[]) { final double[] values = (double[]) value; final Array array = Array.factory(type, new int[] {values.length}); for (int j=0; j<values.length; j++) { array.setDouble(j, fillValues[j]); } attr = new Attribute(atn, array); } else { attr = new Attribute(atn, value.toString()); } var.addAttribute(attr); } } } variables[i] = var; } } /** * Writes the sample values in the given NetCDF file. This method can be invoked only when * the NetCDF file is no longer in "define mode". * <p> * This method uses the iterator given to the {@link #createVariables(NetcdfFileWriteable, * RectIter)} method. This iterator must take the source bands, source region and source * sub-sampling in account. This is done automatically if the iterator has been created by * {@link SpatialImageWriter#createRectIter(IIOImage, ImageWriteParam)}. * * @param file The UCAR NetCDF object where to write the new variables. * @throws IOException if an error occurred while writing the NetCDF variables. */ final void write(final NetcdfFileWriteable file) throws IOException { final RectIter iter = iterator; /* * Creates a buffer large enough for at least one row, and possibly a few more * rows if they are not too large. */ Variable var = variables[0]; final int[] shape = var.getShape(); final int[] origin = new int[shape.length]; final int xDimension = shape.length - (X_DIMENSION + 1); final int yDimension = shape.length - (Y_DIMENSION + 1); final int zDimension = shape.length - (bandDimension + 1); final int width = shape[xDimension]; final int height = shape[yDimension]; final int bufHeight = bufferHeight(height); final int capacity = width * bufHeight; shape[yDimension] = bufHeight; Arrays.fill(shape, 0, yDimension, 1); // Set all extra dimensions (if any) to a length of 1. final Array buffer = Array.factory(var.getDataType(), shape); /* * Everytime we start an iteration over a new bands, the target variable may change. * After every iteration on a row, we will flush the buffer if it is full. */ int band = 0; try { iter.startBands(); if (!iter.finishedBands()) do { if (bandDimension < 0) { var = variables[band]; } final String name = var.getFullNameEscaped(); origin[yDimension] = 0; int index = 0; // Flat index in the matrix. iter.startLines(); if (!iter.finishedLines()) do { iter.startPixels(); if (!iter.finishedPixels()) do { switch (dataType) { case DataBuffer.TYPE_DOUBLE: buffer.setDouble(index, iter.getSampleDouble()); break; case DataBuffer.TYPE_FLOAT: buffer.setFloat (index, iter.getSampleFloat()); break; default: buffer.setInt (index, iter.getSample()); break; } index++; } while (!iter.nextPixelDone()); assert (index % width) == 0 : index; if (index == capacity) { file.write(name, origin, buffer); origin[yDimension] += bufHeight; index = 0; } } while (!iter.nextLineDone()); assert index == 0 : index; band++; if (zDimension < origin.length) { origin[zDimension] = band; } } while (!iter.nextBandDone()); } catch (InvalidRangeException e) { throw new IIOException("Invalid section: origin=" + Arrays.toString(origin) + " shape=" + Arrays.toString(shape) + " band=" + band, e); } } /** * Suggests a height for the temporary buffer to create when writing a two-dimensional * slice of a NetCDF variable of the given size. This method returns a multiple of the * given height in order to avoid a dimension reduction applied by the UCAR library when * the length of a dimension is 1. * * @param height The image height, in pixels. * @return Proposed buffer height, in pixels. */ private static int bufferHeight(final int height) { final int[] divisors = divisors(height); int i = Arrays.binarySearch(divisors, BUFFER_SIZE / height); if (i < 0) { i = ~i; // Tild operator, not minus. } return divisors[Math.min(divisors.length-1, i)]; } }