blob: 82d090097a560855a63cd1909117089b2ee392c7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.internal.feature;
import java.io.Serializable;
import java.util.Collections;
import java.util.Map;
import javax.measure.Unit;
import javax.measure.IncommensurableException;
import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
import org.opengis.util.FactoryException;
import org.opengis.geometry.DirectPosition;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.cs.CartesianCS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.GeneralDerivedCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.apache.sis.referencing.operation.DefaultConversion;
import org.apache.sis.referencing.crs.DefaultProjectedCRS;
import org.apache.sis.referencing.ImmutableIdentifier;
import org.apache.sis.referencing.CRS;
import org.apache.sis.measure.Units;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.metadata.iso.citation.Citations;
// Branch-dependent imports
import org.opengis.filter.SpatialOperatorName;
import org.opengis.filter.DistanceOperatorName;
/**
* Context (such as desired CRS) in which a spatial operator will be executed.
*
* <p>Instances of this class are immutable and thread-safe.</p>
*
* <p>The serialization form is not a committed API and may change in any future version.</p>
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 1.1
* @module
*/
public final class SpatialOperationContext implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -6197547343970700471L;
/**
* The {@value} value, for identifying code that assume two-dimensional objects.
*/
private static final int BIDIMENSIONAL = 2;
/**
* Approximate geographic area of geometries, or {@code null} if unspecified.
*/
private final GeographicBoundingBox areaOfInterest;
/**
* The target CRS in which to transform geometries, or {@code null} for inferring automatically.
*/
private final CoordinateReferenceSystem computationCRS;
/**
* If the CRS needs to be in some units of measurement, the {@link Unit#getSystemUnit()} value.
* For example is units need to be linear, then {@code systemUnit} shall be {@link Units#METRE}.
* Note that it does not mean that the units of measurement must be meters; only that they must
* be compatible with meters.
*/
private final Unit<?> systemUnit;
/**
* Index of the geometry associated to the common CRS, or -1 if none.
* This is used for avoiding unnecessary check of its CRS.
*/
private final int skipIndex;
/**
* The common CRS found by {@link #transform(GeometryWrapper[])}. May be null.
*/
CoordinateReferenceSystem commonCRS;
/**
* Creates a new context.
*
* @param areaOfInterest approximate geographic area of geometries, or {@code null} if unspecified.
* @param literal if a geometry operand is a literal, that literal. Otherwise {@code null}.
* @param systemUnit if the CRS needs to be in some units of measurement, the {@link Unit#getSystemUnit()} value.
* @param skipIndex index of the geometry associated to {@code commonCRS}, or -1 if none.
* @throws FactoryException if an error occurred while fetching {@code literal} CRS.
* @throws TransformException if a coordinate conversion was required but failed.
* @throws IncommensurableException if a coordinate system does not use the expected units.
*/
public SpatialOperationContext(final GeographicBoundingBox areaOfInterest, final GeometryWrapper<?> literal,
final Unit<?> systemUnit, final int skipIndex)
throws FactoryException, TransformException, IncommensurableException
{
this.areaOfInterest = areaOfInterest;
this.systemUnit = systemUnit;
this.skipIndex = skipIndex;
if (literal == null) {
computationCRS = null;
} else try {
CoordinateReferenceSystem crs = to2D(literal.getCoordinateReferenceSystem());
if (systemUnit != null && crs != null) {
crs = usingSystemUnit(literal, crs, crs, systemUnit);
}
computationCRS = crs;
} catch (BackingStoreException e) {
throw e.unwrapOrRethrow(FactoryException.class);
}
}
/**
* Returns the two first dimensions of the given CRS. This is usually the {@code crs} argument unchanged
* (which may be {@code null}), unless a three- or four-dimensional CRS has been specified.
* We work with two dimensional CRS because the wrapped geometries are 2D and for avoiding
* that {@link ReferencingUtilities#getUnit(CoordinateSystem)} returns {@code null}.
*/
private static CoordinateReferenceSystem to2D(CoordinateReferenceSystem crs) {
if (ReferencingUtilities.getDimension(crs) > BIDIMENSIONAL) {
crs = CRS.getComponentAt(crs, 0, BIDIMENSIONAL);
}
return crs;
}
/**
* Transforms the specified geometry to the computation CRS.
* Geometries with unspecified CRS will be used as-is, without transformation.
* The common CRS is stored in the {@link #commonCRS} field.
*
* @param <G> geometry implementation base class.
* @param geometry the geometry to transform.
* @return the transformed geometry, or the same instance if no transformation was applied.
* @throws FactoryException if an error occurred while fetching a CRS.
* @throws TransformException if a coordinate conversion was required but failed.
*/
public <G> GeometryWrapper<G> transform(GeometryWrapper<G> geometry) throws FactoryException, TransformException {
if (computationCRS != null) {
final CoordinateReferenceSystem sourceCRS = to2D(geometry.getCoordinateReferenceSystem());
if (sourceCRS != null && !Utilities.equalsIgnoreMetadata(computationCRS, sourceCRS)) {
geometry = geometry.transform(CRS.findOperation(sourceCRS, computationCRS, areaOfInterest), false);
}
}
return geometry;
}
/**
* Transforms the specified geometries to the common CRS.
* If {@link #computationCRS} is {@code null}, then this method tries to infer one automatically.
* Geometries with unspecified CRS will be used as-is, without transformation.
* The common CRS is stored in the {@link #commonCRS} field.
*
* @param <G> geometry implementation base class.
* @param geometries the geometries to transform. Results will be stored in-place.
* @return whether this method has been able to find a common CRS.
* @throws FactoryException if an error occurred while fetching a CRS.
* @throws TransformException if a coordinate conversion was required but failed.
* @throws IncommensurableException if a geographic CRS does not use angular units (should not happen).
*/
final <G> boolean transform(final GeometryWrapper<G>[] geometries)
throws FactoryException, TransformException, IncommensurableException
{
final CoordinateReferenceSystem[] allCRS = new CoordinateReferenceSystem[geometries.length];
try {
for (int i = allCRS.length; --i >= 0;) {
allCRS[i] = (i == skipIndex) ? computationCRS : to2D(geometries[i].getCoordinateReferenceSystem());
}
} catch (BackingStoreException e) {
throw e.unwrapOrRethrow(FactoryException.class);
}
/*
* Search for an arbitrary non-null CRS. Then check in the next loop if all other CRS are equal,
* ignoring metadata and ignoring null CRS. If this is the case, then we do not transform anything.
*/
{
int i = allCRS.length;
do if (--i < 0) return true;
while ((commonCRS = allCRS[i]) == null);
/*
* Found a CRS potentially common to all geometries (this will be checked in next loop).
* But if units of measurement are restricted to some kind, we will accept that common CRS
* only if its unit is of the requested type.
*/
boolean reject = false;
if (systemUnit != null && commonCRS != computationCRS) {
reject = isCompatibleUnit(commonCRS, systemUnit);
}
if (!reject) {
// Potential common CRS accepted, verify if all other geometries use the same CRS.
CoordinateReferenceSystem crs;
do {
if (--i < 0) return true; // All geometries are in common CRS.
crs = allCRS[i];
} while (crs == null || Utilities.equalsIgnoreMetadata(commonCRS, crs));
}
}
/*
* At least one geometry needs to be transformed. Get a CRS which can be a common target
* for the specified geometries. The `if` block should be executed only if both operands
* are dynamic (non-literal) values. The rules applied inside the block are arbitrary
* and may change in any future version.
*/
commonCRS = computationCRS;
select: if (commonCRS == null) {
/*
* If there is a restriction on the unit of measurement, check if an existing CRS
* met that criterion. We do this check before to invoke `suggestCommonTarget(…)`
* because that method may replace `ProjectedCRS` by `GeographicCRS` in order to
* cover a larger area, and we usually want the `ProjectedCRS`.
*/
if (systemUnit != null) {
for (int i=allCRS.length; --i >= 0;) {
commonCRS = allCRS[i];
final Unit<?> unit = ReferencingUtilities.getUnit(commonCRS);
if (unit != null && systemUnit.equals(unit.getSystemUnit())) {
break select; // Use the `commonCRS` we just found.
}
}
}
/*
* If there are no restrictions on units of measurement, or if no geometry CRS met that restriction,
* request a CRS which may be different than the CRS of all geometries. The search takes in account
* the CRS domains of validity. The CRS found may be derived in order to be made compatible with the
* desired units of measurement.
*/
commonCRS = CRS.suggestCommonTarget(areaOfInterest, allCRS);
if (commonCRS == null) {
return false; // Geometries are in incompatible CRS.
}
if (systemUnit != null) {
for (int i=allCRS.length; --i >= 0;) {
final CoordinateReferenceSystem crs = allCRS[i];
if (crs != null) {
commonCRS = usingSystemUnit(geometries[i], crs, commonCRS, systemUnit);
break select;
}
}
return false; // Unable to use the desired units of measurement.
}
}
/*
* At this point, `commonCRS` is the CRS to use for converting all geometries.
* Perform the conversions in-place.
*/
for (int i=0; i<allCRS.length; i++) {
if (i != skipIndex) {
final CoordinateReferenceSystem sourceCRS = allCRS[i];
if (!Utilities.equalsIgnoreMetadata(commonCRS, sourceCRS)) {
geometries[i] = geometries[i].transform(CRS.findOperation(sourceCRS, commonCRS, areaOfInterest), false);
}
}
}
return true;
}
/**
* Returns {@code true} if the units of measurement of the given CRS are compatible with the given units.
* All CRS axes should have the same units, otherwise this method returns {@code false}.
*
* @param crs the CRS for which to test the units of measurement.
* @param systemUnit the {@link Unit#getSystemUnit()} value of the desired unit.
* @return whether the CRS units of measurement are compatible.
*/
private static boolean isCompatibleUnit(final CoordinateReferenceSystem crs, final Unit<?> systemUnit) {
final Unit<?> unit = ReferencingUtilities.getUnit(crs);
return (unit != null) && systemUnit.equals(unit.getSystemUnit());
}
/**
* Returns a coordinate reference system using the unit of measurement compatible with given system unit.
* This is usually for creating a {@link ProjectedCRS} when the user requested linear units, but it can
* also return {@link GeographicCRS} if the user requested angular units.
*
* @param geometry one of the geometry used in the operation. Will determine projection center.
* @param geometryCRS the CRS of {@code geometry}.
* @param targetCRS initial proposal of CRS to use for coordinate operation.
* @param systemUnit {@link Unit#getSystemUnit()} value on the unit requested by user.
* @return a CRS derived from {@code targetCRS} with units compatible with the specified units.
* @throws FactoryException if an error occurred while fetching a CRS.
* @throws TransformException if a coordinate conversion was required but failed.
* @throws IncommensurableException if a coordinate system does not use the expected units.
*/
private static CoordinateReferenceSystem usingSystemUnit(final GeometryWrapper<?> geometry,
final CoordinateReferenceSystem geometryCRS,
CoordinateReferenceSystem targetCRS,
final Unit<?> systemUnit)
throws FactoryException, TransformException, IncommensurableException
{
while (!isCompatibleUnit(targetCRS, systemUnit)) {
/*
* If the target CRS uses (latitude, longitude) coordinates and the requested units
* are metres (or compatible linear units), apply a map projection. We will use the
* same datum than `targetCRS` for avoiding datum shift.
*/
if (Units.isLinear(systemUnit) && targetCRS instanceof GeographicCRS) {
return Projector.instance().create((GeographicCRS) targetCRS, geometry.getCentroid(), geometryCRS);
}
if (targetCRS instanceof GeneralDerivedCRS) {
targetCRS = ((GeneralDerivedCRS) targetCRS).getBaseCRS();
} else {
throw new IncommensurableException(Errors.format(Errors.Keys.InconsistentUnitsForCS_1, systemUnit));
}
}
return targetCRS;
}
/**
* Creates projections centered on a given geometry.
* This is defined in a separated class for lazy static field initialization.
*/
private static final class Projector {
/** A singleton map containing the name to assign to the CRS. */
private final Map<String,?> name;
/** The operation method for the map projection to use. */
private final OperationMethod method;
/** The coordinate system for projected CRS. */
private final CartesianCS cartCS;
/** Creates the {@link #INSTANCE} singleton. */
private Projector() throws FactoryException {
final ReferencingFactoryContainer f = new ReferencingFactoryContainer();
method = f.getCoordinateOperationFactory().getOperationMethod("Mercator_2SP");
cartCS = f.getStandardProjectedCS();
name = Collections.singletonMap(DefaultConversion.NAME_KEY,
new ImmutableIdentifier(Citations.SIS, "SIS", "Mercator for geometry"));
}
/**
* Creates a projected CRS derived from the given geographic CRS.
*
* @param baseCRS the geographic CRS for which to derive a projected CRS.
* @param centroid coordinate a the center of the geometry.
* @param geometryCRS CRS of {@code centroid}.
* @return CRS using Cartesian coordinate system.
* @throws TransformException if a coordinate conversion was required but failed.
* @throws IncommensurableException if a coordinate system does not use the expected units.
*/
ProjectedCRS create(final GeographicCRS baseCRS, DirectPosition centroid, CoordinateReferenceSystem geometryCRS)
throws FactoryException, TransformException, IncommensurableException
{
/*
* We will need the (latitude, longitude) coordinates of projection center. If the CRS is derived
* (including projected CRS case), convert the position to the base CRS, which should be geographic.
* Note that a CRS can be both derived and geographic, so we need to do this check first in order to
* avoid derived geographic CRS such as the ones having rotated poles.
*/
while (geometryCRS instanceof GeneralDerivedCRS) {
final GeneralDerivedCRS g = (GeneralDerivedCRS) geometryCRS;
centroid = g.getConversionFromBase().getMathTransform().inverse().transform(centroid, centroid);
geometryCRS = g.getBaseCRS();
}
if (!(geometryCRS instanceof GeographicCRS)) {
throw new FactoryException(Errors.format(Errors.Keys.IllegalCRSType_1,
ReferencingUtilities.getInterface(CoordinateReferenceSystem.class, geometryCRS)));
}
/*
* Get the latitude and longitude values in degrees, applying unit conversions if needed.
* This code is much lighter than a call to `CRS.findOperation(…)` and also intentionally
* avoids datum shifts.
*/
final CoordinateSystem cs = geometryCRS.getCoordinateSystem();
double latitude = Double.NaN, longitude = Double.NaN;
for (int i=0; i<BIDIMENSIONAL; i++) {
final CoordinateSystemAxis axis = cs.getAxis(i);
double coordinate = centroid.getOrdinate(i);
coordinate = axis.getUnit().getConverterToAny(Units.DEGREE).convert(coordinate);
final AxisDirection direction = axis.getDirection();
if (direction == AxisDirection.NORTH) latitude = coordinate;
else if (direction == AxisDirection.EAST) longitude = coordinate;
else if (direction == AxisDirection.WEST) longitude = -coordinate;
else if (direction == AxisDirection.SOUTH) latitude = -coordinate;
else throw new FactoryException(Errors.format(Errors.Keys.UnsupportedAxisDirection_1, direction));
}
/*
* Create a projected coordinate reference system for the geometry center.
*/
final ParameterValueGroup p = method.getParameters().createValue();
p.parameter(Constants.STANDARD_PARALLEL_1).setValue(latitude);
p.parameter(Constants.CENTRAL_MERIDIAN).setValue(longitude);
final DefaultConversion conversion = new DefaultConversion(name, method, null, p);
return new DefaultProjectedCRS(name, baseCRS, conversion, cartCS);
}
/**
* Returns an instance. Should be a singleton instance, unless its creating failed
* at class initialization time in which case a new attempt will be made now.
*/
static Projector instance() throws FactoryException {
return (INSTANCE != null) ? INSTANCE : new Projector();
}
/** The singleton instance, or {@code null} if its creation failed. */
private static final Projector INSTANCE;
static {
Projector b;
try {
b = new Projector();
} catch (FactoryException e) {
b = null;
}
INSTANCE = b;
}
}
/**
* The value to return when a test can not be applied. This method is defined for
* having a single place to update if more operator types need to be recognized.
*
* @param type the test that could not be applied.
* @return the operation result to assume.
*/
public static boolean negativeResult(final SpatialOperatorName type) {
return type == SpatialOperatorName.DISJOINT;
}
/**
* The value to return when a test can not be applied. This method is defined for
* having a single place to update if more operator types need to be recognized.
*
* @param type the test that could not be applied.
* @return the operation result to assume.
*/
public static boolean negativeResult(final DistanceOperatorName type) {
return type == DistanceOperatorName.BEYOND;
}
}