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 a3ab666ad53ae82a057b70fb4f17689dad5a12ca
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Feb 9 16:46:09 2022 +0100

    Try to make the search for coordinate operations (between a pair of CRS) 
less costly
    by avoiding extensive searches of CRS candidates before simpler attempts 
failed.
    As a side effect, make the cache of above-cited searches more deterministic.
---
 .../referencing/factory/AuthorityFactoryProxy.java |  16 +-
 .../factory/ConcurrentAuthorityFactory.java        |  83 +++++-----
 .../factory/IdentifiedObjectFinder.java            |  51 +++---
 .../factory/MultiAuthoritiesFactory.java           |   4 +-
 .../operation/CoordinateOperationContext.java      |   4 +-
 .../operation/CoordinateOperationRegistry.java     | 172 +++++++++++++++------
 .../sis/referencing/operation/package-info.java    |   2 +-
 .../operation/transform/ProjectiveTransform.java   |  11 +-
 .../factory/ConcurrentAuthorityFactoryTest.java    |  17 +-
 .../referencing/factory/sql/EPSGFactoryTest.java   |   9 +-
 10 files changed, 250 insertions(+), 119 deletions(-)

diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/AuthorityFactoryProxy.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/AuthorityFactoryProxy.java
index 5d7feaf..b3cf48a 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/AuthorityFactoryProxy.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/AuthorityFactoryProxy.java
@@ -58,7 +58,7 @@ import org.apache.sis.internal.util.Strings;
  * }</div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.2
  * @since   0.7
  * @module
  */
@@ -91,6 +91,20 @@ abstract class AuthorityFactoryProxy<T> {
     }
 
     /**
+     * Returns whether the objects created by this proxy may have axes.
+     */
+    final boolean hasAxes() {
+        final Class<? extends IdentifiedObject> base;
+        switch (factoryType) {
+            default: return false;
+            case AuthorityFactoryIdentifier.CRS: return true;
+            case AuthorityFactoryIdentifier.CS:  base = 
CoordinateSystem.class; break;
+            case AuthorityFactoryIdentifier.ANY: base = 
IdentifiedObject.class; break;
+        }
+        return base.isAssignableFrom(type);
+    }
+
+    /**
      * Casts the given factory into a datum authority factory, or throws a 
{@code FactoryException}
      * if the given factory is not of the expected type.
      */
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
index 3d19915..328ea7c 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/ConcurrentAuthorityFactory.java
@@ -62,6 +62,7 @@ import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.util.logging.PerformanceLevel;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Messages;
+import org.apache.sis.util.ArraysExt;
 
 
 /**
@@ -1886,6 +1887,7 @@ public abstract class ConcurrentAuthorityFactory<DAO 
extends GeodeticAuthorityFa
 
         /**
          * Returns the cached value for the given object, or {@code null} if 
none.
+         * The returned set (if non-null) is unmodifiable.
          */
         @Override
         final Set<IdentifiedObject> getFromCache(final IdentifiedObject 
object) {
@@ -1894,15 +1896,15 @@ public abstract class ConcurrentAuthorityFactory<DAO 
extends GeodeticAuthorityFa
                 final FindEntry entry = findPool.get(object);
                 if (entry != null) {
                     // `finder` may be null if this method is invoked directly 
by this Finder.
-                    return entry.get((finder != null ? finder : 
this).isIgnoringAxes());
+                    return entry.get(finder != null ? finder : this);
                 }
             }
             return null;
         }
 
         /**
-         * Stores the given result in the cache.
-         * This method shall be invoked only when {@link #getSearchDomain()} 
is not {@link Domain#DECLARATION}.
+         * Stores the given result in the cache. This method wraps or copies 
the given set
+         * in an unmodifiable set and returns the result.
          */
         @Override
         final Set<IdentifiedObject> cache(final IdentifiedObject object, 
Set<IdentifiedObject> result) {
@@ -1915,7 +1917,7 @@ public abstract class ConcurrentAuthorityFactory<DAO 
extends GeodeticAuthorityFa
                     entry = c;          // May happen if the same set has been 
computed in another thread.
                 }
                 // `finder` should never be null since this method is not 
