You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2026/02/22 16:31:40 UTC

(sis) 01/01: Finer-grain detection of when the `CoordinateOperation` can be cached for a pair of CRS. In particular, we now detect the case when the Area Of Interest (AOI) has not been used. It covers a lot of cases, in particular map projections from grid geometry to target CRS.

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit de19b988f90d84c05efb4ea2f34a1729df5bc0e5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Feb 22 11:45:45 2026 +0100

    Finer-grain detection of when the `CoordinateOperation` can be cached for a pair of CRS.
    In particular, we now detect the case when the Area Of Interest (AOI) has not been used.
    It covers a lot of cases, in particular map projections from grid geometry to target CRS.
---
 .../operation/CoordinateOperationContext.java      | 31 ++++++++++-
 .../operation/CoordinateOperationFinder.java       | 30 +++--------
 .../operation/CoordinateOperationRegistry.java     | 63 +++++++++++++++++-----
 .../DefaultCoordinateOperationFactory.java         | 58 ++++++++++----------
 .../referencing/operation/SubOperationInfo.java    |  8 +--
 .../sis/referencing/operation/package-info.java    |  2 +-
 6 files changed, 124 insertions(+), 68 deletions(-)

diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
index 1df1f3f0d0..93c24183af 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationContext.java
@@ -62,7 +62,7 @@ import org.apache.sis.measure.Longitude;
  * to Texas, but not suitable to the rest of North-America.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   0.7
  *
  * @todo Should also take the country of a {@link java.util.Locale}. The EPSG database contains ISO2 and ISO3
@@ -98,6 +98,15 @@ public class CoordinateOperationContext implements Localized {
      */
     private transient Filter logFilter;
 
+    /**
+     * Whether the coordinate operation created with this context depends on the context values.
+     * This flag is set to {@code true} when {@link CoordinateOperationFinder} has used a contextual information such
+     * as the area of interest, desired accuracy, constant coordinate values or operation filter for choosing a result.
+     *
+     * @see #resultWasContextSensitive()
+     */
+    boolean resultWasContextSensitive;
+
     /**
      * Creates a new context with no area of interest and the best accuracy available.
      */
@@ -414,4 +423,24 @@ public class CoordinateOperationContext implements Localized {
         }
         logger.log(record);
     }
+
+    /**
+     * Returns whether the coordinate operation created with this context depends on the context values.
+     * This is {@code true} if the last {@link CoordinateOperationFinder} that used this context needed
+     * information such as
+     * the  {@linkplain #getAreaOfInterest() area of interest},
+     * the  {@linkplain #getDesiredAccuracy() desired accuracy},
+     * some {@linkplain #getConstantCoordinates() constant coordinate values} or
+     * the  {@linkplain #getOperationFilter() operation filter} for choosing a result.
+     * If this flag is {@code false}, then multiple calls to {@code createOperation(…)}
+     * with the same pair of <abbr>CRS</abbr> are likely to provide the same result no matter the context.
+     * This information information is useful for deciding whether to cache a result.
+     *
+     * @return whether the coordinate operation created with this context depends on the context values.
+     *
+     * @since 1.7
+     */
+    public boolean resultWasContextSensitive() {
+        return resultWasContextSensitive;
+    }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
index ca2d90fdd4..be099c5216 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationFinder.java
@@ -113,7 +113,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.7
  *
  * @see DefaultCoordinateOperationFactory#createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)
  *
@@ -146,14 +146,6 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
      */
     private final boolean canReadFromCache;
 
