/*
* 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.referencing.adapters;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import javax.imageio.IIOException;
import javax.measure.Unit;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.constants.CF;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.AxisType;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.CoordinateAxis2D;
import org.opengis.util.GenericName;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.cs.RangeMeaning;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.util.CharSequences;
import org.apache.sis.measure.Units;
import org.apache.sis.io.wkt.Formatter;
import org.geotoolkit.io.wkt.Formattable;
import org.geotoolkit.metadata.Citations;
import org.apache.sis.referencing.NamedIdentifier;
import org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis;
import org.geotoolkit.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* Wraps a NetCDF {@link CoordinateAxis} as an implementation of GeoAPI interfaces.
* <p>
* {@code NetcdfAxis} is a <cite>view</cite>: every methods in this class delegate their work to the
* wrapped NetCDF axis. Consequently any change in the wrapped axis is immediately reflected in this
* {@code NetcdfAxis} instance. However users are encouraged to not change the wrapped axis after
* construction, since GeoAPI referencing objects are expected to be immutable.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.20
*
* @since 3.08
* @module
*/
public class NetcdfAxis extends NetcdfIdentifiedObject implements CoordinateSystemAxis, Formattable {
/**
* The NetCDF coordinate axis wrapped by this {@code NetcdfAxis} instance.
*/
final CoordinateAxis axis;
/**
* The unit, computed when first needed.
*/
volatile Unit<?> unit;
/**
* Creates a copy of the given axis. Copy are normally not necessary since {@code NetcdfAxis}
* is immutable. This constructor is provided only for subclasses that need to create almost
* identical copies.
*/
NetcdfAxis(final NetcdfAxis axis) {
this.axis = axis.axis;
this.unit = axis.unit;
}
/**
* Creates a new {@code NetcdfAxis} object wrapping the given NetCDF coordinate axis.
*
* @param axis The NetCDF coordinate axis to wrap.
*/
public NetcdfAxis(final CoordinateAxis axis) {
ensureNonNull("axis", axis);
this.axis = axis;
}
/**
* Creates a new {@code NetcdfAxis} object wrapping the given NetCDF coordinate axis.
*
* @param axis The NetCDF coordinate axis to wrap.
* @param domain Dimensions of the variable for which we are wrapping an axis, in natural order
* (reverse of NetCDF order). They are often, but not necessarily, the coordinate system
* dimensions.
* @return The {@code NetcdfAxis} object wrapping the given axis.
* @throws IIOException If the axis domain is not contained in the given list of dimensions.
*/
static NetcdfAxis wrap(final CoordinateAxis axis, final Dimension[] domain) throws IIOException {
if (axis instanceof CoordinateAxis1D) {
return new NetcdfAxis1D((CoordinateAxis1D) axis, domain);
}
if (axis instanceof CoordinateAxis2D) {
return new NetcdfAxis2D((CoordinateAxis2D) axis, domain);
}
return new NetcdfAxis(axis);
}
/**
* Returns the index in the given {@code domain} array of the dimension equals to the given
* axis dimension. This is a convenience method for subclasses.
*
* @param axis The axis for which to search the dimension in the {@code domain} array.
* @param index The index of the axis dimension to search.
* @param domain The array in which to search for the axis dimension.
* @return Index of the requested dimension in the {@code domain} array.
* @throws IIOException If the dimension has not been found in the given array.
*/
static int indexOfDimension(final CoordinateAxis axis, final int index, final Dimension[] domain)
throws IIOException
{
final Dimension toSearch = axis.getDimension(index);
if (toSearch != null) { // Null if the variable is a scalar.
for (int i=0; i<domain.length; i++) {
if (toSearch.equals(domain[i])) {
return i;
}
}
}
final StringBuilder buffer = new StringBuilder(40);
for (final Dimension dimension : domain) {
if (buffer.length() != 0) {
buffer.append(", ");
}
buffer.append(dimension.getName());
}
throw new IIOException(Errors.format(Errors.Keys.UnexpectedAxisDomain_2, axis.getShortName(), buffer));
}
/**
* Returns the wrapped NetCDF axis.
*/
@Override
public CoordinateAxis delegate() {
return axis;
}
/**
* Returns the axis name. The default implementation delegates to
* {@link CoordinateAxis1D#getShortName()}.
*
* @see CoordinateAxis1D#getShortName()
*/
@Override
public String getCode() {
return axis.getShortName();
}
/**
* Returns NetCDF axis standard name and long name, if available.
*/
@Override
public Collection<GenericName> getAlias() {
String standardName = null;
final List<GenericName> names = new ArrayList<>(2);
Attribute attribute = axis.findAttributeIgnoreCase(CF.STANDARD_NAME);
if (attribute != null) {
standardName = attribute.getStringValue();
if (standardName != null) {
names.add(new NamedIdentifier(Citations.NETCDF_CF, standardName));
}
}
attribute = axis.findAttributeIgnoreCase(CDM.LONG_NAME);
if (attribute != null) {
final String name = attribute.getStringValue();
if (name != null && !name.equals(standardName)) {
names.add(new NamedIdentifier(Citations.NETCDF, name));
}
}
return names;
}
/**
* Returns the axis abbreviation. The default implementation returns
* an acronym of the value returned by {@link CoordinateAxis1D#getShortName()}.
*
* @see CoordinateAxis1D#getShortName()
*/
@Override
public String getAbbreviation() {
final String name = axis.getShortName().trim();
String abbreviation = CharSequences.camelCaseToAcronym(name).toString().toLowerCase();
if (abbreviation.startsWith("l")) {
// Heuristic disambiguity.
final int length = Math.min(9, name.length()); // 9 is the length of "longitude".
int s = 0;
while (++s != length && Character.isLetter(name.charAt(s)));
final char prefix;
if (name.regionMatches(true, 0, "longitude", 0, s)) {
prefix = '\u03BB';
} else if (name.regionMatches(true, 0, "latitude", 0, Math.min(8, s))) {
prefix = '\u03C6';
} else {
return abbreviation;
}
final StringBuilder buffer = new StringBuilder(abbreviation);
buffer.setCharAt(0, prefix);
abbreviation = buffer.toString();
}
return abbreviation;
}
/**
* Returns the axis direction. The default implementation delegates to
* {@link #getDirection(CoordinateAxis)}.
*
* @see CoordinateAxis1D#getAxisType()
* @see CoordinateAxis1D#getPositive()
*/
@Override
public AxisDirection getDirection() {
return getDirection(axis);
}
/**
* Returns the direction of the given axis. This method infers the direction from
* {@link CoordinateAxis#getAxisType()} and {@link CoordinateAxis#getPositive()}.
* If the direction can not be determined, then this method returns
* {@link AxisDirection#OTHER}.
*
* @param axis The axis for which to get the direction.
* @return The direction of the given axis.
*/
public static AxisDirection getDirection(final CoordinateAxis axis) {
final AxisType type = axis.getAxisType();
final boolean down = CF.POSITIVE_DOWN.equals(axis.getPositive());
if (type != null) {
switch (type) {
case Time: return down ? AxisDirection.PAST : AxisDirection.FUTURE;
case Lon:
case GeoX: return down ? AxisDirection.WEST : AxisDirection.EAST;
case Lat:
case GeoY: return down ? AxisDirection.SOUTH : AxisDirection.NORTH;
case Pressure:
case Height:
case GeoZ: return down ? AxisDirection.DOWN : AxisDirection.UP;
}
}
return AxisDirection.OTHER;
}
/**
* Returns the axis minimal value. The default implementation delegates
* to {@link CoordinateAxis#getMinValue()}.
*
* @see CoordinateAxis#getMinValue()
*/
@Override
public double getMinimumValue() {
return axis.getMinValue();
}
/**
* Returns the axis maximal value. The default implementation delegates
* to {@link CoordinateAxis#getMaxValue()}.
*
* @see CoordinateAxis#getMaxValue()
*/
@Override
public double getMaximumValue() {
return axis.getMaxValue();
}
/**
* Returns {@code null} since the range meaning is unspecified.
*/
@Override
public RangeMeaning getRangeMeaning() {
return null;
}
/**
* Returns {@code true} if the NetCDF axis is an instance of {@link CoordinateAxis1D} and
* {@linkplain CoordinateAxis1D#isRegular() is regular}.
*
* {@note We do not allow overriding of this method, because callers assume that a
* value of <code>true</code> implies that the NetCDF axis is an instance of
* <code>CoordinateAxis1D</code>.}
*
* @return {@code true} if the NetCDF axis is regular.
*
* @see CoordinateAxis1D#isRegular()
*
* @since 3.20
*/
final boolean isRegular() {
return (axis instanceof CoordinateAxis1D) && ((CoordinateAxis1D) axis).isRegular();
}
/**
* Returns the source dimension of this axis, or {@code null} if unknown. The dimensions are
* stored in the values of the returned map. The keys are the indices at which the dimensions
* are expected to be found in a source coordinates.
* <p>
* Note that the source indices are <strong>not</strong> the dimension in the coordinate system
* (while "source" and "target" dimensions are often the same, they could also be different).
* This method is not public in order to avoid confusion.
*
* @return The source dimensions associated to their indices as expected by a math transform
* from pixel indices to geodetic coordinates, or {@code null} if unknown.
*/
Map<Integer,Dimension> getDomain() {
return null;
}
/**
* Returns a NetCDF axis which is part of the given domain.
* This method does not modify this axis. Instead, it will create a new one if necessary.
*
* @param domain The new domain in <em>natural</em> order (<strong>not</strong> the NetCDF order).
* @return A NetCDF axis which is part of the given domain.
* @throws IIOException If the given domain does not contains this axis domain.
*/
NetcdfAxis forDomain(final Dimension[] domain) throws IIOException {
return this;
}
/**
* Returns the number of source ordinate values along the given <em>source</em> dimension,
* or -1 if none. Note that the given argument is <strong>not</strong> the dimension in the
* coordinate system (while "source" and "target" dimensions are often the same, they could
* also be different). This method is not public in order to avoid confusion.
*
* {@note It would almost be possible to infer the length from the <code>Dimension</code> object
* returned by <code>getDomain()</code>. However it would not work for "unlimited" dimensions,
* so we still need this method.}
*
* @param sourceDimension The source dimension in a math transform from pixel indices to
* geodetic coordinates.
* @return Number of ordinate values in the given dimension, or -1 if unknown.
*/
int length(final int sourceDimension) {
return -1;
}
/**
* Interpolates the ordinate value for the given grid coordinate. The {@code gridPts} array
* shall contains a complete grid coordinate - not only the grid index value for this axis -
* starting at index {@code srcOff}.
* <p>
* The interpolated ordinate value shall maps the
* {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CENTER cell center}.
* <p>
* The default implementation throws an exception in all cases.
* The actual implementation needs to be provided by subclasses.
*
* @param gridPts An array containing grid coordinates.
* @param srcOff Index of the first ordinate value in the {@code gridPts}.
* @return The ordinate value of cell center interpolated from the given grid coordinate.
* @throws TransformException If the ordinate value can not be computed.
*
* @since 3.20
*/
public double getOrdinateValue(final double[] gridPts, final int srcOff) throws TransformException {
throw new TransformException(Errors.format(Errors.Keys.UnspecifiedTransform));
}
/**
* The reverse of {@link #getOrdinateValue(double[], int)}, finding the index of a given
* ordinate value.
*
* @todo This method is currently implemented only for the 1D-case. Generalization to the
* 2D case would probably require a change in the method signature.
*
* @param ordinate The ordinate value to convert.
* @param gridPts The array where to store the grid index.
* @param dstOff Offset of the first ordinate to write in {@code gridPts}.
*
* @since 3.21
*/
void getOrdinateIndex(final double ordinate, final double[] gridPts, final int dstOff) throws TransformException {
throw new TransformException(Errors.format(Errors.Keys.NoninvertibleTransform));
}
/**
* Returns the units as a string. If the axis direction or the time epoch
* was appended to the units, then this part of the string is removed.
*/
private String getUnitsString() {
String symbol = axis.getUnitsString();
if (symbol != null) {
int i = symbol.lastIndexOf('_');
if (i > 0) {
final String direction = getDirection().name();
if (symbol.regionMatches(true, i+1, direction, 0, direction.length())) {
symbol = symbol.substring(0, i).trim();
}
}
i = symbol.indexOf(" since ");
if (i > 0) {
symbol = symbol.substring(0, i);
}
symbol = symbol.trim();
}
return symbol;
}
/**
* Returns the units, or {@code null} if unknown.
*
* @see CoordinateAxis1D#getUnitsString()
* @see Units#valueOf(String)
*/
@Override
public Unit<?> getUnit() {
Unit<?> unit = this.unit;
if (unit == null) {
final String symbol = getUnitsString();
if (symbol != null && !symbol.isEmpty()) try {
this.unit = unit = Units.valueOf(symbol);
return unit;
} catch (IllegalArgumentException e) {
// TODO: use Unit library in order to parse this kind of units.
}
// TODO: The following fallback on default values is probably the wrong thing to do.
final AxisType type = axis.getAxisType();
if (type != null) {
switch (type) {
case Lat:
case Lon: unit = Units.DEGREE; break;
case GeoZ: unit = Units.METRE; break;
}
}
this.unit = unit;
}
return unit;
}
/**
* Delegates to the Geotk formatting code.
*/
@Override
public String formatTo(final Formatter formatter) {
return new DefaultCoordinateSystemAxis(this) {
@Override
public String formatTo(final Formatter f) {
return super.formatTo(f);
}
}.formatTo(formatter);
}
}