/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2015-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.coverage.io.netcdf.crs;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
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.quantity.Length;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import org.geotools.imageio.netcdf.utilities.NetCDFUtilities;
import org.geotools.referencing.CRS;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.Ellipsoid;
import ucar.nc2.Attribute;
import ucar.nc2.Variable;
import ucar.nc2.constants.CF;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.unidata.geoloc.LatLonPointImpl;
/**
* Class used to properly setup NetCDF CF Projection parameters.
* Given a known OGC Projection, it will take care of remapping the Projection's
* parameters to NetCDF CF GridMapping parameters if supported.
*
* @see <a href="http://cfconventions.org/Data/cf-conventions/cf-conventions-1.6/build/cf-conventions.html#appendix-grid-mappings">NetCDF CF, Appendix
* F: Grid Mappings</a>
*/
public class NetCDFProjection {
/** A Custom {@link CRSAuthorityFactory} used to parse custom NetCDF/GRIB CRSs */
private static List<CRSAuthorityFactory> crsFactories = new LinkedList<CRSAuthorityFactory>();
public final static String PARAMS_SEPARATOR = "#";
private final static java.util.logging.Logger LOGGER = Logger.getLogger(NetCDFProjection.class.toString());
/**
* NetCDF CF projection constructor
*/
public NetCDFProjection(String projectionName,
String ogcName, Map<String, String> parametersMapping) {
this.name = projectionName;
this.ogcName = ogcName;
this.netCDFParametersMapping = Collections.unmodifiableMap(parametersMapping);
}
/**
* Mapping between OGC Referencing Parameters and NetCDF Projection attributes
*
* As an instance:
* CENTRAL_MERIDIAN <-> CF.LONGITUDE_OF_PROJECTION_ORIGIN
* SCALE_FACTOR <-> CF.SCALE_FACTOR_AT_CENTRAL_MERIDIAN
*/
private Map<String, String> netCDFParametersMapping;
/** The NetCDF-CF GridMapping name */
private String name;
/** The OGC Projection name, needed to instantiate projection parameters */
private String ogcName;
/**
* Returns the underlying unmodifiable Referencing to NetCDF parameters mapping.
*
* @return
*/
public Map<String, String> getParameters() {
return netCDFParametersMapping;
}
/**
* Return the NetCDF CF GridMapping name
*
* @return
*/
public String getName() {
return name;
}
/**
* Return the OGC/GeoTools projection name
*
* @return
*/
public String getOGCName() {
return ogcName;
}
/**
* Subclasses override this if they wish to adjust OGC parameters after they are read from NetCDF. This is the inverse of
* {@link #getNetcdfParameters(ParameterValueGroup)}.
*
* @param netcdfParameters parameter values read from NetCDF
* @return parameter values used for OGC projection
*/
public ParameterValueGroup getOgcParameters(ParameterValueGroup netcdfParameters) {
return netcdfParameters;
}
/**
* Subclasses override this if they wish to adjust OGC parameters before they are written to NetCDF. This is the inverse of
* {@link #getOgcParameters(ParameterValueGroup)}.
*
* @param ogcParameters parameter values used for OGC projection
* @return parameter values written to NetCDF
*/
public ParameterValueGroup getNetcdfParameters(ParameterValueGroup ogcParameters) {
return ogcParameters;
}
/**
* Currently supported NetCDF projections. Check the CF Document
*
* @see <a href="http://cfconventions.org/Data/cf-conventions/cf-conventions-1.6/build/cf-conventions.html#appendix-grid-mappings">NetCDF CF,
* Appendix F: Grid Mappings</a>
*/
public final static NetCDFProjection ALBERS_EQUAL_AREA;
public final static NetCDFProjection MERCATOR_1SP;
public final static NetCDFProjection MERCATOR_2SP;
public final static NetCDFProjection LAMBERT_AZIMUTHAL_EQUAL_AREA;
public final static NetCDFProjection TRANSVERSE_MERCATOR;
public final static NetCDFProjection ORTHOGRAPHIC;
public final static NetCDFProjection POLAR_STEREOGRAPHIC;
public final static NetCDFProjection STEREOGRAPHIC;
public final static NetCDFProjection LAMBERT_CONFORMAL_CONIC_1SP;
public final static NetCDFProjection LAMBERT_CONFORMAL_CONIC_2SP;
public final static NetCDFProjection ROTATED_POLE;
/** The map of currently supported NetCDF CF Grid mappings */
private final static Map<String, NetCDFProjection> supportedProjections = new HashMap<String, NetCDFProjection>();
static {
// CF parameters may be made of 2 Parameters separated by a SEPARATOR_CHAR
// This is used on writing operations when we need to map the same OGC input parameter
// to N different NetCDF parameters
// As an instance this happens in Lambert_conformal_conic_1sp where
// latitude of origin maps on both latitude_of_projection_origin and standard_parallel
// Setting up Albers conical equal area
Map<String, String> alberseq_mapping = new HashMap<String, String>();
alberseq_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.LONGITUDE_OF_CENTRAL_MERIDIAN);
alberseq_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN);
alberseq_mapping.put(NetCDFUtilities.STANDARD_PARALLEL_1, CF.STANDARD_PARALLEL);
alberseq_mapping.put(NetCDFUtilities.STANDARD_PARALLEL_2, CF.STANDARD_PARALLEL);
alberseq_mapping.put(NetCDFUtilities.FALSE_EASTING, CF.FALSE_EASTING);
alberseq_mapping.put(NetCDFUtilities.FALSE_NORTHING, CF.FALSE_NORTHING);
ALBERS_EQUAL_AREA = new NetCDFProjection(CF.ALBERS_CONICAL_EQUAL_AREA, "Albers_Conic_Equal_Area", alberseq_mapping);
// Setting up Lambert Azimuthal equal area
Map<String, String> lazeq_mapping = new HashMap<String, String>();
lazeq_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.LONGITUDE_OF_PROJECTION_ORIGIN);
lazeq_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN);
lazeq_mapping.put(NetCDFUtilities.FALSE_EASTING, CF.FALSE_EASTING);
lazeq_mapping.put(NetCDFUtilities.FALSE_NORTHING, CF.FALSE_NORTHING);
LAMBERT_AZIMUTHAL_EQUAL_AREA = new NetCDFProjection(CF.LAMBERT_AZIMUTHAL_EQUAL_AREA, CF.LAMBERT_AZIMUTHAL_EQUAL_AREA, lazeq_mapping);
// Setting up Transverse Mercator
Map<String, String> tm_mapping = new HashMap<String, String>();
tm_mapping.put(NetCDFUtilities.SCALE_FACTOR, CF.SCALE_FACTOR_AT_CENTRAL_MERIDIAN);
tm_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.LONGITUDE_OF_CENTRAL_MERIDIAN);
tm_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN);
tm_mapping.put(NetCDFUtilities.FALSE_EASTING, CF.FALSE_EASTING);
tm_mapping.put(NetCDFUtilities.FALSE_NORTHING, CF.FALSE_NORTHING);
TRANSVERSE_MERCATOR = new NetCDFProjection(CF.TRANSVERSE_MERCATOR, CF.TRANSVERSE_MERCATOR, tm_mapping);
// Setting up Orthographic
Map<String, String> ortho_mapping = new HashMap<String, String>();
ortho_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.LONGITUDE_OF_PROJECTION_ORIGIN);
ortho_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN);
ortho_mapping.put(NetCDFUtilities.FALSE_EASTING, CF.FALSE_EASTING);
ortho_mapping.put(NetCDFUtilities.FALSE_NORTHING, CF.FALSE_NORTHING);
ORTHOGRAPHIC = new NetCDFProjection(CF.ORTHOGRAPHIC, CF.ORTHOGRAPHIC, ortho_mapping);
// Setting up Polar Stereographic
Map<String, String> polarstereo_mapping = new HashMap<String, String>();
polarstereo_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.STRAIGHT_VERTICAL_LONGITUDE_FROM_POLE);
polarstereo_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN);
polarstereo_mapping.put(NetCDFUtilities.SCALE_FACTOR, CF.SCALE_FACTOR_AT_PROJECTION_ORIGIN);
polarstereo_mapping.put(NetCDFUtilities.FALSE_EASTING, CF.FALSE_EASTING);
polarstereo_mapping.put(NetCDFUtilities.FALSE_NORTHING, CF.FALSE_NORTHING);
POLAR_STEREOGRAPHIC = new NetCDFProjection(CF.POLAR_STEREOGRAPHIC, CF.POLAR_STEREOGRAPHIC, polarstereo_mapping);
// Setting up Stereographic
Map<String, String> stereo_mapping = new HashMap<String, String>();
stereo_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.LONGITUDE_OF_PROJECTION_ORIGIN);
stereo_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN);
stereo_mapping.put(NetCDFUtilities.SCALE_FACTOR, CF.SCALE_FACTOR_AT_PROJECTION_ORIGIN);
stereo_mapping.put(NetCDFUtilities.FALSE_EASTING, CF.FALSE_EASTING);
stereo_mapping.put(NetCDFUtilities.FALSE_NORTHING, CF.FALSE_NORTHING);
STEREOGRAPHIC = new NetCDFProjection(CF.STEREOGRAPHIC, CF.STEREOGRAPHIC, stereo_mapping);
// Setting up Lambert Conformal Conic base params
Map<String, String> lcc_mapping = new HashMap<String, String>();
lcc_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.LONGITUDE_OF_CENTRAL_MERIDIAN);
lcc_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN);
lcc_mapping.put(NetCDFUtilities.FALSE_EASTING, CF.FALSE_EASTING);
lcc_mapping.put(NetCDFUtilities.FALSE_NORTHING, CF.FALSE_NORTHING);
// Setting up Lambert Conformal Conic 1SP
Map<String, String> lcc_1sp_mapping = new HashMap<String, String>();
lcc_1sp_mapping.putAll(lcc_mapping);
lcc_1sp_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN +
PARAMS_SEPARATOR + CF.STANDARD_PARALLEL);
LAMBERT_CONFORMAL_CONIC_1SP = new NetCDFProjection(CF.LAMBERT_CONFORMAL_CONIC, CF.LAMBERT_CONFORMAL_CONIC + "_1SP", lcc_1sp_mapping);
// Setting up Lambert Conformal Conic 2SP
Map<String, String> lcc_2sp_mapping = new HashMap<String, String>();
lcc_2sp_mapping.putAll(lcc_mapping);
lcc_2sp_mapping.put(NetCDFUtilities.STANDARD_PARALLEL_1, CF.STANDARD_PARALLEL);
lcc_2sp_mapping.put(NetCDFUtilities.STANDARD_PARALLEL_2, CF.STANDARD_PARALLEL);
LAMBERT_CONFORMAL_CONIC_2SP = new NetCDFProjection(CF.LAMBERT_CONFORMAL_CONIC, CF.LAMBERT_CONFORMAL_CONIC + "_2SP", lcc_2sp_mapping);
// Settinc up Mercator base params
Map<String, String> mercator_mapping = new HashMap<String, String>();
mercator_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.LONGITUDE_OF_CENTRAL_MERIDIAN);
mercator_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.LATITUDE_OF_PROJECTION_ORIGIN);
mercator_mapping.put(NetCDFUtilities.FALSE_EASTING, CF.FALSE_EASTING);
mercator_mapping.put(NetCDFUtilities.FALSE_NORTHING, CF.FALSE_NORTHING);
// Setting up Mercator 1SP
Map<String, String> mercator_1sp_mapping = new HashMap<String, String>();
mercator_1sp_mapping.putAll(mercator_mapping);
mercator_1sp_mapping.put(NetCDFUtilities.SCALE_FACTOR, CF.SCALE_FACTOR_AT_PROJECTION_ORIGIN);
MERCATOR_1SP = new NetCDFProjection(CF.MERCATOR, CF.MERCATOR + "_1SP", mercator_1sp_mapping);
// Setting up Mercator 2SP
Map<String, String> mercator_2sp_mapping = new HashMap<String, String>();
mercator_2sp_mapping.putAll(mercator_mapping);
mercator_2sp_mapping.put(NetCDFUtilities.STANDARD_PARALLEL_1, CF.STANDARD_PARALLEL);
MERCATOR_2SP = new NetCDFProjection(CF.MERCATOR, CF.MERCATOR + "_2SP", mercator_2sp_mapping);
// Setting up Rotated Pole
Map<String, String> rotated_pole_mapping = new HashMap<String, String>();
rotated_pole_mapping.put(NetCDFUtilities.CENTRAL_MERIDIAN, CF.GRID_NORTH_POLE_LONGITUDE);
rotated_pole_mapping.put(NetCDFUtilities.LATITUDE_OF_ORIGIN, CF.GRID_NORTH_POLE_LATITUDE);
ROTATED_POLE = new NetCDFProjection(CF.ROTATED_LATITUDE_LONGITUDE, "Rotated_Pole",
rotated_pole_mapping) {
/*
* Convert north_pole_longitude and north_pole_latitude to central_meridian and latitude_of_origin.
*/
@Override
public ParameterValueGroup getOgcParameters(ParameterValueGroup netcdfParameters) {
double lonNorthPole = (Double) netcdfParameters
.parameter(NetCDFUtilities.CENTRAL_MERIDIAN).getValue();
double latNorthPole = (Double) netcdfParameters
.parameter(NetCDFUtilities.LATITUDE_OF_ORIGIN).getValue();
// Rotated pole is ambiguous so we assume an origin in the northern hemisphere
if (latNorthPole >= 90 || latNorthPole <= 0) {
throw new RuntimeException("Unexpected north pole latitude: " + latNorthPole);
}
double lonOrigin = LatLonPointImpl.lonNormal(lonNorthPole + 180);
double latOrigin = 90 - latNorthPole;
ParameterValueGroup ogcParameters = netcdfParameters.clone();
ogcParameters.parameter(NetCDFUtilities.CENTRAL_MERIDIAN).setValue(lonOrigin);
ogcParameters.parameter(NetCDFUtilities.LATITUDE_OF_ORIGIN).setValue(latOrigin);
return ogcParameters;
}
/*
* Convert central_meridian and latitude_of_origin to north_pole_longitude and north_pole_latitude.
*/
@Override
public ParameterValueGroup getNetcdfParameters(ParameterValueGroup ogcParameters) {
double lonOrigin = (Double) ogcParameters
.parameter(NetCDFUtilities.CENTRAL_MERIDIAN).getValue();
double latOrigin = (Double) ogcParameters
.parameter(NetCDFUtilities.LATITUDE_OF_ORIGIN).getValue();
// Rotated pole is ambiguous so we assumed above an origin in the
// northern hemisphere and do not expect anything else here
if (latOrigin >= 90 || latOrigin <= 0) {
throw new RuntimeException("Unexpected latitude of origin: " + latOrigin);
}
double lonNorthPole = LatLonPointImpl.lonNormal(lonOrigin + 180);
double latNorthPole = 90 - latOrigin;
ParameterValueGroup netcdfParameters = ogcParameters.clone();
netcdfParameters.parameter(NetCDFUtilities.CENTRAL_MERIDIAN).setValue(lonNorthPole);
netcdfParameters.parameter(NetCDFUtilities.LATITUDE_OF_ORIGIN)
.setValue(latNorthPole);
return netcdfParameters;
}
};
supportedProjections.put(CF.ALBERS_CONICAL_EQUAL_AREA, ALBERS_EQUAL_AREA);
supportedProjections.put(CF.MERCATOR + "_1SP", MERCATOR_1SP);
supportedProjections.put(CF.MERCATOR + "_1SP", MERCATOR_2SP);
supportedProjections.put(TRANSVERSE_MERCATOR.name, TRANSVERSE_MERCATOR);
supportedProjections.put(CF.LAMBERT_CONFORMAL_CONIC + "_1SP", LAMBERT_CONFORMAL_CONIC_1SP);
supportedProjections.put(CF.LAMBERT_CONFORMAL_CONIC + "_2SP", LAMBERT_CONFORMAL_CONIC_2SP);
supportedProjections.put(LAMBERT_AZIMUTHAL_EQUAL_AREA.name, LAMBERT_AZIMUTHAL_EQUAL_AREA);
supportedProjections.put(ORTHOGRAPHIC.name, ORTHOGRAPHIC);
supportedProjections.put(POLAR_STEREOGRAPHIC.name, POLAR_STEREOGRAPHIC);
supportedProjections.put(STEREOGRAPHIC.name, STEREOGRAPHIC);
supportedProjections.put(ROTATED_POLE.name, ROTATED_POLE);
for (final CRSAuthorityFactory factory : ReferencingFactoryFinder
.getCRSAuthorityFactories(null)) {
// Retrieve the registered custom factory
final CRSAuthorityFactory f = (CRSAuthorityFactory) factory;
// There may be multiple factories. Let take them in prioritized order
// using the linkedList
if (f instanceof NetCDFCRSAuthorityFactory) {
crsFactories.add(f);
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("NetCDF CRS Factory found: " + f);
}
}
}
// TODO:
// AZIMUTHAL_EQUIDISTANT, LAMBERT_CYLINDRICAL_EQUAL_AREA,
}
/**
* Get a NetCDF Projection definition referred by name
*/
public static NetCDFProjection getSupportedProjection(String projectionName) {
if (supportedProjections.containsKey(projectionName)) {
return supportedProjections.get(projectionName);
} else {
LOGGER.severe("The specified projection isn't currently supported: " + projectionName);
return null;
}
}
/**
* Extract the georeferencing projection information from the specified variable
* and setup a {@link CoordinateReferenceSystem} instance
* @throws FactoryException
* */
public static CoordinateReferenceSystem parseProjection(Variable var) throws FactoryException {
// Preliminar check on spatial_ref attribute which may contain a fully defined WKT
// as an instance, being set from GDAL, or a GeoTools NetCDF ouput
Attribute spatialRef = var.findAttribute(NetCDFUtilities.SPATIAL_REF);
CoordinateReferenceSystem crs = parseSpatialRef(spatialRef);
if (crs != null) {
return crs;
}
// Spatial ref is missing: fallback on GridMapping
Attribute gridMappingName = var.findAttribute(NetCDFUtilities.GRID_MAPPING_NAME);
if (gridMappingName == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("No grid_mapping_name attribute has been found.\n "
+ "Unable to parse a CF projection from this variable.\n"
+ "This probably means that is WGS84 or unsupported");
}
return null;
}
// special Management for multiple standard parallels to use
// the proper projection
String mappingName = gridMappingName.getStringValue();
String projectionName = getProjectionName(mappingName, var);
// Getting the proper projection and set the projection parameters
NetCDFProjection projection = supportedProjections.get(projectionName);
if (projection == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Unsupported grid_mapping_name: " + projectionName);
}
return null;
}
String ogcName = projection.getOGCName();
// The OGC projection parameters
ParameterValueGroup netcdfParameters = ProjectionBuilder.getDefaultparameters(ogcName);
// Get the OGC to NetCDF projection parameters
Map<String, String> netCDFParamsMapping = projection.getParameters();
Set<String> ogcParameterKeys = netCDFParamsMapping.keySet();
for (String ogcParameterKey: ogcParameterKeys) {
handleParam(netCDFParamsMapping, netcdfParameters, ogcParameterKey, var);
}
ParameterValueGroup ogcParameters = projection.getOgcParameters(netcdfParameters);
// Ellipsoid
Ellipsoid ellipsoid = buildEllipsoid(var, SI.METER);
return ProjectionBuilder.buildCRS(java.util.Collections.singletonMap(NetCDFUtilities.NAME, projectionName), ogcParameters, ellipsoid);
}
/**
* Get the NetCDF Attribute related to the specified OGC parameter and set the
* proper value within the OGC parameters map.
*
* @param parametersMapping
* @param ogcParameters
* @param ogcParameterKey
* @param var
*/
private static void handleParam(Map<String, String> parametersMapping,
ParameterValueGroup ogcParameters,
String ogcParameterKey, Variable var) {
String netCDFattributeName = getInputAttribute(parametersMapping.get(ogcParameterKey));
Double value = null;
// Special case for standard parallels
if (ogcParameterKey.equalsIgnoreCase(NetCDFUtilities.STANDARD_PARALLEL_1)
|| ogcParameterKey.equalsIgnoreCase(NetCDFUtilities.STANDARD_PARALLEL_2)) {
Attribute attribute = var.findAttribute(netCDFattributeName);
if (attribute != null) {
final int numValues = attribute.getLength();
if (numValues > 1) {
// Get the proper standard parallel if that's the case
int index = ogcParameterKey.equalsIgnoreCase(NetCDFUtilities.STANDARD_PARALLEL_1) ? 0 : 1;
Number number = (Number) attribute.getValue(index);
value = number.doubleValue();
} else {
value = attribute.getNumericValue().doubleValue();
}
}
} else {
Attribute attribute = var.findAttribute(netCDFattributeName);
if (attribute != null) {
// Get the parameter value and handle special management for longitudes outside -180, 180
value = attribute.getNumericValue().doubleValue();
if (netCDFattributeName.contains("meridian") || netCDFattributeName.contains("longitude")) {
value = value - (360) * Math.floor(value / (360) + 0.5);
}
}
}
if (value != null) {
// Set the OGC parameter
ogcParameters.parameter(ogcParameterKey).setValue(value);
}
}
private static String getInputAttribute(String cfParam) {
if (cfParam != null) {
return cfParam.contains(PARAMS_SEPARATOR) ? cfParam.split(PARAMS_SEPARATOR)[0] : cfParam;
}
return null;
}
/**
* Extract the {@link CoordinateReferenceSystem} from the
* {@link NetCDFUtilities#SPATIAL_REF} attribute if present.
* @param spatialRef the NetCDF SPATIAL_REF {@link Attribute} if any
* @return
*/
private static CoordinateReferenceSystem parseSpatialRef(Attribute spatialRef) {
CoordinateReferenceSystem crs = null;
if (spatialRef != null) {
String wkt = spatialRef.getStringValue();
try {
crs = CRS.parseWKT(wkt);
} catch (FactoryException e) {
if (LOGGER.isLoggable(Level.WARNING)){
LOGGER.warning("Unable to setup a CRS from the specified WKT: " + wkt);
}
}
}
return crs;
}
/**
* Build a custom ellipsoid, looking for definition parameters from a
* GridMapping variable
* @param gridMappingVariable the variable to be analyzed
* @param linearUnit the linear Unit to be used for the ellipsoid
* @return
*/
private static Ellipsoid buildEllipsoid(Variable gridMappingVariable, Unit<Length> linearUnit) {
Number semiMajorAxis = null;
Number semiMinorAxis = null;
Double inverseFlattening = Double.NEGATIVE_INFINITY;
// Preparing ellipsoid params to be sent to the NetCDFProjectionBuilder class
// in order to get back an Ellipsoid
Map<String, Number> ellipsoidParams = new HashMap<String, Number>();
// Looking for semiMajorAxis first
Attribute semiMajorAxisAttribute = gridMappingVariable.findAttribute(CF.SEMI_MAJOR_AXIS);
if (semiMajorAxisAttribute != null) {
semiMajorAxis = semiMajorAxisAttribute.getNumericValue();
ellipsoidParams.put(NetCDFUtilities.SEMI_MAJOR, semiMajorAxis);
}
// If not present, maybe it's a sphere. Looking for the radius
if (semiMajorAxis == null) {
semiMajorAxisAttribute = gridMappingVariable.findAttribute(CF.EARTH_RADIUS);
if (semiMajorAxisAttribute != null) {
semiMajorAxis = semiMajorAxisAttribute.getNumericValue();
ellipsoidParams.put(NetCDFUtilities.SEMI_MAJOR, semiMajorAxis);
}
}
// Looking for semiMinorAxis
Attribute semiMinorAxisAttribute = gridMappingVariable.findAttribute(CF.SEMI_MINOR_AXIS);
if (semiMinorAxisAttribute != null) {
semiMinorAxis = semiMinorAxisAttribute.getNumericValue();
ellipsoidParams.put(NetCDFUtilities.SEMI_MINOR, semiMinorAxis);
}
if (semiMinorAxis == null) {
// Looking for inverse Flattening
Attribute inverseFlatteningAttribute = gridMappingVariable.findAttribute(CF.INVERSE_FLATTENING);
if (inverseFlatteningAttribute != null) {
inverseFlattening = inverseFlatteningAttribute.getNumericValue().doubleValue();
}
ellipsoidParams.put(NetCDFUtilities.INVERSE_FLATTENING, inverseFlattening);
}
// Ellipsoid parameters have been set. Getting back an Ellipsoid from the
// builder
return ProjectionBuilder.createEllipsoid(NetCDFUtilities.UNKNOWN, ellipsoidParams);
}
/**
* Adjust the mappingName if needed. This may happen for some
* projections where different standard parallels may require
* _1SP or _2SP suffix.
*
* @param the input netCDF mappingName
* @param var the gridMapping variable
* @return
*/
private static String getProjectionName(String mappingName, Variable var) {
String projectionName = mappingName;
if (mappingName.equalsIgnoreCase(CF.LAMBERT_CONFORMAL_CONIC)) {
Attribute standardParallel = var.findAttribute(CF.STANDARD_PARALLEL);
projectionName = CF.LAMBERT_CONFORMAL_CONIC +
(standardParallel.getLength() == 1 ? "_1SP" : "_2SP");
} else if (mappingName.equalsIgnoreCase(CF.MERCATOR)) {
Attribute standardParallel = var.findAttribute(CF.STANDARD_PARALLEL);
projectionName = CF.MERCATOR + (standardParallel == null ? "_2SP" : "_1SP");
}
return projectionName;
}
/**
* Look for a SPATIAL_REF global attribute and parsing it (as WKT)
* to setup a {@link CoordinateReferenceSystem}
* @param dataset
* @return
*/
public static CoordinateReferenceSystem parseProjection(NetcdfDataset dataset) {
Attribute attribute = dataset.findAttribute(NetCDFUtilities.SPATIAL_REF);
return parseSpatialRef(attribute);
}
/**
* Check if any custom EPSG maps the provided crs and return that one
*
* @throws FactoryException
*/
public static CoordinateReferenceSystem lookupForCustomEpsg(CoordinateReferenceSystem crs)
throws FactoryException {
if (!crsFactories.isEmpty()) {
for (CRSAuthorityFactory crsFactory : crsFactories) {
Set<String> codes = crsFactory.getAuthorityCodes(CoordinateReferenceSystem.class);
for (String code : codes) {
CoordinateReferenceSystem decodedCRS = CRS.decode("EPSG:" + code);
if (CRS.equalsIgnoreMetadata(decodedCRS, crs)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Found valid epsgCode for the custom CRS: " + code);
}
return decodedCRS;
}
}
}
}
return crs;
}
/**
* Look for a CoordinateReferenceSystem defined into a gridMapping variable
*
* @param dataset
* @param defaultCrs
* @return
* @throws FactoryException
*/
public static CoordinateReferenceSystem lookForVariableCRS(NetcdfDataset dataset, CoordinateReferenceSystem defaultCrs) throws FactoryException {
List<Variable> variables = dataset.getVariables();
CoordinateReferenceSystem crs = defaultCrs;
for (Variable variable: variables) {
// TODO: Support for multiple coordinates 2D definitions within the same dataset
Attribute attrib = variable.findAttribute(NetCDFUtilities.GRID_MAPPING_NAME);
if (attrib != null) {
// Grid Mapping found
crs = NetCDFProjection.parseProjection(variable);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Detected NetCDFProjection through gridMapping variable: " + crs.toWKT());
}
break;
}
}
return crs;
}
/**
* Look for a dataset global {@link CoordinateReferenceSystem} definition
* provided through a spatial_ref global attribute.
*
* @param dataset
* @return
*/
public static CoordinateReferenceSystem lookForDatasetCRS(NetcdfDataset dataset) {
CoordinateReferenceSystem projection = NetCDFProjection.parseProjection(dataset);
if (projection != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Detected NetCDFProjection through spatial_ref attribute: "
+ projection.toWKT());
}
}
return projection;
}
}