-    /**
-     * Whether the operation can be cached. This flag shall be {@code false} if
-     * the operation depends on parameters that may vary between two executions.
-     *
-     * @see #canStoreInCache()
-     */
-    private boolean canStoreInCache;
-
     /**
      * Creates a new instance for the given factory and context.
      *
@@ -172,7 +164,6 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
         identifierOfStepCRS = new HashMap<>(8);
         previousSearches    = new HashMap<>(8);
         canReadFromCache    = (context == null || context.canReadFromCache()) && (factory == factorySIS);
-        canStoreInCache     = (context == null);
     }
 
     /**
@@ -253,13 +244,13 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
         /*
          * If this method is invoked recursively, verify if the requested operation is already in the cache.
          * We do not perform this verification on the first invocation because it was already verified by
-         * DefaultCoordinateOperationFactory.createOperation(…). We do not block if the operation is in
+         * `DefaultCoordinateOperationFactory.createOperation(…)`. We do not block if the operation is in
          * process of being computed in another thread because of the risk of deadlock. If the operation
          * is not in the cache, store the key in our internal map for preventing infinite recursion.
          */
         final CRSPair key = new CRSPair(sourceCRS, targetCRS);
         if (canReadFromCache && stopAtFirst && !previousSearches.isEmpty()) {
-            final CoordinateOperation op = factorySIS.cache.peek(key);
+            final CoordinateOperation op = factorySIS.getCachedOperation(key);
             if (op != null) return asList(op);      // Must be a modifiable list as per this method contract.
         }
         if (previousSearches.put(key, Boolean.TRUE) != null) {
@@ -559,6 +550,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
                 final var impl = (DefaultGeodeticDatum) sourceDatum;
                 datumShift = impl.getPositionVectorTransformation(targetDatum, areaOfInterest);
                 if (datumShift != null) typeOfChange = DATUM_SHIFT;
+                resultWasContextSensitive(specifiedAOI);
             } else if (targetDatum instanceof DefaultGeodeticDatum) {
                 final var impl = (DefaultGeodeticDatum) targetDatum;
                 Matrix matrix = impl.getPositionVectorTransformation(sourceDatum, areaOfInterest);
@@ -570,6 +562,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
                     matrix = null;
                 }
                 datumShift = matrix;
+                resultWasContextSensitive(specifiedAOI);
             } else {
                 datumShift = null;
             }
@@ -586,6 +579,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
             }
             method     = builder.getMethod();
             parameters = builder.parametersForMetadata();
