/* * 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); } } } }