blob: 7c9070000d22697606ffe47fcb91368fc4be4e41 [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.referencing.privy;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.function.Supplier;
import javax.measure.UnitConverter;
import javax.measure.IncommensurableException;
import org.opengis.referencing.ObjectFactory;
import org.opengis.referencing.cs.CSFactory;
import org.opengis.referencing.cs.RangeMeaning;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.SingleOperation;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.operation.MathTransformFactory;
import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory;
import org.apache.sis.referencing.factory.GeodeticObjectFactory;
import org.apache.sis.metadata.privy.NameToIdentifier;
import org.apache.sis.util.Deprecable;
import org.apache.sis.util.Static;
import org.apache.sis.util.privy.CollectionsExt;
import org.apache.sis.util.privy.Numerics;
import org.apache.sis.util.collection.Containers;
// Specific to the main and geoapi-3.1 branches:
import org.opengis.referencing.crs.GeneralDerivedCRS;
/**
* Utility methods related to coordinate operations.
*
* @author Martin Desruisseaux (Geomatys)
*/
public final class CoordinateOperations extends Static {
/**
* The {@link org.apache.sis.referencing.datum.DefaultGeodeticDatum#BURSA_WOLF_KEY} value.
*/
public static final String BURSA_WOLF_KEY = "bursaWolf";
/**
* The key for specifying explicitly the value to be returned by
* {@link org.apache.sis.referencing.operation.DefaultConversion#getParameterValues()}.
* It is usually not necessary to specify those parameters because they are inferred either from the
* {@link org.opengis.referencing.operation.MathTransform}, or specified explicitly in a {@code DefiningConversion}.
* However, there is a few cases, for example the Molodenski transform, where none of the above can apply,
* because SIS implements those operations as a concatenation of math transforms,
* and such concatenations do not have {@link org.opengis.parameter.ParameterValueGroup}.
*/
public static final String PARAMETERS_KEY = "parameters";
/**
* The key for specifying the base type of the coordinate operation to create. This optional entry
* is used by {@code DefaultCoordinateOperationFactory.createSingleOperation(…)}. Apache SIS tries
* to infer this value automatically, but this entry may help SIS to perform a better choice in
* some cases. For example, an "Affine" operation can be both a conversion or a transformation
* (the latter is used in datum shift in geocentric coordinates).
*/
public static final String OPERATION_TYPE_KEY = "operationType";
/**
* Value of {@link org.apache.sis.referencing.operation.CoordinateOperationContext#getConstantCoordinates()}.
* This thread-local is used as a workaround for the fact that we do not yet provide a public API for this
* functionality. This workaround should be deleted after a public API is defined.
*/
public static final ThreadLocal<Supplier<double[]>> CONSTANT_COORDINATES = new ThreadLocal<>();
/**
* Cached values of {@link #wrapAroundChanges wrapAroundChanges(…)}, created when first needed.
* Indices are bit masks computed by {@link #changes changes(…)}. Since the most common "wrap around" axes
* are longitude at dimension 0 or 1, and some measurement of time (in climatology) at dimension 2 or 3,
* then the most likely values are (binary digits):
*
* <pre class="text">
* 0000 0100 1000
* 0001 0101 1001
* 0010 0110 1010</pre>
*
* The last decimal value is 10 (binary {@code 1010}); we don't need to cache more.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private static final Set<Integer>[] CACHE = new Set[11];
/**
* Do not allow instantiation.
*/
private CoordinateOperations() {
}
/**
* Returns the coordinate operation factory to use for the given properties and math transform factory.
* If the given properties are empty and the {@code mtFactory} is the system default, then this method
* returns the system default {@code CoordinateOperationFactory} instead of creating a new one.
*
* <p>It is okay to set all parameters to {@code null} in order to get the system default factory.</p>
*
* @param properties the default properties.
* @param mtFactory the math transform factory to use.
* @param crsFactory the factory to use if the operation factory needs to create CRS for intermediate steps.
* @param csFactory the factory to use if the operation factory needs to create CS for intermediate steps.
* @return the coordinate operation factory to use.
*/
public static CoordinateOperationFactory getCoordinateOperationFactory(Map<String,?> properties,
final MathTransformFactory mtFactory, final CRSFactory crsFactory, final CSFactory csFactory)
{
if (Containers.isNullOrEmpty(properties)) {
if (isDefaultInstance(mtFactory) &&
isDefaultInstance(crsFactory) &&
isDefaultInstance(csFactory))
{
return DefaultCoordinateOperationFactory.provider();
}
properties = Map.of();
}
final HashMap<String,Object> p = new HashMap<>(properties);
p.putIfAbsent(ReferencingFactoryContainer.CRS_FACTORY, crsFactory);
p.putIfAbsent(ReferencingFactoryContainer.CS_FACTORY, csFactory);
properties = p;
return new DefaultCoordinateOperationFactory(properties, mtFactory);
}
/**
* Returns the method of the given coordinate operation, or {@code null} if none.
*
* @param operation the operation from which to get the method, or {@code null}.
* @return the coordinate operation method, or {@code null} if none.
*/
public static OperationMethod getMethod(final CoordinateOperation operation) {
if (operation instanceof SingleOperation) {
return ((SingleOperation) operation).getMethod();
}
return null;
}
/**
* Returns the operation method for the specified name or identifier. The given argument shall be either a
* method name (e.g. <q>Transverse Mercator</q>) or one of its identifiers (e.g. {@code "EPSG:9807"}).
*
* @param methods the method candidates.
* @param identifier the name or identifier of the operation method to search.
* @return the coordinate operation method for the given name or identifier, or {@code null} if none.
*
* @see org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory#getOperationMethod(String)
* @see org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory#getOperationMethod(String)
*/
public static OperationMethod getOperationMethod(final Iterable<? extends OperationMethod> methods, final String identifier) {
OperationMethod fallback = null;
for (final OperationMethod method : methods) {
if (NameToIdentifier.isHeuristicMatchForName(method, identifier) ||
NameToIdentifier.isHeuristicMatchForIdentifier(method.getIdentifiers(), identifier))
{
/*
* Stop the iteration at the first non-deprecated method.
* If we find only deprecated methods, take the first one.
*/
if (!(method instanceof Deprecable) || !((Deprecable) method).isDeprecated()) {
return method;
}
if (fallback == null) {
fallback = method;
}
}
}
return fallback;
}
/**
* Returns {@code true} if the given transform factory is the default instances used by Apache SIS.
*
* @param factory the factory to test. May be null.
* @return whether the given factory is the default instance.
*/
public static boolean isDefaultInstance(final MathTransformFactory factory) {
return factory == DefaultMathTransformFactory.provider();
}
/**
* Returns {@code true} if the given factory is the default instances used by Apache SIS.
*
* @param factory the factory to test. May be null.
* @return whether the given factory is the default instance.
*/
private static boolean isDefaultInstance(final ObjectFactory factory) {
return factory == GeodeticObjectFactory.provider();
}
/**
* Returns {@code true} if the given axis is of kind "Wrap Around".
* Defined here because used together with {@link #wrapAroundChanges wrapAroundChanges(…)}.
* This method can be used in contexts of coordinate operations, when the caller wants to
* be consistent with whatever policy change may happen in the future in this class.
*
* @param axis the axis to test.
* @return {@code true} if the given axis has "wrap around" range meaning.
*
* @see org.apache.sis.geometry.AbstractEnvelope#isWrapAround(CoordinateSystemAxis)
*/
public static boolean isWrapAround(final CoordinateSystemAxis axis) {
return axis.getRangeMeaning() == RangeMeaning.WRAPAROUND;
}
/**
* Returns indices of target dimensions where "wrap around" may happen as a result of a coordinate operation.
* This is usually the longitude axis when the source CRS uses the [-180 … +180]° range and the target CRS
* uses the [0 … 360]° range, or the converse.
*
* @param op the coordinate operation for which to get "wrap around" target dimensions.
* @return target dimensions where "wrap around" may happen, or an empty set if none.
*
* @see AbstractCoordinateOperation#getWrapAroundChanges()
*/
public static Set<Integer> wrapAroundChanges(final CoordinateOperation op) {
if (op instanceof AbstractCoordinateOperation) {
return ((AbstractCoordinateOperation) op).getWrapAroundChanges();
} else if (op != null) {
final CoordinateReferenceSystem source, target;
if ((source = op.getSourceCRS()) != null &&
(target = op.getTargetCRS()) != null)
{
return wrapAroundChanges(source, target.getCoordinateSystem());
}
}
return Set.of();
}
/**
* Computes indices of target dimensions where "wrap around" may happen as a result of a coordinate operation.
* This is usually the longitude axis when the source CRS uses the [-180 … +180]° range and the target CRS uses
* the [0 … 360]° range, or the converse.
*
* @param source the source of the coordinate operation.
* @param target the target of the coordinate operation.
* @return target dimensions where "wrap around" may happen, or an empty set if none.
*/
@SuppressWarnings("deprecation")
public static Set<Integer> wrapAroundChanges(CoordinateReferenceSystem source, final CoordinateSystem target) {
long changes = changes(source.getCoordinateSystem(), target);
while (source instanceof GeneralDerivedCRS) {
source = ((GeneralDerivedCRS) source).getBaseCRS();
changes |= changes(source.getCoordinateSystem(), target);
}
final boolean useCache = (changes >= 0 && changes < CACHE.length);
if (useCache) {
/*
* In most cases we have an existing instance. Since WrapAroundAxes are immutable, if we got a reference,
* the object should be okay ("published" in memory model terminology) even if we did not synchronized.
* The reference however may not be visible in the array, but we will check later in a synchronized block.
*/
final Set<Integer> existing = CACHE[(int) changes];
if (existing != null) {
return existing;
}
}
/*
* No existing instance found. Expand the `changes` bit mask in an array of integers in order to create an
* unmodifiable List<Integer>. The list is for public API; internally, Apache SIS will use toBitMask(…).
*/
long r = changes;
final Integer[] indices = new Integer[Long.bitCount(r)];
for (int i=0; i<indices.length; i++) {
final int dim = Long.numberOfTrailingZeros(r);
indices[i] = dim;
r &= ~(1L << dim);
}
final Set<Integer> dimensions = CollectionsExt.immutableSet(true, indices);
if (useCache) {
synchronized (CACHE) {
final Set<Integer> existing = CACHE[(int) changes];
if (existing != null) {
return existing;
}
CACHE[(int) changes] = dimensions;
}
}
return dimensions;
}
/**
* Returns the packed indices of target dimensions where coordinate values may need to be wrapped around.
* This method matches target coordinate system axes having {@link RangeMeaning#WRAPAROUND} with source
* axes, then verifies if the range of values changed (taking unit conversions in account). A target
* dimension {@code i} may need to "wrap around" the coordinate values if the {@code 1 << i} bit is set.
* If there is no change, then the value is zero.
*/
private static long changes(final CoordinateSystem source, final CoordinateSystem target) {
long changes = 0;
if (source != target) { // Optimization for a common case.
/*
* Get the dimensions of all axes in the source coordinate system as bit fields.
* We create this list first because we may iterate more than once on those axes
* and we want to clear the axes that we already matched. We use a bitmask for
* efficiency, with the bits of dimensions to consider set to 1.
*
* Note: a previous version was creating a list of "wraparound" axes only. We removed that filter
* because a target wraparound axis may match a source infinite axis. For example, when converting
* dates on a temporal axis (with infinite span toward past and future) to months on a climatology
* axis (January to December months without year), the same cycle is repeated after every 12 months
* even if the source axis had no cycle.
*/
long isWrapAroundAxis = (1 << source.getDimension()) - 1;
/*
* For each "wrap around" axis in the target CRS, search a matching axis in the source CRS
* which is also "wrap around", is colinear and uses compatible unit of measurement. There
* is usually at most one "wrap around" axis, but this code is nevertheless generic enough
* for an arbitrary number of axes.
*/
final int dim = Math.min(Long.SIZE, target.getDimension());
compare: for (int i=0; i<dim; i++) {
final CoordinateSystemAxis axis = target.getAxis(i);
if (isWrapAround(axis)) {
long candidates = isWrapAroundAxis;
do {
final long mask = Long.lowestOneBit(candidates);
final CoordinateSystemAxis src = source.getAxis(Long.numberOfTrailingZeros(mask));
if (AxisDirections.isColinear(src.getDirection(), axis.getDirection())) {
try {
final UnitConverter c = src.getUnit().getConverterToAny(axis.getUnit());
final double minimum = axis.getMinimumValue();
final double maximum = axis.getMaximumValue();
final double tolerance = (maximum - minimum) * Numerics.COMPARISON_THRESHOLD;
if (!Numerics.epsilonEqual(c.convert(src.getMinimumValue()), minimum, tolerance) ||
!Numerics.epsilonEqual(c.convert(src.getMaximumValue()), maximum, tolerance))
{
changes |= (1 << i);
}
isWrapAroundAxis &= ~mask; // We are done with this source axis.
if (isWrapAroundAxis == 0) {
break compare; // Useless to continue if there are no more source axes.
}
continue compare; // Match next pair of wrap around axes.
} catch (IncommensurableException e) {
// Ignore (should not happen often). We will try to match another pair of axes.
}
}
candidates &= ~mask; // Unable to use that axis. Check if we can use another one.
} while (candidates != 0);
}
}
}
return changes;
}
}