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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 1f4f8a9 Reduce the number of CRS created during search of coordinate operations. We address the issue by adding IdentifiedObjectFinder.Domain.EXHAUSTIVE_VALID_DATASET enumeration value, whih also appears to simplify code as a side-effect. 1f4f8a9 is described below commit 1f4f8a9deafda2625e3dc87aab0b052dcc9b17da Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Thu Feb 10 14:59:49 2022 +0100 Reduce the number of CRS created during search of coordinate operations. We address the issue by adding IdentifiedObjectFinder.Domain.EXHAUSTIVE_VALID_DATASET enumeration value, whih also appears to simplify code as a side-effect. https://issues.apache.org/jira/browse/SIS-535 --- .../referencing/factory/AuthorityFactoryProxy.java | 16 +-- .../factory/ConcurrentAuthorityFactory.java | 33 +++-- .../factory/IdentifiedObjectFinder.java | 63 +++++----- .../factory/MultiAuthoritiesFactory.java | 4 +- .../operation/CoordinateOperationRegistry.java | 134 +++++++++------------ 5 files changed, 115 insertions(+), 135 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 b3cf48a..5d7feaf 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 1.2 + * @version 0.7 * @since 0.7 * @module */ @@ -91,20 +91,6 @@ 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 328ea7c..cbc8d9d 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 @@ -1896,7 +1896,7 @@ 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); + return entry.get(finder != null ? finder : this, object == searching); } } return null; @@ -1917,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, result, object != searching); + result = entry.set(finder, result, object == searching); } return result; } @@ -1967,7 +1967,7 @@ public abstract class ConcurrentAuthorityFactory<DAO extends GeodeticAuthorityFa * Hard-coded for efficiency. Value is verified using reflection by the test * {@code ConcurrentAuthorityFactoryTest.verifyDomainCount()}. */ - private static final int DOMAIN_COUNT = 3; + private static final int DOMAIN_COUNT = 4; /** Result of the search with or without ignoring axes. */ private final Set<IdentifiedObject>[] caches; @@ -1989,17 +1989,30 @@ public abstract class ConcurrentAuthorityFactory<DAO extends GeodeticAuthorityFa } /** Returns the cached instance, or {@code null} if none. */ - Set<IdentifiedObject> get(final IdentifiedObjectFinder finder) { - return caches[index(finder)]; + Set<IdentifiedObject> get(final IdentifiedObjectFinder finder, final boolean explicit) { + final int i = index(finder); + if (explicit) { + dependencyFlags &= ~(1 << i); // Clear the bit telling that this cache was only for a dependency. + } + return caches[i]; } /** Caches an instance, or return previous instance if computed concurrently. */ - Set<IdentifiedObject> set(final IdentifiedObjectFinder finder, final Set<IdentifiedObject> result, final boolean dependency) { + Set<IdentifiedObject> set(final IdentifiedObjectFinder finder, Set<IdentifiedObject> result, final boolean explicit) { 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; + final Set<IdentifiedObject> existing = caches[i]; + if (existing != null) { + if (explicit) dependencyFlags &= ~(1 << i); + return existing; + } + if (!explicit) dependencyFlags |= (1 << i); + for (Set<IdentifiedObject> other : caches) { + if (result.equals(other)) { + result = other; // Share existing instance. + break; + } + } + return caches[i] = result; } /** Forgets the sets that were not explicitly requested. */ 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 af7af50..cac325a 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 @@ -72,8 +72,11 @@ public class IdentifiedObjectFinder { * The domain of the search (for example whether to include deprecated objects in the search). * * @author Martin Desruisseaux (Geomatys) - * @version 0.7 - * @since 0.7 + * @version 1.2 + * + * @see #getSearchDomain() + * + * @since 0.7 * @module */ public enum Domain { @@ -106,10 +109,34 @@ public class IdentifiedObjectFinder { * {@link #DECLARATION} will give no result. The {@code find(…)} method will then scan the dataset for * geographic CRS using equivalent datum and coordinate system. This may be a costly operation. * </div> + * + * This is the default domain of {@link IdentifiedObjectFinder}. */ VALID_DATASET, /** + * Lookup unconditionally based on all valid (non-deprecated) objects known to the factory. + * This is similar to {@link #VALID_DATASET} except that the fast {@link #DECLARATION} lookup is skipped. + * Instead, a potentially costly scan of the database is unconditionally performed + * (unless the result is already in the cache). + * + * <p>This domain can be useful when the search {@linkplain #isIgnoringAxes() ignores axis order}. + * If axis order is <em>not</em> ignored, then this domain usually has no advantage over {@link #VALID_DATASET} + * (unless the geodetic dataset contains duplicated entries) to justify the performance cost.</p> + * + * <div class="note"><b>Use case:</b> + * the EPSG database sometime contains two definitions for almost identical geographic CRS, + * one with (<var>latitude</var>, <var>longitude</var>) axis order and one with reverse order + * (e.g. EPSG::4171 versus EPSG::7084). It is sometime useful to know all variants of a given CRS. + * The {@link #VALID_DATASET} domain may not give a complete set because the "fast lookup by identifier" + * optimization may prevent {@link IdentifiedObjectFinder} to scan the rest of the database. + * This {@code EXHAUSTIVE_VALID_DATASET} domain forces such scan.</div> + * + * @since 1.2 + */ + EXHAUSTIVE_VALID_DATASET, + + /** * Lookup based on all objects (both valid and deprecated) known to the factory. * This is the same search than {@link #VALID_DATASET} except that deprecated objects * are included in the search. @@ -156,22 +183,9 @@ public class IdentifiedObjectFinder { private boolean ignoreAxes; /** - * {@code true} if the search should ignore names, aliases and identifiers. This is set to {@code true} - * only when this finder is wrapped by a {@link MultiAuthoritiesFactory} finder, which performs it own - * search based on identifiers. - */ - boolean ignoreIdentifiers; - - /** - * {@code true} if the result can be restricted to a singleton. - * This is set by {@link #findSingleton(IdentifiedObject)} only. - */ - private boolean wantSingleton; - - /** * Creates a finder using the specified factory. * - * <div class="note"><b>Design note:</b> + * <div class="note"><b>API note:</b> * this constructor is protected because instances of this class should not be created directly. * Use {@link GeodeticAuthorityFactory#newIdentifiedObjectFinder()} instead.</div> * @@ -306,24 +320,15 @@ public class IdentifiedObjectFinder { ArgumentChecks.ensureNonNull("object", object); Set<IdentifiedObject> result = getFromCache(object); if (result == null) { - final boolean previousIgnoreAxes = ignoreAxes; final AuthorityFactoryProxy<?> previousProxy = proxy; proxy = AuthorityFactoryProxy.getInstance(object.getClass()); try { - /* - * 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 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 || !proxy.hasAxes())) { + if (domain != Domain.EXHAUSTIVE_VALID_DATASET) { /* * 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; // It makes a difference only if `wantSingleton = true`. IdentifiedObject candidate = createFromIdentifiers(object); if (candidate == null) { /* @@ -349,8 +354,7 @@ public class IdentifiedObjectFinder { } } } finally { - proxy = previousProxy; - ignoreAxes = previousIgnoreAxes; + proxy = previousProxy; } result = cache(object, result); // Costly operations (even if the result is empty) are worth to cache. } @@ -382,7 +386,6 @@ public class IdentifiedObjectFinder { IdentifiedObject result = null; boolean sameAxisOrder = false; boolean ambiguous = false; - wantSingleton = true; try { for (final IdentifiedObject candidate : find(object)) { final boolean equalsIncludingAxes = !ignoreAxes || Utilities.deepEquals(candidate, object, COMPARISON_MODE); @@ -397,8 +400,6 @@ public class IdentifiedObjectFinder { } } catch (BackingStoreException e) { throw e.unwrapOrRethrow(FactoryException.class); - } finally { - wantSingleton = false; } return (sameAxisOrder || !ambiguous) ? result : null; } 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 33a1d0d..77f579a 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 @@ -146,7 +146,7 @@ import org.apache.sis.util.collection.BackingStoreException; * do not need to be thread-safe. See constructor Javadoc for more information. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 0.8 + * @version 1.2 * * @see org.apache.sis.referencing.CRS#getAuthorityFactory(String) * @@ -1807,7 +1807,7 @@ public class MultiAuthoritiesFactory extends GeodeticAuthorityFactory implements if (candidate instanceof GeodeticAuthorityFactory && unique.put(candidate, Boolean.TRUE) == null) { IdentifiedObjectFinder finder = ((GeodeticAuthorityFactory) candidate).newIdentifiedObjectFinder(); if (finder != null) { // Should never be null according method contract, but we are paranoiac. - finder.ignoreIdentifiers = true; + finder.setSearchDomain(Domain.EXHAUSTIVE_VALID_DATASET); finder.setWrapper(this); list.add(finder); } 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 72eb053..1696af5 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 @@ -44,6 +44,7 @@ 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.crs.CompoundCRS; import org.opengis.referencing.cs.EllipsoidalCS; import org.opengis.referencing.operation.*; @@ -170,7 +171,7 @@ class CoordinateOperationRegistry { * operation, even if an equivalent definition was provided by another authority.</div> * * @see #authorityCodes - * @see #findCode(CoordinateReferenceSystem, boolean) + * @see #findCode(CoordinateReferenceSystem) */ private final IdentifiedObjectFinder codeFinder; @@ -214,7 +215,7 @@ class CoordinateOperationRegistry { * 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) + * @see #search(CoordinateReferenceSystem, CoordinateReferenceSystem) */ boolean stopAtFirst; @@ -227,7 +228,7 @@ class CoordinateOperationRegistry { private Predicate<CoordinateOperation> filter; /** - * Authority codes found for CRS. This is a cache for {@link #findCode(CoordinateReferenceSystem, boolean)}. + * Authority codes found for CRS. This is a cache for {@link #findCode(CoordinateReferenceSystem)}. * This map may be non-empty only if {@link #codeFinder} is non-null. * * <div class="note"><b>Design note:</b> @@ -237,20 +238,11 @@ class CoordinateOperationRegistry { * provide such cache, and the values cached here are the result of a little bit more work.</div> * * @see #codeFinder - * @see #findCode(CoordinateReferenceSystem, boolean) + * @see #findCode(CoordinateReferenceSystem) */ 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. @@ -313,41 +305,34 @@ 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. + * @param crs the CRS for which to search authority codes. * @return authority codes for the given CRS, or an empty list if none. * <b>Do not modify</b> since this list may be cached. */ - private List<String> findCode(final CoordinateReferenceSystem crs, final boolean full) throws FactoryException { + private List<String> findCode(final CoordinateReferenceSystem crs) throws FactoryException { List<String> codes = authorityCodes.get(crs); if (codes == null) { if (codeFinder == null) { return Collections.emptyList(); } codes = new ArrayList<>(); - codeFinder.setIgnoringAxes(full); - codeFinder.setSearchDomain(full ? IdentifiedObjectFinder.Domain.VALID_DATASET - : IdentifiedObjectFinder.Domain.DECLARATION); + codeFinder.setIgnoringAxes(true); + codeFinder.setSearchDomain(isEasySearch(crs) + ? IdentifiedObjectFinder.Domain.EXHAUSTIVE_VALID_DATASET + : IdentifiedObjectFinder.Domain.VALID_DATASET); 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)) { + if (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); - } + authorityCodes.put(crs, codes); } return codes; } @@ -429,7 +414,17 @@ class CoordinateOperationRegistry { * because they may be the only ones available.</p> */ private static boolean isEasySearch(final CoordinateReferenceSystem crs) { - return (crs instanceof SingleCRS) && !(crs instanceof GeneralDerivedCRS); + if (crs instanceof GeneralDerivedCRS) { + return false; + } + if (crs instanceof CompoundCRS) { + for (CoordinateReferenceSystem c : ((CompoundCRS) crs).getComponents()) { + if (!isEasySearch(c)) { + return false; + } + } + } + return true; } /** @@ -483,45 +478,37 @@ 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. + * Search for coordinate operations between the pair of CRS components that we just found. + * We may need to force a search of all CRS variants (equivalent CRS except for axis order) + * for resolving the case where the geodetic database contains such pairs of equivalent CRS + * (e.g. EPSG::4171 versus EPSG::7084). For reducing the cost, we will force full scan only + * for CRS for which `isEasySearch(…)` returns `true`. */ if (source != null && target != null) try { - 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); - } + final List<CoordinateOperation> operations = search(source, target); + 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; - } } - done = (allSources & allTargets); - allSources = true; - allTargets = true; - } while (!done); + if (!operations.isEmpty()) { + return operations; + } + } } catch (IllegalArgumentException | IncommensurableException e) { String message = Resources.format(Resources.Keys.CanNotInstantiateGeodeticObject_1, new CRSPair(sourceCRS, targetCRS)); String details = e.getLocalizedMessage(); @@ -537,30 +524,23 @@ class CoordinateOperationRegistry { /** * Returns operations for conversions or transformations between two coordinate reference systems. * This method extracts the authority code from the supplied {@code sourceCRS} and {@code targetCRS}, - * and submit them to the {@link #registry}. If no operation is found for those codes, then this method + * and submits 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. - * @param allSources whether to perform an extensive search for source CRS candidates. - * @param allTargets whether to perform an extensive search for target CRS candidates. + * @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. * @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 boolean allSources, - final CoordinateReferenceSystem targetCRS, final boolean allTargets) + private List<CoordinateOperation> search(final CoordinateReferenceSystem sourceCRS, + final CoordinateReferenceSystem targetCRS) throws IllegalArgumentException, IncommensurableException, FactoryException { - 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<String> sources = findCode(sourceCRS); if (sources.isEmpty()) return null; + final List<String> targets = findCode(targetCRS); if (targets.isEmpty()) return null; final List<CoordinateOperation> operations = new ArrayList<>(); boolean foundDirectOperations = false; boolean useDeprecatedOperations = false;