invoked directly by this Finder.
-                result = entry.set(finder.isIgnoringAxes(), result, object == 
searching);
+                result = entry.set(finder, result, object != searching);
             }
             return result;
         }
@@ -1957,46 +1959,57 @@ public abstract class ConcurrentAuthorityFactory<DAO 
extends GeodeticAuthorityFa
 
     /**
      * Cache for the result of {@link 
IdentifiedObjectFinder#find(IdentifiedObject)} operations.
-     * All access to this object must be done in a block synchronized on 
{@link #findPool}.
+     * All accesses to this object must be done in a block synchronized on 
{@link #findPool}.
      */
     private static final class FindEntry {
+        /**
+         * Number of values in the {@link IdentifiedObjectFinder.Domain} 
enumeration.
+         * Hard-coded for efficiency. Value is verified using reflection by 
the test
+         * {@code ConcurrentAuthorityFactoryTest.verifyDomainCount()}.
+         */
+        private static final int DOMAIN_COUNT = 3;
+
         /** Result of the search with or without ignoring axes. */
-        private Set<IdentifiedObject> strict, lenient;
+        private final Set<IdentifiedObject>[] caches;
 
-        /** Whether the cache is the result of an explicit request instead of 
a dependency search. */
-        private boolean explicitStrict, explicitLenient;
+        /** Whether the cache is the result of a dependency search instead of 
an explicit request. */
+        private int dependencyFlags;
 
-        /** Returns the cached instance. */
-        Set<IdentifiedObject> get(final boolean ignoreAxes) {
-            return ignoreAxes ? lenient : strict;
+        /** Creates an initially empty entry. */
+        @SuppressWarnings({"unchecked", "rawtypes"})        // Generic array 
creation.
+        FindEntry() {
+            caches = new Set[DOMAIN_COUNT * 2];
         }
 
-        /** Cache an instance, or return previous instance if computed 
concurrently. */
-        @SuppressWarnings({"AssignmentToCollectionOrArrayFieldFromParameter", 
"ReturnOfCollectionOrArrayField"})
-        Set<IdentifiedObject> set(final boolean ignoreAxes, 
Set<IdentifiedObject> result, final boolean explicit) {
-            if (ignoreAxes) {
-                if (lenient != null) {
-                    result = lenient;
-                } else {
-                    lenient = result;
-                }
-                explicitLenient |= explicit;
-            } else {
-                if (strict != null) {
-                    result = strict;
-                } else {
-                    strict = result;
-                }
-                explicitStrict |= explicit;
-            }
-            return result;
+        /** Returns the index in the cache for a result using the given 
finder. */
+        private static int index(final IdentifiedObjectFinder finder) {
+            int i = finder.getSearchDomain().ordinal();
+            if (finder.isIgnoringAxes()) i += DOMAIN_COUNT;
+            return i;
+        }
+
+        /** Returns the cached instance, or {@code null} if none. */
+        Set<IdentifiedObject> get(final IdentifiedObjectFinder finder) {
+            return caches[index(finder)];
         }
 
-        /** Forgets the set that were not explicitly requested. */
+        /** Caches an instance, or return previous instance if computed 
concurrently. */
+        Set<IdentifiedObject> set(final IdentifiedObjectFinder finder, final 
Set<IdentifiedObject> result, final boolean dependency) {
+            final int i = index(finder);
+            if (dependency) dependencyFlags |= (1 << i);
+            Set<IdentifiedObject> existing = caches[i];
+            if (existing != null) return existing;
+            else return caches[i] = result;
+        }
+
+        /** Forgets the sets that were not explicitly requested. */
         boolean cleanup() {
-            if (!explicitStrict)  strict  = null;
-            if (!explicitLenient) lenient = null;
-            return (strict == null) && (lenient == null);
+            while (dependencyFlags != 0) {
+                int i = Integer.numberOfTrailingZeros(dependencyFlags);
+                dependencyFlags &= ~(1 << i);
+                caches[i] = null;
+            }
+            return ArraysExt.allEquals(caches, null);
         }
     }
 
@@ -2189,7 +2202,7 @@ public abstract class ConcurrentAuthorityFactory<DAO 
extends GeodeticAuthorityFa
                 return s;
             }
         }
