Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.n52.sos.util.GeometryHandler Maven / Gradle / Ivy
/*
* Copyright (C) 2012-2019 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 java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.CRS;
import org.geotools.referencing.CRS.AxisOrder;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.factory.AbstractAuthorityFactory;
import org.geotools.referencing.factory.DeferredAuthorityFactory;
import org.geotools.util.factory.Hints;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.n52.faroe.ConfigurationError;
import org.n52.faroe.Validation;
import org.n52.faroe.annotation.Configurable;
import org.n52.faroe.annotation.Setting;
import org.n52.iceland.util.Range;
import org.n52.janmayen.lifecycle.Constructable;
import org.n52.janmayen.lifecycle.Destroyable;
import org.n52.shetland.ogc.filter.SpatialFilter;
import org.n52.shetland.ogc.ows.exception.CodedException;
import org.n52.shetland.ogc.ows.exception.InvalidParameterValueException;
import org.n52.shetland.ogc.ows.exception.NoApplicableCodeException;
import org.n52.shetland.ogc.ows.exception.OwsExceptionReport;
import org.n52.shetland.util.CollectionHelper;
import org.n52.shetland.util.EnvelopeOrGeometry;
import org.n52.shetland.util.GeometryTransformer;
import org.n52.shetland.util.JTSHelper;
import org.n52.shetland.util.JavaHelper;
import org.n52.shetland.util.StringHelper;
import org.n52.sos.ds.FeatureQuerySettingsProvider;
import org.n52.svalbard.CodingSettings;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
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.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* Class to provide some methods for JTS Geometry which is used by
* {@link org.n52.sos.ds.FeatureQueryHandler}.
*
* @since 4.0.0
*
*/
@Configurable
public class GeometryHandler implements GeometryTransformer, Constructable, Destroyable {
/*
* longitude = east-west latitude = north-south
*/
private static final Logger LOGGER = LoggerFactory.getLogger(GeometryHandler.class);
private static final String EPSG = "EPSG";
private static final String EPSG_PREFIX = EPSG + ":";
private static final String EPSG_NOT_SUPPORTED_TMEPLATE = "The EPSG code '%s' is not supported!";
private boolean datasoureUsesNorthingFirst;
private final List epsgsWithNorthingFirstAxisOrder = Lists.newArrayList();
private int storageEPSG;
private int storage3DEPSG;
private int defaultResponseEPSG;
private int defaultResponse3DEPSG;
private final Set supportedCRS = Sets.newHashSet();
private boolean spatialDatasource;
private String authority;
private CRSAuthorityFactory crsAuthority;
private final Map crsCache = Maps.newConcurrentMap();
private String srsNamePrefixUrl;
@Setting(CodingSettings.SRS_NAME_PREFIX_URL)
public GeometryHandler setSrsNamePrefixUrl(String srsNamePrefixUrl) {
this.srsNamePrefixUrl = srsNamePrefixUrl;
return this;
}
@Override
public void init() {
boolean eastingFirstEpsgCode = true;
try {
eastingFirstEpsgCode = isEastingFirstEpsgCode(getStorageEPSG());
} catch (Exception e) {
LOGGER.error("The storage EPSG code '{}' is invalid. Easting first = true would be used!",
getStorageEPSG());
}
Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, eastingFirstEpsgCode);
this.crsAuthority = ReferencingFactoryFinder.getCRSAuthorityFactory(this.authority, hints);
}
@Override
public void destroy() {
if (this.crsAuthority == null) {
return;
}
if (this.crsAuthority instanceof DeferredAuthorityFactory) {
DeferredAuthorityFactory.exit();
}
if (this.crsAuthority instanceof AbstractAuthorityFactory) {
try {
((AbstractAuthorityFactory) this.crsAuthority).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 ConfigurationError
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.STORAGE_EPSG)
public void setStorageEpsg(int epsgCode) throws ConfigurationError {
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 ConfigurationError
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.STORAGE_3D_EPSG)
public void setStorage3DEpsg(int epsgCode3D) throws ConfigurationError {
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 ConfigurationError
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.DEFAULT_RESPONSE_EPSG)
public void setDefaultResponseEpsg(int epsgCode) throws ConfigurationError {
Validation.greaterZero("Default response EPSG Code", epsgCode);
defaultResponseEPSG = epsgCode;
addToSupportedCrs(epsgCode);
}
/**
* Set default response 3D EPSG code from settings
*
* @param epsgCode3D
* 3D EPSG code from settings
*
* @throws ConfigurationError
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.DEFAULT_RESPONSE_3D_EPSG)
public void setDefaultResponse3DEpsg(int epsgCode3D) throws ConfigurationError {
Validation.greaterZero("Default response 3D EPSG Code", epsgCode3D);
defaultResponse3DEPSG = epsgCode3D;
addToSupportedCrs(epsgCode3D);
}
/**
* Set the supported EPSG codes
*
* @param supportedCRS
* Supported EPSG codes
*
* @throws ConfigurationError
* If an error occurs
*/
@Setting(FeatureQuerySettingsProvider.SUPPORTED_CRS_KEY)
public void setSupportedCRS(final String supportedCRS) throws ConfigurationError {
// Validation.notNull("Supported CRS codes as CSV string",
// supportedCRS);
this.supportedCRS.clear();
this.supportedCRS.addAll(StringHelper.splitToSet(supportedCRS, ","));
}
/**
* Get List of supported EPSG codes
*
* @return Supported EPSG codes
*/
public Set getSupportedCRS() {
try {
Set 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();
}
@Setting(FeatureQuerySettingsProvider.AUTHORITY)
public GeometryHandler setAuthority(final String authority) {
Validation.notNull("The CRS authority", authority);
this.authority = authority;
return this;
}
public String getAuthority() {
return authority;
}
/**
* Add integer EPSG code to supported CRS set.
*
* @param epsgCode
* Integer EPSG code
*
* @return this
*/
private GeometryHandler addToSupportedCrs(int epsgCode) {
this.supportedCRS.add(Integer.toString(epsgCode));
return this;
}
/**
* Set the northing first indicator for the datasource.
*
* @param datasoureUsesNorthingFirst
* Northing first indicator
*
* @return this
*/
@Setting(FeatureQuerySettingsProvider.DATASOURCE_NORTHING_FIRST)
public GeometryHandler setDatasourceNorthingFirst(boolean datasoureUsesNorthingFirst) {
this.datasoureUsesNorthingFirst = datasoureUsesNorthingFirst;
return this;
}
/**
* Check if the datasource uses northing first coordinates.
*
* @return true
, 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 ConfigurationError
* If an error occurs
* @return this
*/
@Setting(FeatureQuerySettingsProvider.EPSG_CODES_WITH_NORTHING_FIRST)
public GeometryHandler setEpsgCodesWithNorthingFirstAxisOrder(String codes) throws ConfigurationError {
Validation.notNullOrEmpty("EPSG Codes to switch coordinates for", codes);
final String[] splitted = codes.split(";");
List newEpsgCodes = Lists.newArrayListWithCapacity(splitted.length);
for (final String entry : splitted) {
final String[] splittedEntry = entry.split("-");
Range r = null;
try {
switch (splittedEntry.length) {
case 1:
r = new Range(Integer.parseInt(splittedEntry[0]), Integer.parseInt(splittedEntry[0]));
break;
case 2:
r = new Range(Integer.parseInt(splittedEntry[0]), Integer.parseInt(splittedEntry[1]));
break;
default:
throw createException(entry, null);
}
} catch (NumberFormatException ex) {
throw createException(entry, ex);
}
newEpsgCodes.add(r);
epsgsWithNorthingFirstAxisOrder.add(r);
}
epsgsWithNorthingFirstAxisOrder.clear();
epsgsWithNorthingFirstAxisOrder.addAll(newEpsgCodes);
return this;
}
/**
* 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(boolean spatialDatasource) {
this.spatialDatasource = spatialDatasource;
}
/**
* Is datasource a spatial datasource.
*
* @return Spatial datasource or not
*/
public boolean isSpatialDatasource() {
return spatialDatasource;
}
/**
* Check if the EPSG code is northing first.
*
* @param epsgCode
* EPSG code to check
*
* @return true
, if the EPSG code is northing first
* @throws CodedException
* If an error occurs
*/
public boolean isNorthingFirstEpsgCode(int epsgCode) throws CodedException {
try {
return AxisOrder.NORTH_EAST.equals(CRS.getAxisOrder(CRS.decode(EPSG_PREFIX + epsgCode))) ? true : false;
} catch (FactoryException e) {
throw new NoApplicableCodeException().causedBy(e).withMessage("The EPSG '%d' is invalid", epsgCode);
}
// return this.epsgsWithNorthingFirstAxisOrder.stream()
// .filter(r -> r.contains(epsgCode))
// .findAny().isPresent();
}
/**
* Check if the EPSG code is easting first.
*
* @param epsgCode
* EPSG code to check
*
* @return true
, if the EPSG code is easting first
* @throws CodedException
* If an error occurs
*/
public boolean isEastingFirstEpsgCode(int epsgCode) throws CodedException {
return !isNorthingFirstEpsgCode(epsgCode);
}
/**
* 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(EnvelopeOrGeometry geom) throws OwsExceptionReport {
return switchCoordinateAxisFromToDatasourceIfNeeded(geom.toGeometry());
}
/**
* 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(Geometry geom) throws OwsExceptionReport {
if (!shouldSwitchCoordinateAxis(geom)) {
return geom;
}
return JTSHelper.switchCoordinateAxisOrder(geom);
}
private Geometry switchCoordinateAxisIfNeeded(Geometry geometry, int targetSRID) throws OwsExceptionReport {
if (!shouldSwitchCoordinateAxis(geometry, targetSRID)) {
return geometry;
}
return JTSHelper.switchCoordinateAxisOrder(geometry);
}
private boolean shouldSwitchCoordinateAxis(Geometry geom) throws CodedException {
if (geom == null || geom.isEmpty()) {
return false;
}
return isDatasourceNorthingFirst() != isNorthingFirstEpsgCode(geom.getSRID());
}
private boolean shouldSwitchCoordinateAxis(Geometry geom, int targetSRID) throws CodedException {
if (geom == null || geom.isEmpty()) {
return false;
}
return isNorthingFirstEpsgCode(geom.getSRID()) != isNorthingFirstEpsgCode(targetSRID);
}
/**
* 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(SpatialFilter filter) throws OwsExceptionReport {
switch (filter.getOperator()) {
case BBOX:
return switchCoordinateAxisFromToDatasourceIfNeeded(filter.getGeometry());
default:
throw new InvalidParameterValueException("spatialFilter", filter.getOperator().name());
// Sos2Constants.GetObservationParams.spatialFilter =
// "spatialFilter"
}
}
/**
* Get WKT string from longitude and latitude.
*
* @param longitude
* Longitude coordinate
* @param latitude
* Latitude coordinate
*
* @return WKT string
*/
public String getWktString(Object longitude, Object latitude) {
return getWktString(latitude, longitude, datasoureUsesNorthingFirst);
}
/**
* 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
* @throws CodedException
* If an error occurs
*/
public String getWktString(Object longitude, Object latitude, int epsg) throws CodedException {
return getWktString(latitude, longitude, isNorthingFirstEpsgCode(epsg));
}
private String getWktString(Object latitude, Object longitude, boolean northingFirst) {
return northingFirst ? createWktString(latitude, longitude) : createWktString(longitude, latitude);
}
private String createWktString(Object x, Object y) {
StringBuilder builder = new StringBuilder();
builder.append("POINT (");
builder.append(JavaHelper.asString(x));
builder.append(' ');
builder.append(JavaHelper.asString(y));
builder.append(')');
return builder.toString();
}
/**
* 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(Geometry geometry, List envelopes) {
return geometry != null && !geometry.isEmpty() && envelopes.stream().anyMatch(e -> e.contains(geometry));
}
/**
* Transforms the geometry to the storage EPSG code.
*
* @param geometry
* Geometry to transform
*
* @return Transformed geometry
*
* @throws OwsExceptionReport
* If an error occurs
*/
public Geometry transformToStorageEpsg(EnvelopeOrGeometry geometry) throws OwsExceptionReport {
return transformToStorageEpsg(geometry.toGeometry());
}
/**
* Transforms the geometry to the storage EPSG code.
*
* @param geometry
* Geometry to transform
*
* @return Transformed geometry
*
* @throws OwsExceptionReport
* If an error occurs
*/
public Geometry transformToStorageEpsg(Geometry geometry) throws OwsExceptionReport {
if (geometry == null || geometry.isEmpty()) {
return geometry;
}
CoordinateReferenceSystem sourceCRS = getCRS(geometry.getSRID());
int targetSRID = sourceCRS.getCoordinateSystem().getDimension() == 3 ? getStorage3DEPSG() : getStorageEPSG();
return transform(geometry, targetSRID, sourceCRS, getCRS(targetSRID));
}
/**
* Transform geometry to this EPSG code.
*
* @param geometry
* Geometry to transform
* @param targetSRID
* Target EPSG code
*
* @return Transformed geometry
*
* @throws OwsExceptionReport
* If an error occurs
*/
@Override
public Geometry transform(Geometry geometry, int targetSRID) throws OwsExceptionReport {
if (geometry == null || geometry.isEmpty() || geometry.getSRID() == targetSRID) {
return geometry;
}
CoordinateReferenceSystem sourceCRS = getCRS(geometry.getSRID());
CoordinateReferenceSystem targetCRS = getCRS(targetSRID);
return transform(geometry, targetSRID, sourceCRS, targetCRS);
}
/**
* Transform geometry.
*
* @param geometry
* Geometry to transform
* @param targetSRID
* TargetEPSG code
* @param sourceCRS
* Source CRS
* @param targetCRS
* Target CRS
*
* @return Transformed geometry
*
* @throws OwsExceptionReport
* If an error occurs
*/
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 | MismatchedDimensionException | TransformException fe) {
throw new NoApplicableCodeException().causedBy(fe).withMessage(EPSG_NOT_SUPPORTED_TMEPLATE,
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(int epsgCode) throws OwsExceptionReport {
try {
return this.crsCache.computeIfAbsent(epsgCode, code -> {
try {
return createCRS(code);
} catch (OwsExceptionReport ex) {
throw new RuntimeException(ex);
}
});
} catch (RuntimeException ex) {
Throwables.throwIfInstanceOf(ex.getCause(), OwsExceptionReport.class);
throw ex;
}
}
/**
* 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(int epsgCode) throws OwsExceptionReport {
try {
return getCrsAuthorityFactory().createCoordinateReferenceSystem(EPSG_PREFIX + epsgCode);
} catch (FactoryException nsace) {
throw new NoApplicableCodeException().causedBy(nsace).withMessage(EPSG_NOT_SUPPORTED_TMEPLATE,
epsgCode);
}
}
/**
* Get CSR authority.
*
* @return CRS authority
*/
private CRSAuthorityFactory getCrsAuthorityFactory() {
return crsAuthority;
}
/**
* 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 OwsExceptionReport {
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(EPSG_NOT_SUPPORTED_TMEPLATE,
sourceSRID);
} catch (MismatchedDimensionException | TransformException mde) {
throw new NoApplicableCodeException().causedBy(mde)
.withMessage("Transformation from EPSG code '%s' to '%s' fails!", sourceSRID, targetSRID);
}
}
return envelope;
}
/**
* Clears the supported Coordinate Reference Systems map.
*/
@VisibleForTesting
protected void clearSupportedCRSMap() {
crsCache.clear();
}
public Set addAuthorityCrsPrefix(Collection crses) {
return crses.stream().map(this::addAuthorityCrsPrefix).collect(Collectors.toSet());
}
public String addAuthorityCrsPrefix(int crs) {
return new StringBuilder(getAuthority()).append("::").append(crs).toString();
}
public Set addOgcCrsPrefix(Collection crses) {
return crses.stream().map(this::addOgcCrsPrefix).collect(Collectors.toSet());
}
public String addOgcCrsPrefix(int crs) {
return this.srsNamePrefixUrl + crs;
}
private ConfigurationError createException(String entry, Throwable ex) {
return new ConfigurationError(String.format("Invalid format of entry in '%s': %s",
FeatureQuerySettingsProvider.EPSG_CODES_WITH_NORTHING_FIRST, entry), ex);
}
}