/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2016, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.imageio.netcdf.utilities;
import it.geosolutions.imageio.stream.AccessibleStream;
import it.geosolutions.imageio.stream.input.URIImageInputStream;
import it.geosolutions.imageio.utilities.ImageIOUtilities;
import thredds.featurecollection.FeatureCollectionConfig;
import thredds.featurecollection.FeatureCollectionConfigBuilder;
import org.geotools.data.DataUtilities;
import org.geotools.gce.imagemosaic.ImageMosaicFormat;
import org.geotools.imageio.netcdf.cv.CoordinateHandlerFinder;
import org.geotools.imageio.netcdf.cv.CoordinateHandlerSpi;
import org.geotools.referencing.operation.projection.MapProjection;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.AxisType;
import ucar.nc2.dataset.CoordinateAxis1D;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dataset.VariableDS;
import ucar.nc2.ft.fmrc.Fmrc;
import ucar.nc2.jni.netcdf.Nc4Iosp;
import java.awt.image.DataBuffer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DateFormat;
import java.text.Format;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;
/**
* Set of NetCDF utility methods.
*
* @author Alessio Fabiani, GeoSolutions SAS
* @author Daniele Romagnoli, GeoSolutions SAS
* @author Simone Giannecchini, GeoSolutions SAS
*/
public class NetCDFUtilities {
public static final boolean CHECK_COORDINATE_PLUGINS;
public static final String CHECK_COORDINATE_PLUGINS_KEY = "netcdf.coordinates.enablePlugins";
public static final String NETCDF4_MIMETYPE = "application/x-netcdf4";
public static final String NETCDF3_MIMETYPE = "application/x-netcdf";
public static final String NETCDF = "NetCDF";
public static final String NETCDF4 = "NetCDF4";
public static final String NETCDF_4C = "NetCDF-4C";
public static final String NETCDF_3 = "NetCDF-3";
public static final String STANDARD_PARALLEL_1 = MapProjection.AbstractProvider.STANDARD_PARALLEL_1
.getName().getCode();
public static final String STANDARD_PARALLEL_2 = MapProjection.AbstractProvider.STANDARD_PARALLEL_2
.getName().getCode();
public static final String CENTRAL_MERIDIAN = MapProjection.AbstractProvider.CENTRAL_MERIDIAN
.getName().getCode();
public static final String LATITUDE_OF_ORIGIN = MapProjection.AbstractProvider.LATITUDE_OF_ORIGIN
.getName().getCode();
public static final String SCALE_FACTOR = MapProjection.AbstractProvider.SCALE_FACTOR.getName()
.getCode();
public static final String FALSE_EASTING = MapProjection.AbstractProvider.FALSE_EASTING
.getName().getCode();
public static final String FALSE_NORTHING = MapProjection.AbstractProvider.FALSE_NORTHING
.getName().getCode();
public static final String SEMI_MINOR = MapProjection.AbstractProvider.SEMI_MINOR
.getName().getCode();
public static final String SEMI_MAJOR = MapProjection.AbstractProvider.SEMI_MAJOR
.getName().getCode();
public static final String INVERSE_FLATTENING = "inverse_flattening";
public static final String UNKNOWN = "unknown";
public static final double DEFAULT_EARTH_RADIUS = 6371229.0d;
private NetCDFUtilities() {
}
/** The LOGGER for this class. */
private static final Logger LOGGER = Logger.getLogger(NetCDFUtilities.class.toString());
/** Set containing all the definition of the UNSUPPORTED DIMENSIONS to set as vertical ones*/
private static final Set<String> UNSUPPORTED_DIMENSIONS;
/** Set containing all the dimensions to be ignored */
private static final Set<String> IGNORED_DIMENSIONS;
/** Boolean indicating if GRIB library is available*/
private static boolean IS_GRIB_AVAILABLE;
/** Boolean indicating if NC4 C Library is available */
private static boolean IS_NC4_LIBRARY_AVAILABLE;
public static final String EXTERNAL_DATA_DIR;
private static final String NETCDF_DATA_DIR = "NETCDF_DATA_DIR";
public static final String FILL_VALUE = "_FillValue";
public static final String MISSING_VALUE = "missing_value";
public final static String LOWER_LEFT_LONGITUDE = "lower_left_longitude";
public final static String LOWER_LEFT_LATITUDE = "lower_left_latitude";
public final static String UPPER_RIGHT_LONGITUDE = "upper_right_longitude";
public final static String UPPER_RIGHT_LATITUDE = "upper_right_latitude";
public static final String COORDSYS = "latLonCoordSys";
public final static String Y = "y";
public final static String Y_COORD_PROJ = "y coordinate of projection";
public final static String Y_PROJ_COORD = "projection_y_coordinate";
public final static String X = "x";
public final static String X_COORD_PROJ = "x coordinate of projection";
public final static String X_PROJ_COORD = "projection_x_coordinate";
public final static String LATITUDE = "latitude";
public final static String LAT = "lat";
public final static String LONGITUDE = "longitude";
public final static String LON = "lon";
public final static String GRID_LATITUDE = "grid_latitude";
public final static String RLAT = "rlat";
public final static String GRID_LONGITUDE = "grid_longitude";
public final static String RLON = "rlon";
public final static String DEPTH = "depth";
public final static String ZETA = "z";
public static final String BOUNDS = "bounds";
private static final String BNDS = "bnds";
public final static String HEIGHT = "height";
public final static String TIME = "time";
public static final String POSITIVE = "positive";
public static final String UNITS = "units";
public static final String NAME = "name";
public static final String LONG_NAME = "long_name";
public static final String ELEVATION_DIM = ImageMosaicFormat.ELEVATION.getName().toString();
public static final String TIME_DIM = ImageMosaicFormat.TIME.getName().toString();
public final static String STANDARD_NAME = "standard_name";
public final static String DESCRIPTION = "description";
public final static String M = "m";
public final static String BOUNDS_SUFFIX = "_bnds";
public final static String LON_UNITS = "degrees_east";
public final static String LAT_UNITS = "degrees_north";
public final static String RLATLON_UNITS = "degrees";
public final static String NO_COORDS = "NoCoords";
public final static String TIME_ORIGIN = "seconds since 1970-01-01 00:00:00 UTC";
public final static long START_TIME;
public final static String BOUNDARY_DIMENSION = "nv";
public final static TimeZone UTC;
public final static String GRID_MAPPING = "grid_mapping";
public final static String GRID_MAPPING_NAME = "grid_mapping_name";
public final static String COORDINATE_AXIS_TYPE = "_CoordinateAxisType";
public final static String CONVENTIONS = "Conventions";
public final static String COORD_SYS_BUILDER = "_CoordSysBuilder";
public final static String COORD_SYS_BUILDER_CONVENTION = "ucar.nc2.dataset.conv.CF1Convention";
public final static String COORDINATE_TRANSFORM_TYPE = "_CoordinateTransformType";
public final static String COORDINATES = "coordinates";
// They are recognized from GDAL
public final static String SPATIAL_REF = "spatial_ref";
public final static String GEO_TRANSFORM = "GeoTransform";
public final static String UNIQUE_TIME_ATTRIBUTE = "uniqueTimeAttribute";
final static Set<String> EXCLUDED_ATTRIBUTES = new HashSet<String>();
public static final String ENHANCE_COORD_SYSTEMS = "org.geotools.coverage.io.netcdf.enhance.CoordSystems";
public static final String ENHANCE_SCALE_MISSING = "org.geotools.coverage.io.netcdf.enhance.ScaleMissing";
public static final String ENHANCE_CONVERT_ENUMS = "org.geotools.coverage.io.netcdf.enhance.ConvertEnums";
public static final String ENHANCE_SCALE_MISSING_DEFER = "org.geotools.coverage.io.netcdf.enhance.ScaleMissingDefer";
public static boolean ENHANCE_SCALE_OFFSET = false;
/**
* Number of bytes at the start of a file to search for a GRIB signature. Some GRIB files have WMO headers prepended by a telecommunications
* gateway. NetCDF-Java Grib{1,2}RecordScanner look for the header in this many bytes.
*/
private static final int GRIB_SEARCH_BYTES = 16000;
static {
//TODO remove this block when enhance mode can be set some other way, possibly via read params
//Default used to be to just enhance coord systems
EnumSet<NetcdfDataset.Enhance> defaultEnhanceMode = EnumSet.of(NetcdfDataset.Enhance.CoordSystems);
if (System.getProperty(ENHANCE_COORD_SYSTEMS) != null
&& !Boolean.getBoolean(ENHANCE_COORD_SYSTEMS)) {
defaultEnhanceMode.remove(NetcdfDataset.Enhance.CoordSystems);
}
if (Boolean.getBoolean(ENHANCE_SCALE_MISSING)) {
defaultEnhanceMode.add(NetcdfDataset.Enhance.ScaleMissing);
ENHANCE_SCALE_OFFSET = true;
}
if (Boolean.getBoolean(ENHANCE_CONVERT_ENUMS)) {
defaultEnhanceMode.add(NetcdfDataset.Enhance.ConvertEnums);
}
if (Boolean.getBoolean(ENHANCE_SCALE_MISSING_DEFER)) {
defaultEnhanceMode.add(NetcdfDataset.Enhance.ScaleMissingDefer);
}
NetcdfDataset.setDefaultEnhanceMode(defaultEnhanceMode);
}
/**
* Global attribute for coordinate coverageDescriptorsCache.
*
* @author Simone Giannecchini, GeoSolutions S.A.S.
*
*/
public static enum Axis {
X, Y, Z, T;
}
public static enum CheckType {
NONE, UNSET, NOSCALARS, ONLYGEOGRIDS
}
public static enum FileFormat {
NONE, CDF, HDF5, GRIB, NCML, FC
}
/**
* The dimension <strong>relative to the rank</strong> in {@link #variable} to use as image width. The actual dimension is
* {@code variable.getRank() - X_DIMENSION}. Is hard-coded because the loop in the {@code read} method expects this order.
*/
public static final int X_DIMENSION = 1;
/**
* The dimension <strong>relative to the rank</strong> in {@link #variable}
* to use as image height. The actual dimension is
* {@code variable.getRank() - Y_DIMENSION}. Is hard-coded because the loop
* in the {@code read} method expects this order.
*/
public static final int Y_DIMENSION = 2;
/**
* The default dimension <strong>relative to the rank</strong> in
* {@link #variable} to use as Z dimension. The actual dimension is
* {@code variable.getRank() - Z_DIMENSION}.
* <p>
*/
public static final int Z_DIMENSION = 3;
/**
* The data type to accept in images. Used for automatic detection of which
* coverageDescriptorsCache to assign to images.
*/
public static final Set<DataType> VALID_TYPES = new HashSet<DataType>(12);
public static final String NC4_ERROR_MESSAGE = "Native NetCDF C library is not available. "
+ "Unable to handle NetCDF4 files on input/output."
+ "\nPlease make sure to add the paht of the Native NetCDF C libraries to the "
+ "PATH environment variable\n if you want to support NetCDF4-Classic files";
static {
String property = System.getProperty(CHECK_COORDINATE_PLUGINS_KEY);
CHECK_COORDINATE_PLUGINS = Boolean.getBoolean(CHECK_COORDINATE_PLUGINS_KEY);
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Value of Check Coordinate Plugins:" + property);
LOGGER.info("Should check for coordinate handler plugins:"
+ CHECK_COORDINATE_PLUGINS);
}
IGNORED_DIMENSIONS = initializeIgnoreSet();
// Setting the LINUX Epoch as start time
final GregorianCalendar calendar = new GregorianCalendar(1970, 00, 01, 00, 00, 00);
UTC = TimeZone.getTimeZone("UTC");
calendar.setTimeZone(UTC);
START_TIME = calendar.getTimeInMillis();
EXCLUDED_ATTRIBUTES.add(UNITS);
EXCLUDED_ATTRIBUTES.add(LONG_NAME);
EXCLUDED_ATTRIBUTES.add(DESCRIPTION);
EXCLUDED_ATTRIBUTES.add(STANDARD_NAME);
HashSet<String> unsupportedSet = new HashSet<String>();
unsupportedSet.add("OSEQD");
UNSUPPORTED_DIMENSIONS = Collections.unmodifiableSet(unsupportedSet);
VALID_TYPES.add(DataType.BOOLEAN);
VALID_TYPES.add(DataType.BYTE);
VALID_TYPES.add(DataType.SHORT);
VALID_TYPES.add(DataType.INT);
VALID_TYPES.add(DataType.LONG);
VALID_TYPES.add(DataType.FLOAT);
VALID_TYPES.add(DataType.DOUBLE);
// Didn't extracted to a separate method
// since we can't initialize the static fields
final Object externalDir = System.getProperty(NETCDF_DATA_DIR);
String finalDir = null;
if (externalDir != null) {
String dir = (String) externalDir;
final File file = new File(dir);
if (isValidDir(file)) {
finalDir = dir;
}
}
EXTERNAL_DATA_DIR = finalDir;
try {
Class.forName("ucar.nc2.grib.collection.GribIosp");
Class.forName("org.geotools.coverage.io.grib.GribUtilities");
IS_GRIB_AVAILABLE = true;
} catch (ClassNotFoundException cnf) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("No Grib library found on classpath. GRIB will not be supported");
}
IS_GRIB_AVAILABLE = false;
}
IS_NC4_LIBRARY_AVAILABLE = Nc4Iosp.isClibraryPresent();
if (!IS_NC4_LIBRARY_AVAILABLE && LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(NC4_ERROR_MESSAGE);
}
}
static boolean isLatLon(String bandName) {
return bandName.equalsIgnoreCase(LON) || bandName.equalsIgnoreCase(LAT);
}
private static Set<String> initializeIgnoreSet() {
Set<CoordinateHandlerSpi> handlers = CoordinateHandlerFinder.getAvailableHandlers();
Iterator<CoordinateHandlerSpi> iterator = handlers.iterator();
Set<String> ignoredSet = new HashSet<String>();
while (iterator.hasNext()) {
CoordinateHandlerSpi handler = iterator.next();
Set<String> ignored = handler.getIgnoreSet();
if (ignored != null && !ignored.isEmpty()) {
ignoredSet.addAll(ignored);
}
}
if (!ignoredSet.isEmpty()) {
return ignoredSet;
}
return new HashSet<>();
}
public static boolean isValidDir(File file) {
String dir = file.getAbsolutePath();
if (!file.exists()) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.warning("The specified " + NETCDF_DATA_DIR + " property doesn't refer "
+ "to an existing folder. Please check the path: " + dir);
}
return false;
} else if (!file.isDirectory()) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.warning("The specified " + NETCDF_DATA_DIR + " property doesn't refer "
+ "to a directory. Please check the path: " + dir);
}
return false;
} else if (!file.canWrite()) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.warning("The specified " + NETCDF_DATA_DIR + " property refers to "
+ "a directory which can't be written. Please check the path and"
+ " the permissions for: " + dir);
}
return false;
}
return true;
}
/**
* Get Z Dimension Lenght for standard CF variables
* @param var
* @return
*/
public static int getZDimensionLength(Variable var) {
final int rank = var.getRank();
if (rank > 2) {
return var.getDimension(rank - Z_DIMENSION).getLength();
}
// TODO: Should I avoid use this method in case of 2D Variables?
return 0;
}
public static int getDimensionLength(Variable var, final int dimensionIndex) {
return var.getDimension(dimensionIndex).getLength();
}
/**
* Returns the data type which most closely represents the "raw" internal
* data of the variable. This is the value returned by the default
* implementation of {@link NetcdfImageReader#getRawDataType}.
*
* @param variable
* The variable.
* @return The data type, or {@link DataBuffer#TYPE_UNDEFINED} if unknown.
*
* @see NetcdfImageReader#getRawDataType
*/
public static int getRawDataType(final VariableIF variable) {
VariableDS ds = (VariableDS) variable;
final DataType type;
if (Boolean.getBoolean(ENHANCE_SCALE_MISSING)) {
type = ds.getDataType();
} else {
type = ds.getOriginalDataType();
}
return transcodeNetCDFDataType(type,variable.isUnsigned());
}
/**
* Transcode a NetCDF data type into a java2D DataBuffer type.
*
* @param type the {@link DataType} to transcode.
* @param unsigned if the original data is unsigned or not
* @return an int representing the correct DataBuffer type.
*/
public static int transcodeNetCDFDataType(final DataType type, final boolean unsigned) {
if (DataType.BOOLEAN.equals(type) || DataType.BYTE.equals(type)) {
return DataBuffer.TYPE_BYTE;
}
if (DataType.CHAR.equals(type)) {
return DataBuffer.TYPE_USHORT;
}
if (DataType.SHORT.equals(type)) {
return unsigned ? DataBuffer.TYPE_USHORT: DataBuffer.TYPE_SHORT;
}
if (DataType.INT.equals(type)) {
return DataBuffer.TYPE_INT;
}
if (DataType.FLOAT.equals(type)) {
return DataBuffer.TYPE_FLOAT;
}
if (DataType.LONG.equals(type) || DataType.DOUBLE.equals(type)) {
return DataBuffer.TYPE_DOUBLE;
}
return DataBuffer.TYPE_UNDEFINED;
}
/**
* NetCDF files may contains a wide set of coverageDescriptorsCache. Some of them are unuseful for our purposes. The method returns {@code true}
* if the specified variable is accepted.
*/
public static boolean isVariableAccepted( final Variable var, final CheckType checkType ) {
return isVariableAccepted(var, checkType, null);
}
/**
* NetCDF files may contains a wide set of coverageDescriptorsCache. Some of them are unuseful for our purposes. The method returns {@code true}
* if the specified variable is accepted.
*/
public static boolean isVariableAccepted( final Variable var, final CheckType checkType, final NetcdfDataset dataset ) {
if (var instanceof CoordinateAxis1D) {
return false;
} else if (checkType == CheckType.NOSCALARS) {
List<Dimension> dimensions = var.getDimensions();
if (dimensions.size() < 2) {
return false;
}
DataType dataType = var.getDataType();
if (dataType == DataType.CHAR) {
return false;
}
return isVariableAccepted(var.getFullName(), CheckType.NONE);
} else if (checkType == CheckType.ONLYGEOGRIDS) {
List<Dimension> dimensions = var.getDimensions();
if (dimensions.size() < 2) {
return false;
}
int twoDimensionalCoordinates = 0;
for (Dimension dimension : dimensions) {
String dimName = dimension.getFullName();
// check the dimension to be defined
Group group = dimension.getGroup();
// Simple check if the group is not present. In that case false is returned.
// This situation could happen with anonymous dimensions inside variables which
// indicates the bounds of another variable. These kind of variable are not useful
// for displaying the final raster.
if (group == null) {
return false;
}
if (IGNORED_DIMENSIONS.contains(dimName)) {
continue;
}
Variable dimVariable = group.findVariable(dimName);
if (dimVariable == null && dataset != null) {
//fallback on coordinates attribute for auxiliary coordinates.
dimVariable = getAuxiliaryCoordinate(dataset, group, var, dimName);
}
if (dimVariable instanceof CoordinateAxis1D) {
CoordinateAxis1D axis = (CoordinateAxis1D) dimVariable;
AxisType axisType = axis.getAxisType();
if (axisType == null) {
return false;
}
switch (axisType) {
case GeoX:
case GeoY:
case Lat:
case Lon:
twoDimensionalCoordinates++;
break;
default:
break;
}
}
}
if (twoDimensionalCoordinates < 2) {
// 2D Grid is missing
return false;
}
DataType dataType = var.getDataType();
if (dataType == DataType.CHAR) {
return false;
}
return isVariableAccepted(var.getFullName(), CheckType.NONE);
} else {
return isVariableAccepted(var.getFullName(), checkType);
}
}
private static Variable getAuxiliaryCoordinate(NetcdfDataset dataset, Group group,
Variable var, String dimName) {
Variable coordinateVariable = null;
Attribute attribute = var.findAttribute(NetCDFUtilities.COORDINATES);
if (attribute != null) {
String coordinates = attribute.getStringValue();
String [] coords = coordinates.split(" ");
for (String coord: coords) {
Variable coordVar = dataset.findVariable(group, coord);
List<Dimension> varDimensions = coordVar.getDimensions();
if (varDimensions != null && varDimensions.size() == 1 && varDimensions.get(0).getFullName().equalsIgnoreCase(dimName)) {
coordinateVariable = coordVar;
break;
}
}
}
return coordinateVariable;
}
/**
* NetCDF files may contain a wide set of coverageDescriptorsCache. Some of them are
* unuseful for our purposes. The method returns {@code true} if the
* specified variable is accepted.
*/
public static boolean isVariableAccepted(final String name,
final CheckType checkType) {
if (checkType == CheckType.NONE) {
return true;
} else if (name.equalsIgnoreCase(LATITUDE)
|| name.equalsIgnoreCase(LONGITUDE)
|| name.equalsIgnoreCase(LON)
|| name.equalsIgnoreCase(LAT)
|| name.equalsIgnoreCase(TIME)
|| name.equalsIgnoreCase(DEPTH)
|| name.equalsIgnoreCase(ZETA)
|| name.equalsIgnoreCase(HEIGHT)
|| name.toLowerCase().contains(COORDSYS.toLowerCase())
|| name.endsWith(BOUNDS)
|| name.endsWith(BNDS)
|| UNSUPPORTED_DIMENSIONS.contains(name)
) {
return false;
} else {
return true;
}
}
public static FileFormat getFormat(URI uri) throws IOException {
//try binary
try (InputStream input = uri.toURL().openStream()) {
// Checking Magic Number
byte[] b = new byte[GRIB_SEARCH_BYTES];
int count = input.read(b);
if (count < 3) {
return FileFormat.NONE;
}
// CDF signature at start of file
if ((b[0] == (byte) 0x43 && b[1] == (byte) 0x44
&& b[2] == (byte) 0x46)) {
return FileFormat.CDF;
}
// HDF signature at start of file
if ((b[0] == (byte) 0x89 && b[1] == (byte) 0x48
&& b[2] == (byte) 0x44)) {
return FileFormat.HDF5;
}
// Search for GRIB signature in first count bytes (up to GRIB_SEARCH_BYTES)
for (int i = 0; i < count - 3; i++) {
if (b[i] == (byte) 0x47 && b[i + 1] == (byte) 0x52 && b[i + 2] == (byte) 0x49
&& b[i + 3] == (byte) 0x42) {
return FileFormat.GRIB;
}
}
}
//try XML
try (InputStream input = uri.toURL().openStream()) {
StreamSource streamSource = null;
XMLStreamReader reader = null;
try {
streamSource = new StreamSource(input);
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
reader = inputFactory.createXMLStreamReader(streamSource);
reader.nextTag();
if ("netcdf".equals(reader.getName().getLocalPart())) {
return FileFormat.NCML;
}
if ("featureCollection".equals(reader.getName().getLocalPart())) {
return FileFormat.FC;
}
} catch (XMLStreamException e) {
} catch (FactoryConfigurationError e) {
} finally {
if (input != null) {
input.close();
}
if (reader != null) {
if (streamSource.getInputStream() != null) {
streamSource.getInputStream().close();
}
try {
reader.close();
} catch (XMLStreamException e) {
}
}
}
}
return FileFormat.NONE;
}
public static NetcdfDataset acquireFeatureCollection(String path) throws IOException {
Formatter formatter = new Formatter(System.err);
FeatureCollectionConfigBuilder builder
= new FeatureCollectionConfigBuilder(formatter);
FeatureCollectionConfig config
= builder.readConfigFromFile(path.toString()); //this is the path to the feature collection XML
Fmrc fmrc = Fmrc.open(config, formatter);
NetcdfDataset dataset = new NetcdfDataset();
fmrc.getDataset2D(dataset);
dataset.setLocation(path);
return dataset;
}
public static NetcdfDataset acquireDataset(URI uri) throws IOException {
if (getFormat(uri) == FileFormat.FC) {
return acquireFeatureCollection(uri.toString());
} else {
return NetcdfDataset.acquireDataset(uri.toString(), null);
}
}
/**
* Returns a {@code NetcdfDataset} given an input object
*
* @param input
* the input object (usually a {@code File}, a
* {@code String} or a {@code FileImageInputStreamExt).
* @return {@code NetcdfDataset} in case of success.
* @throws IOException
* if some error occur while opening the dataset.
* @throws {@link IllegalArgumentException}
* in case the specified input is a directory
*/
public static NetcdfDataset getDataset(Object input) throws IOException {
NetcdfDataset dataset = null;
if (input instanceof URI) {
dataset = acquireDataset((URI) input);
}
else if (input instanceof File) {
final File file= (File) input;
if (!file.isDirectory()) {
dataset = acquireDataset(file.toURI());
} else {
throw new IllegalArgumentException("Error occurred during NetCDF file reading: The input file is a Directory.");
}
} else if (input instanceof String) {
File file = new File((String) input);
if (!file.isDirectory()) {
dataset = acquireDataset(file.toURI());
} else {
throw new IllegalArgumentException( "Error occurred during NetCDF file reading: The input file is a Directory.");
}
} else if (input instanceof URL) {
final URL tempURL = (URL) input;
String protocol = tempURL.getProtocol();
if (protocol.equalsIgnoreCase("file")) {
File file = ImageIOUtilities.urlToFile(tempURL);
if (!file.isDirectory()) {
dataset = acquireDataset(file.toURI());
} else {
throw new IllegalArgumentException( "Error occurred during NetCDF file reading: The input file is a Directory.");
}
} else if (protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("dods")) {
try {
dataset = acquireDataset(tempURL.toURI());
} catch (URISyntaxException e) {
throw new IOException(e);
}
}
} else if (input instanceof AccessibleStream) {
final AccessibleStream<?> stream= (AccessibleStream<?>) input;
if (stream.getBinding().isAssignableFrom(File.class)) {
final File file = ((AccessibleStream<File>) input).getTarget();
if (!file.isDirectory()) {
dataset = acquireDataset(file.toURI());
} else {
throw new IllegalArgumentException("Error occurred during NetCDF file reading: The input file is a Directory.");
}
} else if (stream.getBinding().isAssignableFrom(URI.class)) {
final URI uri = ((AccessibleStream<URI>) input).getTarget();
dataset = acquireDataset(uri);
}
}
return dataset;
}
/**
* Checks if the input is file based, and if yes, returns the file.
*
* @param input the input to check.
* @return the file or <code>null</code> if it is not file based.
* @throws IOException
*/
public static File getFile (Object input) throws IOException {
File guessedFile = null;
if (input instanceof File) {
guessedFile = (File) input;
} else if (input instanceof String) {
guessedFile = new File((String) input);
} else if (input instanceof URL) {
final URL tempURL = (URL) input;
String protocol = tempURL.getProtocol();
if (protocol.equalsIgnoreCase("file")) {
guessedFile = ImageIOUtilities.urlToFile(tempURL);
}
} else if (input instanceof URIImageInputStream) {
final URIImageInputStream uriInStream = (URIImageInputStream) input;
String uri = uriInStream.getUri().toString();
guessedFile = new File(uri);
} else if (input instanceof AccessibleStream) {
final AccessibleStream<?> stream= (AccessibleStream<?>) input;
if(stream.getBinding().isAssignableFrom(File.class)){
guessedFile = ((AccessibleStream<File>) input).getTarget();
}
}
// check
if (guessedFile.exists() && !guessedFile.isDirectory()) {
return guessedFile;
}
return null;
}
/**
* Returns a format to use for parsing values along the specified axis type.
* This method is invoked when parsing the date part of axis units like "<cite>days
* since 1990-01-01 00:00:00</cite>". Subclasses should override this
* method if the date part is formatted in a different way. The default
* implementation returns the following formats:
* <p>
* <ul>
* <li>For {@linkplain AxisType#Time time axis}, a {@link DateFormat}
* using the {@code "yyyy-MM-dd HH:mm:ss"} pattern in UTC
* {@linkplain TimeZone timezone}.</li>
* <li>For all other kind of axis, a {@link NumberFormat}.</li>
* </ul>
* <p>
* The {@linkplain Locale#CANADA Canada locale} is used by default for most
* formats because it is relatively close to ISO (for example regarding days
* and months order in dates) while using the English symbols.
*
* @param type
* The type of the axis.
* @param prototype
* An example of the values to be parsed. Implementations may
* parse this prototype when the axis type alone is not
* sufficient. For example the {@linkplain AxisType#Time time
* axis type} should uses the {@code "yyyy-MM-dd"} date
* pattern, but some files do not follow this convention and
* use the default local instead.
* @return The format for parsing values along the axis.
*/
public static Format getAxisFormat(final AxisType type,
final String prototype) {
if (!type.equals(AxisType.Time) && !(type.equals(AxisType.RunTime))) {
return NumberFormat.getNumberInstance(Locale.CANADA);
}
char dateSeparator = '-'; // The separator used in ISO format.
boolean twoDigitYear = false; //Year is two digits
boolean yearLast = false; // Year is first in ISO pattern.
boolean namedMonth = false; // Months are numbers in the ISO pattern.
boolean monthFirst = false; // Month first (assumes yearLast AND namedMonth true as well)
boolean addT = false;
boolean appendZ = false;
int dateLength = 0;
if (prototype != null) {
/*
* Performs a quick check on the prototype content. If the prototype
* seems to use a different date separator than the ISO one, we will
* adjust the pattern accordingly. Also checks if the year seems to
* appears last rather than first, and if the month seems to be
* written using letters rather than digits.
*/
int field = 1;
int digitCount = 0;
final int length = prototype.length();
for (int i = 0; i < length; i++) {
final char c = prototype.charAt(i);
if (Character.isWhitespace(c)) {
if (monthFirst && field == 1) {
dateLength++; //move to next field
} else {
break; // Checks only the dates, ignore the hours.
}
} else if (Character.isDigit(c)) {
digitCount++;
dateLength++;
continue; // Digits are legal in all cases.
} else if (Character.isLetter(c) && field <= 2) {
if (field == 1) {
yearLast = true;
monthFirst = true;
}
namedMonth = true;
dateLength++;
continue; // Letters are legal for month only.
} else if (field == 1 || monthFirst && field == 2) {
dateSeparator = c;
dateLength++;
} else if (c == dateSeparator) {
dateLength++;
} else if (c=='T') {
addT = true;
} else if (c=='Z' && i==length-1) {
appendZ = true;
}
if ((field == 1 || yearLast && field == 3 ) && digitCount <= 2) {
twoDigitYear = true;
}
digitCount = 0;
field++;
}
if (digitCount >= 4) {
yearLast = true;
twoDigitYear = false;
}
}
String pattern = null;
if (yearLast) {
pattern = (monthFirst? "MMM dd-" : "dd-" + (namedMonth ? "MMM-" : "MM-")) + (twoDigitYear ? "yy" : "yyyy" );
} else {
pattern = (twoDigitYear ? "yy-" : "yyyy-" ) + (namedMonth ? "MMM-" : "MM-") + "dd";
if (dateLength < pattern.length()) {
// case of truncated date
pattern = pattern.substring(0, dateLength);
}
}
pattern = pattern.replace('-', dateSeparator);
int lastColon = prototype.lastIndexOf(":"); //$NON-NLS-1$
if (lastColon != -1) {
pattern += addT ? "'T'" : " ";
pattern += prototype != null && lastColon >= 16 ? "HH:mm:ss" : "HH:mm";
}
//TODO: Improve me:
//Handle timeZone
pattern += appendZ ? "'Z'" : "";
final DateFormat format = new SimpleDateFormat(pattern, Locale.CANADA);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
/**
* Depending on the type of model/netcdf file, we will check for the
* presence of some coverageDescriptorsCache rather than some others. The method returns
* the type of check on which we need to leverage to restrict the set of
* interesting coverageDescriptorsCache. The method will check for some
* KEY/FLAGS/ATTRIBUTES within the input dataset in order to define the
* proper check type to be performed.
*
* @param dataset
* the input dataset.
* @return the proper {@link CheckType} to be performed on the specified
* dataset.
*/
public static CheckType getCheckType(NetcdfDataset dataset) {
CheckType ct = CheckType.UNSET;
if (dataset != null) {
ct = CheckType.ONLYGEOGRIDS;
}
return ct;
}
/**
* @param schemaDef
* @param crs
* @return
*/
public static SimpleFeatureType createFeatureType(String schemaName,String schemaDef, CoordinateReferenceSystem crs) {
SimpleFeatureType indexSchema=null;
if (schemaDef == null) {
throw new IllegalArgumentException("Unable to create feature type from null definition!");
}
schemaDef = schemaDef.trim();
// get the schema
try {
indexSchema = DataUtilities.createType(schemaName, schemaDef);
indexSchema = DataUtilities.createSubType(indexSchema, DataUtilities.attributeNames(indexSchema), crs);
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, e.getLocalizedMessage(), e);
indexSchema = null;
}
return indexSchema;
}
/**
* @return true if the GRIB library is available
*/
public static boolean isGribAvailable() {
return IS_GRIB_AVAILABLE;
}
/**
* @return true if the C Native NetCDF 4 library is available
*/
public static boolean isNC4CAvailable() {
return IS_NC4_LIBRARY_AVAILABLE;
}
public static boolean isCheckCoordinatePlugins() {
return CHECK_COORDINATE_PLUGINS;
}
/**
* @return An unmodifiable Set of Unsupported Dimension names
*/
public static Set<String> getUnsupportedDimensions() {
return UNSUPPORTED_DIMENSIONS;
}
/**
* @return an unmodifiable Set of the Dimensions to be ignored by the
* Coordinate parsing machinery
*/
public static Set<String> getIgnoredDimensions() {
return Collections.unmodifiableSet(IGNORED_DIMENSIONS);
}
/**
* Adds a dimension to the ignored dimensions set.
*/
public static void addIgnoredDimension(String dimensionName) {
IGNORED_DIMENSIONS.add(dimensionName);
}
/**
* Utility method for getting NoData from an input {@link Variable}
*
* @param var Variable instance
* @return a Number representing NoData
*/
public static Number getNodata(Variable var) {
if (var != null) {
// Getting all the Variable attributes
List<Attribute> attributes = var.getAttributes();
String fullName;
// Searching for FILL_VALUE or MISSING_VALUE attributes
for (Attribute attribute : attributes) {
fullName = attribute.getFullName();
if (fullName.equalsIgnoreCase(FILL_VALUE)
|| fullName.equalsIgnoreCase(MISSING_VALUE)) {
return attribute.getNumericValue();
}
}
}
return null;
}
/**
* Return the propery NetCDF dataType for the input datatype class
*
* @param classDataType
* @return
*/
public static DataType getNetCDFDataType(String classDataType) {
if (isATime(classDataType)) {
return DataType.DOUBLE;
} else if (classDataType.endsWith("Integer")) {
return DataType.INT;
} else if (classDataType.endsWith("Double")) {
return DataType.DOUBLE;
} else if (classDataType.endsWith("String")) {
return DataType.STRING;
}
return DataType.STRING;
}
/**
* Transcode a DataBuffer type into a NetCDF DataType .
*
* @param type the beam {@link ProductData} type to transcode.
* @return an NetCDF DataType type.
*/
public static DataType transcodeImageDataType(final int dataType) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
return DataType.BYTE;
case DataBuffer.TYPE_SHORT:
return DataType.SHORT;
case DataBuffer.TYPE_INT:
return DataType.INT;
case DataBuffer.TYPE_DOUBLE:
return DataType.DOUBLE;
case DataBuffer.TYPE_FLOAT:
return DataType.FLOAT;
case DataBuffer.TYPE_UNDEFINED:
default:
throw new IllegalArgumentException("Invalid input data type:" + dataType);
}
}
/**
* Return true in case that dataType refers to something which need to be handled
* as a Time (TimeStamp, Date)
* @param classDataType
* @return
*/
public final static boolean isATime(String classDataType) {
return (classDataType.endsWith("Timestamp") || classDataType.endsWith("Date"));
}
/**
* Get an Array of proper size and type.
*
* @param dimensions the dimensions
* @param varDataType the DataType of the required array
* @return
*/
public static Array getArray(int[] dimensions, DataType varDataType) {
if (dimensions == null)
throw new IllegalArgumentException("Illegal dimensions");
final int nDims = dimensions.length;
switch (nDims) {
case 6:
// 6D Arrays
if (varDataType == DataType.FLOAT) {
return new ArrayFloat.D6(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4], dimensions[5]);
} else if (varDataType == DataType.DOUBLE) {
return new ArrayDouble.D6(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4], dimensions[5]);
} else if (varDataType == DataType.BYTE) {
return new ArrayByte.D6(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4], dimensions[5]);
} else if (varDataType == DataType.SHORT) {
return new ArrayShort.D6(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4], dimensions[5]);
} else if (varDataType == DataType.INT) {
return new ArrayInt.D6(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4], dimensions[5]);
} else
throw new IllegalArgumentException("unsupported Datatype");
case 5:
// 5D Arrays
if (varDataType == DataType.FLOAT) {
return new ArrayFloat.D5(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4]);
} else if (varDataType == DataType.DOUBLE) {
return new ArrayDouble.D5(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4]);
} else if (varDataType == DataType.BYTE) {
return new ArrayByte.D5(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4]);
} else if (varDataType == DataType.SHORT) {
return new ArrayShort.D5(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4]);
} else if (varDataType == DataType.INT) {
return new ArrayInt.D5(dimensions[0], dimensions[1],
dimensions[2], dimensions[3], dimensions[4]);
} else
throw new IllegalArgumentException("unsupported Datatype");
case 4:
// 4D Arrays
if (varDataType == DataType.FLOAT) {
return new ArrayFloat.D4(dimensions[0], dimensions[1],
dimensions[2], dimensions[3]);
} else if (varDataType == DataType.DOUBLE) {
return new ArrayDouble.D4(dimensions[0], dimensions[1],
dimensions[2], dimensions[3]);
} else if (varDataType == DataType.BYTE) {
return new ArrayByte.D4(dimensions[0], dimensions[1],
dimensions[2], dimensions[3]);
} else if (varDataType == DataType.SHORT) {
return new ArrayShort.D4(dimensions[0], dimensions[1],
dimensions[2], dimensions[3]);
} else if (varDataType == DataType.INT) {
return new ArrayInt.D4(dimensions[0], dimensions[1],
dimensions[2], dimensions[3]);
} else
throw new IllegalArgumentException("unsupported Datatype");
case 3:
// 3D Arrays
if (varDataType == DataType.FLOAT) {
return new ArrayFloat.D3(dimensions[0], dimensions[1],
dimensions[2]);
} else if (varDataType == DataType.DOUBLE) {
return new ArrayDouble.D3(dimensions[0], dimensions[1],
dimensions[2]);
} else if (varDataType == DataType.BYTE) {
return new ArrayByte.D3(dimensions[0], dimensions[1],
dimensions[2]);
} else if (varDataType == DataType.SHORT) {
return new ArrayShort.D3(dimensions[0], dimensions[1],
dimensions[2]);
} else if (varDataType == DataType.INT) {
return new ArrayInt.D3(dimensions[0], dimensions[1],
dimensions[2]);
} else
throw new IllegalArgumentException("unsupported Datatype");
case 2:
// 2D Arrays
if (varDataType == DataType.FLOAT) {
return new ArrayFloat.D2(dimensions[0], dimensions[1]);
} else if (varDataType == DataType.DOUBLE) {
return new ArrayDouble.D2(dimensions[0], dimensions[1]);
} else if (varDataType == DataType.BYTE) {
return new ArrayByte.D2(dimensions[0], dimensions[1]);
} else if (varDataType == DataType.SHORT) {
return new ArrayShort.D2(dimensions[0], dimensions[1]);
} else if (varDataType == DataType.INT) {
return new ArrayInt.D2(dimensions[0], dimensions[1]);
} else
throw new IllegalArgumentException("unsupported Datatype");
case 1:
// 1D Arrays
if (varDataType == DataType.FLOAT) {
return new ArrayFloat.D1(dimensions[0]);
} else if (varDataType == DataType.DOUBLE) {
return new ArrayDouble.D1(dimensions[0]);
} else if (varDataType == DataType.BYTE) {
return new ArrayByte.D1(dimensions[0]);
} else if (varDataType == DataType.SHORT) {
return new ArrayShort.D1(dimensions[0]);
} else if (varDataType == DataType.INT) {
return new ArrayInt.D1(dimensions[0]);
} else
throw new IllegalArgumentException("unsupported Datatype");
}
throw new IllegalArgumentException("Unable to create a proper array unsupported Datatype");
}
/**
* Transcode a NetCDF Number into a proper Number instance.
*
* @param type the {@link DataType} to transcode.
* @return the proper number instance
*/
public static Number transcodeNumber(final DataType type, Number value) {
if (DataType.DOUBLE.equals(type)) {
return Double.valueOf(value.doubleValue());
} else if (DataType.FLOAT.equals(type)) {
return Float.valueOf(value.floatValue());
} else if (DataType.LONG.equals(type)) {
return Long.valueOf(value.longValue());
} else if (DataType.INT.equals(type)) {
return Integer.valueOf(value.intValue());
} else if (DataType.SHORT.equals(type)) {
return Short.valueOf(value.shortValue());
} else if (DataType.BYTE.equals(type)) {
return Byte.valueOf(value.byteValue());
}
throw new IllegalArgumentException("Unsupported type or value: type = " +
type.toString() + " value = " + value);
}
/**
* Default parameter behavior properties
* TODO: better way of handling configuration settings, such as read parameters.
*/
public final static String PARAMS_MAX_KEY = "org.geotools.coverage.io.netcdf.param.max";
public final static String PARAMS_MIN_KEY = "org.geotools.coverage.io.netcdf.param.min";
private static Set<String> PARAMS_MAX;
private static Set<String> PARAMS_MIN;
static {
refreshParameterBehaviors();
}
public static void refreshParameterBehaviors() {
PARAMS_MAX = new HashSet<String>();
String maxProperty = System.getProperty(PARAMS_MAX_KEY);
if (maxProperty != null) {
for (String param : maxProperty.split(",")) {
PARAMS_MAX.add(param.trim().toUpperCase());
}
}
String minProperty = System.getProperty(PARAMS_MIN_KEY);
PARAMS_MIN = new HashSet<String>();
if (minProperty != null) {
for (String param : minProperty.split(",")) {
PARAMS_MIN.add(param.trim().toUpperCase());
}
}
}
public enum ParameterBehaviour {
DO_NOTHING, MAX, MIN
}
public static ParameterBehaviour getParameterBehaviour(String parameter) {
if (PARAMS_MAX.contains(parameter.toUpperCase())) {
return ParameterBehaviour.MAX;
} else if (PARAMS_MIN.contains(parameter.toUpperCase())) {
return ParameterBehaviour.MIN;
} else {
return ParameterBehaviour.DO_NOTHING;
}
}
}