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 5b8757c47e06eb6e51c0c1f5316f4f64a703b472 Author: Martin Desruisseaux <[email protected]> AuthorDate: Wed Jul 23 16:05:15 2025 +0200 Partial update of the EPSG geodetic dataset from version 9.9.1 to version 12.013. The "source_geogcrs_code" column is renamed "base_crs_code", but a compatibility mode is kept if the previous name is detected. With this commit, the "ensemble" datum type in the "Datum" table is recognized. Starting from this commit, Apache SIS can read at least the CRSs from EPSG 10+, but not all metadata are read and the WKT 2 format is not yet updated. --- .../apache/sis/io/wkt/GeodeticObjectParser.java | 2 +- .../sis/referencing/crs/DefaultGeodeticCRS.java | 24 +- .../referencing/datum/DefaultDatumEnsemble.java | 122 +++++---- .../factory/GeodeticAuthorityFactory.java | 68 ++++- .../sis/referencing/factory/sql/AxisName.java | 18 +- .../factory/sql/CoordinateOperationSet.java | 2 +- .../referencing/factory/sql/EPSGCodeFinder.java | 4 +- .../referencing/factory/sql/EPSGDataAccess.java | 276 +++++++++++++-------- .../sis/referencing/factory/sql/EPSG_Finish.sql | 2 +- .../sis/referencing/factory/sql/SQLTranslator.java | 159 +++++++----- .../internal/PositionalAccuracyConstant.java | 40 ++- .../org/apache/sis/util/resources/Vocabulary.java | 5 + .../sis/util/resources/Vocabulary.properties | 1 + .../sis/util/resources/Vocabulary_fr.properties | 1 + .../referencing/factory/sql/epsg/DebugTools.sql | 2 +- 15 files changed, 487 insertions(+), 239 deletions(-) diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java index 7751ec3922..2b969c5339 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java @@ -2315,7 +2315,7 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo final Map<String,Object> properties = parseParametersAndClose(element, name, method); if (accuracy != null) { properties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, - PositionalAccuracyConstant.create(accuracy.pullDouble("accuracy"))); + PositionalAccuracyConstant.transformation(accuracy.pullDouble("accuracy"))); accuracy.close(ignoredElements); } try { diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java index 7fa342873c..e1ad9ffe9b 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/crs/DefaultGeodeticCRS.java @@ -33,6 +33,7 @@ import org.opengis.referencing.datum.PrimeMeridian; import org.apache.sis.referencing.AbstractReferenceSystem; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.cs.AbstractCS; +import org.apache.sis.referencing.datum.DatumOrEnsemble; import org.apache.sis.referencing.internal.Legacy; import org.apache.sis.referencing.privy.AxisDirections; import org.apache.sis.referencing.privy.WKTKeywords; @@ -214,18 +215,19 @@ class DefaultGeodeticCRS extends AbstractSingleCRS<GeodeticDatum> implements Geo formatter.newLine(); formatter.append(WKTUtilities.toFormattable(datum)); formatter.newLine(); - final PrimeMeridian pm = datum.getPrimeMeridian(); final Unit<Angle> angularUnit = AxisDirections.getAngularUnit(cs, null); - if (convention != Convention.WKT2_SIMPLIFIED || // Really this specific enum, not Convention.isSimplified(). - ReferencingUtilities.getGreenwichLongitude(pm, Units.DEGREE) != 0) - { - final Unit<Angle> oldUnit = formatter.addContextualUnit(angularUnit); - formatter.indent(1); - formatter.append(WKTUtilities.toFormattable(pm)); - formatter.indent(-1); - formatter.newLine(); - formatter.restoreContextualUnit(angularUnit, oldUnit); - } + DatumOrEnsemble.getPrimeMeridian(this).ifPresent((PrimeMeridian pm) -> { + if (convention != Convention.WKT2_SIMPLIFIED || // Really this specific enum, not Convention.isSimplified(). + ReferencingUtilities.getGreenwichLongitude(pm, Units.DEGREE) != 0) + { + final Unit<Angle> oldUnit = formatter.addContextualUnit(angularUnit); + formatter.indent(1); + formatter.append(WKTUtilities.toFormattable(pm)); + formatter.indent(-1); + formatter.newLine(); + formatter.restoreContextualUnit(angularUnit, oldUnit); + } + }); /* * Get the coordinate system to format. This will also determine the units to write and the keyword to * return in WKT 1 format. Note that for the WKT 1 format, we need to replace the coordinate system by diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultDatumEnsemble.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultDatumEnsemble.java index 3345849257..26b7d5bb3d 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultDatumEnsemble.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultDatumEnsemble.java @@ -45,6 +45,7 @@ import org.apache.sis.referencing.internal.Resources; import org.apache.sis.referencing.privy.WKTKeywords; import org.apache.sis.metadata.privy.SecondaryTrait; import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.Classes; import org.apache.sis.util.ComparisonMode; import org.apache.sis.util.Utilities; import org.apache.sis.util.resources.Errors; @@ -144,8 +145,8 @@ public class DefaultDatumEnsemble<D extends Datum> extends AbstractIdentifiedObj * @param properties the properties to be given to the identified object. * @param members datum or reference frames which are members of this ensemble. * @param accuracy inaccuracy introduced through use of this ensemble (mandatory). - * @param type the base type of datum members contained in this ensemble. - * @throws ClassCastException if a member is not an instance of {@code type}. + * @param memberType the base type of datum members contained in this ensemble. + * @throws ClassCastException if a member is not an instance of {@code memberType}. * @throws IllegalArgumentException if a member is an instance of {@link DatumEnsemble}, of if at least two * different {@linkplain AbstractDatum#getConventionalRS() conventional reference systems} are found. * @@ -154,13 +155,13 @@ public class DefaultDatumEnsemble<D extends Datum> extends AbstractIdentifiedObj protected DefaultDatumEnsemble(final Map<String,?> properties, final Collection<? extends D> members, final PositionalAccuracy accuracy, - final Class<? super D> type) + final Class<D> memberType) { super(properties); ArgumentChecks.ensureNonNull("accuracy", accuracy); ensembleAccuracy = accuracy; this.members = List.copyOf(members); - validate(type); + validate(memberType); } /** @@ -170,40 +171,40 @@ public class DefaultDatumEnsemble<D extends Datum> extends AbstractIdentifiedObj * * <p>This constructor performs a shallow copy, i.e. the properties are not cloned.</p> * - * @param ensemble the ensemble to copy. - * @param type the base type of datum members contained in this ensemble. - * @throws ClassCastException if a member is not an instance of {@code type}. + * @param ensemble the ensemble to copy. + * @param memberType the base type of datum members contained in this ensemble. + * @throws ClassCastException if a member is not an instance of {@code memberType}. * @throws IllegalArgumentException if a member is an instance of {@link DatumEnsemble}, of if at least two * different {@linkplain AbstractDatum#getConventionalRS() conventional reference systems} are found. * * @see #castOrCopy(DatumEnsemble) */ - protected DefaultDatumEnsemble(final DatumEnsemble<? extends D> ensemble, final Class<? super D> type) { + protected DefaultDatumEnsemble(final DatumEnsemble<? extends D> ensemble, final Class<D> memberType) { super(ensemble); members = List.copyOf(ensemble.getMembers()); ensembleAccuracy = Objects.requireNonNull(ensemble.getEnsembleAccuracy()); - validate(type); + validate(memberType); } /** * Verifies this ensemble. All members shall be instances of the specified type and shall have * the same conventional reference system. No member can be an instance of {@link DatumEnsemble}. * - * @param type the base type of datum members contained in this ensemble. - * @throws ClassCastException if a member is not an instance of {@code type}. + * @param memberType the base type of datum members contained in this ensemble. + * @throws ClassCastException if a member is not an instance of {@code memberType}. * @throws IllegalArgumentException if a member is an instance of {@link DatumEnsemble}, of if at least two * different {@linkplain AbstractDatum#getConventionalRS() conventional reference systems} are found. */ - private void validate(final Class<? super D> type) { + private void validate(final Class<D> memberType) { IdentifiedObject rs = null; for (final D datum : members) { if (datum instanceof DatumEnsemble<?>) { throw new IllegalArgumentException( Errors.format(Errors.Keys.IllegalPropertyValueClass_2, "members", DatumEnsemble.class)); } - if (!type.isInstance(datum)) { + if (!memberType.isInstance(datum)) { throw new ClassCastException( - Errors.format(Errors.Keys.IllegalClass_2, type, datum.getClass())); + Errors.format(Errors.Keys.IllegalClass_2, memberType, Classes.getClass(datum))); } final IdentifiedObject dr = datum.getConventionalRS().orElse(null); if (dr != null) { @@ -235,7 +236,7 @@ public class DefaultDatumEnsemble<D extends Datum> extends AbstractIdentifiedObj final Collection<? extends D> members, final PositionalAccuracy accuracy) { - return Factory.forMemberType(null, properties, List.copyOf(members), accuracy); + return Factory.forMemberType(Datum.class, null, properties, List.copyOf(members), accuracy); } /** @@ -264,7 +265,36 @@ public class DefaultDatumEnsemble<D extends Datum> extends AbstractIdentifiedObj if (object == null || object instanceof DefaultDatumEnsemble<?>) { return (DefaultDatumEnsemble<D>) object; } - return Factory.forMemberType(object, null, List.copyOf(object.getMembers()), null); + return Factory.forMemberType(Datum.class, object, null, List.copyOf(object.getMembers()), object.getEnsembleAccuracy()); + } + + /** + * Returns this datum ensemble as a collection of datum of the given type. + * This method casts the datum members, it does not copy or rewrite them. + * However, the returned {@code DatumEnsemble} may be a different instance. + * + * @param <N> compile-time value of {@code memberType}. + * @param memberType the new desired type of datum members. + * @return an ensemble of datum of the given type. + * @throws ClassCastException if at least one member is not an instance of the specified type. + */ + public <N extends Datum> DefaultDatumEnsemble<N> cast(final Class<N> memberType) { + for (final D member : members) { + if (!memberType.isInstance(member)) { + throw new ClassCastException(Errors.format(Errors.Keys.IllegalClass_2, memberType, Classes.getClass(member))); + } + } + /* + * At this point, we verified that all members are of the requested type. + * Now verify if the ensemble as a whole is also of the requested type. + * This part is not mandatory however. + */ + @SuppressWarnings("unchecked") + DefaultDatumEnsemble<N> ensemble = (DefaultDatumEnsemble<N>) this; + if (!memberType.isInstance(ensemble)) { + ensemble = Factory.forMemberType(memberType, ensemble, null, ensemble.members, ensemble.ensembleAccuracy); + } + return ensemble; } /** @@ -297,12 +327,13 @@ public class DefaultDatumEnsemble<D extends Datum> extends AbstractIdentifiedObj /** * Returns the datum or reference frames which are members of this ensemble. + * The returned list is immutable. * * @return datum or reference frames which are members of this ensemble. */ @Override @SuppressWarnings("ReturnOfCollectionOrArrayField") // Collection is unmodifiable. - public Collection<D> getMembers() { + public final Collection<D> getMembers() { // Must be final for type safety. See `forMemberType(…)` return members; } @@ -685,15 +716,15 @@ check: if (it.hasNext()) { /** * Base type of all members in the ensembles constructed by this factory instance. */ - private final Class<D> type; + private final Class<D> memberType; /** * Creates a new factory for ensembles in which all members are instances of the given type. * - * @param type base type of all members in the ensembles constructed by this factory instance. + * @param memberType base type of all members in the ensembles constructed by this factory instance. */ - private Factory(final Class<D> type) { - this.type = type; + private Factory(final Class<D> memberType) { + this.memberType = memberType; } /** @@ -703,12 +734,14 @@ check: if (it.hasNext()) { * * <ul> * <li>{@link #getMembers()}, which is a read-only collection (it is safe to cast {@code List<? extends D>} - * as {@code List<D>} when no write operation is allowed).</li> + * as {@code List<D>} when no write operation is allowed). That method is made final for enforcing this + * assumption.</li> * </ul> * * Exactly one of {@code object} and {@code properties} should be non-null. * * @param <D> base type of all members in the ensembles to create. + * @param memberType the base class of datum type to take in account. * @param object the source ensemble to copy, or {@code null} if none. * @param properties the properties of the ensemble to create, or {@code null}. * @param members members of the ensemble to copy or create. @@ -718,35 +751,40 @@ check: if (it.hasNext()) { * Should never happen if the parameterized type of {@code members} is respected. */ static <D extends Datum> DefaultDatumEnsemble<D> forMemberType( + final Class<? extends Datum> memberType, final DatumEnsemble<? extends D> object, final Map<String,?> properties, final List<? extends D> members, final PositionalAccuracy accuracy) { + Object illegal = null; nextType: for (final Factory<?> factory : FACTORIES) { - for (final Object member : members) { - if (!factory.type.isInstance(member)) { - continue nextType; + if (memberType.isAssignableFrom(factory.memberType)) { + for (final Object member : members) { + if (!factory.memberType.isInstance(member)) { + illegal = member; + continue nextType; + } + } + /* + * A more correct type would be `Factory<? super D>` because of the use of `isInstance(member)` + * instead of strict class equality. However, even `? super D` is not guaranteed to be correct, + * because nothing prevent a member from implementing two interfaces. In such case, the type of + * the first matching factory could be unrelated to `D` (e.g., `D` is `ParametricDatum` but all + * members also implement `VerticalDatum`). However, despite this uncertainty, the cast is okay + * because `D` appears only in `getMembers()` which returns a read-only collection. There is no + * method returning `Class<D>` and no guarantees that the returned object will implement `D`. + */ + @SuppressWarnings("unchecked") + final var selected = (Factory<D>) factory; // See above comment. + if (object != null) { + return selected.copy(object); + } else { + return selected.create(properties, members, accuracy); } - } - /* - * A more correct type would be `Factory<? super D>` because of the use of `isInstance(member)` - * instead of strict class equality. However, even `? super D` is not guaranteed to be correct, - * because nothing prevent a member from implementing two interfaces. In such case, the type of - * the first matching factory could be unrelated to `D` (e.g., `D` is `ParametricDatum` but all - * members also implement `VerticalDatum`). However, despite this uncertainty, the cast is okay - * because `D` appears only in `getMembers()` which returns a read-only collection. There is no - * method returning `Class<D>` and no guarantees that the returned object will implement `D`. - */ - @SuppressWarnings("unchecked") - final var selected = (Factory<D>) factory; // See above comment. - if (object != null) { - return selected.copy(object); - } else { - return selected.create(properties, members, accuracy); } } - throw new ClassCastException(); + throw new ClassCastException(Errors.format(Errors.Keys.IllegalClass_2, Datum.class, Classes.getClass(illegal))); } /** diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java index 0c5056725c..8016fd352c 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java @@ -537,13 +537,18 @@ public abstract class GeodeticAuthorityFactory extends AbstractFactory implement * <table class="sis"> * <caption>Authority codes examples</caption> * <tr><th>Code</th> <th>Type</th> <th>Description</th></tr> - * <tr><td>EPSG:6326</td> <td>Geodetic</td> <td>World Geodetic System 1984</td></tr> + * <tr><td>EPSG:6326</td> <td>Ensemble</td> <td>World Geodetic System 1984</td></tr> * <tr><td>EPSG:6322</td> <td>Geodetic</td> <td>World Geodetic System 1972</td></tr> * <tr><td>EPSG:1027</td> <td>Vertical</td> <td>EGM2008 geoid</td></tr> * <tr><td>EPSG:5100</td> <td>Vertical</td> <td>Mean Sea Level</td></tr> * <tr><td>EPSG:9315</td> <td>Engineering</td> <td>Seismic bin grid datum</td></tr> * </table> * + * <p><b>Note:</b> strictly speaking, the reference frames of type <i>ensemble</i> cannot be returned by this method. + * They should be obtained by the {@link #createDatumEnsemble(String)} method instead. In the particular case of the + * Apache <abbr>SIS</abbr> implementation, datum ensembles can nevertheless be obtained by this method, but this is + * not guaranteed to be true for all implementations of the {@link DatumAuthorityFactory} interface.</p> + * * <h4>Default implementation</h4> * The default implementation delegates to {@link #createObject(String)} and casts the result. * If the result cannot be cast, then a {@link NoSuchAuthorityCodeException} is thrown. @@ -571,13 +576,18 @@ public abstract class GeodeticAuthorityFactory extends AbstractFactory implement * * <table class="sis"> * <caption>Authority codes examples</caption> - * <tr><th>Code</th> <th>Description</th></tr> - * <tr><td>EPSG:6326</td> <td>World Geodetic System 1984</td></tr> - * <tr><td>EPSG:6322</td> <td>World Geodetic System 1972</td></tr> - * <tr><td>EPSG:6269</td> <td>North American Datum 1983</td></tr> - * <tr><td>EPSG:6258</td> <td>European Terrestrial Reference System 1989</td></tr> + * <tr><th>Code</th> <th>Type</th> <th>Description</th></tr> + * <tr><td>EPSG:6326</td> <td>Ensemble</td> <td>World Geodetic System 1984</td></tr> + * <tr><td>EPSG:6322</td> <td>Dynamic</td> <td>World Geodetic System 1972</td></tr> + * <tr><td>EPSG:6269</td> <td></td> <td>North American Datum 1983</td></tr> + * <tr><td>EPSG:6258</td> <td>Ensemble</td> <td>European Terrestrial Reference System 1989</td></tr> * </table> * + * <p><b>Note:</b> strictly speaking, the reference frames of type <i>ensemble</i> cannot be returned by this method. + * They should be obtained by the {@link #createDatumEnsemble(String)} method instead. In the particular case of the + * Apache <abbr>SIS</abbr> implementation, datum ensembles can nevertheless be obtained by this method, but this is + * not guaranteed to be true for all implementations of the {@link DatumAuthorityFactory} interface.</p> + * * <h4>Default implementation</h4> * The default implementation delegates to {@link #createDatum(String)} and casts the result. * If the result cannot be cast, then a {@link NoSuchAuthorityCodeException} is thrown. @@ -698,6 +708,45 @@ public abstract class GeodeticAuthorityFactory extends AbstractFactory implement return cast(EngineeringDatum.class, createDatum(code), code); } + /** + * Creates an arbitrary datum ensemble from a code. + * A datum ensemble is a collection of datums which for low accuracy requirements + * may be considered to be insignificantly different from each other. + * + * <h4>Examples</h4> + * The {@linkplain #getAuthorityCodes(Class) set of available codes} depends on the defining + * {@linkplain #getAuthority() authority} and the {@code GeodeticAuthorityFactory} subclass in use. + * A frequently used authority is "EPSG", which includes the following codes: + * + * <table class="sis"> + * <caption>Authority codes examples</caption> + * <tr><th>Code</th> <th>Description</th></tr> + * <tr><td>EPSG:6326</td> <td>World Geodetic System 1984</td></tr> + * <tr><td>EPSG:6258</td> <td>European Terrestrial Reference System 1989</td></tr> + * </table> + * + * <h4>Default implementation</h4> + * The default implementation delegates to {@link #createDatum(String)} and casts the result. + * If the result cannot be cast, then a {@link NoSuchAuthorityCodeException} is thrown. + * Note that the delegation to {@code createDatum(…)} works only if the implementation + * stores datum and datum ensembles together. This is the case of the <abbr>EPSG</abbr> + * geodetic dataset for instance. In such case, the datum ensemble can be returned as an + * object that implements both the {@link Datum} and {@link DatumEnsemble} interfaces, + * such as the {@link org.apache.sis.referencing.datum.DefaultDatumEnsemble} class. + * + * @param code value allocated by authority. + * @return the datum for the given code. + * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. + * @throws FactoryException if the object creation failed for some other reason. + * + * @see org.apache.sis.referencing.datum.DefaultDatumEnsemble + * + * @since 1.5 + */ + public DatumEnsemble<?> createDatumEnsemble(final String code) throws NoSuchAuthorityCodeException, FactoryException { + return cast(DatumEnsemble.class, createDatum(code), code); + } + /** * Creates a geometric figure that can be used to describe the approximate shape of the earth. * In mathematical terms, it is a surface formed by the rotation of an ellipse about its minor axis. @@ -1315,8 +1364,11 @@ public abstract class GeodeticAuthorityFactory extends AbstractFactory implement */ final Identifier id = object.getName(); final Citation authority = (id != null) ? id.getAuthority() : getAuthority(); - throw new NoSuchAuthorityCodeException(Errors.format(Errors.Keys.UnexpectedTypeForReference_3, code, type, actual), - Citations.getIdentifier(authority), trimNamespace(code), code); + throw new NoSuchAuthorityCodeException( + Errors.format(Errors.Keys.UnexpectedTypeForReference_3, code, type, actual), + Citations.getIdentifier(authority), + trimNamespace(code), + code); } /** diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AxisName.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AxisName.java index 5713f8ead8..5009c16488 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AxisName.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/AxisName.java @@ -17,6 +17,7 @@ package org.apache.sis.referencing.factory.sql; import java.util.Objects; +import org.apache.sis.pending.jdk.Record; /** @@ -24,7 +25,7 @@ import java.util.Objects; * * @author Martin Desruisseaux (IRD) */ -final class AxisName { +final class AxisName extends Record { /** * The coordinate system axis name (never {@code null}). */ @@ -32,15 +33,22 @@ final class AxisName { /** * The coordinate system axis description, or {@code null} if none. + * Example: "Angle from the equatorial plane to the perpendicular to the ellipsoid". */ final String description; + /** + * The remarks. Example: "Used in geographic 2D and geographic 3D coordinate reference systems". + */ + final String remarks; + /** * Creates a new coordinate system axis name. */ - AxisName(final String name, final String description) { + AxisName(final String name, final String description, final String remarks) { this.name = name; this.description = description; + this.remarks = remarks; } /** @@ -57,8 +65,10 @@ final class AxisName { @Override public boolean equals(final Object object) { if (object instanceof AxisName) { - final AxisName that = (AxisName) object; - return name.equals(that.name) && Objects.equals(description, that.description); + final var that = (AxisName) object; + return name.equals(that.name) && + Objects.equals(description, that.description) && + Objects.equals(remarks, that.remarks); } return false; } 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 c52f883131..6002437391 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 @@ -44,7 +44,7 @@ import org.opengis.referencing.crs.DerivedCRS; * one result, because {@code COORD_REF_SYS_CODE} is a primary key): * * {@snippet lang="sql" : - * SELECT PROJECTION_CONV_CODE FROM "Coordinate Reference System" WHERE SOURCE_GEOGCRS_CODE = ? AND COORD_REF_SYS_CODE = ? + * SELECT PROJECTION_CONV_CODE FROM "Coordinate Reference System" WHERE BASE_CRS_CODE = ? AND COORD_REF_SYS_CODE = ? * } * </li> * 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 a925a30e9b..d2f4d66493 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 @@ -310,7 +310,7 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { } /* * For Coordinate Reference System, the SQL statement may be something like below - * (with DATUM_CODE replaced by SOURCE_GEOGCRS_CODE in a projected CRS): + * (with DATUM_CODE replaced by BASE_CRS_CODE projected CRS): * * SELECT COORD_REF_SYS_CODE FROM "Coordinate Reference System" * WHERE CAST(COORD_REF_SYS_KIND AS VARCHAR(80)) LIKE 'geographic%' @@ -319,7 +319,7 @@ crs: if (isInstance(CoordinateReferenceSystem.class, object)) { */ final Condition filter; if (object instanceof DerivedCRS) { // No need to use isInstance(Class, Object) from here. - filter = dependencies("SOURCE_GEOGCRS_CODE", SingleCRS.class, ((DerivedCRS) object).getBaseCRS(), true); + filter = dependencies("BASE_CRS_CODE", SingleCRS.class, ((DerivedCRS) object).getBaseCRS(), true); } else if (object instanceof GeodeticCRS) { filter = dependencies("DATUM_CODE", GeodeticDatum.class, ((GeodeticCRS) object).getDatum(), true); } else if (object instanceof VerticalCRS) { 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 3f3f06c54c..0b3b995688 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 @@ -71,6 +71,7 @@ import org.apache.sis.referencing.ImmutableIdentifier; import org.apache.sis.referencing.AbstractIdentifiedObject; import org.apache.sis.referencing.cs.CoordinateSystems; import org.apache.sis.referencing.datum.BursaWolfParameters; +import org.apache.sis.referencing.datum.DefaultDatumEnsemble; import org.apache.sis.referencing.datum.DefaultGeodeticDatum; import org.apache.sis.referencing.operation.DefaultOperationMethod; import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory; @@ -88,7 +89,7 @@ import org.apache.sis.referencing.internal.ParameterizedTransformBuilder; import org.apache.sis.referencing.internal.PositionalAccuracyConstant; import org.apache.sis.referencing.internal.SignReversalComment; import org.apache.sis.referencing.internal.Resources; -import static org.apache.sis.referencing.internal.ServicesForMetadata.CONNECTION; +import org.apache.sis.referencing.internal.ServicesForMetadata; import org.apache.sis.parameter.DefaultParameterDescriptor; import org.apache.sis.parameter.DefaultParameterDescriptorGroup; import org.apache.sis.system.Loggers; @@ -98,6 +99,7 @@ import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.CharSequences; import org.apache.sis.util.Localized; import org.apache.sis.util.Version; +import org.apache.sis.util.Utilities; import org.apache.sis.util.Workaround; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.collection.BackingStoreException; @@ -108,8 +110,6 @@ import org.apache.sis.util.privy.Constants; import org.apache.sis.util.privy.CollectionsExt; import org.apache.sis.util.privy.Strings; import org.apache.sis.util.privy.URLs; -import static org.apache.sis.util.privy.Constants.UTC; -import static org.apache.sis.util.Utilities.equalsIgnoreMetadata; import org.apache.sis.temporal.LenientDateFormat; import org.apache.sis.metadata.iso.citation.DefaultCitation; import org.apache.sis.metadata.iso.citation.DefaultOnlineResource; @@ -261,7 +261,7 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho /** * Cache for axis names. This service is not provided by {@code ConcurrentAuthorityFactory} - * since {@link AxisName} objects are particular to the EPSG database. + * because {@link AxisName} objects are specific to the <abbr>EPSG</abbr> authority factory. * * @see #getAxisName(Integer) */ @@ -271,7 +271,7 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho * Cache of naming systems other than EPSG. There is usually few of them (at most 15). * This is used for aliases. * - * @see #createProperties(String, String, Integer, CharSequence, boolean) + * @see #createProperties(String, Integer, String, CharSequence, String, String, CharSequence, boolean) */ private final Map<String,NameSpace> namingSystems = new HashMap<>(); @@ -381,7 +381,7 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho @SuppressWarnings("ReturnOfDateField") private Calendar getCalendar() { if (calendar == null) { - calendar = Calendar.getInstance(TimeZone.getTimeZone(UTC), Locale.CANADA); + calendar = Calendar.getInstance(TimeZone.getTimeZone(Constants.UTC), Locale.CANADA); // Canada locale is closer to ISO than US. } calendar.clear(); @@ -455,7 +455,7 @@ addURIs: for (int i=0; ; i++) { case 1: url = URLs.EPSG; function = OnLineFunction.DOWNLOAD; break; case 2: { url = SQLUtilities.getSimplifiedURL(metadata); - function = OnLineFunction.valueOf(CONNECTION); + function = OnLineFunction.valueOf(ServicesForMetadata.CONNECTION); description = Resources.formatInternational(Resources.Keys.GeodeticDataBase_4, Constants.EPSG, version, metadata.getDatabaseProductName(), Version.valueOf(metadata.getDatabaseMajorVersion(), @@ -639,7 +639,7 @@ addURIs: for (int i=0; ; i++) { } /** - * Returns {@code true} if the specified code may be a primary key in some table. + * Returns {@code true} if the specified code may be a primary key in some tables. * This method does not need to check any entry in the database. * It should just check from the syntax if the code looks like a valid EPSG identifier. * @@ -654,12 +654,14 @@ addURIs: for (int i=0; ; i++) { * * <h4>Default implementation</h4> * The default implementation returns {@code true} if all characters are decimal digits 0 to 9. + * Currently, this default implementation cannot be overridden. But we may allow that in a future + * version if it appears to be useful. * * @param code the code the inspect. * @return {@code true} if the code is probably a primary key. * @throws FactoryException if an unexpected error occurred while inspecting the code. */ - private boolean isPrimaryKey(final String code) throws FactoryException { + private static boolean isPrimaryKey(final String code) throws FactoryException { int i = code.length(); if (i == 0) { return false; @@ -901,11 +903,12 @@ codes: for (int i=0; i<codes.length; i++) { /** * Formats an error message for an unexpected null value. */ + @SuppressWarnings("ConvertToTryWithResources") private String nullValue(final ResultSet result, final int columnIndex, final Comparable<?> code) throws SQLException { final ResultSetMetaData metadata = result.getMetaData(); final String column = metadata.getColumnName(columnIndex); final String table = metadata.getTableName (columnIndex); - result.close(); + result.close(); // Only an optimization. The actual try-with-resource is done by the caller. return error().getString(Errors.Keys.NullValueInTable_3, table, column, code); } @@ -1094,15 +1097,25 @@ codes: for (int i=0; i<codes.length; i++) { * Returns the name and aliases for the {@link IdentifiedObject} to construct. * * @param table the table on which a query has been executed. - * @param name the name for the {@link IdentifiedObject} to construct. * @param code the EPSG code of the object to construct. + * @param name the name for the {@link IdentifiedObject} to construct. + * @param description a description associated with the name, or {@code null} if none. + * @param domainCode the code for the domain of validity, or {@code null} if none. + * @param scope the scope, or {@code null} if none. * @param remarks remarks as a {@link String} or {@link InternationalString}, or {@code null} if none. * @param deprecated {@code true} if the object to create is deprecated. * @return the name together with a set of properties. */ @SuppressWarnings("ReturnOfCollectionOrArrayField") - private Map<String,Object> createProperties(final String table, String name, final Integer code, - CharSequence remarks, final boolean deprecated) throws SQLException, FactoryException + private Map<String,Object> createProperties(final String table, + final Integer code, + String name, // May be replaced by an alias. + final CharSequence description, + final String domainCode, + String scope, // May replace "?" by text. + final CharSequence remarks, + final boolean deprecated) + throws SQLException, FactoryException { /* * Search for aliases. Note that searching for the object code is not sufficient. We also need to check if the @@ -1138,7 +1151,7 @@ codes: for (int i=0; i<codes.length; i++) { } } if (CharSequences.toASCII(alias).toString().equals(name)) { - name = alias; + name = alias; // Same name but with accented letters. } else { aliases.add(owner.nameFactory.createLocalName(ns, alias)); } @@ -1156,9 +1169,10 @@ codes: for (int i=0; i<codes.length; i++) { if (name != null) { gn = owner.nameFactory.createGenericName(namespace, Constants.EPSG, name); properties.put("name", gn); - properties.put(NamedIdentifier.CODE_KEY, name); - properties.put(NamedIdentifier.VERSION_KEY, version); - properties.put(NamedIdentifier.AUTHORITY_KEY, authority); + properties.put(NamedIdentifier.CODE_KEY, name); + properties.put(NamedIdentifier.VERSION_KEY, version); + properties.put(NamedIdentifier.AUTHORITY_KEY, authority); + properties.put(NamedIdentifier.DESCRIPTION_KEY, description); properties.put(AbstractIdentifiedObject.LOCALE_KEY, locale); final var id = new NamedIdentifier(properties); properties.clear(); @@ -1185,30 +1199,9 @@ codes: for (int i=0; i<codes.length; i++) { properties.put(IdentifiedObject.REMARKS_KEY, remarks); properties.put(AbstractIdentifiedObject.LOCALE_KEY, locale); properties.put(ReferencingFactoryContainer.MT_FACTORY, owner.mtFactory); - return properties; - } - - /** - * Returns the name, aliases and domain of validity for the {@link IdentifiedObject} to construct. - * - * @param table the table on which a query has been executed. - * @param name the name for the {@link IdentifiedObject} to construct. - * @param code the EPSG code of the object to construct. - * @param domainCode the code for the domain of validity, or {@code null} if none. - * @param scope the scope, or {@code null} if none. - * @param remarks remarks, or {@code null} if none. - * @param deprecated {@code true} if the object to create is deprecated. - * @return the name together with a set of properties. - */ - private Map<String,Object> createProperties(final String table, final String name, final Integer code, - final String domainCode, String scope, final String remarks, final boolean deprecated) - throws SQLException, FactoryException - { if ("?".equals(scope)) { // EPSG sometimes uses this value for unspecified scope. scope = null; } - @SuppressWarnings("LocalVariableHidesMemberVariable") - final Map<String,Object> properties = createProperties(table, name, code, remarks, deprecated); if (domainCode != null) { properties.put(ObjectDomain.DOMAIN_OF_VALIDITY_KEY, owner.createExtent(domainCode)); } @@ -1344,14 +1337,14 @@ codes: for (int i=0; i<codes.length; i++) { try (ResultSet result = executeQuery("Coordinate Reference System", "COORD_REF_SYS_CODE", "COORD_REF_SYS_NAME", "SELECT COORD_REF_SYS_CODE," + // [ 1] " COORD_REF_SYS_NAME," + // [ 2] - " AREA_OF_USE_CODE," + // [ 3] + " AREA_OF_USE_CODE," + // [ 3] Deprecated since EPSG version 10 (always null) " CRS_SCOPE," + // [ 4] " REMARKS," + // [ 5] " DEPRECATED," + // [ 6] " COORD_REF_SYS_KIND," + // [ 7] " COORD_SYS_CODE," + // [ 8] Null for CompoundCRS " DATUM_CODE," + // [ 9] Null for ProjectedCRS - " SOURCE_GEOGCRS_CODE," + // [10] For ProjectedCRS + " BASE_CRS_CODE," + // [10] For ProjectedCRS " PROJECTION_CONV_CODE," + // [11] For ProjectedCRS " CMPD_HORIZCRS_CODE," + // [12] For CompoundCRS only " CMPD_VERTCRS_CODE" + // [13] For CompoundCRS only @@ -1390,7 +1383,7 @@ codes: for (int i=0; i<codes.length; i++) { } final EllipsoidalCS cs = owner.createEllipsoidalCS(csCode.toString()); final String datumCode = getOptionalString(result, 9); - final GeodeticDatum datum; + GeodeticDatum datum; if (datumCode != null) { datum = owner.createGeodeticDatum(datumCode); } else { @@ -1403,8 +1396,10 @@ codes: for (int i=0; i<codes.length; i++) { endOfRecursion(GeographicCRS.class, epsg); } } + DatumEnsemble<GeodeticDatum> ensemble = tryAsEnsemble(datum, GeodeticDatum.class); + if (ensemble != null) datum = null; crs = crsFactory.createGeographicCRS(createProperties("Coordinate Reference System", - name, epsg, area, scope, remarks, deprecated), datum, cs); + epsg, name, null, area, scope, remarks, deprecated), datum, ensemble, cs); break; } /* ---------------------------------------------------------------------- @@ -1478,7 +1473,7 @@ codes: for (int i=0; i<codes.length; i++) { */ @SuppressWarnings("LocalVariableHidesMemberVariable") final Map<String,Object> properties = createProperties("Coordinate Reference System", - name, epsg, area, scope, remarks, deprecated); + epsg, name, null, area, scope, remarks, deprecated); if (baseCRS instanceof GeodeticCRS) { crs = crsFactory.createProjectedCRS(properties, (GeodeticCRS) baseCRS, op, cs); } else { @@ -1496,10 +1491,12 @@ codes: for (int i=0; i<codes.length; i++) { * VERTICAL CRS * ---------------------------------------------------------------------- */ case "vertical": { - final VerticalCS cs = owner.createVerticalCS (getString(code, result, 8)); - final VerticalDatum datum = owner.createVerticalDatum(getString(code, result, 9)); + VerticalCS cs = owner.createVerticalCS (getString(code, result, 8)); + VerticalDatum datum = owner.createVerticalDatum(getString(code, result, 9)); + DatumEnsemble<VerticalDatum> ensemble = tryAsEnsemble(datum, VerticalDatum.class); + if (ensemble != null) datum = null; crs = crsFactory.createVerticalCRS(createProperties("Coordinate Reference System", - name, epsg, area, scope, remarks, deprecated), datum, cs); + epsg, name, null, area, scope, remarks, deprecated), datum, ensemble, cs); break; } /* ---------------------------------------------------------------------- @@ -1510,10 +1507,12 @@ codes: for (int i=0; i<codes.length; i++) { * ---------------------------------------------------------------------- */ case "time": case "temporal": { - final TimeCS cs = owner.createTimeCS (getString(code, result, 8)); - final TemporalDatum datum = owner.createTemporalDatum(getString(code, result, 9)); + TimeCS cs = owner.createTimeCS (getString(code, result, 8)); + TemporalDatum datum = owner.createTemporalDatum(getString(code, result, 9)); + DatumEnsemble<TemporalDatum> ensemble = tryAsEnsemble(datum, TemporalDatum.class); + if (ensemble != null) datum = null; crs = crsFactory.createTemporalCRS(createProperties("Coordinate Reference System", - name, epsg, area, scope, remarks, deprecated), datum, cs); + epsg, name, null, area, scope, remarks, deprecated), datum, ensemble, cs); break; } /* ---------------------------------------------------------------------- @@ -1536,23 +1535,24 @@ codes: for (int i=0; i<codes.length; i++) { } // Note: Do not invoke `createProperties` sooner. crs = crsFactory.createCompoundCRS(createProperties("Coordinate Reference System", - name, epsg, area, scope, remarks, deprecated), crs1, crs2); + epsg, name, null, area, scope, remarks, deprecated), crs1, crs2); break; } /* ---------------------------------------------------------------------- * GEOCENTRIC CRS * ---------------------------------------------------------------------- */ case "geocentric": { - final CoordinateSystem cs = owner.createCoordinateSystem(getString(code, result, 8)); - final GeodeticDatum datum = owner.createGeodeticDatum (getString(code, result, 9)); - final DatumEnsemble<GeodeticDatum> datumEnsemble = null; // TODO + CoordinateSystem cs = owner.createCoordinateSystem(getString(code, result, 8)); + GeodeticDatum datum = owner.createGeodeticDatum (getString(code, result, 9)); + DatumEnsemble<GeodeticDatum> ensemble = tryAsEnsemble(datum, GeodeticDatum.class); + if (ensemble != null) datum = null; @SuppressWarnings("LocalVariableHidesMemberVariable") final Map<String,Object> properties = createProperties("Coordinate Reference System", - name, epsg, area, scope, remarks, deprecated); + epsg, name, null, area, scope, remarks, deprecated); if (cs instanceof CartesianCS) { - crs = crsFactory.createGeodeticCRS(properties, datum, datumEnsemble, (CartesianCS) cs); + crs = crsFactory.createGeodeticCRS(properties, datum, ensemble, (CartesianCS) cs); } else if (cs instanceof SphericalCS) { - crs = crsFactory.createGeodeticCRS(properties, datum, datumEnsemble, (SphericalCS) cs); + crs = crsFactory.createGeodeticCRS(properties, datum, ensemble, (SphericalCS) cs); } else { throw new FactoryDataException(error().getString( Errors.Keys.IllegalCoordinateSystem_1, cs.getName())); @@ -1563,21 +1563,24 @@ codes: for (int i=0; i<codes.length; i++) { * ENGINEERING CRS * ---------------------------------------------------------------------- */ case "engineering": { - final CoordinateSystem cs = owner.createCoordinateSystem(getString(code, result, 8)); - final EngineeringDatum datum = owner.createEngineeringDatum(getString(code, result, 9)); + CoordinateSystem cs = owner.createCoordinateSystem(getString(code, result, 8)); + EngineeringDatum datum = owner.createEngineeringDatum(getString(code, result, 9)); + DatumEnsemble<EngineeringDatum> ensemble = tryAsEnsemble(datum, EngineeringDatum.class); + if (ensemble != null) datum = null; crs = crsFactory.createEngineeringCRS(createProperties("Coordinate Reference System", - name, epsg, area, scope, remarks, deprecated), datum, cs); + epsg, name, null, area, scope, remarks, deprecated), datum, ensemble, cs); break; } /* ---------------------------------------------------------------------- * PARAMETRIC CRS * ---------------------------------------------------------------------- */ case "parametric": { - final ParametricCS cs = owner.createParametricCS (getString(code, result, 8)); - final ParametricDatum datum = owner.createParametricDatum(getString(code, result, 9)); - final DatumEnsemble<ParametricDatum> datumEnsemble = null; // TODO + ParametricCS cs = owner.createParametricCS (getString(code, result, 8)); + ParametricDatum datum = owner.createParametricDatum(getString(code, result, 9)); + DatumEnsemble<ParametricDatum> ensemble = tryAsEnsemble(datum, ParametricDatum.class); + if (ensemble != null) datum = null; crs = crsFactory.createParametricCRS(createProperties("Coordinate Reference System", - name, epsg, area, scope, remarks, deprecated), datum, datumEnsemble, cs); + epsg, name, null, area, scope, remarks, deprecated), datum, ensemble, cs); break; } /* ---------------------------------------------------------------------- @@ -1594,6 +1597,8 @@ codes: for (int i=0; i<codes.length; i++) { } } catch (SQLException exception) { throw databaseFailure(CoordinateReferenceSystem.class, code, exception); + } catch (ClassCastException exception) { + throw new FactoryDataException(error().getString(exception.getLocalizedMessage()), exception); } if (returnValue == null) { throw noSuchAuthorityCode(CoordinateReferenceSystem.class, code); @@ -1601,6 +1606,29 @@ codes: for (int i=0; i<codes.length; i++) { return returnValue; } + /** + * Returns the given datum as a datum ensemble if it should be considered as such. + * This method exists because the datum and datum ensemble are stored in the same table, + * and Apache <abbr>SIS</abbr> creates those two kinds of objects with the same method. + * The real type is resolved by inspection of the {@link #createDatum(String)} return value. + * + * <h4>Design restriction</h4> + * We cannot resolve the type with a private field which would be set by {@code #createDatumEnsemble(…)} + * because that method will not be invoked if the datum is fetched from the cache. + * + * @param <D> compile-time value of {@code memberType}. + * @param datum the datum to check if it is a datum ensemble. + * @param memberType the expected type of datum members. + * @return the given datum as an ensemble if it should be considered as such, or {@code null} otherwise. + * @throws ClassCastException if at least one member is not an instance of the specified type. + */ + private static <D extends Datum> DatumEnsemble<D> tryAsEnsemble(final D datum, final Class<D> memberType) { + if (datum instanceof DatumEnsemble<?>) { + return DefaultDatumEnsemble.castOrCopy((DatumEnsemble<?>) datum).cast(memberType); + } + return null; + } + /** * Creates an arbitrary datum from a code. The returned object will typically be an * instance of {@link GeodeticDatum}, {@link VerticalDatum} or {@link TemporalDatum}. @@ -1610,12 +1638,12 @@ codes: for (int i=0; i<codes.length; i++) { * * <table class="sis"> * <caption>EPSG codes examples</caption> - * <tr><th>Code</th> <th>Type</th> <th>Description</th></tr> - * <tr><td>6326</td> <td>Geodetic</td> <td>World Geodetic System 1984</td></tr> - * <tr><td>6322</td> <td>Geodetic</td> <td>World Geodetic System 1972</td></tr> - * <tr><td>1027</td> <td>Vertical</td> <td>EGM2008 geoid</td></tr> - * <tr><td>5100</td> <td>Vertical</td> <td>Mean Sea Level</td></tr> - * <tr><td>9315</td> <td>Engineering</td> <td>Seismic bin grid datum</td></tr> + * <tr><th>Code</th> <th>Type</th> <th>Description</th></tr> + * <tr><td>6326</td> <td>Datum ensemble</td> <td>World Geodetic System 1984</td></tr> + * <tr><td>6322</td> <td>Dynamic geodetic</td><td>World Geodetic System 1972</td></tr> + * <tr><td>1027</td> <td>Vertical</td> <td>EGM2008 geoid</td></tr> + * <tr><td>5100</td> <td>Vertical</td> <td>Mean Sea Level</td></tr> + * <tr><td>9315</td> <td>Engineering</td> <td>Seismic bin grid datum</td></tr> * </table> * * @param code value allocated by EPSG. @@ -1633,7 +1661,7 @@ codes: for (int i=0; i<codes.length; i++) { " DATUM_TYPE," + " ORIGIN_DESCRIPTION," + " REALIZATION_EPOCH," + - " AREA_OF_USE_CODE," + + " AREA_OF_USE_CODE," + // Deprecated since EPSG version 10 (always null) " DATUM_SCOPE," + " REMARKS," + " DEPRECATED," + @@ -1653,10 +1681,9 @@ codes: for (int i=0; i<codes.length; i++) { final String remarks = getOptionalString (result, 8); final boolean deprecated = getOptionalBoolean(result, 9); @SuppressWarnings("LocalVariableHidesMemberVariable") - Map<String,Object> properties = createProperties("Datum", name, epsg, area, scope, remarks, deprecated); - if (anchor != null) { - properties.put(Datum.ANCHOR_DEFINITION_KEY, anchor); - } + Map<String,Object> properties = createProperties("Datum", + epsg, name, null, area, scope, remarks, deprecated); + properties.put(Datum.ANCHOR_DEFINITION_KEY, anchor); if (epoch != null) try { /* * Parse the date manually because it is declared as a VARCHAR instead of DATE in original @@ -1696,6 +1723,7 @@ codes: for (int i=0; i<codes.length; i++) { * createEllipsoid(String) and createPrimeMeridian(String), so we must protect * the properties map from changes. */ + case "dynamic geodetic": case "geodetic": { properties = new HashMap<>(properties); // Protect from changes final Ellipsoid ellipsoid = owner.createEllipsoid (getString(code, result, 10)); @@ -1739,6 +1767,11 @@ codes: for (int i=0; i<codes.length; i++) { datum = datumFactory.createParametricDatum(properties); break; } + case "ensemble": { + properties = new HashMap<>(properties); // Protect from changes + datum = DefaultDatumEnsemble.castOrCopy(createDatumEnsemble(epsg, properties)); + break; + } default: { throw new FactoryDataException(error().getString(Errors.Keys.UnknownType_1, type)); } @@ -1757,6 +1790,44 @@ codes: for (int i=0; i<codes.length; i++) { return returnValue; } + /** + * Creates an arbitrary datum ensemble from a code. + * + * @param code value allocated by EPSG. + * @param properties properties to assign to the datum ensemble. + * @return the datum ensemble for the given code, or {@code null} if not found. + */ + private DatumEnsemble<?> createDatumEnsemble(final Integer code, final Map<String,Object> properties) + throws SQLException, FactoryException + { + double accuracy = Double.NaN; + try (ResultSet result = executeQuery("DatumEnsemble", + "SELECT ENSEMBLE_ACCURACY" + + " FROM \"DatumEnsemble\"" + + " WHERE DATUM_ENSEMBLE_CODE = ?", code)) + { + // Should have exactly one value. The loop is a paranoiac safety. + while (result.next()) { + final double value = getDouble(code, result, 1); + if (Double.isNaN(accuracy) || value > accuracy) { + accuracy = value; + } + } + } + final var members = new ArrayList<Datum>(); + try (ResultSet result = executeQuery("DatumEnsembleMember", + "SELECT DATUM_CODE" + + " FROM \"DatumEnsembleMember\"" + + " WHERE DATUM_ENSEMBLE_CODE = ?" + + " ORDER BY DATUM_SEQUENCE", code)) + { + while (result.next()) { + members.add(owner.createDatum(getInteger(code, result, 1).toString())); + } + } + return owner.datumFactory.createDatumEnsemble(properties, members, PositionalAccuracyConstant.ensemble(accuracy)); + } + /** * Returns Bursa-Wolf parameters for a geodetic reference frame. * If the specified datum has no conversion information, then this method returns {@code null}. @@ -1785,7 +1856,7 @@ codes: for (int i=0; i<codes.length; i++) { "SELECT COORD_OP_CODE," + " COORD_OP_METHOD_CODE," + " TARGET_CRS_CODE," + - " AREA_OF_USE_CODE"+ + " AREA_OF_USE_CODE" + // Deprecated since EPSG version 10 (always null). " FROM \"Coordinate_Operation\"" + " WHERE DEPRECATED=0" + // Do not put spaces around "=" - SQLTranslator searches for this exact match. " AND TARGET_CRS_CODE = " + BursaWolfInfo.TARGET_CRS + @@ -1844,7 +1915,7 @@ codes: for (int i=0; i<codes.length; i++) { * all datum seen by this method use Greenwich. But we nevertheless perform this check * as a safety for future evolution or customized EPSG dataset. */ - if (!equalsIgnoreMetadata(meridian, datum.getPrimeMeridian())) { + if (!Utilities.equalsIgnoreMetadata(meridian, datum.getPrimeMeridian())) { continue; } final var bwp = new BursaWolfParameters(datum, info.getDomainOfValidity(owner)); @@ -1931,17 +2002,17 @@ codes: for (int i=0; i<codes.length; i++) { final boolean deprecated = getOptionalBoolean(result, 8); final Unit<Length> unit = owner.createUnit(unitCode).asType(Length.class); @SuppressWarnings("LocalVariableHidesMemberVariable") - final Map<String,Object> properties = createProperties("Ellipsoid", name, epsg, remarks, deprecated); + final Map<String,Object> properties = createProperties("Ellipsoid", + epsg, name, null, null, null, remarks, deprecated); final Ellipsoid ellipsoid; if (Double.isNaN(inverseFlattening)) { if (Double.isNaN(semiMinorAxis)) { // Both are null, which is not allowed. final String column = result.getMetaData().getColumnName(3); throw new FactoryDataException(error().getString(Errors.Keys.NullValueInTable_3, code, column)); - } else { - // We only have semiMinorAxis defined. It is OK - ellipsoid = owner.datumFactory.createEllipsoid(properties, semiMajorAxis, semiMinorAxis, unit); } + // We only have semiMinorAxis defined. It is OK + ellipsoid = owner.datumFactory.createEllipsoid(properties, semiMajorAxis, semiMinorAxis, unit); } else { if (!Double.isNaN(semiMinorAxis)) { // Both `inverseFlattening` and `semiMinorAxis` are defined. @@ -2014,7 +2085,8 @@ codes: for (int i=0; i<codes.length; i++) { final boolean deprecated = getOptionalBoolean(result, 6); final Unit<Angle> unit = owner.createUnit(unitCode).asType(Angle.class); final PrimeMeridian primeMeridian = owner.datumFactory.createPrimeMeridian( - createProperties("Prime Meridian", name, epsg, remarks, deprecated), longitude, unit); + createProperties("Prime Meridian", epsg, name, null, null, null, remarks, deprecated), + longitude, unit); returnValue = ensureSingleton(primeMeridian, returnValue, code); } } catch (SQLException exception) { @@ -2039,6 +2111,15 @@ codes: for (int i=0; i<codes.length; i++) { * <tr><td>3391</td> <td>World - between 80°S and 84°N</td></tr> * </table> * + * <h4>History</h4> + * The table name was {@code "Area"} before version 10 of the <abbr>EPSG</abbr> geodetic dataset. + * Starting from <abbr>EPSG</abbr> version 10, the table name is {@code "Extent"} but the first 7 + * columns are the same with different names and order. The last columns are news. + * + * <p>Before <abbr>EPSG</abbr> version 10, extents were referenced in columns named {@code AREA_OF_USE_CODE}. + * Starting with version 10, that column still exists but is deprecated and contains only {@code null} values. + * An {@code "Usage"} intersection table is used instead.</p> + * * @param code value allocated by EPSG. * @return the extent for the given code. * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. @@ -2153,7 +2234,8 @@ codes: for (int i=0; i<codes.length; i++) { final boolean deprecated = getOptionalBoolean(result, 6); final CoordinateSystemAxis[] axes = createCoordinateSystemAxes(epsg, dimension); @SuppressWarnings("LocalVariableHidesMemberVariable") - final Map<String,Object> properties = createProperties("Coordinate System", name, epsg, remarks, deprecated); // Must be after axes. + final Map<String,Object> properties = createProperties("Coordinate System", + epsg, name, null, null, null, remarks, deprecated); // Must be after axes. /* * The following switch statement should have a case for all "epsg_cs_kind" values enumerated * in the "EPSG_Prepare.sql" file, except that the values in this Java code are in lower cases. @@ -2335,7 +2417,7 @@ codes: for (int i=0; i<codes.length; i++) { } final AxisName an = getAxisName(nameCode); final CoordinateSystemAxis axis = owner.csFactory.createCoordinateSystemAxis( - createProperties("Coordinate Axis", an.name, epsg, an.description, false), + createProperties("Coordinate Axis", epsg, an.name, an.description, null, null, an.remarks, false), abbreviation, direction, owner.createUnit(unit)); returnValue = ensureSingleton(axis, returnValue, code); } @@ -2365,12 +2447,7 @@ codes: for (int i=0; i<codes.length; i++) { final String name = getString(code, result, 1); String description = getOptionalString(result, 2); String remarks = getOptionalString(result, 3); - if (description == null) { - description = remarks; - } else if (remarks != null) { - description += System.lineSeparator() + remarks; - } - final AxisName axis = new AxisName(name, description); + final var axis = new AxisName(name, description, remarks); returnValue = ensureSingleton(axis, returnValue, code); } } @@ -2597,8 +2674,8 @@ next: while (r.next()) { Double.POSITIVE_INFINITY, false, CollectionsExt.first(units)); break; } @SuppressWarnings("LocalVariableHidesMemberVariable") - final Map<String,Object> properties = - createProperties("Coordinate_Operation Parameter", name, epsg, isReversible, deprecated); + final Map<String,Object> properties = createProperties("Coordinate_Operation Parameter", + epsg, name, null, null, null, isReversible, deprecated); properties.put(Identifier.DESCRIPTION_KEY, description); final var descriptor = new DefaultParameterDescriptor<>(properties, 1, 1, type, valueDomain, null, null); returnValue = ensureSingleton(descriptor, returnValue, code); @@ -2761,7 +2838,8 @@ next: while (r.next()) { final boolean deprecated = getOptionalBoolean(result, 4); final ParameterDescriptor<?>[] descriptors = createParameterDescriptors(epsg); @SuppressWarnings("LocalVariableHidesMemberVariable") - Map<String,Object> properties = createProperties("Coordinate_Operation Method", name, epsg, remarks, deprecated); + final Map<String,Object> properties = createProperties("Coordinate_Operation Method", + epsg, name, null, null, null, remarks, deprecated); /* * Note: we do not store the formula at this time, because the text is very verbose and rarely used. */ @@ -2816,7 +2894,7 @@ next: while (r.next()) { " COORD_OP_METHOD_CODE," + " COORD_TFM_VERSION," + " COORD_OP_ACCURACY," + - " AREA_OF_USE_CODE," + + " AREA_OF_USE_CODE," + // Deprecated since EPSG version 10 (always null) " COORD_OP_SCOPE," + " REMARKS," + " DEPRECATED" + @@ -2894,12 +2972,10 @@ next: while (r.next()) { * overwrite the properties map. */ Map<String,Object> opProperties = createProperties("Coordinate_Operation", - name, epsg, area, scope, remarks, deprecated); + epsg, name, null, area, scope, remarks, deprecated); opProperties.put(CoordinateOperation.OPERATION_VERSION_KEY, version); - if (!Double.isNaN(accuracy)) { - opProperties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, - PositionalAccuracyConstant.create(accuracy)); - } + opProperties.put(CoordinateOperation.COORDINATE_OPERATION_ACCURACY_KEY, + PositionalAccuracyConstant.transformation(accuracy)); /* * Creates the operation. Conversions should be the only operations allowed to have * null source and target CRS. In such case, the operation is a defining conversion @@ -3033,7 +3109,7 @@ next: while (r.next()) { do { /* * This `do` loop is executed twice: the first time for searching defining conversions, and the second - * time for searching all other kind of operations. Defining conversions are searched first because + * time for searching all other kinds of operations. Defining conversions are searched first because * they are, by definition, the most accurate operations. */ final String key, sql; @@ -3053,7 +3129,7 @@ next: while (r.next()) { key = "ConversionFromCRS"; sql = "SELECT PROJECTION_CONV_CODE" + " FROM \"Coordinate Reference System\"" + - " WHERE SOURCE_GEOGCRS_CODE = ?" + + " WHERE BASE_CRS_CODE = ?" + " AND COORD_REF_SYS_CODE = ?"; } final Integer targetKey = searchTransformations ? null : pair[1]; diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Finish.sql b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Finish.sql index 1250e6e24a..2c28d81c73 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Finish.sql +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/EPSG_Finish.sql @@ -75,7 +75,7 @@ CREATE INDEX ix_alias ON epsg_alias (object_ta -- Indexes used by EPSGDataAccess.Finder for reverse operation. -- ----------------------------------------------------------------------------- CREATE INDEX ix_major_ellipsoid ON epsg_ellipsoid (semi_major_axis); -CREATE INDEX ix_geogcrs_crs ON epsg_coordinatereferencesystem (source_geogcrs_code); +CREATE INDEX ix_geogcrs_crs ON epsg_coordinatereferencesystem (base_crs_code); CREATE INDEX ix_ellipsoid_datum ON epsg_datum (ellipsoid_code); diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java index 0f28c94563..d3c836baf9 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/SQLTranslator.java @@ -17,6 +17,7 @@ package org.apache.sis.referencing.factory.sql; import java.util.Map; +import java.util.HashMap; import java.util.Locale; import java.util.function.Function; import java.sql.DatabaseMetaData; @@ -35,17 +36,17 @@ import org.apache.sis.referencing.internal.Resources; /** - * Adapter of <abbr>SQL</abbr> statements for variations in syntax and table names. + * Translator of <abbr>SQL</abbr> statements for variations in schema, table and column names. * The {@link #apply(String)} method is invoked when a new {@link PreparedStatement} * is about to be created from a <abbr>SQL</abbr> string. - * This class allows {@link EPSGFactory} to accept some variants of table names. + * That method can modify the string before execution. * For example, the following <abbr>SQL</abbr> query: * * <ul> * <li>{@code SELECT * FROM "Coordinate Reference System"}</li> * </ul> * - * may be converted to one of the following possibilities: + * can be translated to one of the following possibilities: * * <ul> * <li>{@code SELECT * FROM "Coordinate Reference System"} (no change)</li> @@ -53,55 +54,65 @@ import org.apache.sis.referencing.internal.Resources; * <li>{@code SELECT * FROM epsg_coordinatereferencesystem}</li> * </ul> * - * The mixed-case variant is used in the dataset distributed by <abbr>EPSG</abbr> in the MS-Access format, - * while the lower-case variant is used in the <abbr>SQL</abbr> scripts distributed by <abbr>EPSG</abbr>. - * By default, this class auto-detects which database schema contains the <abbr>EPSG</abbr> tables - * and whether the database uses the lower-case or mixed-case variant of table names. - * It is legal to use the mixed-case variant in a PostgreSQL database (for example) - * even if <abbr>EPSG</abbr> distributes the PosthreSQL scripts in lower-case. - * The following table gives the mapping between the two variants accepted by {@link EPSGFactory}: + * Above possibilities differ in the letter cases, spaces and {@code "epsg_"} prefix (non-exhaustive). + * Those differences exist between the <abbr>EPSG</abbr> databases distributed in MS-Access format or + * as <abbr>SQL</abbr> scripts. More differences may exist for handling the differences between version + * 9 and 10 of the <abbr>EPSG</abbr> database schema. + * + * <p>Apache <abbr>SIS</abbr> generally uses the naming convention found in the MS-Access database, + * because it provides more readable <abbr>SQL</abbr> statements. <abbr>SIS</abbr> also assumes an + * <abbr>EPSG</abbr> database schema version 10 or latter. If {@link EPSGFactory} is connected to + * a database which uses a different table naming convention, or to <abbr>EPSG</abbr> version 9, + * then this {@code SQLTranslator} class will translate the <abbr>SQL</abbr> statements on-the-fly. + * The following table gives the mapping between the two naming conventions:</p> * * <table class="sis"> * <caption>Table and column names</caption> - * <tr><th>Element</th><th>Name in MS-Access database</th> <th>Name in <abbr>SQL</abbr> scripts</th></tr> - * <tr><td>Table</td> <td>{@code Alias}</td> <td>{@code epsg_alias}</td></tr> - * <tr><td>Table</td> <td>{@code Area}</td> <td>{@code epsg_area}</td></tr> - * <tr><td>Table</td> <td>{@code Change}</td> <td>{@code epsg_change}</td></tr> - * <tr><td>Table</td> <td>{@code ConventionalRS}</td> <td>{@code epsg_conventionalrs}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate Axis}</td> <td>{@code epsg_coordinateaxis}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate Axis Name}</td> <td>{@code epsg_coordinateaxisname}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate_Operation}</td> <td>{@code epsg_coordoperation}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate_Operation Method}</td> <td>{@code epsg_coordoperationmethod}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate_Operation Parameter}</td> <td>{@code epsg_coordoperationparam}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate_Operation Parameter Usage}</td> <td>{@code epsg_coordoperationparamusage}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate_Operation Parameter Value}</td> <td>{@code epsg_coordoperationparamvalue}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate_Operation Path}</td> <td>{@code epsg_coordoperationpath}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate Reference System}</td> <td>{@code epsg_coordinatereferencesystem}</td></tr> - * <tr><td>Table</td> <td>{@code Coordinate System}</td> <td>{@code epsg_coordinatesystem}</td></tr> - * <tr><td>Table</td> <td>{@code Datum}</td> <td>{@code epsg_datum}</td></tr> - * <tr><td>Table</td> <td>{@code DatumEnsemble}</td> <td>{@code epsg_datumensemble}</td></tr> - * <tr><td>Table</td> <td>{@code DatumEnsembleMember}</td> <td>{@code epsg_datumensemblemember}</td></tr> - * <tr><td>Table</td> <td>{@code DatumRealizationMethod}</td> <td>{@code epsg_datumrealizationmethod}</td></tr> - * <tr><td>Table</td> <td>{@code DefiningOperation}</td> <td>{@code epsg_definingoperation}</td></tr> - * <tr><td>Table</td> <td>{@code Deprecation}</td> <td>{@code epsg_deprecation}</td></tr> - * <tr><td>Table</td> <td>{@code Ellipsoid}</td> <td>{@code epsg_ellipsoid}</td></tr> - * <tr><td>Table</td> <td>{@code Extent}</td> <td>{@code epsg_extent}</td></tr> - * <tr><td>Table</td> <td>{@code Naming System}</td> <td>{@code epsg_namingsystem}</td></tr> - * <tr><td>Table</td> <td>{@code Prime Meridian}</td> <td>{@code epsg_primemeridian}</td></tr> - * <tr><td>Table</td> <td>{@code Scope}</td> <td>{@code epsg_scope}</td></tr> - * <tr><td>Table</td> <td>{@code Supersession}</td> <td>{@code epsg_supersession}</td></tr> - * <tr><td>Table</td> <td>{@code Unit of Measure}</td> <td>{@code epsg_unitofmeasure}</td></tr> - * <tr><td>Table</td> <td>{@code Version History}</td> <td>{@code epsg_versionhistory}</td></tr> - * <tr><td>Table</td> <td>{@code Usage}</td> <td>{@code epsg_usage}</td></tr> - * <tr><td>Column</td> <td>{@code ORDER}</td> <td>{@code coord_axis_order}</td></tr> + * <tr><th>Element</th> <th>Name in MS-Access database</th> <th>Name in <abbr>SQL</abbr> scripts</th></tr> + * <tr><td>Table</td> <td>{@code Alias}</td> <td>{@code epsg_alias}</td></tr> + * <tr><td>Legacy table</td> <td>{@code Area}</td> <td>{@code epsg_area}</td></tr> + * <tr><td>Table</td> <td>{@code Change}</td> <td>{@code epsg_change}</td></tr> + * <tr><td>Table</td> <td>{@code ConventionalRS}</td> <td>{@code epsg_conventionalrs}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate Axis}</td> <td>{@code epsg_coordinateaxis}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate Axis Name}</td> <td>{@code epsg_coordinateaxisname}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate_Operation}</td> <td>{@code epsg_coordoperation}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate_Operation Method}</td> <td>{@code epsg_coordoperationmethod}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate_Operation Parameter}</td> <td>{@code epsg_coordoperationparam}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate_Operation Parameter Usage}</td> <td>{@code epsg_coordoperationparamusage}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate_Operation Parameter Value}</td> <td>{@code epsg_coordoperationparamvalue}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate_Operation Path}</td> <td>{@code epsg_coordoperationpath}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate Reference System}</td> <td>{@code epsg_coordinatereferencesystem}</td></tr> + * <tr><td>Table</td> <td>{@code Coordinate System}</td> <td>{@code epsg_coordinatesystem}</td></tr> + * <tr><td>Table</td> <td>{@code Datum}</td> <td>{@code epsg_datum}</td></tr> + * <tr><td>Table</td> <td>{@code DatumEnsemble}</td> <td>{@code epsg_datumensemble}</td></tr> + * <tr><td>Table</td> <td>{@code DatumEnsembleMember}</td> <td>{@code epsg_datumensemblemember}</td></tr> + * <tr><td>Table</td> <td>{@code DatumRealizationMethod}</td> <td>{@code epsg_datumrealizationmethod}</td></tr> + * <tr><td>Table</td> <td>{@code DefiningOperation}</td> <td>{@code epsg_definingoperation}</td></tr> + * <tr><td>Table</td> <td>{@code Deprecation}</td> <td>{@code epsg_deprecation}</td></tr> + * <tr><td>Table</td> <td>{@code Ellipsoid}</td> <td>{@code epsg_ellipsoid}</td></tr> + * <tr><td>Table</td> <td>{@code Extent}</td> <td>{@code epsg_extent}</td></tr> + * <tr><td>Table</td> <td>{@code Naming System}</td> <td>{@code epsg_namingsystem}</td></tr> + * <tr><td>Table</td> <td>{@code Prime Meridian}</td> <td>{@code epsg_primemeridian}</td></tr> + * <tr><td>Table</td> <td>{@code Scope}</td> <td>{@code epsg_scope}</td></tr> + * <tr><td>Table</td> <td>{@code Supersession}</td> <td>{@code epsg_supersession}</td></tr> + * <tr><td>Table</td> <td>{@code Unit of Measure}</td> <td>{@code epsg_unitofmeasure}</td></tr> + * <tr><td>Table</td> <td>{@code Version History}</td> <td>{@code epsg_versionhistory}</td></tr> + * <tr><td>Table</td> <td>{@code Usage}</td> <td>{@code epsg_usage}</td></tr> + * <tr><td>Column</td> <td>{@code ORDER}</td> <td>{@code coord_axis_order}</td></tr> * </table> * + * Apache <abbr>SIS</abbr> automatically detects which convention is used, regardless the database engine. + * For example, it is legal to use the mixed-case variant in a PostgreSQL database + * even if <abbr>EPSG</abbr> distributes the PostgreSQL scripts in lower-case. + * The {@code "epsg_"} prefix is redundant with database schema and can be omitted. + * {@code SQLTranslator} automatically detects which database schema contains the <abbr>EPSG</abbr> tables. + * * <h2>Thread safety</h2> * All {@code SQLTranslator} instances given to the {@link EPSGFactory} constructor * <strong>shall</strong> be immutable and thread-safe. * * @author Rueben Schulz (UBC) - * @author Martin Desruisseaux (IRD) + * @author Martin Desruisseaux (IRD, Geomatys) * @author Didier Richard (IGN) * @author John Grange * @version 1.5 @@ -272,17 +283,21 @@ public class SQLTranslator implements Function<String,String> { } /** - * Sets the value of all non-final fields. This method performs two steps: + * Sets the value of all non-final fields. This method performs the following steps: * * <ol class="verbose"> - * <li>Find the schema that seems to contain the EPSG tables. If there is more than one schema containing the - * tables, gives precedence to the schema named "EPSG" if one is found. If there is no schema named "EPSG", - * take an arbitrary schema. It may be the empty string if the tables are not contained in a schema.</li> + * <li>Find the schema that seems to contain the <abbr>EPSG</abbr> tables. + * If there is more than one schema containing the tables, give precedence to the schema named + * {@code "EPSG"} (ignoring case), or an arbitrary schema if none has been found with that name. + * The schema name may be the empty string if the tables are not contained in a schema.</li> + * + * <li>Determine whether the table names are prefixed by {@value #TABLE_PREFIX} + * and whether table names are in lower-case or mixed-case.</li> * * <li>Fill the {@link #tableRenaming} and {@link #columnRenaming} maps. These maps translate table * and column names used in the <abbr>SQL</abbr> statements into the names used by the database. * Two conventions are understood: the names used in the MS-Access database or the names used - * in the <abbr>SQL</abbr> scripts. Both of them are distributed by <abbr>EPSG</abbr>.</li> + * in the <abbr>SQL</abbr> scripts, potentially with {@linkplain #TABLE_PREFIX prefix} removed.</li> * </ol> */ final void setup(final DatabaseMetaData md) throws SQLException { @@ -317,31 +332,62 @@ public class SQLTranslator implements Function<String,String> { /* * At this point, the catalog and schema have been found or have been confirmed, * or are still null if we did not found the EPSG table used as a sentinel value. + * The following map contains renaming not covered by the generic algorithm + * implemented in `toActualTableName(…)`. */ if (!useMixedCaseTableNames) { tableRenaming = Map.of("Coordinate_Operation", "coordoperation", "Parameter", "param"); } + /* + * Column name patterns which will be used in the rest of this method. + * They need to be adapted to the letter case convention of the database. + */ + String order = "ORDER"; + String baseCRS = "%CRS_CODE"; // "BASE_CRS_CODE" or "SOURCE_GEOGCRS_CODE". + String deprecated = "DEPRECATED"; + String objectTable = ENUMERATION_COLUMN; + if (md.storesLowerCaseIdentifiers()) { + order = order.toLowerCase(Locale.US); + baseCRS = baseCRS.toLowerCase(Locale.US); + deprecated = deprecated.toLowerCase(Locale.US); + objectTable = objectTable.toLowerCase(Locale.US); + } /* * MS-Access database uses a column named "ORDER" in the "Coordinate Axis" table. * This column has been renamed "coord_axis_order" in DLL scripts. * We need to check which name our current database uses. */ - try (ResultSet result = md.getColumns(catalog, schemaPattern, toActualTableName("Coordinate Axis"), "ORDER")) { + try (ResultSet result = md.getColumns(catalog, schemaPattern, toActualTableName("Coordinate Axis"), order)) { if (result.next()) { columnRenaming = Map.of("COORD_AXIS_ORDER", "ORDER"); } } + /* + * A column named "BASE_CRS_CODE" in EPSG version 9 has been renamed "SOURCE_GEOGCRS_CODE" in version 10. + * Detects which name is used, with precedence to latest database version if the two columns are found. + */ +skip: try (ResultSet result = md.getColumns(catalog, schemaPattern, toActualTableName("Coordinate Reference System"), baseCRS)) { + boolean isOldSchema = false; + while (result.next()) { + final String column = result.getString(Reflection.COLUMN_NAME); + if ("BASE_CRS_CODE".equalsIgnoreCase(column)) { + break skip; // Found the column of EPSG version 10. + } + if (!isOldSchema) { + isOldSchema = "SOURCE_GEOGCRS_CODE".equalsIgnoreCase(column); + } + } + if (isOldSchema) { + columnRenaming = new HashMap<>(columnRenaming); + columnRenaming.put("BASE_CRS_CODE", "SOURCE_GEOGCRS_CODE"); + columnRenaming = Map.copyOf(columnRenaming); + } + } /* * Detect if the database uses boolean types where applicable. * We arbitrarily use the Datum table as a representative value. */ - String deprecated = "DEPRECATED"; - String objectTable = ENUMERATION_COLUMN; - if (md.storesLowerCaseIdentifiers()) { - deprecated = deprecated.toLowerCase(Locale.US); - objectTable = objectTable.toLowerCase(Locale.US); - } final String tablePattern = usePrefixedTableNames ? SQLUtilities.escape(TABLE_PREFIX, escape) + '%' : null; try (ResultSet result = md.getColumns(catalog, schemaPattern, tablePattern, deprecated)) { while (result.next()) { @@ -436,13 +482,12 @@ public class SQLTranslator implements Function<String,String> { /** * Converts a mixed-case table name to the convention used in the database. * The names of the tables for the two conventions are listed in a table in the Javadoc of this class. - * The rreturned string does not include the identifier quotes. + * The returned string does not include the identifier quotes. * * @param name the mixed-case table name. * @return the name converted to the convention used by the database. - * @since 1.5 */ - public final String toActualTableName(String name) { + final String toActualTableName(String name) { if (useMixedCaseTableNames) { return name; } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/PositionalAccuracyConstant.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/PositionalAccuracyConstant.java index 123aea11c2..50307e14c7 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/PositionalAccuracyConstant.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/PositionalAccuracyConstant.java @@ -126,6 +126,12 @@ public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPos */ public static final PositionalAccuracy SAME_DATUM_ENSEMBLE; + /** + * Name for accuracy metadata of datum ensemble. + */ + private static final DefaultMeasureReference ENSEMBLE_REFERENCE = + new DefaultMeasureReference(Vocabulary.formatInternational(Vocabulary.Keys.EnsembleAccuracy)); + /** * Name for accuracy metadata of coordinate transformations. */ @@ -138,13 +144,14 @@ public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPos * compared by the database maintainers against some results taken as true. By contrast, the accuracies that * we have set to conservative values are considered "direct internal". */ - private static final DefaultEvaluationMethod TRANSFORMATION_METHOD = + private static final DefaultEvaluationMethod EVALUATION_METHOD = new DefaultEvaluationMethod(EvaluationMethodType.DIRECT_EXTERNAL, Resources.formatInternational(Resources.Keys.AccuracyFromGeodeticDatase)); static { + ENSEMBLE_REFERENCE .transitionTo(DefaultMeasureReference.State.FINAL); TRANSFORMATION_REFERENCE.transitionTo(DefaultMeasureReference.State.FINAL); - TRANSFORMATION_METHOD .transitionTo(DefaultEvaluationMethod.State.FINAL); + EVALUATION_METHOD .transitionTo(DefaultEvaluationMethod.State.FINAL); final var desc = Resources.formatInternational(Resources.Keys.ConformanceMeansDatumShift); final var method = new DefaultEvaluationMethod(EvaluationMethodType.DIRECT_INTERNAL, desc); final var pass = new DefaultConformanceResult(Citations.SIS, desc, true); @@ -193,27 +200,38 @@ public final class PositionalAccuracyConstant extends DefaultAbsoluteExternalPos } /** - * Creates a positional accuracy for a value specified in the EPSG database. + * Creates a transformation accuracy for the given value, in metres. + * This method may return a cached value. * - * @param accuracy the linear accuracy in metres. + * @param accuracy the accuracy in metres. + * @return a positional accuracy with the given value, or {@code null} if the value is not positive. */ - private PositionalAccuracyConstant(final Double accuracy) { - this(TRANSFORMATION_REFERENCE, TRANSFORMATION_METHOD, null, accuracy); + public static PositionalAccuracy transformation(final double accuracy) { + if (accuracy >= 0) { + return CACHE.computeIfAbsent(Math.abs(accuracy), // For making sure that we have positive zero. + (key) -> new PositionalAccuracyConstant(TRANSFORMATION_REFERENCE, EVALUATION_METHOD, null, key)); + } + return null; } /** - * Creates a positional accuracy for the given value, in metres. + * Creates a datum ensemble accuracy for the given value, in metres. * This method may return a cached value. * * @param accuracy the accuracy in metres. - * @return a positional accuracy with the given value. + * @return a positional accuracy with the given value, or {@code null} if the value is not positive. */ - public static PositionalAccuracy create(final Double accuracy) { - return CACHE.computeIfAbsent(accuracy, PositionalAccuracyConstant::new); + public static PositionalAccuracy ensemble(final double accuracy) { + if (accuracy >= 0) { + return CACHE.computeIfAbsent(-Math.abs(accuracy), // For making sure that we have negative zero. + (key) -> new PositionalAccuracyConstant(ENSEMBLE_REFERENCE, EVALUATION_METHOD, null, -key)); + } + return null; } /** - * Cache the positional accuracies of coordinate transformations. + * Cache of positional accuracies of coordinate transformations (positive) or datum ensembles (negative). + * The sign is used for differentiating whether the cache value is for an ensemble or a transformation. * Most coordinate operations use a small set of accuracy values. */ private static final WeakValueHashMap<Double,PositionalAccuracy> CACHE = new WeakValueHashMap<>(Double.class); diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java index 9964ee5081..4bb8178aaf 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java @@ -489,6 +489,11 @@ public class Vocabulary extends IndexedResourceBundle { */ public static final short Engineering = 77; + /** + * Ensemble accuracy + */ + public static final short EnsembleAccuracy = 278; + /** * {0} entr{0,choice,0#y|2#ies} */ diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties index 5c17e977e3..5541020861 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties @@ -103,6 +103,7 @@ EndDate = End date EndPoint = End point Engineering = Engineering EntryCount_1 = {0} entr{0,choice,0#y|2#ies} +EnsembleAccuracy = Ensemble accuracy Envelope = Envelope Errors = Errors Extent = Extent diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties index e7d786b763..c51e88a567 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties @@ -110,6 +110,7 @@ EndDate = Date de fin EndPoint = Point d\u2019arriv\u00e9 Engineering = Ing\u00e9nierie EntryCount_1 = {0} entr\u00e9e{0,choice,0#|2#s} +EnsembleAccuracy = Pr\u00e9cision de l\u2019ensemble Envelope = Enveloppe Errors = Erreurs Extent = \u00c9tendue diff --git a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql index fa51bec083..b9aa24ba56 100644 --- a/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql +++ b/optional/src/org.apache.sis.referencing.epsg/test/org/apache/sis/referencing/factory/sql/epsg/DebugTools.sql @@ -44,7 +44,7 @@ CREATE VIEW "OperationMethod dimension" AS (SELECT COORD_OP_METHOD_CODE, COORD_OP_TYPE, FALSE AS IS_CONVERSION, SOURCE_CRS_CODE, TARGET_CRS_CODE FROM "Coordinate_Operation" WHERE SOURCE_CRS_CODE IS NOT NULL OR TARGET_CRS_CODE IS NOT NULL UNION - SELECT COORD_OP_METHOD_CODE, COORD_OP_TYPE, TRUE AS IS_CONVERSION, SOURCE_GEOGCRS_CODE AS SOURCE_CRS_CODE, COORD_REF_SYS_CODE AS TARGET_CRS_CODE + SELECT COORD_OP_METHOD_CODE, COORD_OP_TYPE, TRUE AS IS_CONVERSION, BASE_CRS_CODE AS SOURCE_CRS_CODE, COORD_REF_SYS_CODE AS TARGET_CRS_CODE FROM "Coordinate Reference System" AS CRS INNER JOIN "Coordinate_Operation" AS CO ON CRS.PROJECTION_CONV_CODE = CO.COORD_OP_CODE) AS P LEFT JOIN "CRS dimension" AS DS ON DS.COORD_REF_SYS_CODE = P.SOURCE_CRS_CODE
