/**
* Copyright (C) 2012-2017 52°North Initiative for Geospatial Open Source
* Software GmbH
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published
* by the Free Software Foundation.
*
* If the program is linked with libraries which are licensed under one of
* the following licenses, the combination of the program with the linked
* library is not considered a "derivative work" of the program:
*
* - Apache License, version 2.0
* - Apache Software License, version 1.0
* - GNU Lesser General Public License, version 3
* - Mozilla Public License, versions 1.0, 1.1 and 2.0
* - Common Development and Distribution License (CDDL), version 1.0
*
* Therefore the distribution of the program linked with libraries licensed
* under the aforementioned licenses, is permitted by the copyright holders
* if the distribution is compliant with both the GNU General Public
* License version 2 and the aforementioned licenses.
*
* 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 General
* Public License for more details.
*/
package org.n52.sos.util;
import static org.geotools.factory.Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER;
import static org.geotools.referencing.ReferencingFactoryFinder.getCRSAuthorityFactory;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import org.geotools.factory.Hints;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.CRS;
import org.geotools.referencing.factory.AbstractAuthorityFactory;
import org.geotools.referencing.factory.DeferredAuthorityFactory;
import org.geotools.util.WeakCollectionCleaner;
import org.n52.sos.config.SettingsManager;
import org.n52.sos.config.annotation.Configurable;
import org.n52.sos.config.annotation.Setting;
import org.n52.sos.ds.FeatureQuerySettingsProvider;
import org.n52.sos.exception.CodedException;
import org.n52.sos.exception.ConfigurationException;
import org.n52.sos.exception.ows.InvalidParameterValueException;
import org.n52.sos.exception.ows.NoApplicableCodeException;
import org.n52.sos.ogc.filter.SpatialFilter;
import org.n52.sos.ogc.ows.OwsExceptionReport;
import org.n52.sos.ogc.sos.Range;
import org.n52.sos.ogc.sos.Sos2Constants;
import org.n52.sos.service.ServiceConfiguration;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchAuthorityCodeException;
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.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
/**
* Class to provide some methods for JTS Geometry which is used by
* {@link org.n52.sos.ds.FeatureQueryHandler} and SpatialFilteringProfile DAO.
*
* @since 4.0.0
*
*/
@Configurable
public class GeometryHandler implements Cleanupable, EpsgConstants {
/*
* longitude = east-west latitude = north-south
*/
private static final Logger LOGGER = LoggerFactory.getLogger(GeometryHandler.class);
private static GeometryHandler instance;
private static ReentrantLock creationLock = new ReentrantLock();
private boolean datasoureUsesNorthingFirst;
private List<Range> epsgsWithNorthingFirstAxisOrder = Lists.newArrayList();
private int storageEPSG;
private int storage3DEPSG;
private int defaultResponseEPSG;
private int defaultResponse3DEPSG;
private Set<String> supportedCRS = Sets.newHashSet();
private boolean spatialDatasource;
private String authority;
private CRSAuthorityFactory crsAuthority;
private Map<Integer, CoordinateReferenceSystem> supportedCRSMap = Maps.newHashMap();;
/**
* Private constructor
*/
private GeometryHandler() {
}
/**
* @return Returns a singleton instance of the GeometryHandler.
*/
public static GeometryHandler getInstance() {
if (instance == null) {
creationLock.lock();
try {
if (instance == null) {
// don't set instance before configuring, or other threads
// can get access to unconfigured instance!
final GeometryHandler newInstance = new GeometryHandler();
SettingsManager.getInstance().configure(newInstance);
newInstance.initCrsAuthoritycrsAuthority();
instance = newInstance;
}
} finally {
creationLock.unlock();
}
}
return instance;
}
private void initCrsAuthoritycrsAuthority() {
crsAuthority =
getCRSAuthorityFactory(authority, new Hints(FORCE_LONGITUDE_FIRST_AXIS_ORDER,
isEastingFirstEpsgCode(getStorageEPSG())));
}
@Override
public void cleanup() {
if (getCrsAuthorityFactory() != null) {
if (getCrsAuthorityFactory() instanceof DeferredAuthorityFactory) {
DeferredAuthorityFactory.exit();
}
if (getCrsAuthorityFactory() instanceof AbstractAuthorityFactory) {
try {
((AbstractAuthorityFactory) getCrsAuthorityFactory()).dispose();
} catch (FactoryException fe) {
LOGGER.error("Error while GeometryHandler clean up", fe);
}
}
}
/*
* close {@link WeakCollectionCleaner}
*
* Note: Not required if
* se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor is
* defined in the web.xml
*/
// WeakCollectionCleaner.DEFAULT.exit();
}
/**
* Get configured storage EPSG code
*
* @return Storage EPSG code
*/
public int getStorageEPSG() {
return storageEPSG;
}
/**
* Get configured storage 3D EPSG code
*
* @return Storage 3D EPSG code
*/
public int getStorage3DEPSG() {
return storage3DEPSG;
}
/**
* Get configured default response EPSG code
*
* @return Default response EPSG code
*/
public int getDefaultResponseEPSG() {
return defaultResponseEPSG;
}
/**
* Get configured default response 3D EPSG code
*
* @return Default response 3D EPSG code
*/
public int getDefaultResponse3DEPSG() {
return defaultResponse3DEPSG;
}
/**
* Set storage EPSG code from settings
*
* @param epsgCode
* EPSG code from settings
* @throws ConfigurationException
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.STORAGE_EPSG)
public void setStorageEpsg(final int epsgCode) throws ConfigurationException {
Validation.greaterZero("Storage EPSG Code", epsgCode);
storageEPSG = epsgCode;
addToSupportedCrs(epsgCode);
}
/**
* Set storage 3D EPSG code from settings
*
* @param epsgCode3D
* 3D EPSG code from settings
* @throws ConfigurationException
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.STORAGE_3D_EPSG)
public void setStorage3DEpsg(final int epsgCode3D) throws ConfigurationException {
Validation.greaterZero("Storage 3D EPSG Code", epsgCode3D);
storage3DEPSG = epsgCode3D;
addToSupportedCrs(epsgCode3D);
}
/**
* Set default response EPSG code from settings
*
* @param epsgCode
* EPSG code from settings
* @throws ConfigurationException
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.DEFAULT_RESPONSE_EPSG)
public void setDefaultResponseEpsg(final int epsgCode) throws ConfigurationException {
Validation.greaterZero("Storage EPSG Code", epsgCode);
defaultResponseEPSG = epsgCode;
addToSupportedCrs(epsgCode);
}
/**
* Set default response 3D EPSG code from settings
*
* @param epsgCode3D
* 3D EPSG code from settings
* @throws ConfigurationException
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.DEFAULT_RESPONSE_3D_EPSG)
public void setDefaultResponse3DEpsg(final int epsgCode3D) throws ConfigurationException {
Validation.greaterZero("Storage 3D EPSG Code", epsgCode3D);
defaultResponse3DEPSG = epsgCode3D;
addToSupportedCrs(epsgCode3D);
}
/**
* Set the supported EPSG codes
*
* @param supportedCRS
* Supported EPSG codes
* @throws ConfigurationException
*/
@Setting(FeatureQuerySettingsProvider.SUPPORTED_CRS_KEY)
public void setSupportedCRS(final String supportedCRS) throws ConfigurationException {
// Validation.notNull("Supported CRS codes as CSV string",
// supportedCRS);
this.supportedCRS.addAll(StringHelper.splitToSet(supportedCRS, Constants.COMMA_STRING));
}
@Setting(FeatureQuerySettingsProvider.AUTHORITY)
public void setAuthority(final String authority) {
Validation.notNull("The CRS authority", authority);
this.authority = authority;
}
public String getAuthority() {
return authority;
}
/**
* Add integer EPSG code to supported CRS set
*
* @param epsgCode
* Integer EPSG code
*/
private void addToSupportedCrs(int epsgCode) {
this.supportedCRS.add(Integer.toString(epsgCode));
}
/**
* Set the northing first indicator for the datasource
*
* @param datasoureUsesNorthingFirst
* Northing first indicator
*/
@Setting(FeatureQuerySettingsProvider.DATASOURCE_NORTHING_FIRST)
public void setDatasourceNorthingFirst(final boolean datasoureUsesNorthingFirst) {
this.datasoureUsesNorthingFirst = datasoureUsesNorthingFirst;
}
/**
* Check if the datasource uses northing first coordinates
*
* @return <code>true</code>, if the datasource uses northing first
* coordinates
*/
public boolean isDatasourceNorthingFirst() {
return datasoureUsesNorthingFirst;
}
/**
* Set the EPSG code ranges for which the coordinates should be switched
*
* @param codes
* EPSG code ranges
* @throws ConfigurationException
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.EPSG_CODES_WITH_NORTHING_FIRST)
public void setEpsgCodesWithNorthingFirstAxisOrder(final String codes) throws ConfigurationException {
Validation.notNullOrEmpty("EPSG Codes to switch coordinates for", codes);
final String[] splitted = codes.split(";");
for (final String entry : splitted) {
final String[] splittedEntry = entry.split("-");
Range r = null;
if (splittedEntry.length == 1) {
r = new Range(Integer.parseInt(splittedEntry[0]), Integer.parseInt(splittedEntry[0]));
} else if (splittedEntry.length == 2) {
r = new Range(Integer.parseInt(splittedEntry[0]), Integer.parseInt(splittedEntry[1]));
} else {
throw new ConfigurationException(String.format("Invalid format of entry in '%s': %s",
FeatureQuerySettingsProvider.EPSG_CODES_WITH_NORTHING_FIRST, entry));
}
epsgsWithNorthingFirstAxisOrder.add(r);
}
}
/**
* Set flag if the used datasource is a spatial datasource (provides spatial
* functions)
*
* @param spatialDatasource
* Flag if spatial datasource
*/
@Setting(FeatureQuerySettingsProvider.SPATIAL_DATASOURCE)
public void setSpatialDatasource(final boolean spatialDatasource) {
this.spatialDatasource = spatialDatasource;
}
/**
* Check if the EPSG code is northing first
*
* @param epsgCode
* EPSG code to check
* @return <code>true</code>, if the EPSG code is northing first
*/
public boolean isNorthingFirstEpsgCode(final int epsgCode) {
for (final Range r : epsgsWithNorthingFirstAxisOrder) {
if (r.contains(epsgCode)) {
return true;
}
}
return false;
}
/**
* Check if the EPSG code is easting first
*
* @param epsgCode
* EPSG code to check
* @return <code>true</code>, if the EPSG code is easting first
*/
public boolean isEastingFirstEpsgCode(final int epsgCode) {
return !isNorthingFirstEpsgCode(epsgCode);
}
/**
* Is datasource a spatial datasource
*
* @return Spatial datasource or not
*/
public boolean isSpatialDatasource() {
return spatialDatasource;
}
/**
* Switch the coordinate axis of geometry from or for datasource
*
* @param geom
* Geometry to switch coordinate axis
* @return Geometry with switched coordinate axis if needed
* @throws OwsExceptionReport
* If coordinate axis switching fails
*/
public Geometry switchCoordinateAxisFromToDatasourceIfNeeded(final Geometry geom) throws OwsExceptionReport {
if (geom != null && !geom.isEmpty()) {
if (isDatasourceNorthingFirst()) {
if (!isNorthingFirstEpsgCode(geom.getSRID())) {
return JTSHelper.switchCoordinateAxisOrder(geom);
}
return geom;
} else {
if (isNorthingFirstEpsgCode(geom.getSRID())) {
return JTSHelper.switchCoordinateAxisOrder(geom);
}
return geom;
}
}
return geom;
}
private Geometry switchCoordinateAxisIfNeeded(Geometry geometry, int targetSRID) throws OwsExceptionReport {
if (geometry != null && !geometry.isEmpty()) {
if ((isNorthingFirstEpsgCode(geometry.getSRID()) && isNorthingFirstEpsgCode(targetSRID))
|| (isEastingFirstEpsgCode(geometry.getSRID()) && isEastingFirstEpsgCode(targetSRID))) {
return geometry;
}
return JTSHelper.switchCoordinateAxisOrder(geometry);
}
return geometry;
}
/**
* Get Object value as Double value
*
* @param value
* Value to check
* @return Double value
*/
// TODO replace with JavaHelper.asDouble?
@Deprecated
public double getValueAsDouble(final Object value) {
if (value instanceof String) {
return Double.valueOf((String) value).doubleValue();
} else if (value instanceof BigDecimal) {
final BigDecimal bdValue = (BigDecimal) value;
return bdValue.doubleValue();
} else if (value instanceof Double) {
return ((Double) value).doubleValue();
}
return 0;
}
/**
* Get filter geometry for BBOX spatial filter and non spatial datasource
*
* @param filter
* SpatialFilter
* @return SpatialFilter geometry
* @throws OwsExceptionReport
* If SpatialFilter is not supported
*/
public Geometry getFilterForNonSpatialDatasource(final SpatialFilter filter) throws OwsExceptionReport {
switch (filter.getOperator()) {
case BBOX:
return switchCoordinateAxisFromToDatasourceIfNeeded(filter.getGeometry());
default:
throw new InvalidParameterValueException(Sos2Constants.GetObservationParams.spatialFilter, filter
.getOperator().name());
}
}
/**
* Get WKT string from longitude and latitude
*
* @param longitude
* Longitude coordinate
* @param latitude
* Latitude coordinate
* @return WKT string
*/
public String getWktString(final Object longitude, final Object latitude) {
final StringBuilder builder = new StringBuilder();
builder.append("POINT ").append(Constants.OPEN_BRACE_CHAR);
if (datasoureUsesNorthingFirst) {
builder.append(JavaHelper.asString(latitude)).append(Constants.BLANK_CHAR);
builder.append(JavaHelper.asString(longitude));
} else {
builder.append(JavaHelper.asString(longitude)).append(Constants.BLANK_CHAR);
builder.append(JavaHelper.asString(latitude));
}
builder.append(Constants.CLOSE_BRACE_CHAR);
return builder.toString();
}
/**
* Get WKT string from longitude and latitude with axis order as defined by
* EPSG code.
*
* @param longitude
* Longitude coordinate
* @param latitude
* Latitude coordinate
* @param epsg
* EPSG code to check for axis order
* @return WKT string
*/
public String getWktString(Object longitude, Object latitude, int epsg) {
return getWktString(longitude, latitude);
}
/**
* Check if geometry is in SpatialFilter envelopes
*
* @param geometry
* Geometry to check
* @param envelopes
* SpatialFilter envelopes
* @return True if geometry is contained in envelopes
*/
public boolean featureIsInFilter(final Geometry geometry, final List<Geometry> envelopes) {
if (geometry != null && !geometry.isEmpty()) {
for (final Geometry envelope : envelopes) {
if (envelope.contains(geometry)) {
return true;
}
}
}
return false;
}
/**
* Transforms the geometry to the storage EPSG code
*
* @param geometry
* Geometry to transform
* @return Transformed geometry
* @throws OwsExceptionReport
*/
public Geometry transformToStorageEpsg(final Geometry geometry) throws OwsExceptionReport {
if (geometry != null && !geometry.isEmpty()) {
CoordinateReferenceSystem sourceCRS = getCRS(geometry.getSRID());
int targetSRID;
if (sourceCRS.getCoordinateSystem().getDimension() == 3) {
targetSRID = getStorage3DEPSG();
} else {
targetSRID = getStorageEPSG();
}
return transform(geometry, targetSRID, sourceCRS, getCRS(targetSRID));
}
return geometry;
}
/**
* Transform geometry to this EPSG code
*
* @param geometry
* Geometry to transform
* @param targetSRID
* Target EPSG code
* @return Transformed geometry
* @throws OwsExceptionReport
*/
public Geometry transform(final Geometry geometry, final int targetSRID) throws OwsExceptionReport {
if (geometry != null && !geometry.isEmpty()) {
if (geometry.getSRID() == targetSRID) {
return geometry;
}
CoordinateReferenceSystem sourceCRS = getCRS(geometry.getSRID());
CoordinateReferenceSystem targetCRS = getCRS(targetSRID);
return transform(geometry, targetSRID, sourceCRS, targetCRS);
}
return geometry;
}
/**
* Transform geometry
*
* @param geometry
* Geometry to transform
* @param targetSRID
* TargetEPSG code
* @param sourceCRS
* Source CRS
* @param targetCRS
* Target CRS
* @return Transformed geometry
* @throws OwsExceptionReport
*/
private Geometry transform(final Geometry geometry, final int targetSRID,
final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS)
throws OwsExceptionReport {
if (sourceCRS.equals(targetCRS)) {
return geometry;
}
Geometry switchedCoordiantes = switchCoordinateAxisIfNeeded(geometry, targetSRID);
try {
MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS);
Geometry transformed = JTS.transform(switchedCoordiantes, transform);
transformed.setSRID(targetSRID);
return transformed;
} catch (FactoryException fe) {
throw new NoApplicableCodeException().causedBy(fe).withMessage("The EPSG code '%s' is not supported!",
switchedCoordiantes.getSRID());
} catch (MismatchedDimensionException mde) {
throw new NoApplicableCodeException().causedBy(mde).withMessage("The EPSG code '%s' is not supported!",
switchedCoordiantes.getSRID());
} catch (TransformException te) {
throw new NoApplicableCodeException().causedBy(te).withMessage("The EPSG code '%s' is not supported!",
switchedCoordiantes.getSRID());
}
}
/**
* Get CRS from EPSG code
*
* @param epsgCode
* EPSG code to get CRS for
* @return CRS fro EPSG code
* @throws CodedException
* If the geometry EPSG code is not supported
*/
private CoordinateReferenceSystem getCRS(final int epsgCode) throws CodedException {
CoordinateReferenceSystem coordinateReferenceSystem = supportedCRSMap.get(epsgCode);
if (coordinateReferenceSystem == null) {
coordinateReferenceSystem = createCRS(epsgCode);
supportedCRSMap.put(epsgCode, coordinateReferenceSystem);
}
return coordinateReferenceSystem;
}
/**
* Create CRS for EPSG code
*
* @param epsgCode
* EPSG code to create CRS for
* @return Created CRS
* @throws CodedException
* If the geometry EPSG code is not supported
*/
private CoordinateReferenceSystem createCRS(final int epsgCode) throws CodedException {
try {
return getCrsAuthorityFactory().createCoordinateReferenceSystem(EPSG_PREFIX + epsgCode);
} catch (NoSuchAuthorityCodeException nsace) {
throw new NoApplicableCodeException().causedBy(nsace).withMessage("The EPSG code '%s' is not supported!",
epsgCode);
} catch (FactoryException fe) {
throw new NoApplicableCodeException().causedBy(fe).withMessage("The EPSG code '%s' is not supported!",
epsgCode);
}
}
/**
* Get CSR authority
*
* @return CRS authority
*/
private CRSAuthorityFactory getCrsAuthorityFactory() {
return crsAuthority;
}
/**
* Get List of supported EPSG codes
*
* @return Supported EPSG codes
*/
public Set<String> getSupportedCRS() {
try {
Set<String> authorityCodes = getCrsAuthorityFactory().getAuthorityCodes(CoordinateReferenceSystem.class);
if (CollectionHelper.isNotEmpty(authorityCodes) && CollectionHelper.isNotEmpty(this.supportedCRS)) {
return CollectionHelper.conjunctCollectionsToSet(authorityCodes, this.supportedCRS);
} else if (CollectionHelper.isEmpty(authorityCodes)) {
return Sets.newHashSet(Integer.toString(getStorageEPSG()), Integer.toString(getStorage3DEPSG()));
}
return authorityCodes;
} catch (FactoryException fe) {
LOGGER.warn("Error while querying supported EPSG codes", fe);
}
return Collections.emptySet();
}
/**
* Transform envelope from source to target EPSG code
*
* @param envelope
* Envelope to transform
* @param sourceSRID
* Source EPSG code
* @param targetSRID
* Target EPSG code
* @return Transformed envelope
* @throws CodedException
* If the geometry EPSG code is not supported
*/
public Envelope transformEnvelope(Envelope envelope, int sourceSRID, int targetSRID) throws CodedException {
if (envelope != null && !envelope.isNull() && targetSRID > 0 && sourceSRID != targetSRID) {
CoordinateReferenceSystem sourceCRS = getCRS(sourceSRID);
CoordinateReferenceSystem targetCRS = getCRS(targetSRID);
try {
MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS);
Envelope transformed = JTS.transform(envelope, transform);
return transformed;
} catch (FactoryException fe) {
throw new NoApplicableCodeException().causedBy(fe).withMessage("The EPSG code '%s' is not supported!",
sourceSRID);
} catch (MismatchedDimensionException mde) {
throw new NoApplicableCodeException().causedBy(mde).withMessage(
"Transformation from EPSG code '%s' to '%s' fails!", sourceSRID, targetSRID);
} catch (TransformException te) {
throw new NoApplicableCodeException().causedBy(te).withMessage(
"TTransformation from EPSG code '%s' to '%s' fails!", sourceSRID, targetSRID);
}
}
return envelope;
}
/**
* Clears the supported Coordinate Reference Systems map
*/
@VisibleForTesting
protected void clearSupportedCRSMap() {
supportedCRSMap.clear();
}
public Set<String> addAuthorityCrsPrefix(Collection<Integer> crses) {
HashSet<String> withPrefix = Sets.newHashSetWithExpectedSize(crses.size());
for (Integer crs : crses) {
withPrefix.add(addAuthorityCrsPrefix(crs));
}
return withPrefix;
}
public String addAuthorityCrsPrefix(int crs) {
return new StringBuilder(getAuthority()).append(Constants.DOUBLE_COLON_STRING).append(crs).toString();
}
public Set<String> addOgcCrsPrefix(Collection<Integer> crses) {
HashSet<String> withPrefix = Sets.newHashSetWithExpectedSize(crses.size());
for (Integer crs : crses) {
withPrefix.add(addOgcCrsPrefix(crs));
}
return withPrefix;
}
public String addOgcCrsPrefix(int crs) {
return new StringBuilder(ServiceConfiguration.getInstance().getSrsNamePrefixSosV2()).append(crs).toString();
}
}