/*
* 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.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.Collections;
import java.io.IOException;
import javax.imageio.IIOException;
import javax.measure.Unit;
import ucar.ma2.Array;
import ucar.ma2.DataType;
import ucar.ma2.InvalidRangeException;
import ucar.nc2.Variable;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFileWriteable;
import ucar.nc2.iosp.netcdf3.N3iosp;
import ucar.nc2.constants.CF;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants._Coordinate;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.measure.Units;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.ComparisonMode;
import org.geotoolkit.resources.Errors;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.crs.DefaultTemporalCRS;
import org.geotoolkit.referencing.cs.DiscreteCoordinateSystemAxis;
import org.apache.sis.internal.metadata.AxisDirections;
import org.geotoolkit.internal.image.io.IIOImageHelper;
import org.geotoolkit.image.io.DimensionSlice.API;
import org.geotoolkit.image.io.ImageMetadataException;
import org.geotoolkit.metadata.Citations;
import static org.geotoolkit.image.io.MultidimensionalImageStore.*;
/**
* Describes a CRS dimension to be written into a NetCDF file. The constructor computes the array
* of coordinate values for the given axis. However the actual NetCDF variable and dimension are
* written only by the {@link #create(NetcdfFileWriteable)} method. Before to process to the write
* operation, the {@link #equals(Object)} method can be invoked in order to check if the dimension
* already exists, since many NetCDF variables may share the same dimensions.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @see <a href="http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.6/cf-conventions.html#coordinate-system">NetCDF Coordinate Systems</a>
*
* @since 3.20
* @module
*/
final class NetcdfDimension {
/**
* The default dimension index assigned to bands or images.
* Used only if not explicitely specified by the user.
*/
private static final int BAND_DIMENSION=2, IMAGE_DIMENSION=3;
/**
* The Image I/O API associated to this dimension.
*/
final API api;
/**
* The coordinate system axis to write in the NetCDF file.
* This field may be {@code null} for the band dimension is this dimension is
* not explicitely described by the coordinate system.
*/
private final CoordinateSystemAxis axis;
/**
* The ordinate values for this dimension, as a one-dimensional UCAR array.
*/
private final Array ordinates;
/**
* The type of values in the {@linkplain #ordinates} array. This field exists only because
* as of NetCDF 4.10, the {@link Array} API doesn't seem to provide a {@code getDataType()}
* method.
*/
private final DataType dataType;
/**
* The NetCDF dimension for the axis.
* This array is created by {@link #create(NetcdfFileWriteable)}.
*/
private Dimension dimension;
/**
* The NetCDF variable for the {@linkplain #dimension}. This variable is created
* by {@link #create(NetcdfFileWriteable)} but its values are left uninitialized.
*/
private Variable variable;
/**
* Creates a new {@code NetcdfDimension} instance for a single coordinate system axis.
* The actual writing process will happen when the {@link #create(NetcdfFileWriteable)}
* method will be invoked.
*
* @param image A description about the bounds of the NetCDF image to write.
* @param dimension The dimension for which to create a sequence of ordinate values.
* This method assumes that the same dimension is used for both source
* and target ordinate values (i.e. there is no axis swapping).
* @param flip {@code true} if the axis direction needs to be flipped. This is the
* case of the <var>y</var> axis. This flag must set to a value consistent
* with the behavior of the code writing the actual pixel values.
* @throws ImageMetadataException If an error occurred while computing the grid geometry.
*
* @todo Define a 'sourceToTargetDimension' method somewhere based on the value of the
* derivative at the center position.
*/
NetcdfDimension(final IIOImageHelper image, final int dimension, final boolean flip)
throws ImageMetadataException
{
final CoordinateSystem cs = image.getCoordinateSystem();
axis = (cs.getDimension() > dimension) ? cs.getAxis(dimension) : null;
final boolean canUseDiscreteAxis = image.isDefaultParameters
&& (axis instanceof DiscreteCoordinateSystemAxis<?>);
/*
* TODO: We use DimensionSlice.API as a matter of principle (for keeping room for future
* improvement), but the mapping is hard-coded for now. Futures versions may use some of
* the following methods:
*
* api = DimensionSet.getAPI(dimension, axis);
* api = MultidimensionalImageStore.getAPIForDimension(dimension, axis);
*/
switch (dimension) {
case X_DIMENSION: api=API.COLUMNS; break;
case Y_DIMENSION: api=API.ROWS; break;
case BAND_DIMENSION: api=API.BANDS; break;
case IMAGE_DIMENSION: api=API.IMAGES; break;
default: api=API.NONE; break;
}
int[] sourceIndices = null; // Source ordinate values, or null for [index:subsampling:...]
int subsampling = 1; // The grid ordinates increment. Must be equals or greater than 1.
int length = 1; // Number of ordinate values to write in the NetCDF file.
int index = 0; // Grid ordinate value, from lower inclusive to lower + length × subsampling exclusive.
switch (api) {
case COLUMNS: index=image.sourceRegion.x; subsampling=image.sourceXSubsampling; length=(image.sourceRegion.width +subsampling-1)/subsampling; break;
case ROWS: index=image.sourceRegion.y; subsampling=image.sourceYSubsampling; length=(image.sourceRegion.height+subsampling-1)/subsampling; break;
case BANDS: {
length = image.getNumSourceBands();
sourceIndices = image.sourceBands;
break;
}
case IMAGES: {
// We don't know in advance how many images are going to be written.
// So we rely on the metadata as the best we can do for now.
final GridEnvelope domain = image.getGridDomain();
if (domain != null) {
index = domain.getLow (dimension);
length = domain.getSpan(dimension);
} else {
length = canUseDiscreteAxis ? ((DiscreteCoordinateSystemAxis<?>) axis).length() : 1;
}
break;
}
}
/*
* If the axis declares directly its set of valid ordinate values, use those values.
* This is the case if the coordinate system has been created by NetcdfImageReader.
*/
if (canUseDiscreteAxis) {
final DiscreteCoordinateSystemAxis<?> ds = (DiscreteCoordinateSystemAxis<?>) axis;
final Class<?> type = ds.getElementType();
final boolean isNumeric = Number.class.isAssignableFrom(type);
if (isNumeric || Date.class.isAssignableFrom(type)) {
dataType = isNumeric ? DataType.getType(type) : DataType.DOUBLE;
ordinates = Array.factory(dataType, new int[] {length});
DefaultTemporalCRS converter = null;
if (!isNumeric) {
final CoordinateReferenceSystem subCRS = CRS.getComponentAt(
image.getCoordinateReferenceSystem(), dimension, dimension+1);
if (subCRS instanceof TemporalCRS) {
converter = DefaultTemporalCRS.castOrCopy((TemporalCRS) subCRS);
} else {
throw new ImageMetadataException(Errors.format(Errors.Keys.IncompatibleCoordinateSystemType));
}
}
for (int i=0; i<length; i++) {
if (sourceIndices != null) {
index = sourceIndices[i];
}
final Comparable<?> ordinate = ds.getOrdinateAt(index);
ordinates.setDouble(flip ? (length-1)-i : i, isNumeric
? ((Number) ordinate).doubleValue()
: converter.toValue((Date) ordinate));
index += subsampling;
}
return;
}
}
/*
* If we reach this point, we have not been able to compute the grid cell coordinates
* from the axis. Try to compute them from the grid to CRS transform instead.
*/
final MathTransform gridToCRS = image.getGridToCRS();
if (gridToCRS == null || gridToCRS.getSourceDimensions() <= dimension) {
if (sourceIndices == null) {
sourceIndices = new int[length];
for (int i=0; i<length; i++) {
sourceIndices[i] = index;
index += subsampling;
}
}
if (flip) {
if (sourceIndices == image.sourceBands) {
sourceIndices = sourceIndices.clone();
}
ArraysExt.reverse(sourceIndices);
}
ordinates = Array.factory(dataType = DataType.INT, new int[] {length}, sourceIndices);
} else {
ordinates = Array.factory(dataType = DataType.FLOAT, new int[] {length});
final double[] center = image.getSourceRegionCenter();
final double[] source = new double[(gridToCRS != null) ? gridToCRS.getSourceDimensions() : 2];
final double[] target = new double[(gridToCRS != null) ? gridToCRS.getTargetDimensions() : 2];
System.arraycopy(center, 0, source, 0, Math.min(source.length, center.length));
try {
for (int i=0; i<length; i++) {
if (sourceIndices != null) {
index = sourceIndices[i];
}
source[dimension] = index;
gridToCRS.transform(source, 0, target, 0, 1);
ordinates.setDouble(flip ? (length-1)-i : i, target[dimension]);
index += subsampling;
}
} catch (TransformException e) {
throw new ImageMetadataException(e.getLocalizedMessage(), e);
}
}
}
/**
* Adds this dimension in the given NetCDF file for the axis given at construction time.
* This constructor creates a new {@linkplain #variable} and {@linkplain #dimension},
* which are referenced in this class fields.
* <p>
* The NetCDF file must be in "define mode" when this method is invoked. This method will
* create the dimension and the variable, but will not physically write them to the disk.
* The actual writing will happen in the {@link #write(NetcdfFileWriteable)} method.
*
* @param file The UCAR NetCDF object where to write the new dimension and variable.
* @param locale The locale for dimension and variable names.
*/
void create(final NetcdfFileWriteable file, final Locale locale) {
if (axis == null) {
// Currently, this case occurs only when writing bands.
// We will need to revisit the name if more cases occur.
create(file, "band");
return;
}
final String longName = IdentifiedObjects.getName(axis, null);
final AxisDirection direction = axis.getDirection();
final AxisDirection absdir = AxisDirections.absolute(direction);
final Unit<?> unit = axis.getUnit();
final AxisType type;
String name, positive = null;
if (AxisDirection.EAST.equals(absdir)) {
if (Units.isLinear(unit)) {
type = AxisType.GeoX;
name = "x";
} else {
type = AxisType.Lon;
name = "lon";
}
} else if (AxisDirection.NORTH.equals(absdir)) {
if (Units.isLinear(unit)) {
type = AxisType.GeoY;
name = "y";
} else {
type = AxisType.Lat;
name = "lat";
}
} else if (AxisDirection.UP.equals(absdir)) {
type = Units.isPressure(unit) ? AxisType.Pressure : AxisType.Height;
positive = (absdir == direction) ? CF.POSITIVE_UP : CF.POSITIVE_DOWN;
name = "z";
} else if (AxisDirection.FUTURE.equals(absdir)) {
type = AxisType.Time;
name = "time";
} else if (AxisDirection.GEOCENTRIC_X.equals(absdir)) {
type = AxisType.GeoX;
name = "x";
} else if (AxisDirection.GEOCENTRIC_Y.equals(absdir)) {
type = AxisType.GeoY;
name = "y";
} else if (AxisDirection.GEOCENTRIC_Z.equals(absdir)) {
type = AxisType.GeoZ;
name = "z";
} else {
type = null;
name = N3iosp.createValidNetcdf3ObjectName(longName);
name = (locale != null) ? name.toLowerCase(locale) : name.toLowerCase();
}
/*
* 'name' has been initialized to a reasonable name for the dimension and variable to
* create for the given axis. However if the axis name ('longName') is a valid NetCDF
* name, then it will be used on the assumption that this name come from a previous
* reading of a NetCDF file.
*/
final String ncName = IdentifiedObjects.getName(axis, Citations.NETCDF);
if (ncName != null && N3iosp.isValidNetcdf3ObjectName(ncName)) {
name = ncName;
}
/*
* Create the variable and attach the relevant attribute value.
* Note that the values in the variable are left uninitialized.
*/
create(file, name);
if (!name.equals(longName)) {
addAttribute(CDM.LONG_NAME, longName);
}
if (unit != null && !unit.equals(Units.UNITY)) {
addAttribute(CDM.UNITS, getUnitSymbol(unit, direction));
}
addAttribute(CF.POSITIVE, positive);
if (type != null) {
addAttribute(CF.AXIS, type.getCFAxisName());
addAttribute(_Coordinate.AxisType, type.name());
}
}
/**
* Creates initially empty {@link #dimension} and {@link #variable} instances.
*/
private void create(final NetcdfFileWriteable file, final String name) {
dimension = file.addDimension(name, (int) ordinates.getSize());
variable = file.addVariable(name, dataType, Collections.singletonList(dimension));
}
/**
* Writes this dimension in the given NetCDF file. This method can be invoked after the
* {@link #create(NetcdfFileWriteable)} method, when the NetCDF file is no longer in
* "define mode".
*
* @param file The UCAR NetCDF object where to write the new dimension and variable.
* @throws IOException if an error occurred while writing the NetCDF variable.
*/
void write(final NetcdfFileWriteable file) throws IOException {
try {
file.write(variable.getFullNameEscaped(), ordinates);
} catch (InvalidRangeException e) {
throw new IIOException(e.getLocalizedMessage(), e);
}
}
/**
* Adds the given attribute value to the {@linkplain #variable},
* provided that the value is neither null or empty.
*/
private void addAttribute(final String name, String value) {
if (value != null && !((value = value.trim()).isEmpty())) {
variable.addAttribute(new Attribute(name, value));
}
}
/**
* Returns the symbol for the given units for the axis direction.
*/
private String getUnitSymbol(final Unit<?> unit, final AxisDirection direction) {
if (Units.DEGREE.equals(unit)) {
if (AxisDirection.EAST .equals(direction)) return "degrees_east";
if (AxisDirection.NORTH.equals(direction)) return "degrees_north";
if (AxisDirection.WEST .equals(direction)) return "degrees_west";
if (AxisDirection.SOUTH.equals(direction)) return "degrees_south";
return "degrees";
}
if (Units.isTemporal(unit)) {
final StringBuilder buffer = new StringBuilder();
if (Units.SECOND.equals(unit)) {
buffer.append("seconds");
} else if (Units.MINUTE.equals(unit)) {
buffer.append("minutes");
} else if (Units.HOUR.equals(unit)) {
buffer.append("hours");
} else if (Units.DAY.equals(unit)) {
buffer.append("days");
} else {
buffer.append(unit);
}
// TODO: needs to append " since ", then the epoch.
return buffer.toString();
}
return String.valueOf(unit);
}
/**
* Returns the NetCDF dimension. This method returns a non-null value if and only if the
* {@link #create(NetcdfFileWriteable)} method has been invoked before.
*/
Dimension getDimension() {
return dimension;
}
/**
* Returns a hash code value for this dimension. This method is defined
* for consistency with {@link #equals(Object)}.
*/
@Override
public int hashCode() {
return Objects.hashCode(axis) ^ Utilities.deepHashCode(ordinates.getStorage());
}
/**
* Compares the dimension to be written in the NetCDF file with the given object for equality.
* This method is designed for comparing only the attributes having an influence on the NetCDF
* file content, especially {@link #axis} and {@link #ordinates}. It does not compare the
* {@link #api} attribute, because it has an influence only on the source of the data.
*/
@Override
public boolean equals(final Object other) {
if (other instanceof NetcdfDimension) {
final NetcdfDimension that = (NetcdfDimension) other;
return Utilities.deepEquals(axis, that.axis, ComparisonMode.IGNORE_METADATA) &&
Objects.deepEquals(ordinates.getStorage(), that.ordinates.getStorage());
}
return false;
}
/**
* Returns a string representation for debugging purpose.
*/
@Override
public String toString() {
return api + " → " + IdentifiedObjects.getName(axis, null) + Utilities.deepToString(ordinates.getStorage());
}
}