+            resultWasContextSensitive(specifiedAccuracy);
         }
         /*
          * Adjust the accuracy information if the datum shift has been computed by an indirect path.
@@ -967,7 +961,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
             final SubOperationInfo          info   = infos[i];
             final CoordinateReferenceSystem source = stepComponents[i];
             final CoordinateReferenceSystem target = info.targetComponent;
-            canStoreInCache &= info.canStoreInCache();
+            resultWasContextSensitive(info.resultWasContextSensitive());
             /*
              * In order to compute `stepTargetCRS`, replace in-place a single element in `stepComponents`.
              * For each step except the last one, `stepTargetCRS` is a mix of target CRS and source CRS.
@@ -1021,7 +1015,7 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
                         targetCRS.getCoordinateSystem().getDimension());
             operation = concatenate(operation, createFromAffineTransform(CONSTANTS, stepSourceCRS, targetCRS, null, m));
             for (int i = stepComponents.length; i < infos.length; i++) {
-                canStoreInCache &= infos[i].canStoreInCache();
+                resultWasContextSensitive(infos[i].resultWasContextSensitive());
             }
         }
         return asList(operation);
@@ -1304,12 +1298,4 @@ public class CoordinateOperationFinder extends CoordinateOperationRegistry {
                 Resources.Keys.NonInvertibleOperation_1,
                 CRSPair.label(crs.getConversionFromBase(), locale));
     }
-
-    /**
-     * Returns whether the operation can be cached. This is {@code false} if
-     * the operation depends on parameters that may vary between two executions.
-     */
-    final boolean canStoreInCache() {
-        return canStoreInCache;
-    }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index bb4f667fdc..c3f05e75e7 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -230,13 +230,30 @@ class CoordinateOperationRegistry {
      */
     protected Extent areaOfInterest;
 
+    /**
+     * Whether a non-default {@link #areaOfInterest} value was specified explicitly by the context.
+     * If this flag is {@code true}, then {@link context#operationIsContextSensitive} should be set
+     * to {@code true} when {@link #areaOfInterest} is used.
+     */
+    final boolean specifiedAOI;
+
     /**
      * The desired accuracy in metres, or 0 for the best accuracy available.
+     * When a new {@code CoordinateOperationFinder} instance is created with a non-null
+     * {@link CoordinateOperationContext}, the context is used for initializing this value.
+     * After initialization, this field may be updated at implementation discretion.
      *
      * @see CoordinateOperationContext#getDesiredAccuracy()
      */
     protected double desiredAccuracy;
 
+    /**
+     * Whether a non-default {@link #desiredAccuracy} value was specified explicitly by the context.
+     * If this flag is {@code true}, then {@link context#operationIsContextSensitive} should be set
+     * to {@code true} when {@link #desiredAccuracy} is used.
+     */
+    final boolean specifiedAccuracy;
+
     /**
      * {@code true} if {@code search(…)} should stop after the first coordinate operation found.
      * This field is set to {@code true} when the caller is interested only in the "best" operation
@@ -299,6 +316,8 @@ class CoordinateOperationRegistry {
             areaOfInterest  = context.getAreaOfInterest();
             desiredAccuracy = context.getDesiredAccuracy();
         }
+        specifiedAOI = (areaOfInterest != null);
+        specifiedAccuracy = (desiredAccuracy > 0);
     }
 
     /**
@@ -656,15 +675,18 @@ class CoordinateOperationRegistry {
          * then we need to get one from the CRS. This is necessary for preventing the transformation from
          * NAD27 to NAD83 in Idaho to select the transform for Alaska (since the latter has a larger area).
          */
-        CoordinateOperationSorter.sort(operations, Extents.getGeographicBoundingBox(areaOfInterest));
+        if (operations.size() > 1) {
+            resultWasContextSensitive(specifiedAOI);
+            CoordinateOperationSorter.sort(operations, Extents.getGeographicBoundingBox(areaOfInterest));
+        }
         final ListIterator<CoordinateOperation> it = operations.listIterator();
         while (it.hasNext()) {
             /*
              * At this point we filtered a CoordinateOperation by looking only at its metadata.
              * Code following this point will need the full coordinate operation, including its
-             * MathTransform. So if we got a deferred operation, we need to resolve it now.
+             * `MathTransform`. So if we got a deferred operation, we need to resolve it now.
              * Conversely, we should not use metadata below this point because the call to
-             * inverse(CoordinateOperation) is not guaranteed to preserve all metadata.
+             * `inverse(CoordinateOperation)` is not guaranteed to preserve all metadata.
              */
             CoordinateOperation operation = it.next();
             try {
@@ -705,21 +727,38 @@ class CoordinateOperationRegistry {
              * FactoryException propagate.
              */
             operation = complete(operation, sourceCRS, targetCRS);
-            final Predicate<CoordinateOperation> filter = (context != null) ? context.getOperationFilter() : null;
-            if (filter == null || filter.test(operation)) {
-                if (stopAtFirst) {
-                    operations.clear();
-                    operations.add(operation);
-                    break;
+            if (context != null) {
+                final Predicate<CoordinateOperation> filter = context.getOperationFilter();
+                if (filter != null) {
+                    resultWasContextSensitive(true);
+                    if (!filter.test(operation)) {
+                        it.remove();
+                        continue;
+                    }
                 }
-                it.set(operation);
-            } else {
-                it.remove();
             }
+            if (stopAtFirst) {
+                operations.clear();
+                operations.add(operation);
+                break;
+            }
+            it.set(operation);
         }
         return operations;
     }
 
+    /**
+     * If the given flag is {@code true}, declares that the search for coordinate operations
+     * depends on the values of the {@code context} argument specified at construction time.
+     *
+     * @param  flag  whether the coordinate operation depends on the context according the caller.
+     */
+    final void resultWasContextSensitive(final boolean flag) {
+        if (flag && context != null) {
+            context.resultWasContextSensitive = true;
+        }
+    }
+
     /**
      * Creates the inverse of the given single operation.
      * If this operation succeed, then the returned coordinate operation has the following properties:
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
index 6efdc5f69c..bd6f8f728f 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultCoordinateOperationFactory.java
@@ -80,7 +80,7 @@ import org.apache.sis.util.resources.Errors;
  * This class is safe for multi-thread usage if all referenced factories are thread-safe.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.5
+ * @version 1.7
  * @since   0.6
  */
 public class DefaultCoordinateOperationFactory extends AbstractFactory implements CoordinateOperationFactory {
@@ -124,12 +124,12 @@ public class DefaultCoordinateOperationFactory extends AbstractFactory implement
 
     /**
      * The cache of coordinate operations found for a given pair of source and target CRS.
-     * If current implementation, we cache only operations found without context (otherwise
-     * we would need to take in account the area of interest and desired accuracy in the key).
+     * We cache only operations found when {@link CoordinateOperationContext} values were
+     * not used (otherwise we would need to store some context values in the key).
      *
      * @see #createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateOperationContext)
      */
-    final Cache<CRSPair,CoordinateOperation> cache;
+    private final Cache<CRSPair, CoordinateOperation> cache;
 
     /**
      * The default factory instance.
@@ -665,35 +665,37 @@ next:   for (SingleCRS component : CRS.getSingleComponents(targetCRS)) {
                                                final CoordinateOperationContext context)
             throws OperationNotFoundException, FactoryException
     {
-        final Cache.Handler<CoordinateOperation> handler;
-        CoordinateOperation op;
-        if (context == null) {
-            final CRSPair key = new CRSPair(sourceCRS, targetCRS);
-            op = cache.peek(key);
-            if (op != null) {
-                return op;
-            }
-            handler = cache.lock(key);
-        } else {
-            // We currently do not cache the operation when the result may depend on the context (see `this.cache` javadoc).
-            handler = null;
-            op = null;
-        }
-        boolean canStoreInCache = true;
-        try {
-            if (handler == null || (op = handler.peek()) == null) {
-                final CoordinateOperationFinder finder = createOperationFinder(getFactorySIS(), context);
-                op = finder.createOperation(sourceCRS, targetCRS);
-                canStoreInCache = finder.canStoreInCache();
-            }
-        } finally {
-            if (handler != null) {
-                handler.putAndUnlock(canStoreInCache ? op : null);
+        final var key = new CRSPair(sourceCRS, targetCRS);
+        CoordinateOperation op = getCachedOperation(key);
+        if (op == null) {
+            boolean resultWasContextSensitive = false;
+            final Cache.Handler<CoordinateOperation> handler = cache.lock(key);
+            try {
+                op = handler.peek();
+                if (op == null) {
+                    final CoordinateOperationFinder finder = createOperationFinder(getFactorySIS(), context);
+                    op = finder.createOperation(sourceCRS, targetCRS);
+                    if (context != null) {
+                        resultWasContextSensitive = context.resultWasContextSensitive();
+                    }
+                }
+            } finally {
+                handler.putAndUnlock(resultWasContextSensitive ? null : op);
             }
         }
         return op;
     }
 
+    /**
+     * Returns an operation for the given pair of <abbr>CRS</abbr> if that operation is already in the cache.
+     *
+     * @param  key  pair of <abbr>CRS</abbr> for which to get an operation.
+     * @return the cached operation for the given pair of <abbr>CRS</abbr>, or {@code null} if none.
+     */
+    final CoordinateOperation getCachedOperation(final CRSPair key) {
+        return cache.peek(key);
+    }
+
     /**
      * Finds or creates operations for conversions or transformations between two coordinate reference systems.
      * If at least one operation exists, they are returned in preference order: the operation having the widest
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
index 676674b804..1e85a2f4df 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
@@ -479,10 +479,10 @@ otherRow:           for (int j = last.getNumRow() - 1; --j >= 0;) {     // Ignor
     }
 
     /**
-     * Whether this operation can be cached. This flag shall be {@code false} if
-     * the operation depends on parameters that may vary between two executions.
+     * Whether the operation result depends on the values of the user-supplied {@link CoordinateOperationContext}.
+     * This flag shall be {@code true} if the operation depends on parameters that may vary between two executions.
      */
-    final boolean canStoreInCache() {
-        return constantCoordinates == null;
+    final boolean resultWasContextSensitive() {
+        return constantCoordinates != null;
     }
 }
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/package-info.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/package-info.java
index d9b422b818..ef22720164 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/package-info.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/package-info.java
@@ -72,7 +72,7 @@
  * for example by specifying the area of interest.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   0.6
  */
 @XmlSchema(location = "http://schemas.opengis.net/gml/3.2.1/coordinateOperations.xsd",