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 155af1559e513bfcff6d4fe35ce7c590caf6b6ce Author: Martin Desruisseaux <[email protected]> AuthorDate: Thu Jul 24 15:45:59 2025 +0200 Support the ANCHOR_EPOCH and FRAME_REFERENCE_EPOCH columns of the "Datum" table in EPSG version 10+. --- .../org/apache/sis/temporal/LenientDateFormat.java | 4 +- .../apache/sis/temporal/LenientDateFormatTest.java | 4 + .../referencing/datum/DefaultGeodeticDatum.java | 2 +- .../referencing/datum/DefaultVerticalDatum.java | 2 +- .../referencing/factory/sql/EPSGDataAccess.java | 159 +++++++++++---------- .../sis/referencing/factory/sql/SQLTranslator.java | 7 +- 6 files changed, 100 insertions(+), 78 deletions(-) diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java index 337b5762c0..4649c7c3be 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/LenientDateFormat.java @@ -31,6 +31,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.OffsetTime; +import java.time.YearMonth; +import java.time.Year; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.temporal.Temporal; @@ -103,7 +105,7 @@ public final class LenientDateFormat extends DateFormat { * @see #parseInstantUTC(CharSequence, int, int) */ private static TemporalQuery<?>[] QUERIES = { - Instant::from, LocalDateTime::from, LocalDate::from + Instant::from, LocalDateTime::from, LocalDate::from, YearMonth::from, Year::from }; /** diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/LenientDateFormatTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/LenientDateFormatTest.java index 492d98f7d2..3328d05e86 100644 --- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/LenientDateFormatTest.java +++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/temporal/LenientDateFormatTest.java @@ -19,6 +19,8 @@ package org.apache.sis.temporal; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.YearMonth; +import java.time.Year; import java.util.Date; import java.text.ParseException; @@ -99,6 +101,8 @@ public final class LenientDateFormatTest extends TestCase { assertEquals(LocalDateTime.of(2016, 6, 27, 16, 48), LenientDateFormat.parseBest("2016-06-27T16:48")); assertEquals(LocalDateTime.of(2016, 6, 27, 16, 48), LenientDateFormat.parseBest("2016-06-27 16:48")); assertEquals(LocalDate.of(2016, 6, 27), LenientDateFormat.parseBest("2016-06-27")); + assertEquals(YearMonth.of(2016, 6), LenientDateFormat.parseBest("2016-06")); + assertEquals(Year.of(2016), LenientDateFormat.parseBest("2016")); } /** diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java index 75cc8178e2..736473a79e 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultGeodeticDatum.java @@ -586,7 +586,7 @@ public class DefaultGeodeticDatum extends AbstractDatum implements GeodeticDatum */ @Override public boolean equals(final Object object, final ComparisonMode mode) { - return super.equals(object) && (mode != ComparisonMode.STRICT || + return super.equals(object, mode) && (mode != ComparisonMode.STRICT || frameReferenceEpoch.equals(((Dynamic) object).frameReferenceEpoch)); } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java index 4a097c0eed..0d27307af2 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DefaultVerticalDatum.java @@ -283,7 +283,7 @@ public class DefaultVerticalDatum extends AbstractDatum implements VerticalDatum */ @Override public boolean equals(final Object object, final ComparisonMode mode) { - return super.equals(object) && (mode != ComparisonMode.STRICT || + return super.equals(object, mode) && (mode != ComparisonMode.STRICT || frameReferenceEpoch.equals(((Dynamic) object).frameReferenceEpoch)); } 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 e2cef4ba6a..71746b6337 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,10 +24,8 @@ import java.util.LinkedHashSet; import java.util.LinkedHashMap; import java.util.ArrayList; import java.util.Iterator; -import java.util.Calendar; import java.util.Date; import java.util.Locale; -import java.util.TimeZone; import java.util.Objects; import java.util.Optional; import java.util.logging.Level; @@ -43,6 +41,7 @@ import java.sql.SQLException; import java.net.URI; import java.net.URISyntaxException; import java.time.LocalDate; +import java.time.Year; import java.time.temporal.Temporal; import javax.measure.Unit; import javax.measure.quantity.Angle; @@ -227,15 +226,6 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho */ private String lastTableForName; - /** - * The calendar instance for creating {@link Date} objects from a year (the "epoch" in datum definition). - * We use the UTC timezone, which may not be quite accurate. But there is no obvious timezone for "epoch", - * and the "epoch" is an approximation anyway. - * - * @see #getCalendar() - */ - private Calendar calendar; - /** * A pool of prepared statements. Keys are {@link String} objects related to their originating method * (for example "Ellipsoid" for {@link #createEllipsoid(String)}). @@ -384,19 +374,6 @@ public class EPSGDataAccess extends GeodeticAuthorityFactory implements CRSAutho return owner.getLocale(); } - /** - * Returns the calendar to use for reading dates in the database. - */ - @SuppressWarnings("ReturnOfDateField") - private Calendar getCalendar() { - if (calendar == null) { - calendar = Calendar.getInstance(TimeZone.getTimeZone(Constants.UTC), Locale.CANADA); - // Canada locale is closer to ISO than US. - } - calendar.clear(); - return calendar; - } - /** * Returns the authority for this EPSG dataset. The returned citation contains the database version * in the {@linkplain Citation#getEdition() edition} attribute, together with date of last update in @@ -857,6 +834,54 @@ codes: for (int i=0; i<codes.length; i++) { return stmt; } + /** + * Gets the local date or year from the specified {@link ResultSet}, or {@code null} if none. + * This method parses the date itself from the character string because the column type is sometime declared + * as {@code VARCHAR} instead of {@code DATE} in the <abbr>SQL</abbr> scripts distributed by <abbr>EPSG</abbr>. + * The Apache <abbr>SIS</abbr> installer replaces {@code VARCHAR} by {@code DATE}, but we have no guarantee + * that we are reading an <abbr>EPSG</abbr> database created by our installer. Furthermore, an older version + * of <abbr>EPSG</abbr> installer was using {@code SMALLINT} instead of {@code DATE}, + * because scripts before <abbr>EPSG</abbr> 9.0 were reporting only the epoch year. + * + * @param result the result set to fetch value from. + * @param columnIndex the column index (1-based). + * @param caller the caller, used for reporting a warning in case of parsing error. + * @return the temporal at the specified column, or {@code null}. + * @throws SQLException if an error occurred while querying the database. + */ + private static Temporal getOptionalTemporal(final ResultSet result, final int columnIndex, final String caller) + throws SQLException + { + try { + return LenientDateFormat.parseBest(getOptionalString(result, columnIndex)); + } catch (NumberFormatException exception) { + unexpectedException(caller, exception); // Not a fatal error. + } + return null; + } + + /** + * Gets the epoch from the specified {@link ResultSet}, or {@code null} if none. + * The column type should be a floating point number. + * + * @param result the result set to fetch value from. + * @param columnIndex the column index (1-based). + * @return the epoch at the specified column, or {@code null}. + * @throws SQLException if an error occurred while querying the database. + */ + private static Temporal getOptionalEpoch(final ResultSet result, final int columnIndex) throws SQLException { + final double epoch = getOptionalDouble(result, columnIndex); + if (Double.isNaN(epoch)) { + return null; + } + final var year = Year.of((int) epoch); + final long day = Math.round((epoch - year.getValue()) * year.length()); + if (day == 0) { + return year; + } + return year.atMonth(year.atDay(Math.toIntExact(day)).getMonth()); + } + /** * Gets the value from the specified {@link ResultSet}, or {@code null} if none. * @@ -1669,58 +1694,37 @@ codes: for (int i=0; i<codes.length; i++) { " DATUM_NAME," + // [ 2] " DATUM_TYPE," + // [ 3] " ORIGIN_DESCRIPTION," + // [ 4] - " REALIZATION_EPOCH," + // [ 5] - " AREA_OF_USE_CODE," + // [ 6] — Deprecated since EPSG version 10 (always null) - " DATUM_SCOPE," + // [ 7] - " REMARKS," + // [ 8] - " DEPRECATED," + // [ 9] - " ELLIPSOID_CODE," + // [10] — Only for geodetic type - " PRIME_MERIDIAN_CODE," + // [11] — Only for geodetic type - " REALIZATION_METHOD_CODE" + // [12] — Only for vertical type + " ANCHOR_EPOCH," + // [ 5] + " FRAME_REFERENCE_EPOCH," + // [ 6] — NULL for static datum, non-null if dynamic. + " PUBLICATION_DATE," + // [ 7] — Was REALIZATION_EPOCH in EPSG version 9. + " AREA_OF_USE_CODE," + // [ 8] — Deprecated since EPSG version 10 (always null) + " DATUM_SCOPE," + // [ 9] + " REMARKS," + // [10] + " DEPRECATED," + // [11] + " ELLIPSOID_CODE," + // [12] — Only for geodetic type + " PRIME_MERIDIAN_CODE," + // [13] — Only for geodetic type + " REALIZATION_METHOD_CODE" + // [14] — Only for vertical type " FROM \"Datum\"" + " WHERE DATUM_CODE = ?", code)) { while (result.next()) { - final Integer epsg = getInteger (code, result, 1); - final String name = getString (code, result, 2); - final String type = getString (code, result, 3); - final String anchor = getOptionalString (result, 4); - final String epoch = getOptionalString (result, 5); - final String area = getOptionalString (result, 6); - final String scope = getOptionalString (result, 7); - final String remarks = getOptionalString (result, 8); - final boolean deprecated = getOptionalBoolean(result, 9); + final Integer epsg = getInteger (code, result, 1); + final String name = getString (code, result, 2); + final String type = getString (code, result, 3); + final String anchor = getOptionalString (result, 4); + final Temporal epoch = getOptionalEpoch (result, 5); + final Temporal dynamic = getOptionalEpoch (result, 6); + final Temporal publish = getOptionalTemporal(result, 7, "createDatum"); + final String area = getOptionalString (result, 8); + final String scope = getOptionalString (result, 9); + final String remarks = getOptionalString (result, 10); + final boolean deprecated = getOptionalBoolean (result, 11); @SuppressWarnings("LocalVariableHidesMemberVariable") 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 - * SQL scripts. Apache SIS installer replaces VARCHAR by DATE, but we have no guarantee that - * we are reading an EPSG database created by our installer. Furthermore, an older version of - * EPSG installer was using SMALLINT instead of DATE, because scripts before EPSG 9.0 were - * reporting only the epoch year. - */ - final CharSequence[] fields = CharSequences.split(epoch, '-'); - int year = 0, month = 0, day = 1; - for (int i = Math.min(fields.length, 3); --i >= 0;) { - final int f = Integer.parseInt(fields[i].toString()); - switch (i) { - case 0: year = f; break; - case 1: month = f-1; break; - case 2: day = f; break; - } - } - if (year != 0) { - @SuppressWarnings("LocalVariableHidesMemberVariable") - final Calendar calendar = getCalendar(); - calendar.set(year, month, day); - properties.put(Datum.ANCHOR_EPOCH_KEY, calendar.getTime().toInstant()); - } - } catch (NumberFormatException exception) { - unexpectedException("createDatum", exception); // Not a fatal error. - } + properties.put(Datum.ANCHOR_EPOCH_KEY, epoch); + properties.put(Datum.PUBLICATION_DATE_KEY, publish); /* * The following switch statement should have a case for all "epsg_datum_kind" values enumerated * in the "EPSG_Prepare.sql" file, except that the values in this Java code are in lower cases. @@ -1736,18 +1740,26 @@ codes: for (int i=0; i<codes.length; i++) { case "dynamic geodetic": case "geodetic": { properties = new HashMap<>(properties); // Protect from changes - final Ellipsoid ellipsoid = owner.createEllipsoid (getString(code, result, 10)); - final PrimeMeridian meridian = owner.createPrimeMeridian(getString(code, result, 11)); + final Ellipsoid ellipsoid = owner.createEllipsoid (getString(code, result, 12)); + final PrimeMeridian meridian = owner.createPrimeMeridian(getString(code, result, 13)); final BursaWolfParameters[] param = createBursaWolfParameters(meridian, epsg); if (param != null) { properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, param); } - datum = datumFactory.createGeodeticDatum(properties, ellipsoid, meridian); + if (dynamic != null) { + datum = datumFactory.createGeodeticDatum(properties, ellipsoid, meridian, dynamic); + } else { + datum = datumFactory.createGeodeticDatum(properties, ellipsoid, meridian); + } break; } case "vertical": { - final RealizationMethod method = getRealizationMethod(getOptionalInteger(result, 12)); - datum = datumFactory.createVerticalDatum(properties, method); + final RealizationMethod method = getRealizationMethod(getOptionalInteger(result, 14)); + if (dynamic != null) { + datum = datumFactory.createVerticalDatum(properties, method, dynamic); + } else { + datum = datumFactory.createVerticalDatum(properties, method); + } break; } /* @@ -1870,6 +1882,7 @@ codes: for (int i=0; i<codes.length; i++) { " 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 AREA_OF_USE_CODE IS NOT NULL" + " AND TARGET_CRS_CODE = " + BursaWolfInfo.TARGET_CRS + " AND COORD_OP_METHOD_CODE >= " + BursaWolfInfo.MIN_METHOD_CODE + " AND COORD_OP_METHOD_CODE <= " + BursaWolfInfo.MAX_METHOD_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 83a6998c9d..cd160a4feb 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 @@ -383,8 +383,11 @@ skip: try (ResultSet result = md.getColumns(catalog, schemaPattern, toActualTa } if (isOldSchema) { columnRenaming = new HashMap<>(columnRenaming); - columnRenaming.put("BASE_CRS_CODE", "SOURCE_GEOGCRS_CODE"); // In table "Coordinate Reference System". - addMissingColumn("REALIZATION_METHOD_CODE", "INTEGER"); // In table "Datum". + columnRenaming.put("BASE_CRS_CODE", "SOURCE_GEOGCRS_CODE"); // In table "Coordinate Reference System". + columnRenaming.put("PUBLICATION_DATE", "REALIZATION_EPOCH"); // In table "Datum". + addMissingColumn ("ANCHOR_EPOCH", "DOUBLE PRECISION"); // In table "Datum". + addMissingColumn ("FRAME_REFERENCE_EPOCH", "DOUBLE PRECISION"); // In table "Datum". + addMissingColumn ("REALIZATION_METHOD_CODE", "INTEGER"); // In table "Datum". columnRenaming = Map.copyOf(columnRenaming); } /*
