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 ed8a9d3277 Sort coordinate operations by domain of validity in Java
code instead of in the SQL query. It makes easier to handle the new way that
extent are declared in EPSG version 10+. It also seems to fix a bug in the
ordering of coordinate operations.
ed8a9d3277 is described below
commit ed8a9d327722c70b726faee7484fabf416392ff6
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Aug 4 20:04:00 2025 +0200
Sort coordinate operations by domain of validity in Java code instead of in
the SQL query.
It makes easier to handle the new way that extent are declared in EPSG
version 10+.
It also seems to fix a bug in the ordering of coordinate operations.
---
.../apache/sis/metadata/iso/extent/Extents.java | 6 +-
.../referencing/factory/IdentifiedObjectSet.java | 77 ++++-----
.../factory/sql/CoordinateOperationSet.java | 19 +--
.../referencing/factory/sql/EPSGCodeFinder.java | 16 +-
.../referencing/factory/sql/EPSGDataAccess.java | 178 ++++++++++++++-------
.../referencing/factory/sql/ObjectPertinence.java | 154 ++++++++++++++++++
.../sis/referencing/factory/sql/TableInfo.java | 30 ++--
.../referencing/factory/sql/EPSGFactoryTest.java | 16 +-
8 files changed, 350 insertions(+), 146 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
index 8458017128..e4a5d16c39 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/Extents.java
@@ -681,9 +681,9 @@ public final class Extents extends Static {
}
/**
- * Returns the union of the given geographic bounding boxes. If any of the
arguments is {@code null},
- * then this method returns the other argument (which may be null).
Otherwise this method returns a box
- * which is the union of the two given boxes.
+ * Returns the union of the given geographic bounding boxes.
+ * If any of the arguments is {@code null}, then this method returns the
other argument (which may be null).
+ * Otherwise, this method returns a box which is the union of the two
given boxes.
*
* <p>This method never modify the given boxes, but may return directly
one of the given arguments
* if it already represents the union result.</p>
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
index 3c57d1fbf4..9056d04d0a 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/IdentifiedObjectSet.java
@@ -16,7 +16,6 @@
*/
package org.apache.sis.referencing.factory;
-import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
@@ -53,26 +52,28 @@ import org.apache.sis.util.collection.CheckedContainer;
* {@linkplain #createObject(String) object creation}, or to {@link
#add(IdentifiedObject)} for objects
* that are already instantiated. This collection cannot contain two {@code
IdentifiedObject} instances
* having the same identifier. However, the identifiers used by this class can
be controlled by overriding
- * {@link #getAuthorityCode(IdentifiedObject)}.</p>
+ * the {@link #getAuthorityCode(IdentifiedObject)} method.</p>
*
* <p>Iterations over elements in this collection preserve insertion order.</p>
*
* <h2>Purpose</h2>
- * {@code IdentifiedObjectSet} can be used as the set returned by
implementations of the
- * {@link
GeodeticAuthorityFactory#createFromCoordinateReferenceSystemCodes(String,
String)} method.
- * Deferred creation can have great performance impact since some set may
contain as much as 40 entries
- * (e.g. transformations from <q>ED50</q> (EPSG:4230) to <q>WGS 84</q>
(EPSG:4326))
- * while some users only want to look for the first entry.
+ * {@code IdentifiedObjectSet} is useful mostly for implementers of authority
factories rather than users.
+ * When used as the implementation of the
+ * {@linkplain
GeodeticAuthorityFactory#createFromCoordinateReferenceSystemCodes(String,
String)
+ * set of coordinate operations between a pair of <abbr>CRS</abbr>}, {@code
IdentifiedObjectSet}
+ * can improve significantly the performances because some sets contain tens
of entries
+ * (e.g., transformations from <q>ED50</q> (EPSG:4230) to <q>WGS 84</q>
(EPSG:4326)),
+ * while users typically want to look for only the first entry.
*
* <h2>Exception handling</h2>
* If the underlying factory failed to creates an object because of an
unsupported operation method
- * ({@link NoSuchIdentifierException}), the exception is logged at {@link
Level#WARNING} and the iteration continue.
+ * ({@link NoSuchIdentifierException}), the exception is logged at {@link
Level#WARNING} and the iteration continues.
* If the operation creation failed for any other kind of reason ({@link
FactoryException}), then the exception is
* re-thrown as an unchecked {@link BackingStoreException}. This default
behavior can be changed by overriding
* the {@link #isRecoverableFailure(FactoryException)} method.
*
* <h2>Thread safety</h2>
- * This class is thread-safe is the underlying {@linkplain #factory} is also
thread-safe.
+ * This class is thread-safe if the underlying {@linkplain #factory} is also
thread-safe.
* However, implementers are encouraged to wrap in {@linkplain
java.util.Collections#unmodifiableSet unmodifiable set}
* if they intent to cache {@code IdentifiedObjectSet} instances.
*
@@ -91,14 +92,7 @@ public class IdentifiedObjectSet<T extends IdentifiedObject>
extends AbstractSet
* <p><b>Note:</b> using {@code ConcurrentHahMap} would be more efficient.
* But the latter does not support null values and does not preserve
insertion order.</p>
*/
- final Map<String,T> objects = new LinkedHashMap<>();
-
- /**
- * The {@link #objects} keys, created for iteration purpose when first
needed and cleared when the map is modified.
- * We need to use such array as a snapshot of the map state at the time
the iterator was created because the map
- * may be modified during iteration or by concurrent threads.
- */
- private transient String[] codes;
+ final Map<String,T> objects;
/**
* The factory to use for creating {@code IdentifiedObject}s when first
needed.
@@ -131,6 +125,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
this.factory = Objects.requireNonNull(factory);
this.type = Objects.requireNonNull(type);
this.proxy = AuthorityFactoryProxy.getInstance(type);
+ this.objects = new LinkedHashMap<>();
}
/**
@@ -160,7 +155,6 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
@Override
public void clear() {
synchronized (objects) {
- codes = null;
objects.clear();
}
}
@@ -185,27 +179,15 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
* @return the authority codes in iteration order.
*/
public String[] getAuthorityCodes() {
- return codes().clone();
- }
-
- /**
- * Returns the {@code codes} array, creating it if needed. This method
does not clone the array.
- */
- @SuppressWarnings("ReturnOfCollectionOrArrayField")
- final String[] codes() {
synchronized (objects) {
- if (codes == null) {
- final Set<String> keys = objects.keySet();
- codes = keys.toArray(String[]::new);
- }
- return codes;
+ return objects.keySet().toArray(String[]::new);
}
}
/**
* Sets the content of this collection to the object identified by the
given codes.
* For any code in the given sequence, this method will preserve the
corresponding {@code IdentifiedObject}
- * instance if it was already created. Otherwise objects will be
{@linkplain #createObject(String) created}
+ * instance if it was already created. Otherwise, objects will be
{@linkplain #createObject(String) created}
* only when first needed.
*
* <h4>Use case</h4>
@@ -220,8 +202,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
*/
public void setAuthorityCodes(final String... codes) {
synchronized (objects) {
- this.codes = null;
- final Map<String,T> copy = new HashMap<>(objects);
+ final var copy = new HashMap<String,T>(objects);
objects.clear();
for (final String code : codes) {
objects.put(code, copy.get(code));
@@ -233,15 +214,13 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
* Ensures that this collection contains an object for the specified
authority code.
* If this collection does not contain any element for the given code,
then this method
* will instantiate an {@code IdentifiedObject} for the given code only
when first needed.
- * Otherwise this collection is unchanged.
+ * Otherwise, this collection is unchanged.
*
* @param code the code authority code of the {@code IdentifiedObject}
to include in this set.
*/
public void addAuthorityCode(final String code) {
synchronized (objects) {
- if (objects.putIfAbsent(code, null) == null) {
- codes = null;
- }
+ objects.putIfAbsent(code, null);
}
}
@@ -261,7 +240,6 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
final String code = getAuthorityCode(object);
final T previous;
synchronized (objects) {
- codes = null;
previous = objects.put(code, object);
}
return !Objects.equals(previous, object);
@@ -274,7 +252,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
*
* @see #createObject(String)
*/
- final T get(final String code) throws BackingStoreException {
+ private T get(final String code) throws BackingStoreException {
T object;
boolean success;
synchronized (objects) {
@@ -304,7 +282,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
* The check for `containsKey` is a paranoiac check in
case the element has been removed
* in another thread while we were creating the object.
This is likely to be unnecessary
* in the vast majority of cases where the set of codes is
never modified after this set
- * has been published. However if someone decided to do
such concurrent modifications,
+ * has been published. However, if someone decided to do
such concurrent modifications,
* not checking for concurrent removal could be a subtle
and hard-to-find bug, so we are
* better to be safe. Note that if a concurrent removal
happened, we still return the non-null
* object but we do not put it in this
IdentifiedObjectSet. This behavior is as if this method
@@ -316,8 +294,8 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
object = c; // The object has
been created concurrently.
}
}
- } else if (objects.remove(code, null)) { // Do not remove
if a concurrent thread succeeded.
- codes = null;
+ } else { // Do not remove
if a concurrent thread succeeded.
+ objects.remove(code, null);
}
}
}
@@ -329,6 +307,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
*
* @param object the {@code IdentifiedObject} to test for presence in
this set.
* @return {@code true} if the given object is presents in this set.
+ * @throws ClassCastException if the given object is non-null and not an
instance of {@code <T>}.
*/
@Override
public boolean contains(final Object object) {
@@ -340,10 +319,9 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
*
* @param code the code of the object to remove.
*/
- final void removeAuthorityCode(final String code) {
+ private void removeAuthorityCode(final String code) {
synchronized (objects) {
objects.remove(code);
- codes = null;
}
}
@@ -352,6 +330,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
*
* @param object the {@code IdentifiedObject} to remove from this set.
* @return {@code true} if this set changed as a result of this call.
+ * @throws ClassCastException if the given object is non-null and not an
instance of {@code <T>}.
*/
@Override
public boolean remove(final Object object) {
@@ -361,7 +340,6 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
if (object.equals(current)) {
synchronized (objects) {
objects.remove(code);
- codes = null;
}
return true;
}
@@ -374,8 +352,10 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
*
* @param collection the {@code IdentifiedObject}s to remove from this
set.
* @return {@code true} if this set changed as a result of this call.
+ * @throws ClassCastException if the given collection contains an object
which is not an instance of {@code <T>}.
*/
@Override
+ @SuppressWarnings("element-type-mismatch")
public boolean removeAll(final Collection<?> collection) {
boolean modified = false;
for (final Object object : collection) {
@@ -403,7 +383,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
* We need to take a snapshot because the underlying {@link
IdentifiedObjectSet#objects} map may be
* modified concurrently in other threads.
*/
- private final String[] keys = codes();
+ private final String[] keys = getAuthorityCodes();
/**
* Index of the next element to obtain by a call to {@link
IdentifiedObjectSet#get(String)}.
@@ -524,6 +504,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
* @param code the code for which to create the identified object.
* @return the identified object created from the given code.
* @throws FactoryException if the object creation failed.
+ * @throws ClassCastException if the object created from the given object
is not an instance of {@code <T>}.
*/
protected T createObject(final String code) throws FactoryException {
return type.cast(proxy.createFromAPI(factory, code));
@@ -563,7 +544,7 @@ public class IdentifiedObjectSet<T extends
IdentifiedObject> extends AbstractSet
*/
private static String getCause(Throwable cause) {
final String lineSeparator = System.lineSeparator();
- final StringBuilder trace = new StringBuilder(180);
+ final var trace = new StringBuilder(180);
while (cause != null) {
trace.append(lineSeparator).append(" •
").append(Classes.getShortClassName(cause));
final String message = cause.getMessage(); // Prefer the
local of system administrator.
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CoordinateOperationSet.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CoordinateOperationSet.java
index 6002437391..f289b0f33e 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CoordinateOperationSet.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/CoordinateOperationSet.java
@@ -34,14 +34,13 @@ import org.opengis.referencing.crs.DerivedCRS;
/**
- * A lazy set of {@link CoordinateOperation} objects to be returned by the
- * {@link EPSGDataAccess#createFromCoordinateReferenceSystemCodes(String,
String)} method.
- * There are two different ways in which {@link EPSGDataAccess} get coordinate
operations:
+ * A lazy set of coordinate operations identified by their <abbr>EPSG</abbr>
codes.
+ * There are two different ways in which {@link EPSGDataAccess} gets
coordinate operations:
*
* <ol>
- * <li>The coordinate operation may be the <i>conversion from base</i>
property of a projected CRS.
- * Those conversions are obtained by a SQL query like below (note that
this query can return at most
- * one result, because {@code COORD_REF_SYS_CODE} is a primary key):
+ * <li>The coordinate operation may be the <i>conversion from base</i>
property of a projected <abbr>CRS</abbr>.
+ * Those conversions are obtained by a <abbr>SQL</abbr> query like below
(note that this query can return at
+ * most one result, because {@code COORD_REF_SYS_CODE} is a primary key):
*
* {@snippet lang="sql" :
* SELECT PROJECTION_CONV_CODE FROM "Coordinate Reference System"
WHERE BASE_CRS_CODE = ? AND COORD_REF_SYS_CODE = ?
@@ -49,7 +48,7 @@ import org.opengis.referencing.crs.DerivedCRS;
* </li>
*
* <li>The coordinate operation may be standalone. This is the case of
coordinate transformations having stochastic errors.
- * Those transformations are obtained by a SQL query like below (note
that this query can return many results):
+ * Those transformations are obtained by a <abbr>SQL</abbr> query like
below (note that it may return many results):
*
* {@snippet lang="sql" :
* SELECT COORD_OP_CODE FROM "Coordinate_Operation" … WHERE … AND
SOURCE_CRS_CODE = ? AND TARGET_CRS_CODE = ?
@@ -60,6 +59,8 @@ import org.opengis.referencing.crs.DerivedCRS;
* We distinguish those two cases by the presence or absence of a coordinate
operation code in the {@link #projections} map.
*
* @author Martin Desruisseaux (IRD, Geomatys)
+ *
+ * @see EPSGDataAccess#createFromCoordinateReferenceSystemCodes(String, String)
*/
final class CoordinateOperationSet extends
IdentifiedObjectSet<CoordinateOperation> {
/**
@@ -73,7 +74,7 @@ final class CoordinateOperationSet extends
IdentifiedObjectSet<CoordinateOperati
* </ul>
*
* This map does <strong>not</strong> contain all operations to be
returned by this {@code CoordinateOperationSet},
- * but only the ones to be returned by the first SQL query documented in
the class Javadoc.
+ * but only the ones to be returned by the first <abbr>SQL</abbr> query
documented in the class Javadoc.
*/
private final Map<String,Integer> projections;
@@ -109,7 +110,7 @@ final class CoordinateOperationSet extends
IdentifiedObjectSet<CoordinateOperati
}
/**
- * Creates a coordinate operation for the specified EPSG code.
+ * Creates a coordinate operation for the specified <abbr>EPSG</abbr> code.
*/
@Override
protected CoordinateOperation createObject(final String code) throws
FactoryException {
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
index d2f4d66493..47975c6b37 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGCodeFinder.java
@@ -42,6 +42,7 @@ import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.privy.CollectionsExt;
+import org.apache.sis.pending.jdk.JDK16;
import org.apache.sis.pending.jdk.JDK19;
import org.apache.sis.metadata.privy.ReferencingServices;
import org.apache.sis.metadata.sql.privy.SQLUtilities;
@@ -465,16 +466,11 @@ crs: if (isInstance(CoordinateReferenceSystem.class,
object)) {
result.add(r.getString(1));
}
}
- result.remove(null); // Should not have null
element, but let be safe.
- if (result.size() > 1) {
- final Object[] id = result.toArray();
- if (dao.sort(table.unquoted(), id)) {
- result.clear();
- for (final Object c : id) {
- result.add((String) c);
- }
- }
- }
+ result.remove(null); // Should not have null element, but let
be safe.
+ dao.sort(table.unquoted(), result).ifPresent((sorted) -> {
+ result.clear();
+ result.addAll(JDK16.toList(sorted));
+ });
return result;
} catch (SQLException exception) {
throw dao.databaseFailure(Identifier.class,
String.valueOf(CollectionsExt.first(filters[0].values)), exception);
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
index 11e88bfd25..44aff1352a 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
@@ -24,11 +24,13 @@ import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Iterator;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
+import java.util.stream.Stream;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
@@ -81,7 +83,6 @@ import
org.apache.sis.referencing.factory.IdentifiedObjectFinder;
import org.apache.sis.referencing.privy.WKTKeywords;
import org.apache.sis.referencing.privy.CoordinateOperations;
import org.apache.sis.referencing.privy.ReferencingFactoryContainer;
-import org.apache.sis.referencing.privy.Formulas;
import org.apache.sis.referencing.internal.DeferredCoordinateOperation;
import org.apache.sis.referencing.internal.DeprecatedCode;
import org.apache.sis.referencing.internal.EPSGParameterDomain;
@@ -1129,6 +1130,8 @@ next: for (int i=0; i<codes.length; i++) {
/**
* Returns the scope from the given authority code.
+ * The given code is a value of the {@code scopes} list
+ * after a call to {@link #getUsages getUsages(…)}.
*
* @param code the <abbr>EPSG</code> code.
* @return the scope, or {@code null} if none.
@@ -1150,6 +1153,34 @@ next: for (int i=0; i<codes.length; i++) {
return scope;
}
+ /**
+ * Gets the codes of extents and scopes of the object identified by the
given code in the given table.
+ * The {@code actualTable} argument must be the result of {@code
translator.toActualTableName(table)}.
+ * The {@code extents} and {@code scopes} collections should be initially
empty and will be filled by
+ * this method. The same number of codes will be added in both of them.
+ *
+ * @param actualTable actual name of the table of the object for which
to get the usages.
+ * @param code <abbr>EPSG</abbr> code of the object for which to
get the usages.
+ * @param extents where to store extent codes, or {@code null} for
ignoring extents.
+ * @param scopes where to store usage codes, or {@code null} for
ignoring scopes.
+ */
+ private void getUsages(final String actualTable,
+ final int code,
+ final Collection<String> extents,
+ final Collection<Integer> scopes) throws
SQLException, FactoryDataException
+ {
+ try (ResultSet result = executeMetadataQuery("Usage",
+ "SELECT EXTENT_CODE, SCOPE_CODE FROM \"Usage\""
+ + " WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?",
+ actualTable, code))
+ {
+ while (result.next()) {
+ if (extents != null) extents.add(getString (code, result, 1));
+ if (scopes != null) scopes .add(getInteger(code, result, 2));
+ }
+ }
+ }
+
/**
* Logs a warning saying that the given code is deprecated and returns the
code of the proposed replacement.
*
@@ -1157,7 +1188,7 @@ next: for (int i=0; i<codes.length; i++) {
* @param code the deprecated code.
* @return the proposed replacement (may be the "(none)" text). Never
empty.
*/
- private String getSupersession(final String table, final Integer code,
final Locale locale) throws SQLException {
+ private String getReplacement(final String table, final Integer code,
final Locale locale) throws SQLException {
String reason = null;
String replacedBy;
search: try (ResultSet result = executeMetadataQuery("Deprecation",
@@ -1239,6 +1270,7 @@ search: try (ResultSet result =
executeMetadataQuery("Deprecation",
* se we need to fetch and store the extent before to populate the
`properties` map.
*/
final Extent extent = (extentCode == null) ? null :
createExtent(extentCode);
+ final String actualTable = translator.toActualTableName(table);
/*
* Get all domains for the object identified by the given code.
* The table used nere is new in version 10 of EPSG database.
@@ -1248,21 +1280,14 @@ search: try (ResultSet result =
executeMetadataQuery("Deprecation",
ObjectDomain[] domains = null;
if (translator.isUsageTableFound()) {
final var extents = new ArrayList<String>();
- final var scopes = new ArrayList<InternationalString>();
- try (ResultSet result = executeMetadataQuery("Usage",
- "SELECT EXTENT_CODE, SCOPE_CODE FROM \"Usage\""
- + " WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?",
- translator.toActualTableName(table), code))
- {
- while (result.next()) {
- extents.add(getString(code, result, 1));
- scopes .add(getScope(getInteger(code, result, 2)));
- }
- }
+ final var scopes = new ArrayList<Integer>();
+ getUsages(actualTable, code, extents, scopes);
if (!extents.isEmpty()) {
domains = new ObjectDomain[extents.size()];
for (int i=0; i<domains.length; i++) {
- domains[i] = new DefaultObjectDomain(scopes.get(i),
owner.createExtent(extents.get(i)));
+ domains[i] = new DefaultObjectDomain(
+ getScope(scopes.get(i)),
+ owner.createExtent(extents.get(i)));
}
}
}
@@ -1285,7 +1310,7 @@ search: try (ResultSet result =
executeMetadataQuery("Deprecation",
+ " FROM \"Alias\" INNER JOIN \"Naming System\""
+ " ON \"Alias\".NAMING_SYSTEM_CODE = \"Naming
System\".NAMING_SYSTEM_CODE"
+ " WHERE OBJECT_TABLE_NAME=? AND OBJECT_CODE=?",
- translator.toActualTableName(table), code))
+ actualTable, code))
{
while (result.next()) {
final String naming = getOptionalString(result, 1);
@@ -1334,7 +1359,7 @@ search: try (ResultSet result =
executeMetadataQuery("Deprecation",
final ImmutableIdentifier identifier;
if (deprecated) {
properties.put(AbstractIdentifiedObject.DEPRECATED_KEY,
Boolean.TRUE);
- final String replacedBy = getSupersession(table, code, locale);
+ final String replacedBy = getReplacement(table, code, locale);
identifier = new DeprecatedCode(
authority,
Constants.EPSG,
@@ -3342,15 +3367,11 @@ next: while (r.next()) {
if (searchTransformations) {
key = "TransformationFromCRS";
sql = "SELECT COORD_OP_CODE"
- + " FROM \"Coordinate_Operation\" AS CO"
- + " JOIN \"Area\" ON AREA_OF_USE_CODE = AREA_CODE"
- + " WHERE CO.DEPRECATED=0" // Do not put spaces
around "=" - SQLTranslator searches for this exact match.
+ + " FROM \"Coordinate_Operation\""
+ + " WHERE DEPRECATED=0" // Do not put spaces
around "=" - SQLTranslator searches for this exact match.
+ " AND SOURCE_CRS_CODE = ?"
+ " AND TARGET_CRS_CODE = ?"
- + " ORDER BY COORD_OP_ACCURACY ASC NULLS LAST, "
- + " (AREA_EAST_BOUND_LON - AREA_WEST_BOUND_LON +
CASE WHEN AREA_EAST_BOUND_LON < AREA_WEST_BOUND_LON THEN 360 ELSE 0 END)"
- + " * (AREA_NORTH_BOUND_LAT -
AREA_SOUTH_BOUND_LAT)"
- + " * COS(RADIANS(AREA_NORTH_BOUND_LAT +
AREA_SOUTH_BOUND_LAT)/2) DESC";
+ + " ORDER BY COORD_OP_ACCURACY ASC NULLS LAST";
} else {
key = "ConversionFromCRS";
sql = "SELECT PROJECTION_CONV_CODE"
@@ -3368,12 +3389,13 @@ next: while (r.next()) {
/*
* Search finished. We may have a lot of coordinate operations
* (e.g. about 40 for "ED50" (EPSG:4230) to "WGS 84" (EPSG:4326)).
- * Alter the ordering using the information supplied in the
supersession table.
+ * Alter the ordering using the information supplied in the extents
+ * and supersession tables.
*/
- final String[] codes = set.getAuthorityCodes();
- if (codes.length > 1 && sort("Coordinate_Operation", codes)) {
- set.setAuthorityCodes(codes);
- }
+ List<String> codes = Arrays.asList(set.getAuthorityCodes());
+ sort("Coordinate_Operation", codes).ifPresent((sorted) -> {
+ set.setAuthorityCodes(sorted.toArray(String[]::new));
+ });
} catch (SQLException exception) {
throw databaseFailure(CoordinateOperation.class, label, exception);
}
@@ -3408,60 +3430,92 @@ next: while (r.next()) {
}
/**
- * Sorts an array of codes in preference order. This method orders
pairwise the codes according the information
- * provided in the supersession table. If the same object is superseded by
more than one object, then the most
- * recent one is inserted first. Except for the codes moved as a result of
pairwise ordering, this method tries
- * to preserve the old ordering of the supplied codes (since deprecated
operations should already be last).
- * The ordering is performed in place.
+ * Sorts a collection of codes in preference order.
+ * This method orders pairwise the codes according the information
provided in the supersession table.
+ * If the same object is superseded by more than one object, then the most
recent one is inserted first.
+ * Except for the codes moved as a result of pairwise ordering, this
method tries to preserve the old
+ * ordering of the supplied codes (since deprecated operations should
already be last).
*
- * @param table the table of the objects for which to check for
supersession.
- * @param codes the codes, usually as an array of {@link String}. If the
array do not contains string objects,
- * then the {@link Object#toString()} method must return the
code for each element.
- * @return {@code true} if the array changed as a result of this method
call.
+ * @param table the table of the objects for which to check for
supersession.
+ * @param codes the codes to sort. This collection will not be modified
by this method.
+ * @return codes of sorted elements, or empty if this method did not
changed the codes order.
*/
- final synchronized boolean sort(final String table, final Object[] codes)
throws SQLException, FactoryException {
- int iteration = 0;
- do {
- boolean changed = false;
- for (int i=0; i<codes.length; i++) {
- final int code;
+ final synchronized Optional<Stream<String>> sort(final String table, final
Collection<String> codes)
+ throws SQLException, FactoryException
+ {
+ final int size = codes.size();
+ if (size > 1) try {
+ final var elements = new ObjectPertinence[size];
+ final var extents = new ArrayList<String>();
+ final String actualTable = translator.toActualTableName(table);
+ int count = 0;
+ for (final String code : codes) {
+ final int key;
try {
- code = Integer.parseInt(codes[i].toString());
+ key = Integer.parseInt(code);
} catch (NumberFormatException e) {
unexpectedException("sort", e);
continue;
}
+ if (translator.isUsageTableFound()) {
+ getUsages(actualTable, key, extents, null);
+ } else {
+ /*
+ * For compatibility with EPSG database before version 10.
+ * We may delete this block in a future Apache SIS version.
+ * Note: if this block is deleted, consider deleting also
+ * the finally block and the `TableInfo.areaOfUse` flag.
+ */
+ for (final TableInfo info : TableInfo.EPSG) {
+ if (table.equals(info.unquoted())) {
+ if (info.areaOfUse) {
+ try (ResultSet result = executeQueryForCodes(
+ "Area", // Table from EPSG version
9. Does not exist anymore in version 10.
+ "SELECT AREA_OF_USE_CODE FROM \"" +
table + "\" WHERE " + info.codeColumn + "=?",
+ key))
+ {
+ while (result.next()) {
+ extents.add(getString(code, result,
1));
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+ final ObjectPertinence element = new ObjectPertinence(key,
extents, owner);
+ extents.clear();
try (ResultSet result = executeMetadataQuery(
"Supersession",
"SELECT SUPERSEDED_BY FROM \"Supersession\""
+ " WHERE OBJECT_TABLE_NAME=? AND
OBJECT_CODE=?"
+ " ORDER BY SUPERSESSION_YEAR DESC",
- translator.toActualTableName(table), code))
+ actualTable, key))
{
while (result.next()) {
- final String replacement = result.getString(1);
- if (replacement != null) {
- for (int j=i+1; j<codes.length; j++) {
- final Object candidate = codes[j];
- if (replacement.equals(candidate.toString())) {
- /*
- * Found a code to move in front of the
superceded one.
- */
- System.arraycopy(codes, i, codes, i+1,
j-i);
- codes[i++] = candidate;
- changed = true;
- }
- }
+ final int replacement = result.getInt(1);
+ if (!result.wasNull()) {
+ element.replacedBy.add(replacement);
}
}
}
+ elements[count++] = element;
}
- if (!changed) {
- return iteration != 0;
+ if (ObjectPertinence.sort(elements)) {
+ return
Optional.of(Arrays.stream(elements).map(ObjectPertinence::code));
+ }
+ } finally {
+ /*
+ * Remove from the cache because the table name may change.
+ * Note: this is for compatibility with EPSG before version 10.
+ * This block may be deleted in a future Apache SIS version.
+ */
+ PreparedStatement stmt = statements.remove("Area");
+ if (stmt != null) {
+ stmt.close();
}
}
- while (++iteration < Formulas.MAXIMUM_ITERATIONS); // Arbitrary
limit for avoiding never-ending loop.
- return true;
+ return Optional.empty();
}
/**
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java
new file mode 100644
index 0000000000..82da6646a7
--- /dev/null
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/ObjectPertinence.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.referencing.factory.sql;
+
+import java.util.List;
+import java.util.Arrays;
+import java.util.ArrayList;
+import org.opengis.util.FactoryException;
+import org.opengis.metadata.extent.Extent;
+import org.opengis.metadata.extent.GeographicBoundingBox;
+import org.apache.sis.metadata.iso.extent.Extents;
+import org.apache.sis.referencing.privy.Formulas;
+import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
+import org.apache.sis.util.privy.CollectionsExt;
+import org.apache.sis.util.privy.Strings;
+
+
+/**
+ * Collect information needed for evaluating the pertinence of an object.
+ * The criteria are, in order:
+ *
+ * <ol>
+ * <li>Superseded objects are last.</li>
+ * <li>Largest domain of validity (after intersection with <abbr>AOI</abbr>
are first.</li>
+ * </ol>
+ *
+ * This class defines a {@link #compareTo(ObjectPertinence)} method which is
inconsistent
+ * with {@link #equals(Object)}, but this is okay for the purpose of this
internal class.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+final class ObjectPertinence implements Comparable<ObjectPertinence> {
+ /**
+ * Code of the object for which the pertinence is evaluated.
+ */
+ private final int code;
+
+ /**
+ * An estimation of the surface of the domain of validity as a negative
number, or NaN if none.
+ * The units of measurement do not matter here. The only requirement is
that larger areas are
+ * represented by <em>smaller</em> numbers, in order to have them sorted
first.
+ * The {@link Double#NaN} values will be sorted last.
+ */
+ private final double area;
+
+ /**
+ * The objects to use instead of the object identified by the code given
at construction time.
+ * This list is non-empty when the object is superseded by more recent
objects.
+ * This list is empty if there is no supersession.
+ */
+ final List<Integer> replacedBy;
+
+ /**
+ * Creates a new set of information for the object identified by the given
code.
+ *
+ * @param code the authority code of the object for which to collect
information.
+ * @param extents authority codes of the extents of the object
identified by {@code code}.
+ * @param factory the factory to use for getting extent objects.
+ * @throws FactoryException if an error occurred while fetching extents.
+ */
+ ObjectPertinence(final int code, final List<String> extents, final
GeodeticAuthorityFactory factory)
+ throws FactoryException
+ {
+ this.code = code;
+ GeographicBoundingBox bbox = null;
+ for (final String extentCode : extents) {
+ final Extent extent = factory.createExtent(extentCode);
+ bbox = Extents.union(bbox,
Extents.getGeographicBoundingBox(extent));
+ }
+ area = -Extents.area(bbox);
+ replacedBy = new ArrayList<>();
+ }
+
+ /**
+ * Returns the code of the object for which the pertinence is evaluated.
+ */
+ final String code() {
+ return Integer.toString(code);
+ }
+
+ /**
+ * Determines the ordering based on the extent.
+ * This method does not take supersession in account.
+ * This method is inconsistent with {@link #equals(Object)},
+ * but this is okay for the purpose of this internal class.
+ */
+ @Override
+ public int compareTo(final ObjectPertinence other) {
+ return Double.compare(area, other.area); // Reminder: we want NaN
to be sorted last.
+ }
+
+ /**
+ * Sorts in-place the elements that are in the given array.
+ *
+ * @param elements the elements to sort.
+ * @return {@code true} if the array changed as a result of this method
call.
+ */
+ static boolean sort(final ObjectPertinence[] elements) {
+ boolean changed = false;
+ for (int i=1; i<elements.length; i++) {
+ if (elements[i-1].compareTo(elements[i]) > 0) {
+ Arrays.sort(elements);
+ changed = true;
+ break;
+ }
+ }
+ int iteration = 0;
+ boolean redo;
+ do {
+ redo = false;
+ for (int i=0; i<elements.length; i++) {
+ for (final Integer replacement : elements[i].replacedBy) {
+ for (int j=i+1; j<elements.length; j++) {
+ final ObjectPertinence candidate = elements[j];
+ if (candidate.code == replacement) {
+ /*
+ * Found an element to move in front of the
superseded ones.
+ */
+ System.arraycopy(elements, i, elements, i+1, j-i);
+ elements[i++] = candidate;
+ redo = changed = true;
+ }
+ }
+ }
+ }
+ } while (redo && ++iteration < Formulas.MAXIMUM_ITERATIONS);
+ return changed;
+ }
+
+ /**
+ * Returns a string representation for debugging purposes.
+ */
+ @Override
+ public String toString() {
+ return Strings.toString(getClass(),
+ "code", code,
+ "area", Math.abs((float) (area / 1E+6)), // Square
kilometers
+ "replacedBy", CollectionsExt.first(replacedBy));
+ }
+}
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
index 737a507607..b2afeba4e5 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/TableInfo.java
@@ -84,7 +84,7 @@ final class TableInfo {
new String[] {"projected", "geographic",
"geocentric",
"vertical", "compound",
"engineering",
"derived", "temporal",
"parametric"}, // See comment below
- "SHOW_CRS"),
+ "SHOW_CRS", true),
/*
* Above declaration could omit Derived, Temporal and
Parametric cases because they are not defined
* by the EPSG repository (at least as of version 8.9). In
particular we are not sure if EPSG would
@@ -103,14 +103,14 @@ final class TableInfo {
new String[] {WKTKeywords.Cartesian,
WKTKeywords.ellipsoidal, WKTKeywords.vertical, WKTKeywords.linear,
WKTKeywords.spherical, WKTKeywords.polar,
WKTKeywords.cylindrical,
WKTKeywords.temporal,
WKTKeywords.parametric, WKTKeywords.affine}, // Same comment as in the
CRS case above.
- null),
+ null, false),
new TableInfo(CoordinateSystemAxis.class,
"\"Coordinate Axis\" AS CA INNER JOIN \"Coordinate Axis Name\"
AS CAN " +
"ON
CA.COORD_AXIS_NAME_CODE=CAN.COORD_AXIS_NAME_CODE",
"COORD_AXIS_CODE",
"COORD_AXIS_NAME",
- null, null, null, null),
+ null, null, null, null, false),
DATUM = new TableInfo(Datum.class,
"\"Datum\"",
@@ -121,19 +121,19 @@ final class TableInfo {
TemporalDatum.class, ParametricDatum.class},
new String[] {"geodetic", "vertical",
"engineering",
"temporal", "parametric"},
// Same comment as in the CRS case above.
- null),
+ null, true),
ELLIPSOID = new TableInfo(Ellipsoid.class,
"\"Ellipsoid\"",
"ELLIPSOID_CODE",
"ELLIPSOID_NAME",
- null, null, null, null),
+ null, null, null, null, false),
new TableInfo(PrimeMeridian.class,
"\"Prime Meridian\"",
"PRIME_MERIDIAN_CODE",
"PRIME_MERIDIAN_NAME",
- null, null, null, null),
+ null, null, null, null, false),
new TableInfo(CoordinateOperation.class,
"\"Coordinate_Operation\"",
@@ -142,25 +142,25 @@ final class TableInfo {
"COORD_OP_TYPE",
new Class<?>[] { Conversion.class, Transformation.class},
new String[] {"conversion", "transformation"},
- "SHOW_OPERATION"),
+ "SHOW_OPERATION", true),
new TableInfo(OperationMethod.class,
"\"Coordinate_Operation Method\"",
"COORD_OP_METHOD_CODE",
"COORD_OP_METHOD_NAME",
- null, null, null, null),
+ null, null, null, null, false),
new TableInfo(ParameterDescriptor.class,
"\"Coordinate_Operation Parameter\"",
"PARAMETER_CODE",
"PARAMETER_NAME",
- null, null, null, null),
+ null, null, null, null, false),
new TableInfo(Unit.class,
"\"Unit of Measure\"",
"UOM_CODE",
"UNIT_OF_MEAS_NAME",
- null, null, null, null),
+ null, null, null, null, false),
};
/**
@@ -213,6 +213,12 @@ final class TableInfo {
*/
final String showColumn;
+ /**
+ * Whether the table had an {@code "AREA_OF_USE_CODE"} column
+ * in the legacy versions (before version 10) of the <abbr>EPSG</abbr>
database.
+ */
+ final boolean areaOfUse;
+
/**
* Stores information about a specific table.
*
@@ -224,11 +230,12 @@ final class TableInfo {
* @param subTypes sub-interfaces of {@link #type} to handle, or {@code
null} if none.
* @param typeNames names of {@code subTypes} in the database, or {@code
null} if none.
* @param showColumn the column that specify if the object should be
shown, or {@code null} if none.
+ * @param areaOfUse whether the table had an {@code "AREA_OF_USE_CODE"}
column in the legacy versions.
*/
private TableInfo(final Class<?> type,
final String table, final String codeColumn, final
String nameColumn,
final String typeColumn, final Class<?>[] subTypes,
final String[] typeNames,
- final String showColumn)
+ final String showColumn, final boolean areaOfUse)
{
this.type = type;
this.table = table;
@@ -238,6 +245,7 @@ final class TableInfo {
this.subTypes = subTypes;
this.typeNames = typeNames;
this.showColumn = showColumn;
+ this.areaOfUse = areaOfUse;
}
/**
diff --git
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
index d4139330cc..6543f29648 100644
---
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
+++
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
@@ -45,11 +45,13 @@ import org.opengis.referencing.operation.MathTransform;
import org.opengis.util.FactoryException;
import org.apache.sis.system.Loggers;
import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.crs.DefaultGeographicCRS;
import org.apache.sis.referencing.operation.AbstractCoordinateOperation;
import org.apache.sis.referencing.factory.IdentifiedObjectFinder;
import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.metadata.iso.citation.Citations;
// Test dependencies
import org.junit.jupiter.api.Tag;
@@ -856,7 +858,7 @@ public final class EPSGFactoryTest extends TestCaseWithLogs
{
assertEquals(1.0,
AbstractCoordinateOperation.castOrCopy(operation3).getLinearAccuracy());
/*
* Creates from CRS codes. There is 40 such operations in EPSG version
6.7.
- * The preferred one (according the "supersession" table) is EPSG:1612.
+ * The one with the largest domain of validity is EPSG:1133.
*/
final Set<CoordinateOperation> all =
factory.createFromCoordinateReferenceSystemCodes("4230", "4326");
assertTrue(all.size() >= 3, "Number of coordinate operations.");
@@ -865,14 +867,22 @@ public final class EPSGFactoryTest extends
TestCaseWithLogs {
assertTrue(all.contains(operation3), "contains(“EPSG::1989”)");
int count = 0;
+ boolean found1590 = false; // In Norway, superseded by 1612.
+ boolean found1612 = false; // Replacement for 1590.
for (final CoordinateOperation tr : all) {
assertSame(sourceCRS, tr.getSourceCRS());
assertSame(targetCRS, tr.getTargetCRS());
if (count == 0) { // Preferred transformation (see above
comment).
- assertEpsgNameAndIdentifierEqual("ED50 to WGS 84 (23)", 1612,
tr);
+ assertEpsgNameAndIdentifierEqual("ED50 to WGS 84 (1)", 1133,
tr);
+ }
+ switch (Integer.parseInt(IdentifiedObjects.getIdentifier(tr,
Citations.EPSG).getCode())) {
+ case 1612: found1612 = true; assertFalse(found1590); break;
// Should be find first.
+ case 1590: found1590 = true; assertTrue (found1612); break;
// Should be after 1612.
}
count++;
}
+ assertTrue(found1612);
+ assertFalse(found1590); // TODO `assertTrue` if we
support "Norway Offshore Interpolation".
assertEquals(count, all.size()); // Size may have been modified
after above loop.
loggings.clear(); // Too installation-dependent
for testing them.
}
@@ -977,8 +987,8 @@ public final class EPSGFactoryTest extends TestCaseWithLogs
{
* not be selected in this test.
*/
final Iterator<IdentifiedObject> it = find.iterator();
- assertEpsgNameAndIdentifierEqual("Beijing 1954 / 3-degree Gauss-Kruger
CM 135E", 2442, it.next());
assertEpsgNameAndIdentifierEqual("Beijing 1954 / Gauss-Kruger CM
135E", 21463, it.next());
+ assertEpsgNameAndIdentifierEqual("Beijing 1954 / 3-degree Gauss-Kruger
CM 135E", 2442, it.next());
assertFalse(it.hasNext());
loggings.assertNoUnexpectedLog();
}