/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2015, 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 java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.measure.unit.NonSI;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import javax.measure.unit.UnitFormat;
import org.geotools.factory.GeoTools;
import org.geotools.imageio.Identification;
import org.geotools.metadata.sql.MetadataException;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.cs.DefaultCoordinateSystemAxis;
import org.geotools.referencing.factory.ReferencingFactoryContainer;
import org.geotools.temporal.object.DefaultInstant;
import org.geotools.temporal.object.DefaultPosition;
import org.geotools.util.SimpleInternationalString;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.cs.TimeCS;
import org.opengis.referencing.cs.VerticalCS;
import org.opengis.referencing.datum.TemporalDatum;
import org.opengis.referencing.datum.VerticalDatum;
import org.opengis.referencing.datum.VerticalDatumType;
import org.opengis.temporal.Position;
import ucar.nc2.Attribute;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CF;
import ucar.nc2.dataset.CoordinateAxis;
import ucar.nc2.dataset.VariableDS;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.PrecisionModel;
/**
* Utility class to build {@link CoordinateReferenceSystem} objects.
*/
public class NetCDFCRSUtilities {
private final static java.util.logging.Logger LOGGER = Logger.getLogger(NetCDFCRSUtilities.class.toString());
public static final ReferencingFactoryContainer FACTORY_CONTAINER = ReferencingFactoryContainer.instance(GeoTools.getDefaultHints());
final static PrecisionModel PRECISION_MODEL = new PrecisionModel(PrecisionModel.FLOATING);
public final static GeometryFactory GEOM_FACTORY = new GeometryFactory(PRECISION_MODEL);
/**
* Set of commonly used symbols for "seconds".
*
* @todo Needs a more general way to set unit symbols once the Unit API is
* completed.
*/
private static final String[] DAYS = {"day", "dd", "days since"};
/**
* Set of commonly used symbols for "degrees".
*
* @todo Needs a more general way to set unit symbols once the Unit API is
* completed.
*/
private static final String[] DEGREES = {"degree", "degrees", "deg", "°"};
/**
* Set of commonly used symbols for "seconds".
*
* @todo Needs a more general way to set unit symbols once the Unit API is
* completed.
*/
private static final String[] HOURS = {"hour", "hh", "hours since"};
/**
* Set of commonly used symbols for "metres".
*
* @todo Needs a more general way to set unit symbols once the Unit API is
* completed.
*/
private static final String[] METERS = {"meter", "meters", "metre", "metres", "m"};
/**
* Set of commonly used symbols for "seconds".
*
* @todo Needs a more general way to set unit symbols once the Unit API is
* completed.
*/
private static final String[] MINUTES = {"minute", "min", "minutes since"};
/**
* Set of commonly used symbols for "seconds".
*
* @todo Needs a more general way to set unit symbols once the Unit API is
* completed.
*/
private static final String[] SECONDS = {"second", "sec", "seconds since"};
public final static Set<String> VERTICAL_AXIS_NAMES = new HashSet<String>();
/**
* The mapping between UCAR axis type and ISO axis directions.
*/
private static final Map<AxisType, String> DIRECTIONS = new HashMap<AxisType, String>(16);
private static final Map<AxisType, String> OPPOSITES = new HashMap<AxisType, String>(16);
static {
add(AxisType.Time, "future", "past");
add(AxisType.RunTime, "future", "past");
add(AxisType.GeoX, "east", "west");
add(AxisType.GeoY, "north", "south");
add(AxisType.GeoZ, "up", "down");
add(AxisType.Lat, "north", "south");
add(AxisType.Lon, "east", "west");
add(AxisType.Height, "up", "down");
add(AxisType.Pressure, "up", "down");
VERTICAL_AXIS_NAMES.add("elevation");
VERTICAL_AXIS_NAMES.add("height");
VERTICAL_AXIS_NAMES.add("z");
VERTICAL_AXIS_NAMES.add("depth");
VERTICAL_AXIS_NAMES.add("pressure");
}
/**
* The object to use for parsing and formatting units.
*/
private final static UnitFormat UNIT_FORMAT = UnitFormat.getInstance();
/**
* Adds a mapping between UCAR type and ISO direction.
*/
private static void add( final AxisType type, final String direction, final String opposite ) {
if (DIRECTIONS.put(type, direction) != null) {
throw new IllegalArgumentException(String.valueOf(type));
}
if (OPPOSITES.put(type, opposite) != null) {
throw new IllegalArgumentException(String.valueOf(type));
}
}
static String[] getUnitDirection(CoordinateAxis axis) {
AxisType type = axis.getAxisType();
String units = axis.getUnitsString();
/*
* Gets the axis direction, taking in account the possible reversal or
* vertical axis. Note that geographic and projected
* CoordinateReferenceSystem have the same directions. We can
* distinguish them either using the ISO CoordinateReferenceSystem type
* ("geographic" or "projected"), the ISO CS type ("ellipsoidal" or
* "cartesian") or the units ("degrees" or "m").
*/
String direction = DIRECTIONS.get(type);
if (direction != null) {
if (CF.POSITIVE_DOWN.equalsIgnoreCase(axis.getPositive())) {
direction = OPPOSITES.get(type);
}
final int offset = units.lastIndexOf('_');
if (offset >= 0) {
final String unitsDirection = units.substring(offset + 1).trim();
final String opposite = OPPOSITES.get(type);
if (unitsDirection.equalsIgnoreCase(opposite)) {
// TODO WARNING: INCONSISTENT AXIS ORIENTATION
direction = opposite;
}
if (unitsDirection.equalsIgnoreCase(direction)) {
units = units.substring(0, offset).trim();
}
}
}
return new String[]{units, direction};
}
/**
* Get the {@link AxisDirection} object related to the specified direction
*
* @param direction
* @return
*/
static AxisDirection getDirection( final String direction ) {
return AxisDirection.valueOf(direction);
}
/**
* Check if {@code toSearch} appears in the {@code list} array. Search is
* case-insensitive. This is a temporary patch (will be removed when the
* final API for JSR-108: Units specification will be available).
*/
private static boolean contains( final String toSearch, final String[] list ) {
for( int i = list.length; --i >= 0; ) {
if (toSearch.toLowerCase().contains(list[i].toLowerCase())) {
return true;
}
}
return false;
}
public static VerticalCRS buildVerticalCrs(CoordinateAxis zAxis ) {
VerticalCRS verticalCRS = null;
try {
if (zAxis != null) {
String axisName = zAxis.getFullName();
if (!NetCDFCRSUtilities.VERTICAL_AXIS_NAMES.contains(axisName)) {
return null;
}
String units = zAxis.getUnitsString();
AxisType axisType = zAxis.getAxisType();
String v_crsName = "Unknown";
String v_datumName = "Unknown";
String v_datumType = null;
v_datumName = new Identification("Mean Sea Level", null, null, "EPSG:5100").getName();
if (axisType == AxisType.RadialAzimuth || axisType == AxisType.GeoZ || axisType == AxisType.RadialElevation)
v_datumType = "geoidal";
else if (axisType == AxisType.Height) {
if (!zAxis.getShortName().equalsIgnoreCase("height")) {
v_datumType = "depth";
v_crsName = new Identification("mean sea level depth", null, null, "EPSG:5715").getName();
} else {
v_datumType = "geoidal";
v_crsName = new Identification("mean sea level height", null, null, "EPSG:5714").getName();
}
} else if (axisType == AxisType.Pressure) {
v_datumType = "barometric";
} else {
v_datumType = "other_surface";
}
/*
* Gets the axis direction, taking in account the possible reversal or
* vertical axis. Note that geographic and projected
* CoordinateReferenceSystem have the same directions. We can
* distinguish them either using the ISO CoordinateReferenceSystem type
* ("geographic" or "projected"), the ISO CS type ("ellipsoidal" or
* "cartesian") or the units ("degrees" or "m").
*/
String direction = DIRECTIONS.get(axisType);
if (direction != null) {
if (CF.POSITIVE_DOWN.equalsIgnoreCase(zAxis.getPositive())) {
direction = OPPOSITES.get(axisType);
}
final int offset = units.lastIndexOf('_');
if (offset >= 0) {
final String unitsDirection = units.substring(offset + 1).trim();
final String opposite = OPPOSITES.get(axisType);
if (unitsDirection.equalsIgnoreCase(opposite)) {
// TODO WARNING: INCONSISTENT AXIS ORIENTATION
direction = opposite;
}
if (unitsDirection.equalsIgnoreCase(direction)) {
units = units.substring(0, offset).trim();
}
}
}
final Map<String, String> csMap = Collections.singletonMap("name", "vertical_CS");
VerticalCS verticalCS = NetCDFCRSUtilities.FACTORY_CONTAINER.getCSFactory().createVerticalCS(csMap,
getAxis(zAxis.getShortName(), getDirection(direction), units));
// Creating the Vertical Datum
final Map<String, String> datumMap = Collections.singletonMap("name", v_datumName);
final VerticalDatum verticalDatum = NetCDFCRSUtilities.FACTORY_CONTAINER.getDatumFactory().createVerticalDatum(datumMap,
VerticalDatumType.valueOf(v_datumType));
final Map<String, String> crsMap = Collections.singletonMap("name", v_crsName);
verticalCRS = NetCDFCRSUtilities.FACTORY_CONTAINER.getCRSFactory().createVerticalCRS(crsMap, verticalDatum, verticalCS);
}
} catch (FactoryException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Unable to parse vertical CRS", e);
verticalCRS = null;
}
return verticalCRS;
}
public static TemporalCRS buildTemporalCrs( CoordinateAxis timeAxis ) {
String t_datumName = new Identification("ISO8601", null, null, null).getName();
TemporalCRS temporalCRS = null;
try {
if (timeAxis != null) {
AxisType type = timeAxis.getAxisType();
String units = timeAxis.getUnitsString();
/*
* Gets the axis direction, taking in account the possible reversal or
* vertical axis. Note that geographic and projected
* CoordinateReferenceSystem have the same directions. We can
* distinguish them either using the ISO CoordinateReferenceSystem type
* ("geographic" or "projected"), the ISO CS type ("ellipsoidal" or
* "cartesian") or the units ("degrees" or "m").
*/
String direction = DIRECTIONS.get(type);
if (direction != null) {
if (CF.POSITIVE_DOWN.equalsIgnoreCase(timeAxis.getPositive())) {
direction = OPPOSITES.get(type);
}
final int offset = units.lastIndexOf('_');
if (offset >= 0) {
final String unitsDirection = units.substring(offset + 1).trim();
final String opposite = OPPOSITES.get(type);
if (unitsDirection.equalsIgnoreCase(opposite)) {
// TODO WARNING: INCONSISTENT AXIS ORIENTATION
direction = opposite;
}
if (unitsDirection.equalsIgnoreCase(direction)) {
units = units.substring(0, offset).trim();
}
}
}
Date epoch = null;
String t_originDate = null;
if (AxisType.Time.equals(type) || AxisType.RunTime.equals(type)) {
String origin = null;
final String[] unitsParts = units.split("(?i)\\s+since\\s+");
if (unitsParts.length == 2) {
units = unitsParts[0].trim();
origin = unitsParts[1].trim();
} else {
final Attribute attribute = timeAxis.findAttribute("time_origin");
if (attribute != null) {
origin = attribute.getStringValue();
}
}
if (origin != null) {
origin = NetCDFTimeUtilities.trimFractionalPart(origin);
// add 0 digits if absent
origin = NetCDFTimeUtilities.checkDateDigits(origin);
try {
epoch = (Date) NetCDFUtilities.getAxisFormat(type, origin).parseObject(origin);
GregorianCalendar cal = new GregorianCalendar();
cal.setTime(epoch);
DefaultInstant instant = new DefaultInstant(new DefaultPosition(cal.getTime()));
t_originDate = instant.getPosition().getDateTime().toString();
} catch (ParseException e) {
throw new IllegalArgumentException(e);
// TODO: Change the handle this exception
}
}
}
String axisName = timeAxis.getShortName();
String t_csName = "time_CS";
final Map<String, String> csMap = Collections.singletonMap("name", t_csName);
final TimeCS timeCS = NetCDFCRSUtilities.FACTORY_CONTAINER.getCSFactory().createTimeCS(csMap, getAxis(axisName, getDirection(direction), units));
// Creating the Temporal Datum
if (t_datumName == null) {
t_datumName = "Unknown";
}
final Map<String, String> datumMap = Collections.singletonMap("name", t_datumName);
final Position timeOrigin = new DefaultPosition(new SimpleInternationalString(t_originDate));
final TemporalDatum temporalDatum = NetCDFCRSUtilities.FACTORY_CONTAINER.getDatumFactory().createTemporalDatum(datumMap, timeOrigin.getDate());
// Finally creating the Temporal CoordinateReferenceSystem
String crsName = "time_CRS";
final Map<String, String> crsMap = Collections.singletonMap("name", crsName);
temporalCRS = NetCDFCRSUtilities.FACTORY_CONTAINER.getCRSFactory().createTemporalCRS(crsMap, temporalDatum, timeCS);
}
} catch (FactoryException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Unable to parse temporal CRS", e);
temporalCRS = null;
} catch (ParseException e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Unable to parse temporal CRS", e);
temporalCRS = null;
}
return temporalCRS;
}
public static ucar.nc2.dataset.CoordinateSystem getCoordinateSystem(VariableDS variableDS) {
final List<ucar.nc2.dataset.CoordinateSystem> systems = variableDS.getCoordinateSystems();
if (systems.isEmpty()) {
throw new RuntimeException("Coordinate system for Variable " + variableDS.getFullName() + " haven't been found");
}
return systems.get(0);
}
public static final org.opengis.referencing.crs.CoordinateReferenceSystem WGS84;
static {
CoordinateReferenceSystem internalWGS84 = null;
try {
internalWGS84 = CRS.decode("EPSG:4326", true);
} catch (Exception e) {
internalWGS84 = DefaultGeographicCRS.WGS84;
}
WGS84 = internalWGS84;
}
/**
* Build a proper {@link CoordinateSystemAxis} given the set composed of
* axisName, axisDirection and axis unit of measure.
*
* @param axisName
* the name of the axis to be built.
* @param direction
* the {@linkplain AxisDirection direction} of the axis.
* @param unitName
* the unit of measure string.
* @return a proper {@link CoordinateSystemAxis} instance or {@code null} if
* unable to build it.
* @throws FactoryException
*/
static CoordinateSystemAxis getAxis( final String axisName, final AxisDirection direction, final String unitName )
throws FactoryException {
if (axisName == null) {
return null;
}
final DefaultCoordinateSystemAxis axisFound = DefaultCoordinateSystemAxis.getPredefined(axisName, direction);
if (axisFound != null) {
return axisFound;
}
/*
* The current axis defined in the metadata tree is not already known in
* the Geotools implementation, so one will build it using those
* information.
*/
final Unit< ? > unit = getUnit(unitName);
final Map<String, String> map = Collections.singletonMap("name", axisName);
try {
return FACTORY_CONTAINER.getCSFactory().createCoordinateSystemAxis(map, axisName, direction, unit);
} catch (FactoryException e) {
throw new FactoryException(e.getLocalizedMessage());
}
}
/**
* Returns the unit which matches with the name given.
*
* @param unitName
* The name of the unit. Should not be {@code null}.
* @return The unit matching with the specified name.
* @throws MetadataException
* if the unit name does not match with the
* {@linkplain #UNIT_FORMAT unit format}.
*/
static Unit< ? > getUnit( final String unitName ) throws FactoryException {
if (contains(unitName, METERS)) {
return SI.METER;
} else if (contains(unitName, DEGREES)) {
return NonSI.DEGREE_ANGLE;
} else if (contains(unitName, SECONDS)) {
return SI.SECOND;
} else if (contains(unitName, MINUTES)) {
return NonSI.MINUTE;
} else if (contains(unitName, HOURS)) {
return NonSI.HOUR;
} else if (contains(unitName, DAYS)) {
return NonSI.DAY;
} else {
try {
return (Unit< ? >) UNIT_FORMAT.parseObject(unitName);
} catch (ParseException e) {
throw new FactoryException("Unit not known : " + unitName, e);
}
}
}
}