-        return s + System.lineSeparator() + usage;
+        return s + System.lineSeparator() + "└─" + usage;
     }
 
     /**
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
index da4391a..af7af50 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/IdentifiedObjectFinder.java
@@ -259,18 +259,18 @@ public class IdentifiedObjectFinder {
 
     /**
      * Returns the cached value for the given object, or {@code null} if none.
+     * The returned set (if non-null) should be unmodifiable.
      */
     Set<IdentifiedObject> getFromCache(final IdentifiedObject object) {
         return (wrapper != null) ? wrapper.getFromCache(object) : null;
     }
 
     /**
-     * Stores the given result in the cache, if any.
-     * This method will be invoked by {@link #find(IdentifiedObject)}
-     * only if {@link #getSearchDomain()} is not {@link Domain#DECLARATION}.
+     * Stores the given result in the cache, if any. If this method chooses to 
cache the given set,
+     * then it shall wrap or copy the given set in an unmodifiable set and 
returns the result.
      *
-     * @return the given {@code result}, or another set equal to the result if 
it has been computed
-     *         concurrently in another thread.
+     * @param  result  the search result as a modifiable set.
+     * @return a set with the same content than {@code result}.
      */
     Set<IdentifiedObject> cache(final IdentifiedObject object, 
Set<IdentifiedObject> result) {
         if (wrapper != null) {
@@ -314,42 +314,45 @@ public class IdentifiedObjectFinder {
                  * If user wants to ignore axes, we need to return all 
matches. We can not use the identifier
                  * because it would reduce the set to a single element. Having 
all elements is necessary when
                  * `CoordinateOperationRegistry` searches for a coordinate 
operation between a pair of CRS.
-                 * The only exception to this rule is when this method is 
invoked from `findSingleton(…)`.
+                 * The only exceptions to this rule are when this method is 
invoked from `findSingleton(…)`,
+                 * or when axes are irrelevant to the object to search.
                  */
-                if (!ignoreIdentifiers && (!previousIgnoreAxes || 
wantSingleton)) {
+                if (!ignoreIdentifiers && (!previousIgnoreAxes || 
wantSingleton || !proxy.hasAxes())) {
                     /*
                      * First check if one of the identifiers can be used to 
find directly an identified object.
                      * Verify that the object that we found is actually equal 
to given one; we do not blindly
                      * trust the identifiers in the user object.
                      */
-                    ignoreAxes = false;
+                    ignoreAxes = false;             // It makes a difference 
only if `wantSingleton = true`.
                     IdentifiedObject candidate = createFromIdentifiers(object);
-                    if (candidate != null) {
-                        return Collections.singleton(candidate);    // Not 
worth to cache.
+                    if (candidate == null) {
+                        /*
+                         * We are unable to find the object from its 
identifiers. Try a quick name lookup.
+                         * Some implementations like the one backed by the 
EPSG database are capable to find
+                         * an object from its name.
+                         */
+                        candidate = createFromNames(object);
                     }
-                    /*
-                     * We are unable to find the object from its identifiers. 
Try a quick name lookup.
-                     * Some implementations like the one backed by the EPSG 
database are capable to find
-                     * an object from its name.
-                     */
-                    candidate = createFromNames(object);
                     if (candidate != null) {
-                        return Collections.singleton(candidate);    // Not 
worth to cache.
+                        result = Collections.singleton(candidate);
                     }
                 }
                 /*
                  * Here we exhausted the quick paths.
                  * Perform a full scan (costly) if we are allowed to, 
otherwise abandon.
                  */
-                if (domain == Domain.DECLARATION) {
-                    return Collections.emptySet();              // Do NOT 
cache.
+                if (result == null) {
+                    if (domain == Domain.DECLARATION) {
+                        result = Collections.emptySet();
+                    } else {
+                        result = createFromCodes(object);
+                    }
                 }
-                result = createFromCodes(object);
             } finally {
                 proxy      = previousProxy;
                 ignoreAxes = previousIgnoreAxes;
             }
-            result = cache(object, result);     // Costly operation (even if 
the result is empty) worth to cache.
+            result = cache(object, result);     // Costly operations (even if 
the result is empty) are worth to cache.
         }
         return result;
     }
@@ -382,15 +385,15 @@ public class IdentifiedObjectFinder {
         wantSingleton = true;
         try {
             for (final IdentifiedObject candidate : find(object)) {
-                final boolean so = !ignoreAxes || 
Utilities.deepEquals(candidate, object, COMPARISON_MODE);
+                final boolean equalsIncludingAxes = !ignoreAxes || 
Utilities.deepEquals(candidate, object, COMPARISON_MODE);
                 if (result != null) {
                     ambiguous = true;
-                    if (sameAxisOrder && so) {
+                    if (sameAxisOrder & equalsIncludingAxes) {
                         return null;            // Found two matches even when 
taking in account axis order.
                     }
                 }
                 result = candidate;
-                sameAxisOrder = so;
+                sameAxisOrder = equalsIncludingAxes;
             }
         } catch (BackingStoreException e) {
             throw e.unwrapOrRethrow(FactoryException.class);
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
index 0c5835c..33a1d0d 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java
@@ -1789,8 +1789,8 @@ public class MultiAuthoritiesFactory extends 
GeodeticAuthorityFactory implements
         /**
          * Delegates to every factories registered in the enclosing {@link 
MultiAuthoritiesFactory},
          * in iteration order. This method is invoked only if the parent class 
failed to find the
-         * object by its identifiers and by its name. At this point, as a last 
resource, we will
-         * scan over the objects in the database.
+         * object by its identifiers and by its name. At this point, as a last 
resort, we will scan
+         * over the objects in the database.
          *
          * <p>This method shall <strong>not</strong> delegate the job to the 
parent class, as the default
          * implementation in the parent class is very inefficient. We need to 
delegate to the finders of
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationContext.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationContext.java
index 1d5e76c..becdf84 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationContext.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationContext.java
@@ -218,9 +218,9 @@ public class CoordinateOperationContext implements 
Serializable {
     /**
      * Returns a filter that can be used for applying additional restrictions 
on the coordinate operation.
      *
-     * @todo Not yet implemented.
+     * @todo Not yet implemented. This is currently only a hook for a possible 
future feature.
      */
-    Predicate<CoordinateOperation> getOperationFilter() {
+    final Predicate<CoordinateOperation> getOperationFilter() {
         return null;
     }
 
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
index 6fa5c55..72eb053 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/CoordinateOperationRegistry.java
@@ -34,12 +34,14 @@ import org.opengis.util.FactoryException;
 import org.opengis.util.NoSuchIdentifierException;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.extent.Extent;
+import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.quality.PositionalAccuracy;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.GeneralDerivedCRS;
 import org.opengis.referencing.crs.GeodeticCRS;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.cs.EllipsoidalCS;
@@ -101,7 +103,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * then {@link CoordinateOperationFinder} will use its own fallback.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   0.7
  * @module
  */
@@ -160,7 +162,7 @@ class CoordinateOperationRegistry {
      * An instance is fetched at construction time from the {@link #registry} 
if possible.
      *
      * <div class="note"><b>Design note:</b>
-     * using a provider defined by the {@link #registry} instead of {@code 
MultiAuthoritiesFactory} may cause
+     * using a finder defined by the {@link #registry} instead of {@code 
MultiAuthoritiesFactory} may cause
      * the finder to perform extensive searches because it does not recognize 
the authority code of a given CRS.
      * For example if {@link #registry} is for EPSG and a given CRS is 
"CRS:84", then {@code codeFinder} would
      * not recognize the given CRS and would search for a match in the EPSG 
database. This is desired because
@@ -168,7 +170,7 @@ class CoordinateOperationRegistry {
      * operation, even if an equivalent definition was provided by another 
authority.</div>
      *
      * @see #authorityCodes
-     * @see #findCode(CoordinateReferenceSystem)
+     * @see #findCode(CoordinateReferenceSystem, boolean)
      */
     private final IdentifiedObjectFinder codeFinder;
 
@@ -208,28 +210,47 @@ class CoordinateOperationRegistry {
     protected double desiredAccuracy;
 
     /**
-     * {@code true} if {@link #search(CoordinateReferenceSystem, 
CoordinateReferenceSystem)}
-     * 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 instead of 
all of possibilities.
+     * {@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
+     * instead of all of possibilities.
+     *
+     * @see #search(CoordinateReferenceSystem, boolean, 
CoordinateReferenceSystem, boolean)
      */
     boolean stopAtFirst;
 
     /**
      * A filter that can be used for applying additional restrictions on the 
coordinate operation,
-     * or {@code null} if none.
+     * or {@code null} if none. If non-null, only operations passing this 
filter will be considered.
+     *
+     * @see CoordinateOperationContext#getOperationFilter()
      */
     private Predicate<CoordinateOperation> filter;
 
     /**
-     * Authority codes found for CRS. This is a cache for {@link 
#findCode(CoordinateReferenceSystem)}.
+     * Authority codes found for CRS. This is a cache for {@link 
#findCode(CoordinateReferenceSystem, boolean)}.
      * This map may be non-empty only if {@link #codeFinder} is non-null.
      *
+     * <div class="note"><b>Design note:</b>
+     * a cache is used because codes for the same CRS can be requested many 
times while iterating over the
+     * strategies enumerated by {@link Decomposition}. This cache partially 
duplicates the cache provided by
+     * {@link IdentifiedObjectFinder} implementations, but we have no 
guarantees that those implementations
+     * provide such cache, and the values cached here are the result of a 
little bit more work.</div>
+     *
      * @see #codeFinder
-     * @see #findCode(CoordinateReferenceSystem)
+     * @see #findCode(CoordinateReferenceSystem, boolean)
      */
     private final Map<CoordinateReferenceSystem, List<String>> authorityCodes;
 
     /**
+     * The authority codes of source and target CRSs examined during the last 
call to {@code search(…)}.
+     * This is used for determining if a new call will make any difference 
compared to previous call.
+     * This is usually a reference to a value in the {@link #authorityCodes} 
map.
+     *
+     * @see #search(CoordinateReferenceSystem, boolean, 
CoordinateReferenceSystem, boolean)
+     */
+    private List<String> lastSourceCodes, lastTargetCodes;
+
+    /**
      * Creates a new instance for the given factory and context.
      *
      * @param  registry  the factory to use for creating operations as defined 
by authority.
@@ -277,6 +298,7 @@ class CoordinateOperationRegistry {
     final <T extends IdentifiedObject> T toAuthorityDefinition(final Class<T> 
type, final T object) throws FactoryException {
         if (codeFinder != null) {
             codeFinder.setIgnoringAxes(false);
+            
codeFinder.setSearchDomain(IdentifiedObjectFinder.Domain.VALID_DATASET);
             final IdentifiedObject candidate = 
codeFinder.findSingleton(object);
             if (Utilities.equalsIgnoreMetadata(object, candidate)) {
                 return type.cast(candidate);
@@ -291,26 +313,39 @@ class CoordinateOperationRegistry {
      * This method may return codes even if the axis order does not match;
      * it will be caller's responsibility to make necessary adjustments.
      *
+     * @param  crs   the CRS for which to search authority codes.
+     * @param  full  whether to perform the most expensive search.
      * @return authority codes for the given CRS, or an empty list if none.
-     *         <b>Do not modify</b> since this list is cached.
+     *         <b>Do not modify</b> since this list may be cached.
      */
-    private List<String> findCode(final CoordinateReferenceSystem crs) throws 
FactoryException {
+    private List<String> findCode(final CoordinateReferenceSystem crs, final 
boolean full) throws FactoryException {
         List<String> codes = authorityCodes.get(crs);
         if (codes == null) {
+            if (codeFinder == null) {
+                return Collections.emptyList();
+            }
             codes = new ArrayList<>();
-            if (codeFinder != null) {
-                codeFinder.setIgnoringAxes(true);
-                for (final IdentifiedObject candidate : codeFinder.find(crs)) {
-                    final Identifier identifier = 
IdentifiedObjects.getIdentifier(candidate, registry.getAuthority());
-                    if (identifier != null) {
-                        final String code = identifier.getCode();
-                        if (Utilities.deepEquals(candidate, crs, 
ComparisonMode.APPROXIMATE)) {
-                            codes.add(0, code);     // If axis order match, 
give precedence to that CRS.
-                        } else {
-                            codes.add(code);
-                        }
+            codeFinder.setIgnoringAxes(full);
+            codeFinder.setSearchDomain(full ? 
IdentifiedObjectFinder.Domain.VALID_DATASET
+                                            : 
IdentifiedObjectFinder.Domain.DECLARATION);
+            final Citation authority = registry.getAuthority();
+            for (final IdentifiedObject candidate : codeFinder.find(crs)) {
+                final Identifier identifier = 
IdentifiedObjects.getIdentifier(candidate, authority);
+                if (identifier != null) {
+                    final String code = identifier.getCode();
+                    if (full && Utilities.deepEquals(candidate, crs, 
ComparisonMode.APPROXIMATE)) {
+                        codes.add(0, code);     // If axis order matches, give 
precedence to that CRS.
+                    } else {
+                        codes.add(code);
                     }
                 }
+            }
+            /*
+             * Cache only the result of full search. The result of non-full 
search is fast enough that it
+             * does not need to be cached. Note that it may cause the next 
call to `findCode(crs, false)`
+             * to return the full search even if `full` argument was false; 
this is okay.
+             */
+            if (full) {
                 authorityCodes.put(crs, codes);
             }
         }
@@ -383,6 +418,21 @@ class CoordinateOperationRegistry {
     }
 
     /**
+     * Returns whether it is presumed easy for {@link IdentifiedObjectFinder} 
to perform a full search
+     * (ignoring axis order) for the given CRS. An important criterion is to 
avoid all CRS containing
+     * a coordinate operation (in particular projected CRS), because they are 
difficult to search.
+     *
+     * <p>We allow extensive search of geographic CRS because the EPSG 
database has some geographic CRS
+     * defined with both (longitude, latitude) and (latitude, longitude) axis 
order. But operations may
+     * be defined for only one axis order. So if the user specified the CRS 
with (longitude, latitude)
+     * axis order, we want to check also the coordinate operations using 
(latitude, longitude) axis order
+     * because they may be the only ones available.</p>
+     */
+    private static boolean isEasySearch(final CoordinateReferenceSystem crs) {
+        return (crs instanceof SingleCRS) && !(crs instanceof 
GeneralDerivedCRS);
+    }
+
+    /**
      * Finds or infers operations for conversions or transformations between 
two coordinate reference systems.
      * {@code CoordinateOperationRegistry} implements the 
<cite>late-binding</cite> approach (see definition
      * of terms in class javadoc) by extracting the authority codes from the 
supplied {@code sourceCRS} and
@@ -432,29 +482,46 @@ class CoordinateOperationRegistry {
                 }
                 target = target2D;
             }
+            /*
+             * The search for coordinate operations can be costly if we 
consider the full set of CRS
+             * candidates that `IdentifiedObjectFinder` can find. In an 
attempt to reduce the cost,
+             * we will initially allow full search only for geographic CRS or 
other CRS that are easy
+             * to find (using their properties, not using their authority 
code) in the EPSG database.
+             * Only if we can not find easily the CRS, we will allow extensive 
search for all CRSs.
+             */
             if (source != null && target != null) try {
-                final List<CoordinateOperation> operations = search(source, 
target);
-                if (operations != null) {
-                    /*
-                     * Found an operation. If we had to extract the horizontal 
part of some 3D CRS, then we
-                     * need to modify the coordinate operation in order to 
match the new number of dimensions.
-                     */
-                    if (decompose != Decomposition.NONE) {
-                        for (int i=operations.size(); --i >= 0;) {
-                            CoordinateOperation operation = operations.get(i);
-                            operation = propagateVertical(sourceCRS, 
targetCRS, operation, decompose);
-                            if (operation != null) {
-                                operation = complete(operation, sourceCRS, 
targetCRS);
-                                operations.set(i, operation);
-                            } else {
-                                operations.remove(i);
+                boolean allSources = isEasySearch(source);
+                boolean allTargets = isEasySearch(target);
+                boolean done;
+                do {
+                    final List<CoordinateOperation> operations = 
search(source, allSources, target, allTargets);
+                    if (operations != null) {
+                        /*
+                         * Found at least one operation. If we had to extract 
the horizontal part of some 3D CRS, then
+                         * we need to modify the coordinate operation in order 
to match the new number of dimensions.
+                         * Some operations may be lost if we do not know how 
to propagate the vertical CRS.
+                         * If at least one operation remains, we are done.
+                         */
+                        if (decompose.source | decompose.target) {
+                            for (int i=operations.size(); --i >= 0;) {
+                                CoordinateOperation operation = 
operations.get(i);
+                                operation = propagateVertical(sourceCRS, 
targetCRS, operation, decompose);
+                                if (operation != null) {
+                                    operation = complete(operation, sourceCRS, 
targetCRS);
+                                    operations.set(i, operation);
+                                } else {
+                                    operations.remove(i);
+                                }
                             }
                         }
+                        if (!operations.isEmpty()) {
+                            return operations;
+                        }
                     }
-                    if (!operations.isEmpty()) {
-                        return operations;
-                    }
-                }
+                    done = (allSources & allTargets);
+                    allSources = true;
+                    allTargets = true;
+                } while (!done);
             } catch (IllegalArgumentException | IncommensurableException e) {
                 String message = 
Resources.format(Resources.Keys.CanNotInstantiateGeodeticObject_1, new 
CRSPair(sourceCRS, targetCRS));
                 String details = e.getLocalizedMessage();
@@ -473,20 +540,27 @@ class CoordinateOperationRegistry {
      * and submit them to the {@link #registry}. If no operation is found for 
those codes, then this method
      * returns {@code null}.
      *
-     * @param  sourceCRS  source coordinate reference system.
-     * @param  targetCRS  target coordinate reference system.
-     * @return a coordinate operation from {@code sourceCRS} to {@code 
targetCRS}, or {@code null}
-     *         if no such operation is explicitly defined in the underlying 
database.
+     * @param  sourceCRS   source coordinate reference system.
+     * @param  targetCRS   target coordinate reference system.
+     * @param  allSources  whether to perform an extensive search for source 
CRS candidates.
+     * @param  allTargets  whether to perform an extensive search for target 
CRS candidates.
+     * @return a coordinate operation from {@code sourceCRS} to {@code 
targetCRS},
+     *         or {@code null} if no such operation is explicitly defined in 
the underlying database.
      * @throws IllegalArgumentException if the coordinate systems are not of 
the same type or axes do not match.
      * @throws IncommensurableException if the units are not compatible or a 
unit conversion is non-linear.
      * @throws FactoryException if an error occurred while creating the 
operation.
      */
-    private List<CoordinateOperation> search(final CoordinateReferenceSystem 
sourceCRS,
-                                             final CoordinateReferenceSystem 
targetCRS)
+    private List<CoordinateOperation> search(final CoordinateReferenceSystem 
sourceCRS, final boolean allSources,
+                                             final CoordinateReferenceSystem 
targetCRS, final boolean allTargets)
             throws IllegalArgumentException, IncommensurableException, 
FactoryException
     {
-        final List<String> sources = findCode(sourceCRS); if 
(sources.isEmpty()) return null;
-        final List<String> targets = findCode(targetCRS); if 
(targets.isEmpty()) return null;
+        final List<String> sources = findCode(sourceCRS, allSources); if 
(sources.isEmpty()) return null;
+        final List<String> targets = findCode(targetCRS, allTargets); if 
(targets.isEmpty()) return null;
+        if (sources.equals(lastSourceCodes) && 
targets.equals(lastTargetCodes)) {
+            return null;
+        }
+        lastSourceCodes = sources;
+        lastTargetCodes = targets;
         final List<CoordinateOperation> operations = new ArrayList<>();
         boolean foundDirectOperations = false;
         boolean useDeprecatedOperations = false;
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/package-info.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/package-info.java
index 5eea1bb..c993ff9 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/package-info.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/package-info.java
@@ -91,7 +91,7 @@
  * for example by specifying the area of interest.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   0.6
  * @module
  */
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ProjectiveTransform.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ProjectiveTransform.java
index c90ea4d..f7e65b4 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ProjectiveTransform.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ProjectiveTransform.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing.operation.transform;
 import java.util.Arrays;
 import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.operation.Matrix;
+import org.apache.sis.internal.referencing.DirectPositionView;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
@@ -34,7 +35,7 @@ import org.apache.sis.util.ArgumentChecks;
  * lines in the source is preserved in the output.</p>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.1
+ * @version 1.2
  *
  * @see java.awt.geom.AffineTransform
  *
@@ -216,8 +217,14 @@ class ProjectiveTransform extends AbstractLinearTransform 
implements ExtendedPre
                                   final double[] dstPts, final int dstOff,
                                   final boolean derivate)
     {
+        if (!derivate) {
+            transform(srcPts, srcOff, dstPts, dstOff, 1);
+            return null;
+        }
+        // A non-null position is required in case this transform is 
non-affine.
+        Matrix derivative = derivative(new DirectPositionView.Double(srcPts, 
srcOff, getSourceDimensions()));
         transform(srcPts, srcOff, dstPts, dstOff, 1);
-        return derivate ? derivative((DirectPosition) null) : null;
+        return derivative;
     }
 
     /**
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
index c5c5f27..6b6724f 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/ConcurrentAuthorityFactoryTest.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.TimeUnit;
+import java.lang.reflect.Field;
 import org.opengis.util.FactoryException;
 import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.util.logging.Logging;
@@ -36,7 +37,7 @@ import static org.junit.Assume.assumeTrue;
  * Tests {@link ConcurrentAuthorityFactory}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.2
  * @since   0.7
  * @module
  */
@@ -48,6 +49,20 @@ public final strictfp class ConcurrentAuthorityFactoryTest 
extends TestCase {
     private static final long TIMEOUT = 
ConcurrentAuthorityFactory.TIMEOUT_RESOLUTION * 4;
 
     /**
+     * Verifies the value of {@code 
ConcurrentAuthorityFactory.FindEntry.DOMAIN_COUNT}.
+     * This method uses reflection because the verified class is private.
+     *
+     * @throws ReflectiveOperationException if the class name or field name 
are not as expected.
+     */
+    @Test
+    public void verifyDomainCount() throws ReflectiveOperationException {
+        final Class<?> c = 
Class.forName(ConcurrentAuthorityFactory.class.getName() + "$FindEntry");
+        final Field f = c.getDeclaredField("DOMAIN_COUNT");
+        f.setAccessible(true);
+        assertEquals(IdentifiedObjectFinder.Domain.values().length, 
f.getInt(null));
+    }
+
+    /**
      * A concurrent factory which creates new instances of {@link 
AuthorityFactoryMock}.
      */
     private static final strictfp class Mock extends 
ConcurrentAuthorityFactory<AuthorityFactoryMock> {
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
index 644d577..63f7e43 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
@@ -79,7 +79,7 @@ import static org.apache.sis.test.ReferencingAssert.*;
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Vadim Semenov
- * @version 1.1
+ * @version 1.2
  * @since   0.7
  * @module
  */
@@ -970,9 +970,14 @@ public final strictfp class EPSGFactoryTest extends 
TestCase {
         assertNotNull("With full scan allowed, the CRS should be found.", 
found);
         assertEpsgNameAndIdentifierEqual("WGS 84", 4326, found);
         /*
-         * Should find the CRS without the need of a full scan, because of the 
cache.
+         * Search should behave as specified by `DECLARATION` contract even if 
the CRS is in the cache.
          */
         finder.setSearchDomain(IdentifiedObjectFinder.Domain.DECLARATION);
+        assertNull("Should met `DECLARATION` contract.", 
finder.findSingleton(crs));
+        /*
+         * Should find the CRS without the need of a full scan, because of the 
cache.
+         */
+        finder.setSearchDomain(IdentifiedObjectFinder.Domain.ALL_DATASET);
         assertSame("The CRS should still in the cache.", found, 
finder.findSingleton(crs));
     }
 

Reply via email to