This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 9df7ce65de Allow instantiation of temporal CRS by identifiers. 9df7ce65de is described below commit 9df7ce65de1454489f54eac5795e2b562c38fb10 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Sep 7 15:42:51 2022 +0200 Allow instantiation of temporal CRS by identifiers. https://issues.apache.org/jira/browse/SIS-558 --- .../main/java/org/apache/sis/referencing/CRS.java | 52 ++--- .../java/org/apache/sis/referencing/CommonCRS.java | 109 +++++++--- .../referencing/factory/CommonAuthorityCode.java | 159 +++++++++++++++ .../factory/CommonAuthorityFactory.java | 220 +++++++++++---------- .../factory/GeodeticAuthorityFactory.java | 9 +- .../factory/MultiAuthoritiesFactory.java | 4 +- .../sis/referencing/factory/package-info.java | 2 +- .../java/org/apache/sis/referencing/CRSTest.java | 46 ++++- .../org/apache/sis/referencing/CommonCRSTest.java | 37 +++- .../factory/CommonAuthorityFactoryTest.java | 32 ++- .../apache/sis/internal/util/DefinitionURI.java | 14 +- .../java/org/apache/sis/util/resources/Errors.java | 5 + .../apache/sis/util/resources/Errors.properties | 1 + .../apache/sis/util/resources/Errors_fr.properties | 1 + 14 files changed, 518 insertions(+), 173 deletions(-) diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java index 265f31a557..6f11b645a3 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java @@ -158,35 +158,37 @@ public final class CRS extends Static { * The set of available codes depends on the {@link CRSAuthorityFactory} instances available on the classpath. * There is many thousands of <a href="https://sis.apache.org/tables/CoordinateReferenceSystems.html">CRS * defined by EPSG authority or by other authorities</a>. - * The following table lists a very small subset of codes which are guaranteed to be available + * The following table lists a small subset of codes which are guaranteed to be available * on any installation of Apache SIS: * * <blockquote><table class="sis"> * <caption>Minimal set of supported authority codes</caption> - * <tr><th>Code</th> <th>Enum</th> <th>CRS Type</th> <th>Description</th></tr> - * <tr><td>CRS:27</td> <td>{@link CommonCRS#NAD27 NAD27}</td> <td>Geographic</td> <td>Like EPSG:4267 except for (<var>longitude</var>, <var>latitude</var>) axis order</td></tr> - * <tr><td>CRS:83</td> <td>{@link CommonCRS#NAD83 NAD83}</td> <td>Geographic</td> <td>Like EPSG:4269 except for (<var>longitude</var>, <var>latitude</var>) axis order</td></tr> - * <tr><td>CRS:84</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Geographic</td> <td>Like EPSG:4326 except for (<var>longitude</var>, <var>latitude</var>) axis order</td></tr> - * <tr><td>EPSG:4230</td> <td>{@link CommonCRS#ED50 ED50}</td> <td>Geographic</td> <td>European Datum 1950</td></tr> - * <tr><td>EPSG:4258</td> <td>{@link CommonCRS#ETRS89 ETRS89}</td> <td>Geographic</td> <td>European Terrestrial Reference Frame 1989</td></tr> - * <tr><td>EPSG:4267</td> <td>{@link CommonCRS#NAD27 NAD27}</td> <td>Geographic</td> <td>North American Datum 1927</td></tr> - * <tr><td>EPSG:4269</td> <td>{@link CommonCRS#NAD83 NAD83}</td> <td>Geographic</td> <td>North American Datum 1983</td></tr> - * <tr><td>EPSG:4322</td> <td>{@link CommonCRS#WGS72 WGS72}</td> <td>Geographic</td> <td>World Geodetic System 1972</td></tr> - * <tr><td>EPSG:4326</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Geographic</td> <td>World Geodetic System 1984</td></tr> - * <tr><td>EPSG:4936</td> <td>{@link CommonCRS#ETRS89 ETRS89}</td> <td>Geocentric</td> <td>European Terrestrial Reference Frame 1989</td></tr> - * <tr><td>EPSG:4937</td> <td>{@link CommonCRS#ETRS89 ETRS89}</td> <td>Geographic 3D</td> <td>European Terrestrial Reference Frame 1989</td></tr> - * <tr><td>EPSG:4978</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Geocentric</td> <td>World Geodetic System 1984</td></tr> - * <tr><td>EPSG:4979</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Geographic 3D</td> <td>World Geodetic System 1984</td></tr> - * <tr><td>EPSG:4984</td> <td>{@link CommonCRS#WGS72 WGS72}</td> <td>Geocentric</td> <td>World Geodetic System 1972</td></tr> - * <tr><td>EPSG:4985</td> <td>{@link CommonCRS#WGS72 WGS72}</td> <td>Geographic 3D</td> <td>World Geodetic System 1972</td></tr> - * <tr><td>EPSG:5041</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Projected</td> <td>WGS 84 / UPS North (E,N)</td></tr> - * <tr><td>EPSG:5042</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Projected</td> <td>WGS 84 / UPS South (E,N)</td></tr> - * <tr><td>EPSG:322##</td><td>{@link CommonCRS#WGS72 WGS72}</td> <td>Projected</td> <td>WGS 72 / UTM zone ##N</td></tr> - * <tr><td>EPSG:323##</td><td>{@link CommonCRS#WGS72 WGS72}</td> <td>Projected</td> <td>WGS 72 / UTM zone ##S</td></tr> - * <tr><td>EPSG:326##</td><td>{@link CommonCRS#WGS84 WGS84}</td> <td>Projected</td> <td>WGS 84 / UTM zone ##N</td></tr> - * <tr><td>EPSG:327##</td><td>{@link CommonCRS#WGS84 WGS84}</td> <td>Projected</td> <td>WGS 84 / UTM zone ##S</td></tr> - * <tr><td>EPSG:5715</td> <td>{@link CommonCRS.Vertical#DEPTH DEPTH}</td> <td>Vertical</td> <td>Mean Sea Level depth</td></tr> - * <tr><td>EPSG:5714</td> <td>{@link CommonCRS.Vertical#MEAN_SEA_LEVEL MEAN_SEA_LEVEL}</td> <td>Vertical</td> <td>Mean Sea Level height</td></tr> + * <tr><th>Code</th> <th>Enum</th> <th>CRS Type</th> <th>Description</th></tr> + * <tr><td>CRS:27</td> <td>{@link CommonCRS#NAD27 NAD27}</td> <td>Geographic</td> <td>Like EPSG:4267 except for (<var>longitude</var>, <var>latitude</var>) axis order</td></tr> + * <tr><td>CRS:83</td> <td>{@link CommonCRS#NAD83 NAD83}</td> <td>Geographic</td> <td>Like EPSG:4269 except for (<var>longitude</var>, <var>latitude</var>) axis order</td></tr> + * <tr><td>CRS:84</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Geographic</td> <td>Like EPSG:4326 except for (<var>longitude</var>, <var>latitude</var>) axis order</td></tr> + * <tr><td>EPSG:4230</td> <td>{@link CommonCRS#ED50 ED50}</td> <td>Geographic</td> <td>European Datum 1950</td></tr> + * <tr><td>EPSG:4258</td> <td>{@link CommonCRS#ETRS89 ETRS89}</td> <td>Geographic</td> <td>European Terrestrial Reference Frame 1989</td></tr> + * <tr><td>EPSG:4267</td> <td>{@link CommonCRS#NAD27 NAD27}</td> <td>Geographic</td> <td>North American Datum 1927</td></tr> + * <tr><td>EPSG:4269</td> <td>{@link CommonCRS#NAD83 NAD83}</td> <td>Geographic</td> <td>North American Datum 1983</td></tr> + * <tr><td>EPSG:4322</td> <td>{@link CommonCRS#WGS72 WGS72}</td> <td>Geographic</td> <td>World Geodetic System 1972</td></tr> + * <tr><td>EPSG:4326</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Geographic</td> <td>World Geodetic System 1984</td></tr> + * <tr><td>EPSG:4936</td> <td>{@link CommonCRS#ETRS89 ETRS89}</td> <td>Geocentric</td> <td>European Terrestrial Reference Frame 1989</td></tr> + * <tr><td>EPSG:4937</td> <td>{@link CommonCRS#ETRS89 ETRS89}</td> <td>Geographic 3D</td> <td>European Terrestrial Reference Frame 1989</td></tr> + * <tr><td>EPSG:4978</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Geocentric</td> <td>World Geodetic System 1984</td></tr> + * <tr><td>EPSG:4979</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Geographic 3D</td> <td>World Geodetic System 1984</td></tr> + * <tr><td>EPSG:4984</td> <td>{@link CommonCRS#WGS72 WGS72}</td> <td>Geocentric</td> <td>World Geodetic System 1972</td></tr> + * <tr><td>EPSG:4985</td> <td>{@link CommonCRS#WGS72 WGS72}</td> <td>Geographic 3D</td> <td>World Geodetic System 1972</td></tr> + * <tr><td>EPSG:5041</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Projected</td> <td>WGS 84 / UPS North (E,N)</td></tr> + * <tr><td>EPSG:5042</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Projected</td> <td>WGS 84 / UPS South (E,N)</td></tr> + * <tr><td>EPSG:322##</td> <td>{@link CommonCRS#WGS72 WGS72}</td> <td>Projected</td> <td>WGS 72 / UTM zone ##N</td></tr> + * <tr><td>EPSG:323##</td> <td>{@link CommonCRS#WGS72 WGS72}</td> <td>Projected</td> <td>WGS 72 / UTM zone ##S</td></tr> + * <tr><td>EPSG:326##</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Projected</td> <td>WGS 84 / UTM zone ##N</td></tr> + * <tr><td>EPSG:327##</td> <td>{@link CommonCRS#WGS84 WGS84}</td> <td>Projected</td> <td>WGS 84 / UTM zone ##S</td></tr> + * <tr><td>EPSG:5714</td> <td>{@link CommonCRS.Vertical#MEAN_SEA_LEVEL MEAN_SEA_LEVEL}</td> <td>Vertical</td> <td>Mean Sea Level height</td></tr> + * <tr><td>EPSG:5715</td> <td>{@link CommonCRS.Vertical#DEPTH DEPTH}</td> <td>Vertical</td> <td>Mean Sea Level depth</td></tr> + * <tr><td>OGC:JulianDate</td><td>{@link CommonCRS.Temporal#JULIAN JULIAN}</td> <td>Temporal</td> <td>Julian date (days)</td></tr> + * <tr><td>OGC:UnixTime</td> <td>{@link CommonCRS.Temporal#UNIX UNIX}</td> <td>Unix</td> <td>Unix time (seconds)</td></tr> * </table></blockquote> * * <h4>URI forms</h4> diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java index f9ddfcb9e5..f71dccd8bd 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CommonCRS.java @@ -76,6 +76,7 @@ import org.apache.sis.internal.system.Modules; import org.apache.sis.internal.system.Loggers; import org.apache.sis.internal.util.Constants; import org.apache.sis.internal.jdk9.JDK9; +import org.apache.sis.util.OptionalCandidate; import org.apache.sis.util.resources.Vocabulary; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.logging.Logging; @@ -456,7 +457,7 @@ public enum CommonCRS { * Invoked by when the cache needs to be cleared after a classpath change. */ @SuppressWarnings("NestedSynchronizedStatement") // Safe because cachedProjections never call any method of 'this'. - synchronized void clear() { + final synchronized void clear() { cached = null; cachedGeo3D = null; cachedNormalized = null; @@ -1363,7 +1364,7 @@ public enum CommonCRS { /** * Invoked by when the cache needs to be cleared after a classpath change. */ - synchronized void clear() { + final synchronized void clear() { cached = null; } @@ -1514,21 +1515,22 @@ public enum CommonCRS { * TemporalCRS crs = CommonCRS.Temporal.JULIAN.crs(); * } * - * Below is an alphabetical list of object names available in this enumeration: + * Below is an alphabetical list of object names available in this enumeration. + * Note that the namespace of identifiers ("OGC" versus "SIS") may change in any future version. * * <blockquote><table class="sis"> * <caption>Temporal objects accessible by enumeration constants</caption> - * <tr><th>Name or alias</th> <th>Object type</th> <th>Enumeration value</th></tr> - * <tr><td>Dublin Julian</td> <td>CRS, Datum</td> <td>{@link #DUBLIN_JULIAN}</td></tr> - * <tr><td>Java time</td> <td>CRS</td> <td>{@link #JAVA}</td></tr> - * <tr><td>Julian</td> <td>CRS, Datum</td> <td>{@link #JULIAN}</td></tr> - * <tr><td>Modified Julian</td> <td>CRS, Datum</td> <td>{@link #MODIFIED_JULIAN}</td></tr> - * <tr><td>Truncated Julian</td> <td>CRS, Datum</td> <td>{@link #TRUNCATED_JULIAN}</td></tr> - * <tr><td>Unix/POSIX time</td> <td>CRS, Datum</td> <td>{@link #UNIX}</td></tr> + * <tr><th>Name or alias</th> <th>Identifier</th> <th>Object type</th> <th>Enumeration value</th></tr> + * <tr><td>Dublin Julian</td> <td>{@code SIS:DublinJulian}</td> <td>CRS, Datum</td> <td>{@link #DUBLIN_JULIAN}</td></tr> + * <tr><td>Java time</td> <td>{@code SIS:JavaTime}</td> <td>CRS</td> <td>{@link #JAVA}</td></tr> + * <tr><td>Julian</td> <td>{@code OGC:JulianDate}</td> <td>CRS, Datum</td> <td>{@link #JULIAN}</td></tr> + * <tr><td>Modified Julian</td> <td>{@code SIS:ModifiedJulianDate}</td> <td>CRS, Datum</td> <td>{@link #MODIFIED_JULIAN}</td></tr> + * <tr><td>Truncated Julian</td> <td>{@code OGC:TruncatedJulianDate}</td> <td>CRS, Datum</td> <td>{@link #TRUNCATED_JULIAN}</td></tr> + * <tr><td>Unix/POSIX time</td> <td>{@code OGC:UnixTime}</td> <td>CRS, Datum</td> <td>{@link #UNIX}</td></tr> * </table></blockquote> * * @author Martin Desruisseaux (Geomatys) - * @version 1.0 + * @version 1.3 * * @see Engineering#TIME * @@ -1549,14 +1551,16 @@ public enum CommonCRS { * the {@link java.text.SimpleDateFormat} class is closer to the common practice (but not ISO 8601 * compliant).</p> */ - JULIAN(Vocabulary.Keys.Julian, -2440588L * MILLISECONDS_PER_DAY + MILLISECONDS_PER_DAY/2), + JULIAN(Vocabulary.Keys.Julian, -2440588L * MILLISECONDS_PER_DAY + MILLISECONDS_PER_DAY/2, + "JulianDate", true), /** * Time measured as days since November 17, 1858 at 00:00 UTC. * A <cite>Modified Julian day</cite> (MJD) is defined relative to * <cite>Julian day</cite> (JD) as {@code MJD = JD − 2400000.5}. */ - MODIFIED_JULIAN(Vocabulary.Keys.ModifiedJulian, -40587L * MILLISECONDS_PER_DAY), + MODIFIED_JULIAN(Vocabulary.Keys.ModifiedJulian, -40587L * MILLISECONDS_PER_DAY, + "ModifiedJulianDate", false), /** * Time measured as days since May 24, 1968 at 00:00 UTC. @@ -1564,24 +1568,26 @@ public enum CommonCRS { * A <cite>Truncated Julian day</cite> (TJD) is defined relative to * <cite>Julian day</cite> (JD) as {@code TJD = JD − 2440000.5}. */ - TRUNCATED_JULIAN(Vocabulary.Keys.TruncatedJulian, -587L * MILLISECONDS_PER_DAY), + TRUNCATED_JULIAN(Vocabulary.Keys.TruncatedJulian, -587L * MILLISECONDS_PER_DAY, + "TruncatedJulianDate", true), /** * Time measured as days since December 31, 1899 at 12:00 UTC. * A <cite>Dublin Julian day</cite> (DJD) is defined relative to * <cite>Julian day</cite> (JD) as {@code DJD = JD − 2415020}. */ - DUBLIN_JULIAN(Vocabulary.Keys.DublinJulian, -25568L * MILLISECONDS_PER_DAY + MILLISECONDS_PER_DAY/2), + DUBLIN_JULIAN(Vocabulary.Keys.DublinJulian, -25568L * MILLISECONDS_PER_DAY + MILLISECONDS_PER_DAY/2, + "DublinJulian", false), /** * Time measured as seconds since January 1st, 1970 at 00:00 UTC. */ - UNIX(Vocabulary.Keys.Time_1, 0), + UNIX(Vocabulary.Keys.Time_1, 0, "UnixTime", true), /** * Time measured as milliseconds since January 1st, 1970 at 00:00 UTC. */ - JAVA(Vocabulary.Keys.Time_1, 0); + JAVA(Vocabulary.Keys.Time_1, 0, "JavaTime", false); /** * The resource keys for the name as one of the {@code Vocabulary.Keys} constants. @@ -1593,6 +1599,18 @@ public enum CommonCRS { */ private final long epoch; + /** + * Identifier in OGC or SIS namespace. + * + * @see org.apache.sis.referencing.factory.CommonAuthorityFactory#TEMPORAL_NAMES + */ + private final String identifier; + + /** + * Whether the identifier is in OGC namespace. + */ + private final boolean isOGC; + /** * The cached object. This is initially {@code null}, then set to various kinds of objects depending * on which method has been invoked. The kind of object stored in this field may change during the @@ -1603,9 +1621,11 @@ public enum CommonCRS { /** * Creates a new enumeration value of the given name with time counted since the given epoch. */ - private Temporal(final short name, final long epoch) { - this.key = name; - this.epoch = epoch; + private Temporal(final short name, final long epoch, final String identifier, final boolean isOGC) { + this.key = name; + this.epoch = epoch; + this.identifier = identifier; + this.isOGC = isOGC; } /** @@ -1625,10 +1645,39 @@ public enum CommonCRS { /** * Invoked by when the cache needs to be cleared after a classpath change. */ - synchronized void clear() { + final synchronized void clear() { cached = null; } + /** + * Returns the enumeration value for the given identifier (without namespace). + * Identifiers in OGC namespace are {@code "JulianDate"}, {@code "TruncatedJulianDate"} and {@code "UnixTime"}. + * Identifiers in SIS namespace are {@code "ModifiedJulianDate"}, {@code "DublinJulian"} and {@code "JavaTime"}. + * Note that the content of OGC and SIS namespaces may change in any future version. + * + * @param identifier case-insensitive identifier of the desired temporal CRS, without namespace. + * @param onlyOGC whether to return the CRS only if its identifier is in OGC namespace. + * @return the enumeration value for the given identifier. + * @throws IllegalArgumentException if the given identifier is not recognized. + * + * @see <a href="http://www.opengis.net/def/crs/OGC/0">OGC Definitions Server</a> + * + * @since 1.3 + */ + public static Temporal forIdentifier(final String identifier, final boolean onlyOGC) { + ArgumentChecks.ensureNonEmpty("identifier", identifier); + for (final Temporal candidate : values()) { + if (candidate.identifier.equalsIgnoreCase(identifier)) { + if (onlyOGC & !candidate.isOGC) { + throw new IllegalArgumentException(Errors.format( + Errors.Keys.IdentifierNotInNamespace_2, Constants.OGC, candidate.identifier)); + } + return candidate; + } + } + throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownEnumValue_2, Temporal.class, identifier)); + } + /** * Returns the enumeration value for the given epoch, or {@code null} if none. * If the epoch is January 1st, 1970, then this method returns {@link #UNIX}. @@ -1638,6 +1687,7 @@ public enum CommonCRS { * * @since 1.0 */ + @OptionalCandidate public static Temporal forEpoch(final Instant epoch) { if (epoch != null) { final long e = epoch.toEpochMilli(); @@ -1677,12 +1727,15 @@ public enum CommonCRS { object = crs(cached); if (object == null) { final TemporalDatum datum = datum(); - final Map<String,?> properties; + final Map<String,?> source; if (this == JAVA) { - properties = properties(Vocabulary.formatInternational(key, "Java")); + source = properties(Vocabulary.formatInternational(key, "Java")); } else { - properties = IdentifiedObjects.getProperties(datum, exclude()); + source = IdentifiedObjects.getProperties(datum, exclude()); } + final Map<String,Object> properties = new HashMap<>(source); + properties.put(TemporalCRS.IDENTIFIERS_KEY, + new NamedIdentifier(isOGC ? Citations.OGC : Citations.SIS, identifier)); object = new DefaultTemporalCRS(properties, datum, cs()); cached = object; } @@ -1958,14 +2011,14 @@ public enum CommonCRS { * @param key a constant from {@link org.apache.sis.util.resources.Vocabulary.Keys}. * @return the properties to give to the object constructor. */ - static Map<String,?> properties(final short key) { + private static Map<String,?> properties(final short key) { return properties(Vocabulary.formatInternational(key)); } /** * Puts the given name in a map of properties to be given to object constructors. */ - static Map<String,?> properties(final InternationalString name) { + private static Map<String,?> properties(final InternationalString name) { return singletonMap(NAME_KEY, new NamedIdentifier(null, name)); } @@ -1989,7 +2042,7 @@ public enum CommonCRS { * Returns the EPSG factory to use for creating CRS, or {@code null} if none. * If this method returns {@code null}, then the caller will silently fallback on hard-coded values. */ - static GeodeticAuthorityFactory factory() { + private static GeodeticAuthorityFactory factory() { if (!EPSGFactoryFallback.FORCE_HARDCODED) { final GeodeticAuthorityFactory factory = AuthorityFactories.EPSG(); if (!(factory instanceof EPSGFactoryFallback)) { @@ -2003,7 +2056,7 @@ public enum CommonCRS { * Invoked when a factory failed to create an object. * After invoking this method, the caller will fallback on hard-coded values. */ - static void failure(final Object caller, final String method, final FactoryException e, final int code) { + private static void failure(final Object caller, final String method, final FactoryException e, final int code) { String message = Resources.format(Resources.Keys.CanNotInstantiateGeodeticObject_1, (Constants.EPSG + ':') + code); message = Exceptions.formatChainedMessages(null, message, e); final LogRecord record = new LogRecord(Level.WARNING, message); diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityCode.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityCode.java new file mode 100644 index 0000000000..8aa66bd213 --- /dev/null +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityCode.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.referencing.factory; + +import org.opengis.referencing.NoSuchAuthorityCodeException; +import org.apache.sis.util.CharSequences; +import org.apache.sis.util.resources.Errors; +import org.apache.sis.internal.util.Constants; +import org.apache.sis.internal.referencing.Resources; + + +/** + * Result of parsing a code in "OGC", "CRS", "AUTO" or "AUTO2" namespace. + * + * @author Martin Desruisseaux (IRD, Geomatys) + * @version 1.3 + * @since 0.7 + * @module + */ +final class CommonAuthorityCode { + /** + * The parameter separator for codes in the {@code "AUTO(2)"} namespace. + */ + static final char SEPARATOR = ','; + + /** + * Local part of the code, without the authority part and without the parameters. + */ + final String localCode; + + /** + * The remaining part of the code after {@link #localCode}, or {@code null} if none. + * This part may exist with codes in the {@code AUTO} or {@code AUTO2} namespace. + */ + private String complement; + + /** + * The result of parsing {@link #complement} as numerical parameters. + * Computed by {@link #parameters()} when first requested. + */ + private double[] parameters; + + /** + * If the authority is {@code "AUTO"}, version of that authority (1 or 2). Otherwise 0. + */ + private int versionOfAuto; + + /** + * Whether the first character of {@link #localCode} is a decimal digit, the minus or the plus character. + */ + final boolean isNumeric; + + /** + * {@code true} if the "OGC" namespace was explicitly specified. + */ + boolean isOGC; + + /** + * Finds the index where the code begins, ignoring spaces and the {@code "OGC"}, {@code "CRS"}, {@code "AUTO"}, + * {@code "AUTO1"} or {@code "AUTO2"} namespaces if present. If a namespace is found and is a legacy one, then + * the {@link #isLegacy} flag will be set. + * + * @param code authority, code and parameters to parse. + * @throws NoSuchAuthorityCodeException if an authority is present but is not one of the recognized authorities. + */ + CommonAuthorityCode(final String code) throws NoSuchAuthorityCodeException { + int s = code.indexOf(Constants.DEFAULT_SEPARATOR); + if (s >= 0) { + final int end = CharSequences.skipTrailingWhitespaces(code, 0, s); + final int start = CharSequences.skipLeadingWhitespaces (code, 0, end); + isOGC = GeodeticAuthorityFactory.regionMatches(Constants.OGC, code, start, end); + if (!isOGC && !GeodeticAuthorityFactory.regionMatches(Constants.CRS, code, start, end)) { + if (code.regionMatches(true, start, "AUTO", 0, 4)) { // 4 is the length of "AUTO". + switch (end - start) { + case 4: versionOfAuto = 1; break; // "AUTO". + case 5: versionOfAuto = (code.charAt(end-1) - '0'); break; // "AUTO1" or "AUTO2". + } + } + if (!isAuto(false)) { + throw new NoSuchAuthorityCodeException(Resources.format(Resources.Keys.UnknownAuthority_1, + CharSequences.trimWhitespaces(code, 0, s)), Constants.OGC, code); + } + } + } + final int length = code.length(); + s = CharSequences.skipLeadingWhitespaces(code, s+1, length); + /* + * Above code removed the "CRS" part when it is used as a namespace, as in "CRS:84". + * The code below removes the "CRS" prefix when it is concatenated within the code, + * as in "CRS84". Together, those two checks handle redundant codes like "CRS:CRS84" + * (malformed code, but seen in practice). + */ + if (code.regionMatches(true, s, Constants.CRS, 0, Constants.CRS.length())) { + s = CharSequences.skipLeadingWhitespaces(code, s + Constants.CRS.length(), length); + } + if (s >= length) { + throw new NoSuchAuthorityCodeException(Errors.format(Errors.Keys.EmptyArgument_1, "code"), Constants.OGC, code); + } + /* + * Check whether the code has parameters. It should happen only in code in "AUTO" or "AUTO2" namespace, + * but we nevertheless check for all authorities. + */ + int end = CharSequences.skipTrailingWhitespaces(code, s, length); + final int startOfParameters = code.indexOf(SEPARATOR, s); + if (startOfParameters >= 0) { + complement = code.substring(startOfParameters + 1, end); + end = CharSequences.skipTrailingWhitespaces(code, s, startOfParameters); + } + localCode = code.substring(s, end); + final char c = localCode.charAt(0); + isNumeric = (c >= '0' && c <= '9') || (c == '-' || c == '+'); + } + + /** + * Returns whether the authority is "AUTO", "AUTO1" or "AUTO2". + * + * @param legacy whether to returns {@code true} only if the authority is "AUTO" or "AUTO1". + * @return whether the authority is some "AUTO" namespace. + */ + final boolean isAuto(final boolean legacy) { + return legacy ? (versionOfAuto == 1) : (versionOfAuto >= 1 && versionOfAuto <= 2); + } + + /** + * Returns the result of parsing the comma-separated list of optional parameters after the code. + * If there is no parameter, then this method returns an empty array. + * Caller should not modify the returned array. + * + * @return the parameters after the code, or an empty array if none. + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + final double[] parameters() { + if (parameters == null) { + parameters = CharSequences.parseDoubles(complement, SEPARATOR); // `parseDoubles(…)` is null-safe. + } + return parameters; + } + + /** + * Returns the error message for unexpected parameters after the code. + */ + final String unexpectedParameters() { + return Errors.format(Errors.Keys.UnexpectedCharactersAfter_2, localCode, complement); + } +} diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java index fe06784f67..a80f26a879 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/CommonAuthorityFactory.java @@ -26,6 +26,8 @@ import javax.measure.Unit; import javax.measure.quantity.Length; import org.opengis.util.FactoryException; import org.opengis.util.InternationalString; +import org.opengis.util.GenericName; +import org.opengis.metadata.Identifier; import org.opengis.metadata.citation.Citation; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.NoSuchAuthorityCodeException; @@ -35,6 +37,7 @@ import org.opengis.referencing.crs.EngineeringCRS; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.crs.VerticalCRS; +import org.opengis.referencing.crs.TemporalCRS; import org.opengis.referencing.crs.SingleCRS; import org.opengis.referencing.cs.CartesianCS; import org.apache.sis.internal.referencing.provider.TransverseMercator.Zoner; @@ -47,8 +50,6 @@ import org.apache.sis.measure.Units; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.referencing.cs.CoordinateSystems; import org.apache.sis.util.ArgumentChecks; -import org.apache.sis.util.ArraysExt; -import org.apache.sis.util.CharSequences; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.SimpleInternationalString; @@ -151,6 +152,30 @@ import static java.util.logging.Logger.getLogger; * <td>{@linkplain org.apache.sis.referencing.cs.DefaultCartesianCS Cartesian}</td> * <td>(east, north)</td> * <td>user-specified</td> + * </tr><tr> + * <td>OGC</td> + * <td>JulianDate</td> + * <td>Julian</td> + * <td>{@linkplain org.apache.sis.referencing.crs.DefaultTemporalCRS Temporal}</td> + * <td>{@linkplain org.apache.sis.referencing.cs.DefaultTimeCS Time}</td> + * <td>(future)</td> + * <td>days</td> + * </tr><tr> + * <td>OGC</td> + * <td>TruncatedJulianDate</td> + * <td>Truncated Julian</td> + * <td>{@linkplain org.apache.sis.referencing.crs.DefaultTemporalCRS Temporal}</td> + * <td>{@linkplain org.apache.sis.referencing.cs.DefaultTimeCS Time}</td> + * <td>(future)</td> + * <td>days</td> + * </tr><tr> + * <td>OGC</td> + * <td>UnixTime</td> + * <td>Unix Time</td> + * <td>{@linkplain org.apache.sis.referencing.crs.DefaultTemporalCRS Temporal}</td> + * <td>{@linkplain org.apache.sis.referencing.cs.DefaultTimeCS Time}</td> + * <td>(future)</td> + * <td>seconds</td> * </tr> * </table> * @@ -184,7 +209,7 @@ import static java.util.logging.Logger.getLogger; * switching to polar stereographic projections for high latitudes.</p> * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.1 + * @version 1.3 * * @see CommonCRS * @@ -195,7 +220,7 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements /** * The {@value} prefix for a code identified by parameters. * This is defined in annexes B.7 to B.11 of WMS 1.3 specification. - * The {@code "AUTO(2)"} namespaces are not considered by SIS as real authorities. + * The {@code "AUTO(2)"} namespaces are not considered by Apache SIS as real authorities. */ private static final String AUTO2 = "AUTO2"; @@ -207,11 +232,6 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements private static final Set<String> CODESPACES = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList(Constants.OGC, Constants.CRS, "AUTO", AUTO2))); - /** - * The bit for saying that a namespace is the legacy {@code "AUTO"} namespace. - */ - private static final int LEGACY_MASK = 0x80000000; - /** * First code in the AUTO(2) namespace. */ @@ -232,13 +252,22 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements }; /** - * The parameter separator for codes in the {@code "AUTO(2)"} namespace. + * Names of temporal CRS in OGC namespace. + * Those CRS are defined by {@link CommonCRS.Temporal}. + * Codes in Apache SIS namespace are excluded from this list. + * + * @see CommonCRS.Temporal#identifier */ - static final char SEPARATOR = ','; + private static final String[] TEMPORAL_NAMES = { + "JulianDate", + "TruncatedJulianDate", + "UnixTime" + }; /** * The codes known to this factory, associated with their CRS type. This is set to an empty map * at {@code CommonAuthorityFactory} construction time and filled only when first needed. + * Keys are of the form "AUTHORITY:IDENTIFIER". */ private final Map<String,Class<?>> codes; @@ -274,79 +303,41 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements } /** - * Rewrites the given code in a canonical format. - * If the code can not be reformatted, then this method returns {@code null}. + * Rewrites the given code in a canonical format and without parameters. + * If the code is not in a known namespace, then this method returns {@code null}. */ - static String reformat(final String code) { + static String reformat(String code) { try { - return format(Integer.parseInt(code.substring(skipNamespace(code) & ~LEGACY_MASK))); + final CommonAuthorityCode parsed = new CommonAuthorityCode(code); + code = parsed.localCode; + code = parsed.isNumeric ? format(Integer.parseInt(code)) : format(code); } catch (NoSuchAuthorityCodeException | NumberFormatException e) { - Logging.recoverableException(getLogger(Loggers.CRS_FACTORY), CommonAuthorityFactory.class, "reformat", e); + Logging.ignorableException(getLogger(Loggers.CRS_FACTORY), CommonAuthorityFactory.class, "reformat", e); return null; } + return code; } /** - * Returns the index where the code begins, ignoring spaces and the {@code "OGC"}, {@code "CRS"}, {@code "AUTO"}, - * {@code "AUTO1"} or {@code "AUTO2"} namespaces if present. If a namespace is found and is a legacy one, then - * this {@link #LEGACY_MASK} bit will be set. - * - * @return index where the code begin, possibly with the {@link #LEGACY_MASK} bit set. - * @throws NoSuchAuthorityCodeException if an authority is present but is not one of the recognized authorities. + * Formats the given code with a {@code "CRS:"} or {@code "AUTO2:"} prefix. + * This is used for numerical codes such as "CRS:84". */ - private static int skipNamespace(final String code) throws NoSuchAuthorityCodeException { - int isLegacy = 0; - int s = code.indexOf(Constants.DEFAULT_SEPARATOR); - if (s >= 0) { - final int end = CharSequences.skipTrailingWhitespaces(code, 0, s); - final int start = CharSequences.skipLeadingWhitespaces (code, 0, end); - if (!regionMatches(Constants.CRS, code, start, end) && - !regionMatches(Constants.OGC, code, start, end)) - { - boolean isRecognized = false; - final int length = AUTO2.length() - 1; - if (code.regionMatches(true, start, AUTO2, 0, length)) { - switch (end - start - length) { // Number of extra characters after "AUTO". - case 0: { // Namespace is exactly "AUTO" (ignoring case). - isRecognized = true; - isLegacy = LEGACY_MASK; - break; - } - case 1: { // Namespace has one more character than "AUTO". - final char c = code.charAt(end - 1); - isRecognized = (c >= '1' && c <= '2'); - if (c == '1') { - isLegacy = LEGACY_MASK; - } - } - } - } - if (!isRecognized) { - throw new NoSuchAuthorityCodeException(Resources.format(Resources.Keys.UnknownAuthority_1, - CharSequences.trimWhitespaces(code, 0, s)), Constants.OGC, code); - } - } - } - s = CharSequences.skipLeadingWhitespaces(code, s+1, code.length()); - /* - * Above code removed the "CRS" part when it is used as a namespace, as in "CRS:84". - * The code below removes the "CRS" prefix when it is concatenated within the code, - * as in "CRS84". Together, those two checks handle redundant codes like "CRS:CRS84" - * (malformed code, but seen in practice). - */ - if (code.regionMatches(true, s, Constants.CRS, 0, Constants.CRS.length())) { - s = CharSequences.skipLeadingWhitespaces(code, s + Constants.CRS.length(), code.length()); - } - if (s >= code.length()) { - throw new NoSuchAuthorityCodeException(Errors.format(Errors.Keys.EmptyArgument_1, "code"), Constants.OGC, code); - } - return s | isLegacy; + private static String format(final int code) { + return ((code >= FIRST_PROJECTION_CODE) ? AUTO2 : Constants.CRS) + Constants.DEFAULT_SEPARATOR + code; + } + + /** + * Formats the given code with an {@code "OGC:"} prefix. + * This is used for non-numerical codes such as "OGC:JulianDate". + */ + private static String format(final String code) { + return Constants.OGC + Constants.DEFAULT_SEPARATOR + code; } /** * Provides a complete set of the known codes provided by this factory. - * The returned set contains a namespace followed by numeric identifiers - * like {@code "CRS:84"}, {@code "CRS:27"}, {@code "AUTO2:42001"}, <i>etc</i>. + * The returned set contains a namespace followed by identifiers like + * {@code "CRS:84"}, {@code "CRS:27"}, {@code "AUTO2:42001"}, <i>etc</i>. * * @param type the spatial reference objects type. * @return the set of authority codes for spatial reference objects of the given type. @@ -368,18 +359,14 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements for (int code = FIRST_PROJECTION_CODE; code < FIRST_PROJECTION_CODE + PROJECTION_NAMES.length; code++) { add(code, ProjectedCRS.class); } + for (final String name : TEMPORAL_NAMES) { + codes.put(format(name), TemporalCRS.class); + } } } return new FilteredCodes(codes, type).keySet(); } - /** - * Formats the given code with a {@code "CRS:"} or {@code "AUTO2:"} prefix. - */ - private static String format(final int code) { - return ((code >= FIRST_PROJECTION_CODE) ? AUTO2 : Constants.CRS) + Constants.DEFAULT_SEPARATOR + code; - } - /** * Adds an element in the {@link #codes} map, witch check against duplicated values. */ @@ -434,27 +421,37 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements */ @Override public InternationalString getDescriptionText(final String code) throws FactoryException { - final int s = skipNamespace(code) & ~LEGACY_MASK; - final String localCode = code.substring(s, CharSequences.skipTrailingWhitespaces(code, s, code.length())); - if (localCode.indexOf(SEPARATOR) < 0) { + final CommonAuthorityCode parsed = new CommonAuthorityCode(code); + if (parsed.isNumeric && parsed.parameters().length == 0) { /* * For codes in the "AUTO(2)" namespace without parameters, we can not rely on the default implementation - * since it would fail to create the ProjectedCRS instance. Instead we return a generic description. + * because it would fail to create the ProjectedCRS instance. Instead we return a generic description. * Note that we do not execute this block if parametes were specified. If there is parameters, * then we instead rely on the default implementation for a more accurate description text. + * Note also that we do not restrict to "AUTOx" namespaces because erroneous namespaces exist + * in practice and the numerical codes are non-ambiguous (at least in current version). */ final int codeValue; try { - codeValue = Integer.parseInt(localCode); + codeValue = Integer.parseInt(parsed.localCode); } catch (NumberFormatException exception) { - throw noSuchAuthorityCode(localCode, code, exception); + throw noSuchAuthorityCode(parsed.localCode, code, exception); } final int i = codeValue - FIRST_PROJECTION_CODE; if (i >= 0 && i < PROJECTION_NAMES.length) { return new SimpleInternationalString(PROJECTION_NAMES[i]); } } - return new SimpleInternationalString(createCoordinateReferenceSystem(localCode).getName().getCode()); + /* + * Fallback on fetching the full CRS, then request its name. + * It will include the parsing of parameters if any. + */ + final Identifier name = createCoordinateReferenceSystem(code, parsed).getName(); + if (name instanceof GenericName) { + return ((GenericName) name).tip().toInternationalString(); + } else { + return new SimpleInternationalString(name.getCode()); + } } /** @@ -488,28 +485,38 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements @Override public CoordinateReferenceSystem createCoordinateReferenceSystem(final String code) throws FactoryException { ArgumentChecks.ensureNonNull("code", code); - final String localCode; - final boolean isLegacy; - String complement = null; - { // Block for keeping 'start' and 'end' variables locale. - int start = skipNamespace(code); - isLegacy = (start & LEGACY_MASK) != 0; - start &= ~LEGACY_MASK; - final int startOfParameters = code.indexOf(SEPARATOR, start); - int end = CharSequences.skipTrailingWhitespaces(code, start, code.length()); - if (startOfParameters >= 0) { - complement = code.substring(startOfParameters + 1); - end = CharSequences.skipTrailingWhitespaces(code, start, startOfParameters); - } - localCode = code.substring(start, end); + return createCoordinateReferenceSystem(code, new CommonAuthorityCode(code)); + } + + /** + * Implementation of {@link #createCoordinateReferenceSystem(String)} after the user-supplied code has been parsed. + * + * @param code the user-supplied code of desired CRS. + * @param parsed result of parsing the supplied {@code code}. + * @throws FactoryException if the object creation failed. + */ + private CoordinateReferenceSystem createCoordinateReferenceSystem(final String code, final CommonAuthorityCode parsed) + throws FactoryException + { + /* + * First, handled the case of non-numerical parameterless codes ("OGC:JulianDate", etc.) + * We accept also SIS-specific codes (e.g. "OGC:ModifiedJulianDate", "OGC:JavaTime") if + * the namespace is the more neutral "CRS" instead of "OGC". The SIS-specific codes are + * never listed in `getAuthorityCodes(…)`. + */ + final String localCode = parsed.localCode; + final double[] parameters = parsed.parameters(); + if (!parsed.isNumeric && !parsed.isAuto(false) && parameters.length == 0) try { + return CommonCRS.Temporal.forIdentifier(localCode, parsed.isOGC).crs(); + } catch (IllegalArgumentException e) { + throw noSuchAuthorityCode(localCode, code, e); } - int codeValue = 0; - double[] parameters = ArraysExt.EMPTY_DOUBLE; + /* + * In current version, all non-temporal CRS have a numerical code. + */ + final int codeValue; try { codeValue = Integer.parseInt(localCode); - if (complement != null) { - parameters = CharSequences.parseDoubles(complement, SEPARATOR); - } } catch (NumberFormatException exception) { throw noSuchAuthorityCode(localCode, code, exception); } @@ -528,6 +535,7 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements errorKey = Errors.Keys.TooManyArguments_2; } if (errorKey == 0) { + final boolean isLegacy = parsed.isAuto(true); return createAuto(code, codeValue, isLegacy, (count > 2) ? parameters[0] : isLegacy ? Constants.EPSG_METRE : 1, parameters[count - 2], @@ -536,8 +544,7 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements throw new NoSuchAuthorityCodeException(Errors.format(errorKey, expected, count), AUTO2, localCode, code); } if (count != 0) { - throw new NoSuchAuthorityCodeException(Errors.format(Errors.Keys.UnexpectedCharactersAfter_2, - localCode, complement), Constants.CRS, localCode, code); + throw new NoSuchAuthorityCodeException(parsed.unexpectedParameters(), Constants.CRS, localCode, code); } final CommonCRS crs; switch (codeValue) { @@ -562,7 +569,6 @@ public class CommonAuthorityFactory extends GeodeticAuthorityFactory implements * @param latitude a latitude in the desired projection zone. * @return the projected CRS for the given projection and parameters. */ - @SuppressWarnings("null") private ProjectedCRS createAuto(final String code, final int projection, final boolean isLegacy, final double factor, final double longitude, final double latitude) throws FactoryException { diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java index fd11ee22c4..417804cb6d 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java @@ -1246,9 +1246,16 @@ public abstract class GeodeticAuthorityFactory extends AbstractFactory implement /** * Returns {@code true} if the given portion of the code is equal, ignoring case, to the given namespace. + * + * @param namespace expected namespace (e.g. "OGC" or "EPSG"). + * @param code the code where to check namespace. + * @param start index of first character of the namespace in the code. + * @param end index after last character of the namespace in the code. + * @return whether the specified region of the code is equal to the namespace. */ static boolean regionMatches(final String namespace, final String code, final int start, final int end) { - return (namespace.length() == end - start) && code.regionMatches(true, start, namespace, 0, namespace.length()); + final int length = namespace.length(); + return (length == end - start) && code.regionMatches(true, start, namespace, 0, length); } /** diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java index 77f579a4d2..a7376779f1 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/MultiAuthoritiesFactory.java @@ -847,12 +847,12 @@ public class MultiAuthoritiesFactory extends GeodeticAuthorityFactory implements * depends on whether the authority is "AUTO" or "AUTO2". This works for now, but we may need a more * rigorous approach in a future SIS version. */ - if (parameters != null || code.indexOf(CommonAuthorityFactory.SEPARATOR) >= 0) { + if (parameters != null || code.indexOf(CommonAuthorityCode.SEPARATOR) >= 0) { final StringBuilder buffer = new StringBuilder(authority.length() + code.length() + 1) .append(authority).append(Constants.DEFAULT_SEPARATOR).append(code); if (parameters != null) { for (final String p : parameters) { - buffer.append(CommonAuthorityFactory.SEPARATOR).append(p); + buffer.append(CommonAuthorityCode.SEPARATOR).append(p); } } code = buffer.toString(); diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/package-info.java index 0c2c551c70..618fb78fb1 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/package-info.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/package-info.java @@ -56,7 +56,7 @@ * </table> * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.2 + * @version 1.3 * @since 0.6 * @module */ diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java index b48ee806e1..80421469b2 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CRSTest.java @@ -19,6 +19,7 @@ package org.apache.sis.referencing; import java.util.Map; import java.util.HashMap; import java.util.Arrays; +import java.util.List; import org.apache.sis.internal.system.Loggers; import org.opengis.util.FactoryException; import org.opengis.referencing.NoSuchAuthorityCodeException; @@ -55,7 +56,7 @@ import static org.apache.sis.test.Assert.*; * * @author Martin Desruisseaux (Geomatys) * @author Alexis Manin (Geomatys) - * @version 1.1 + * @version 1.3 * @since 0.4 * @module */ @@ -142,6 +143,19 @@ public final strictfp class CRSTest extends TestCase { verifyForCode(CommonCRS.NAD83.normalizedGeographic(), "http://www.opengis.net/gml/srs/crs.xml#83"); } + /** + * Tests {@link CRS#forCode(String)} with temporal CRS codes. + * + * @throws FactoryException if a CRS can not be constructed. + */ + @Test + public void testForTemporalCode() throws FactoryException { + verifyForCode(CommonCRS.Temporal.JULIAN.crs(), "OGC:JulianDate"); + verifyForCode(CommonCRS.Temporal.UNIX.crs(), "OGC:UnixTime"); + verifyForCode(CommonCRS.Temporal.TRUNCATED_JULIAN.crs(), + "http://www.opengis.net/gml/srs/crs.xml#TruncatedJulianDate"); + } + /** * Test {@link CRS#forCode(String)} with values that should be invalid. * @@ -157,6 +171,36 @@ public final strictfp class CRSTest extends TestCase { } } + /** + * Asserts that the result of {@link CRS#forCode(String)} for a compound CRS are the given components. + */ + private static void verifyForCompoundCode(final String code, final SingleCRS... expected) throws FactoryException { + final List<SingleCRS> components = CRS.getSingleComponents(CRS.forCode(code)); + final int count = Math.min(components.size(), expected.length); + for (int i=0; i<count; i++) { + assertTrue(String.valueOf(i), Utilities.deepEquals(expected[i], components.get(i), ComparisonMode.DEBUG)); + } + assertEquals(expected.length, components.size()); + } + + /** + * Tests {@link CRS#forCode(String)} with compound CRS codes. + * + * @throws FactoryException if a CRS can not be constructed. + */ + @Test + public void testForCompoundCode() throws FactoryException { + verifyForCompoundCode("urn:ogc:def:crs,crs:EPSG::4326,crs:EPSG::5714", + CommonCRS.WGS84.geographic(), CommonCRS.Vertical.MEAN_SEA_LEVEL.crs()); + verifyForCompoundCode("urn:ogc:def:crs,crs:EPSG::4326,crs:EPSG::5714,crs:OGC::TruncatedJulianDate", + CommonCRS.WGS84.geographic(), CommonCRS.Vertical.MEAN_SEA_LEVEL.crs(), CommonCRS.Temporal.TRUNCATED_JULIAN.crs()); + + verifyForCompoundCode("http://www.opengis.net/def/crs-compound?" + + "1=http://www.opengis.net/def/crs/epsg/0/4326&" + + "2=http://www.opengis.net/def/crs/epsg/0/5715", + CommonCRS.WGS84.geographic(), CommonCRS.Vertical.DEPTH.crs()); + } + /** * Tests simple WKT parsing. It is not the purpose of this class to test extensively the WKT parser; * those tests are rather done by {@link org.apache.sis.io.wkt.GeodeticObjectParserTest}. diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java index 547d069c3f..c8d6c17695 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/CommonCRSTest.java @@ -20,6 +20,7 @@ import java.util.Date; import java.util.Map; import java.util.HashMap; import java.time.Instant; +import org.opengis.util.FactoryException; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.crs.TemporalCRS; @@ -33,6 +34,7 @@ import org.opengis.referencing.cs.EllipsoidalCS; import org.opengis.referencing.datum.TemporalDatum; import org.opengis.referencing.datum.VerticalDatum; import org.opengis.referencing.datum.VerticalDatumType; +import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.internal.metadata.AxisNames; import org.apache.sis.internal.referencing.VerticalDatumTypes; import org.apache.sis.internal.util.Constants; @@ -52,7 +54,7 @@ import static org.apache.sis.test.TestUtilities.*; * Tests the {@link CommonCRS} class. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.1 + * @version 1.3 * @since 0.4 * @module */ @@ -301,6 +303,39 @@ public final strictfp class CommonCRSTest extends TestCase { assertTrue(name, name.contains(word)); } + /** + * Tests {@link CommonCRS.Temporal#forIdentifier(String, boolean)}. + */ + @Test + public void testTemporalForIdentifier() { + assertSame(CommonCRS.Temporal.TRUNCATED_JULIAN, CommonCRS.Temporal.forIdentifier("TruncatedJulianDate", false)); + assertSame(CommonCRS.Temporal.TRUNCATED_JULIAN, CommonCRS.Temporal.forIdentifier("TruncatedJulianDate", true)); + assertSame(CommonCRS.Temporal.MODIFIED_JULIAN, CommonCRS.Temporal.forIdentifier("ModifiedJulianDate", false)); + try { + CommonCRS.Temporal.forIdentifier("ModifiedJulianDate", true); + fail("Unexpected because not in OGC namespace."); + } catch (IllegalArgumentException e) { + final String message = e.getMessage(); + assertTrue(message.contains("ModifiedJulianDate")); + assertTrue(message.contains("OGC")); + } + assertEquals("OGC:TruncatedJulianDate", getSingleton(CommonCRS.Temporal.TRUNCATED_JULIAN.crs().getIdentifiers()).toString()); + assertEquals("SIS:ModifiedJulianDate", getSingleton(CommonCRS.Temporal. MODIFIED_JULIAN.crs().getIdentifiers()).toString()); + } + + /** + * Tests the URN lookup on temporal CRS. + * + * @throws FactoryException if a call to {@link IdentifiedObjects#lookupURN lookupURN(…)} failed. + */ + @Test + public void testLookupURN() throws FactoryException { + final TemporalCRS crs = CommonCRS.Temporal.TRUNCATED_JULIAN.crs(); + assertNull(IdentifiedObjects.lookupEPSG(crs)); // Not an EPSG code. + assertNull(IdentifiedObjects.lookupURN(crs, Citations.SIS)); // Not in SIS namespace. + assertEquals("urn:ogc:def:crs:OGC::TruncatedJulianDate", IdentifiedObjects.lookupURN(crs, Citations.OGC)); + } + /** * Tests {@link CommonCRS#universal(double, double)} with Universal Transverse Mercator (UTM) projections. * diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/CommonAuthorityFactoryTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/CommonAuthorityFactoryTest.java index 719fa84b25..310db89267 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/CommonAuthorityFactoryTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/CommonAuthorityFactoryTest.java @@ -18,6 +18,7 @@ package org.apache.sis.referencing.factory; import java.util.Arrays; import java.util.Set; +import java.util.HashSet; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import org.opengis.util.FactoryException; @@ -29,11 +30,13 @@ import org.opengis.referencing.crs.EngineeringCRS; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.crs.VerticalCRS; +import org.opengis.referencing.crs.TemporalCRS; import org.opengis.referencing.cs.AxisDirection; import org.opengis.referencing.datum.Datum; import org.apache.sis.internal.util.Constants; import org.apache.sis.internal.referencing.provider.TransverseMercator; import org.apache.sis.referencing.operation.transform.LinearTransform; +import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.metadata.iso.citation.Citations; import org.apache.sis.referencing.CommonCRS; import org.apache.sis.io.wkt.Convention; @@ -47,13 +50,14 @@ import org.junit.Ignore; import org.junit.Test; import static org.apache.sis.test.ReferencingAssert.*; +import static org.apache.sis.test.TestUtilities.getSingleton; /** * Tests {@link CommonAuthorityFactory}. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 0.7 + * @version 1.3 * @since 0.7 * @module */ @@ -81,7 +85,8 @@ public final strictfp class CommonAuthorityFactoryTest extends TestCase { assertTrue("getAuthorityCodes(Datum.class)", factory.getAuthorityCodes(Datum.class).isEmpty()); assertSetEquals(Arrays.asList("CRS:1", "CRS:27", "CRS:83", "CRS:84", "CRS:88", - "AUTO2:42001", "AUTO2:42002", "AUTO2:42003", "AUTO2:42004", "AUTO2:42005"), + "AUTO2:42001", "AUTO2:42002", "AUTO2:42003", "AUTO2:42004", "AUTO2:42005", + "OGC:JulianDate", "OGC:TruncatedJulianDate", "OGC:UnixTime"), factory.getAuthorityCodes(CoordinateReferenceSystem.class)); assertSetEquals(Arrays.asList("AUTO2:42001", "AUTO2:42002", "AUTO2:42003", "AUTO2:42004", "AUTO2:42005"), factory.getAuthorityCodes(ProjectedCRS.class)); @@ -89,6 +94,8 @@ public final strictfp class CommonAuthorityFactoryTest extends TestCase { factory.getAuthorityCodes(GeographicCRS.class)); assertSetEquals(Arrays.asList("CRS:88"), factory.getAuthorityCodes(VerticalCRS.class)); + assertSetEquals(Arrays.asList("OGC:JulianDate", "OGC:TruncatedJulianDate", "OGC:UnixTime"), + factory.getAuthorityCodes(TemporalCRS.class)); assertSetEquals(Arrays.asList("CRS:1"), factory.getAuthorityCodes(EngineeringCRS.class)); @@ -103,6 +110,27 @@ public final strictfp class CommonAuthorityFactoryTest extends TestCase { assertTrue ("OGC:CRS084", codes.contains("OGC:CRS084")); } + /** + * Verifies that the names of temporal CRS (including namespace) + * are identical to the one defined by {@link CommonCRS.Temporal}. + * + * @throws FactoryException if an error occurred while fetching the set of codes. + */ + @Test + public void verifyTemporalCodes() throws FactoryException { + final Set<String> expected = new HashSet<>(); + for (final CommonCRS.Temporal e : CommonCRS.Temporal.values()) { + assertTrue(expected.add(IdentifiedObjects.toString(getSingleton(e.crs().getIdentifiers())))); + } + assertFalse(expected.isEmpty()); + for (final String code : factory.getAuthorityCodes(TemporalCRS.class)) { + assertTrue(code, expected.remove(code)); + } + for (final String remaining : expected) { + assertFalse(remaining, remaining.startsWith(Constants.OGC)); + } + } + /** * Tests {@link CommonAuthorityFactory#getDescriptionText(String)}. * diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java index 615fdf8572..86d679a1c0 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java +++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/DefinitionURI.java @@ -716,14 +716,18 @@ public final class DefinitionURI { * and "http://www.opengis.net/def/crs-compound?1=(…)/crs/EPSG/9.1/27700&2=(…)/crs/EPSG/9.1/5701". */ if (components != null) { + boolean first = true; for (int i=0; i<components.length;) { final DefinitionURI c = components[i++]; - if (isHTTP) { - buffer.append(i == 1 ? COMPONENT_SEPARATOR_1 - : COMPONENT_SEPARATOR_2) - .append(i).append(KEY_VALUE_SEPARATOR); + if (c != null) { + if (isHTTP) { + buffer.append(first ? COMPONENT_SEPARATOR_1 + : COMPONENT_SEPARATOR_2) + .append(i).append(KEY_VALUE_SEPARATOR); + } + c.appendStringTo(buffer, COMPONENT_SEPARATOR); + first = false; } - c.appendStringTo(buffer, COMPONENT_SEPARATOR); } } } diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java index de7a0030d2..195d419288 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java @@ -328,6 +328,11 @@ public final class Errors extends IndexedResourceBundle { */ public static final short ForbiddenProperty_1 = 41; + /** + * Identifier “{1}” is not in “{0}” namespace. + */ + public static final short IdentifierNotInNamespace_2 = 199; + /** * Argument ‘{0}’ can not be an instance of ‘{1}’. */ diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties index ec0853a874..9919d73574 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties @@ -77,6 +77,7 @@ FactoryNotFound_1 = No factory of kind \u2018{0}\u2019 found. FileNotFound_1 = File \u201c{0}\u201d has not been found. ForbiddenAttribute_2 = Attribute \u201c{0}\u201d is not allowed for an object of type \u2018{1}\u2019. ForbiddenProperty_1 = Property \u201c{0}\u201d is not allowed. +IdentifierNotInNamespace_2 = Identifier \u201c{1}\u201d is not in \u201c{0}\u201d namespace. IllegalArgumentClass_2 = Argument \u2018{0}\u2019 can not be an instance of \u2018{1}\u2019. IllegalArgumentClass_3 = Argument \u2018{0}\u2019 can not be an instance of \u2018{2}\u2019. Expected an instance of \u2018{1}\u2019 or derived type. IllegalArgumentValue_2 = Argument \u2018{0}\u2019 can not take the \u201c{1}\u201d value. diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties index 305e55dfd6..2f74f10f84 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties @@ -74,6 +74,7 @@ FactoryNotFound_1 = Aucune fabrique de type \u2018{0}\u2019 n\u2 FileNotFound_1 = Le fichier \u00ab\u202f{0}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9. ForbiddenAttribute_2 = L\u2019attribut \u00ab\u202f{0}\u202f\u00bb n\u2019est pas autoris\u00e9 pour un objet de type \u2018{1}\u2019. ForbiddenProperty_1 = La propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb n\u2019est pas autoris\u00e9e. +IdentifierNotInNamespace_2 = L\u2019identifiant \u201c{1}\u201d n\u2019est pas dans l\u2019espace de noms \u201c{0}\u201d. IllegalArgumentClass_2 = L\u2019argument \u2018{0}\u2019 ne peut pas \u00eatre de type \u2018{1}\u2019. IllegalArgumentClass_3 = L\u2019argument \u2018{0}\u2019 ne peut pas \u00eatre de type \u2018{2}\u2019. Une instance de \u2018{1}\u2019 ou d\u2019un type d\u00e9riv\u00e9 \u00e9tait attendue. IllegalArgumentValue_2 = L\u2019argument \u2018{0}\u2019 n\u2019accepte pas la valeur \u00ab\u202f{1}\u202f\u00bb.