/** * Copyright (c) Codice Foundation * <p> * This 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, either version 3 of the * License, or any later version. * <p> * This program 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. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package org.codice.ddf.libs.geo.util; import org.apache.commons.collections.CollectionUtils; import org.codice.ddf.libs.geo.GeoFormatException; import org.geotools.factory.Hints; import org.geotools.geometry.jts.JTS; import org.geotools.referencing.CRS; import org.geotools.referencing.ReferencingFactoryFinder; import org.opengis.referencing.FactoryException; import org.opengis.referencing.ReferenceIdentifier; import org.opengis.referencing.crs.CRSAuthorityFactory; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vividsolutions.jts.geom.Geometry; /** * Convenience methods for performing geospatial conversions. */ public class GeospatialUtil { public static final String EPSG_4326 = "EPSG:4326"; public static final String EPSG_4326_URN = "urn:ogc:def:crs:EPSG::4326"; public static final String LAT_LON_ORDER = "LAT_LON"; public static final String LON_LAT_ORDER = "LON_LAT"; private static final Logger LOGGER = LoggerFactory.getLogger(GeospatialUtil.class); /** * Parses Latitude in the DMS format of DD:MM:SS.S N/S * * @param dmsLat Degrees Minutes Seconds formatted latitude. * @return Latitude in decimal degrees */ public static Double parseDMSLatitudeWithDecimalSeconds(String dmsLat) throws GeoFormatException { Double lat = null; if (dmsLat != null) { dmsLat = dmsLat.trim(); String hemi = dmsLat.substring(dmsLat.length() - 1); if (!(hemi.equalsIgnoreCase("N") || hemi.equalsIgnoreCase("S"))) { throw new GeoFormatException(String.format( "Unrecognized hemisphere, %s, should be 'N' or 'S'", hemi)); } int hemisphereMult = 1; if (hemi.equalsIgnoreCase("s")) { hemisphereMult = -1; } String numberPortion = dmsLat.substring(0, dmsLat.length() - 1); if (dmsLat.contains(":")) { String[] dmsArr = numberPortion.split(":"); int degrees = 0; try { degrees = Integer.parseInt(dmsArr[0]); } catch (NumberFormatException nfe) { throw new GeoFormatException(String.format( "Unable to parse degrees: %s from: %s", dmsArr[0], dmsLat), nfe); } int minutes = 0; double seconds = 0.0; if (dmsArr.length >= 2) { try { minutes = Integer.parseInt(dmsArr[1]); } catch (NumberFormatException nfe) { throw new GeoFormatException(String.format( "Unable to parse minutes: %s from: %s", dmsArr[1], dmsLat), nfe); } } if (dmsArr.length == 3) { try { seconds = Double.parseDouble(dmsArr[2]); } catch (NumberFormatException nfe) { throw new GeoFormatException(String.format( "Unable to parse seconds: %s from: %s", dmsArr[2], dmsLat), nfe); } } lat = hemisphereMult * (degrees + ((double) minutes / 60) + (seconds / 3600)); if (lat < -90 || lat > 90) { throw new GeoFormatException(String.format( "Invalid latitude provided (must be between -90 and 90 degrees), converted latitude: %f", lat)); } } } return lat; } /** * Parses Longitude in the DMS format of [D]DD:MM:SS.S E/W * * @param dmsLon Degrees Minutes Seconds formatted longitude. * @return Longitude in decimal degrees. */ public static Double parseDMSLongitudeWithDecimalSeconds(String dmsLon) throws GeoFormatException { Double lon = null; if (dmsLon != null) { dmsLon = dmsLon.trim(); String hemi = dmsLon.substring(dmsLon.length() - 1); int hemisphereMult = 1; if (!(hemi.equalsIgnoreCase("W") || hemi.equalsIgnoreCase("E"))) { throw new GeoFormatException(String.format( "Unrecognized hemisphere, %s, should be 'E' or 'W'", hemi)); } if (hemi.equalsIgnoreCase("w")) { hemisphereMult = -1; } String numberPortion = dmsLon.substring(0, dmsLon.length() - 1); if (dmsLon.contains(":")) { String[] dmsArr = numberPortion.split(":"); int degrees = 0; try { degrees = Integer.parseInt(dmsArr[0]); } catch (NumberFormatException nfe) { throw new GeoFormatException(String.format( "Unable to parse degrees: %s from: %s", dmsArr[0], dmsLon), nfe); } int minutes = 0; double seconds = 0.0; if (dmsArr.length >= 2) { try { minutes = Integer.parseInt(dmsArr[1]); } catch (NumberFormatException nfe) { throw new GeoFormatException(String.format( "Unable to parse minutes: %s from: %s", dmsArr[1], dmsLon), nfe); } } if (dmsArr.length == 3) { try { seconds = Double.parseDouble(dmsArr[2]); } catch (NumberFormatException nfe) { throw new GeoFormatException(String.format( "Unable to parse seconds: %s from: %s", dmsArr[2], dmsLon), nfe); } } lon = hemisphereMult * (degrees + ((double) minutes / 60) + (seconds / 3600)); if (lon < -180 || lon > 180) { throw new GeoFormatException(String.format( "Invalid longitude provided (must be between -180 and 180 degrees), converted longitude: %f", lon)); } } } return lon; } /** * Transform a geometry to EPSG:4326 format with lon/lat coordinate ordering. * NOTE: This method will perform the transform swapping coordinates even if the sourceCrsName is * EPSG:4326 * * @param geometry - Geometry to transform * @param sourceCrsName - Source geometry's coordinate reference system * @return Geometry - Transformed geometry into EPSG:4326 lon/lat coordinate system */ public static Geometry transformToEPSG4326LonLatFormat(Geometry geometry, String sourceCrsName) throws GeoFormatException { if (geometry == null) { throw new GeoFormatException("Unable to convert null geometry"); } //If we don't have source CRS just return geometry as we can't transform without that information if (sourceCrsName == null) { return geometry; } try { CoordinateReferenceSystem sourceCrs = CRS.decode(sourceCrsName); Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE); CRSAuthorityFactory factory = ReferencingFactoryFinder.getCRSAuthorityFactory("EPSG", hints); CoordinateReferenceSystem targetCRS = factory.createCoordinateReferenceSystem(EPSG_4326); MathTransform transform = CRS.findMathTransform(sourceCrs, targetCRS); return JTS.transform(geometry, transform); } catch (FactoryException | TransformException e) { throw new GeoFormatException("Unable to convert coordinate to " + EPSG_4326, e); } } /** * Transform a geometry to EPSG:4326 format with lon/lat coordinate ordering. * NOTE: This method will NOT perform the transform swapping coordinates even if the sourceCrsName is * EPSG:4326. * * @param geometry - Geometry to transform * @param sourceCrs - Source geometry's coordinate reference system * @return Geometry - Transformed geometry into EPSG:4326 lon/lat coordinate system */ public static Geometry transformToEPSG4326LonLatFormat(Geometry geometry, CoordinateReferenceSystem sourceCrs) throws GeoFormatException { if (geometry == null) { throw new GeoFormatException("Unable to convert null geometry"); } //If we don't have source CRS just return geometry as we can't transform without that information if (sourceCrs == null || CollectionUtils.isEmpty(sourceCrs.getIdentifiers())) { return geometry; } Geometry transformedGeometry = geometry; try { boolean sourceCrsMatchesTarget = false; for (ReferenceIdentifier referenceIdentifier : sourceCrs.getIdentifiers()) { if (referenceIdentifier.toString() .equalsIgnoreCase(EPSG_4326)) { sourceCrsMatchesTarget = true; break; } } if (!sourceCrsMatchesTarget) { Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE); CRSAuthorityFactory factory = ReferencingFactoryFinder.getCRSAuthorityFactory("EPSG", hints); CoordinateReferenceSystem targetCRS = factory.createCoordinateReferenceSystem( EPSG_4326); MathTransform transform = CRS.findMathTransform(sourceCrs, targetCRS); transformedGeometry = JTS.transform(geometry, transform); LOGGER.debug("Converted CRS {} into {} : {}", sourceCrs, EPSG_4326, geometry); } } catch (FactoryException | TransformException e) { throw new GeoFormatException("Unable to convert coordinate to " + EPSG_4326, e); } return transformedGeometry; } }