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)); }