This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/sis.git
commit 90b42ea528bcd1c7f2e2d1059c3a6de8da5b7a3e Merge: f4d97c35d8 280e84ecf9 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sun Sep 28 00:11:08 2025 +0200 Merge branch 'geoapi-3.1' .../sis/parameter/DefaultParameterDescriptor.java | 14 +- ...ensorValues.java => MatrixParameterValues.java} | 54 +- .../org/apache/sis/parameter/MatrixParameters.java | 980 +++++++++++++++++++-- .../sis/parameter/MatrixParametersAlphaNum.java | 125 --- .../org/apache/sis/parameter/TensorParameters.java | 796 +---------------- .../main/org/apache/sis/parameter/Verifier.java | 86 +- .../referencing/factory/sql/AuthorityCodes.java | 13 +- .../referencing/factory/sql/EPSGDataAccess.java | 646 ++++++++++---- .../sis/referencing/factory/sql/EPSGFactory.java | 12 + .../referencing/internal/EPSGParameterDomain.java | 53 -- .../internal/ParameterizedTransformBuilder.java | 7 +- .../referencing/internal/SignReversalComment.java | 10 + .../operation/MathTransformContext.java | 4 +- .../sis/referencing/operation/matrix/Matrices.java | 2 +- .../operation/provider/AbstractProvider.java | 21 + .../sis/referencing/operation/provider/Affine.java | 155 ++-- .../referencing/operation/provider/EPSGName.java | 31 +- .../operation/provider/FormulaCategory.java | 51 ++ .../sis/parameter/DefaultParameterValueTest.java | 35 +- ...uesTest.java => MatrixParameterValuesTest.java} | 188 ++-- .../parameter/MatrixParametersAlphaNumTest.java | 66 +- .../sis/parameter/MatrixParametersEPSGTest.java | 101 +++ .../apache/sis/parameter/MatrixParametersTest.java | 253 +++++- .../apache/sis/parameter/TensorParametersTest.java | 296 ------- .../sis/referencing/crs/DefaultDerivedCRSTest.java | 2 + .../referencing/factory/sql/EPSGFactoryTest.java | 43 + .../operation/projection/EquirectangularTest.java | 8 +- .../referencing/operation/provider/AffineTest.java | 25 +- .../operation/provider/LongitudeRotationTest.java | 6 +- .../transform/EllipsoidToRadiusTransformTest.java | 6 +- .../InterpolatedGeocentricTransformTest.java | 16 +- .../transform/SpecializableTransformTest.java | 6 + .../report/CoordinateOperationMethods.java | 198 ++--- .../report/CoordinateReferenceSystems.java | 705 +++++++++++++++ .../sis/referencing/report/HTMLGenerator.java | 24 +- .../resources/embedded/EmbeddedResourcesTest.java | 36 +- .../factory/sql/epsg/DataScriptUpdater.java | 10 + 37 files changed, 3104 insertions(+), 1980 deletions(-) diff --cc endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/MatrixParameters.java index 88d1d54663,d7ad15b234..579f5f5e2e --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/MatrixParameters.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/MatrixParameters.java @@@ -137,29 -686,251 +686,252 @@@ public class MatrixParameters<E> implem } /** - * Creates a new parameter descriptor for a matrix element at the given indices. - * This method creates: + * Returns the default value for the parameter descriptor at the given indices. + * The default implementation returns 1 if all indices are equals, or 0 otherwise. * - * <ul> - * <li>The OGC name (e.g. {@code "elt_1_2"}) as primary name.</li> - * <li>The alpha-numeric name (e.g. {@code "B2"}) as an alias.</li> - * </ul> + * @param indices the indices of the matrix element for which to get the default value, + * in (<var>row</var>, <var>column</var>, …) order. + * @return the default value for the matrix element at the given indices, or {@code null} if none. + * + * @see DefaultParameterDescriptor#getDefaultValue() + */ + protected E getDefaultValue(final int[] indices) { + for (int i=1; i<indices.length; i++) { + if (indices[i] != indices[i-1]) { + return zero; + } + } + return one; + } + + /** + * Returns the descriptor in this group for the specified name. * - * This method does <strong>not</strong> assign the alpha-numeric names to the EPSG authority in order to avoid - * confusion when formatting the parameters as Well Known Text (WKT). However, {@link MatrixParametersAlphaNum} - * subclass will assign some names to the EPSG authority, as well as their identifier (e.g. EPSG:8641). + * @param caller the {@link MatrixParameterValues} instance invoking this method, used only in case of errors. + * @param name the case insensitive name of the parameter to search for. + * @param actualSize the current values of parameters that define the matrix size. + * @return the parameter for the given name. + * @throws ParameterNotFoundException if there is no parameter for the given name. + */ + final ParameterDescriptor<?> descriptor(final ParameterDescriptorGroup caller, + final String name, final int[] actualSize) throws ParameterNotFoundException + { + IllegalArgumentException cause = null; + int[] indices = null; + try { + indices = nameToIndices(name); + } catch (IllegalArgumentException exception) { + cause = exception; + } + if (indices != null && isInBounds(indices, actualSize)) { + return getElementDescriptor(indices); + } + /* + * The given name is not a matrix element name. Verify if the requested parameters + * is one of the parameters that specify the matrix size ("num_row" or "num_col"). + */ + for (final ParameterDescriptor<Integer> param : dimensions) { + if (IdentifiedObjects.isHeuristicMatchForName(param, name)) { + return param; + } + } + throw (ParameterNotFoundException) new ParameterNotFoundException(Resources.format( + Resources.Keys.ParameterNotFound_2, caller.getName(), name), name).initCause(cause); + } + + /** + * Returns {@code true} if the given indices are not out-of-bounds. + * Arguments are in (<var>row</var>, <var>column</var>, …) order. + * + * @param indices the indices parsed from a parameter name. + * @param actualSize the current values of parameters that define the matrix size. + */ + static boolean isInBounds(final int[] indices, final int[] actualSize) { + for (int i=0; i<indices.length; i++) { + final int index = indices[i]; + if (index < 0 || index >= actualSize[i]) { + return false; + } + } + return true; + } + + /** + * Returns all parameters in this group for a matrix of the specified size. + * The returned array contains all descriptors returned by {@link #getDimensionDescriptor(int)} + * and {@link #getElementDescriptor(int...)} for all values that exist for the given size. + * + * @param actualSize the matrix size in ({@code num_row}, {@code num_col}, …) order. + * @return the matrix parameters, including all elements. + * + * @see #getDimensionDescriptor(int) + * @see #getElementDescriptor(int...) + */ + public ParameterDescriptor<?>[] getAllDescriptors(final int... actualSize) { + verifyOrder(actualSize); + int numElements = 1; + for (int s : actualSize) { + ArgumentChecks.ensurePositive("actualSize", s); + numElements *= s; + } + final int order = order(); + final var descriptors = new ParameterDescriptor<?>[order + numElements]; + System.arraycopy(dimensions, 0, descriptors, 0, order); + final int[] indices = new int[order]; + /* + * Iterates on all possible index values. Index on the right side (usually the column index) + * will vary faster, and index on the left side (usually the row index) will vary slowest. + */ + for (int i=0; i<numElements; i++) { + descriptors[order + i] = getElementDescriptor(indices); + for (int j=indices.length; --j >= 0;) { + if (++indices[j] < actualSize[j]) { + break; + } + indices[j] = 0; // We have done a full turn at that dimension. Will increment next dimension. + } + } + return descriptors; + } + + /** + * Creates a new instance of parameter group with default values of 1 on the diagonal, and 0 everywhere else. + * The returned parameter group is extensible, i.e. the number of elements will depend upon the value associated + * to the parameters that define the matrix size. + * + * <p>The properties map is given unchanged to the + * {@linkplain org.apache.sis.referencing.AbstractIdentifiedObject#AbstractIdentifiedObject(Map) + * identified object constructor}. The following table is a reminder of main (not all) properties:</p> + * + * <table class="sis"> + * <caption>Recognized properties (non exhaustive list)</caption> + * <tr> + * <th>Property name</th> + * <th>Value type</th> + * <th>Returned by</th> + * </tr><tr> + * <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td> + * <td>{@link org.opengis.metadata.Identifier} or {@link String}</td> + * <td>{@link DefaultParameterDescriptorGroup#getName()}</td> + * </tr><tr> + * <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td> + * <td>{@link org.opengis.util.GenericName} or {@link CharSequence} (optionally as array)</td> + * <td>{@link DefaultParameterDescriptorGroup#getAlias()}</td> + * </tr><tr> + * <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td> + * <td>{@link org.opengis.metadata.Identifier} (optionally as array)</td> + * <td>{@link DefaultParameterDescriptorGroup#getIdentifiers()}</td> + * </tr><tr> + * <td>{@value org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td> + * <td>{@link org.opengis.util.InternationalString} or {@link String}</td> + * <td>{@link DefaultParameterDescriptorGroup#getRemarks()}</td> + * </tr> + * </table> + * + * @param properties the properties to be given to the identified object. + * @return a new parameter group initialized to the default values. + */ + public ParameterValueGroup createValueGroup(final Map<String,?> properties) { + return new MatrixParameterValues<>(properties, this); + } + + /** + * Creates a new instance of parameter group initialized to the given matrix. + * This operation is allowed only for two-dimensional arrays. + * + * @param properties the properties to be given to the identified object. + * @param matrix the matrix to copy in the new parameter group. + * @return a new parameter group initialized to the given matrix. + * @throws IllegalStateException if {@link #order()} does not return 2. + * + * @see #toMatrix(ParameterValueGroup) + */ + public ParameterValueGroup createValueGroup(final Map<String,?> properties, final Matrix matrix) { + if (order() != 2) { + throw new IllegalStateException(); + } + ArgumentChecks.ensureNonNull("matrix", matrix); + final var values = new MatrixParameterValues<E>(properties, this); + values.setMatrix(matrix); + return values; + } + + /** + * Constructs a matrix from a group of parameters. + * This operation is allowed only for two-dimensional arrays. + * + * @param parameters the group of parameters. + * @return a matrix constructed from the specified group of parameters. + * @throws IllegalStateException if {@link #order()} does not return 2. + * @throws InvalidParameterNameException if a parameter name was not recognized. + * + * @see #createValueGroup(Map, Matrix) + */ + public Matrix toMatrix(final ParameterValueGroup parameters) { + if (order() != 2) { + throw new IllegalStateException(); + } + if (Objects.requireNonNull(parameters) instanceof MatrixParameterValues) { + return ((MatrixParameterValues) parameters).toMatrix(); // More efficient implementation + } + // Fallback on the general case (others implementations) + final ParameterValue<?> numRow = parameters.parameter(dimensions[0].getName().getCode()); + final ParameterValue<?> numCol = parameters.parameter(dimensions[1].getName().getCode()); + final Matrix matrix = Matrices.createDiagonal(numRow.intValue(), numCol.intValue()); + final List<GeneralParameterValue> values = parameters.values(); + if (values != null) { + for (final GeneralParameterValue param : values) { + if (param == numRow || param == numCol) { + continue; + } + final String name = param.getDescriptor().getName().getCode(); + IllegalArgumentException cause = null; + int[] indices = null; + try { + indices = nameToIndices(name); + } catch (IllegalArgumentException e) { + cause = e; + } + if (indices == null) { - throw new InvalidParameterNameException(Errors.format( - Errors.Keys.UnexpectedParameter_1, name), cause, name); ++ throw (InvalidParameterNameException) new InvalidParameterNameException(Errors.format( ++ Errors.Keys.UnexpectedParameter_1, name), name).initCause(cause); ++ + } + matrix.setElement(indices[0], indices[1], ((ParameterValue<?>) param).doubleValue()); + } + } + return matrix; + } + + /** + * Returns a hash code value for this object. + * + * @return a hash code value. */ @Override - protected ParameterDescriptor<Double> createElementDescriptor(final int[] indices) throws IllegalArgumentException { - final Map<String,Object> properties = new HashMap<>(4); - properties.put(ParameterDescriptor.NAME_KEY, - new NamedIdentifier(Citations.OGC, Constants.OGC, indicesToName(indices), null, null)); - final String c = indicesToAlias(indices); - if (c != null) { - properties.put(ParameterDescriptor.ALIAS_KEY, - new NamedIdentifier(Citations.SIS, Constants.SIS, c, null, null)); + public int hashCode() { + return Objects.hash(elementType, prefix, separator) ^ Arrays.hashCode(dimensions); + } + + /** + * Compares this object with the given object for equality. + * + * @param other the other object to compare with this object. + * @return {@code true} if both object are equal. + */ + @Override + public boolean equals(final Object other) { + if (other == this) { + return true; } - return new DefaultParameterDescriptor<>(properties, 0, 1, Double.class, null, null, getDefaultValue(indices)); + if (other.getClass() == getClass()) { + final var that = (MatrixParameters<?>) other; + return elementType.equals(that.elementType) && + prefix .equals(that.prefix) && + separator .equals(that.separator) && + Arrays.equals(dimensions, that.dimensions); + } + return false; } /** diff --cc endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java index fd866faaa6,9e58c81588..125ec8039e --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java @@@ -70,8 -74,9 +74,9 @@@ import static org.apache.sis.referencin import static org.apache.sis.referencing.Assertions.assertAliasTipEquals; import static org.apache.sis.test.TestCase.TAG_SLOW; -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import static org.opengis.test.Assertions.assertMatrixEquals; -import static org.opengis.test.Assertions.assertAxisDirectionsEqual; +// Specific to the main branch: ++import static org.apache.sis.test.GeoapiAssert.assertMatrixEquals; +import static org.apache.sis.test.GeoapiAssert.assertAxisDirectionsEqual; /** diff --cc endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/report/CoordinateOperationMethods.java index 34a8d76720,d8d1f48ec4..0d3a9fcba9 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/report/CoordinateOperationMethods.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/report/CoordinateOperationMethods.java @@@ -234,11 -216,9 +216,9 @@@ public class CoordinateOperationMethod */ private void writeIdentification(final OperationMethod method) throws IOException { final int table = openTag("table class=\"info\""); - /* - * ──────────────── EPSG IDENTIFIERS ──────────────────────────────────── - */ - final StringBuilder buffer = new StringBuilder(); + // ──────────────── EPSG IDENTIFIERS ──────────────────────────────────── + final var buffer = new StringBuilder(); - for (final Identifier id : method.getIdentifiers()) { + for (final ReferenceIdentifier id : method.getIdentifiers()) { if (Constants.EPSG.equalsIgnoreCase(id.getCodeSpace())) { if (buffer.length() != 0) { buffer.append(", "); @@@ -413,8 -406,8 +406,8 @@@ public static Map<String, DefaultGeographicBoundingBox> computeUnionOfAllDomainOfValidity( final CRSAuthorityFactory factory) throws FactoryException { - final Map<String, DefaultGeographicBoundingBox> domainOfValidity = new HashMap<>(); + final var domainOfValidity = new HashMap<String, DefaultGeographicBoundingBox>(); - for (final String code : factory.getAuthorityCodes(DerivedCRS.class)) { + for (final String code : factory.getAuthorityCodes(GeneralDerivedCRS.class)) { final CoordinateReferenceSystem crs; try { crs = factory.createCoordinateReferenceSystem(code); diff --cc endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/report/CoordinateReferenceSystems.java index 0000000000,aee3037235..ac66eebcdf mode 000000,100644..100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/report/CoordinateReferenceSystems.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/report/CoordinateReferenceSystems.java @@@ -1,0 -1,716 +1,705 @@@ + /* + * 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.report; + + import java.util.List; + import java.util.ArrayList; + import java.util.Locale; + import java.util.Set; + import java.util.Map; + import java.util.TreeMap; -import java.util.Optional; + import java.util.Collections; + import java.io.IOException; + import org.opengis.metadata.Identifier; + import org.opengis.util.FactoryException; + import org.opengis.util.InternationalString; + import org.opengis.util.NoSuchIdentifierException; + import org.opengis.referencing.IdentifiedObject; + import org.opengis.referencing.cs.CartesianCS; + import org.opengis.referencing.cs.SphericalCS; + import org.opengis.referencing.cs.CoordinateSystem; + import org.opengis.referencing.crs.CompoundCRS; -import org.opengis.referencing.crs.VerticalCRS; + import org.opengis.referencing.crs.GeodeticCRS; + import org.opengis.referencing.crs.GeographicCRS; + import org.opengis.referencing.crs.EngineeringCRS; + import org.opengis.referencing.crs.DerivedCRS; + import org.opengis.referencing.crs.CoordinateReferenceSystem; + import org.opengis.referencing.crs.CRSAuthorityFactory; + import org.opengis.referencing.datum.Datum; -import org.opengis.referencing.datum.VerticalDatum; -import org.opengis.referencing.datum.RealizationMethod; + import org.opengis.referencing.operation.OperationMethod; + import org.apache.sis.metadata.iso.citation.Citations; + import org.apache.sis.referencing.CRS; + import org.apache.sis.referencing.CommonCRS; + import org.apache.sis.referencing.IdentifiedObjects; + import org.apache.sis.referencing.internal.DeprecatedCode; + import org.apache.sis.util.CharSequences; + import org.apache.sis.util.ComparisonMode; + import org.apache.sis.util.Deprecable; + import org.apache.sis.util.Utilities; + import org.apache.sis.util.Version; + import org.apache.sis.util.internal.shared.Constants; + import org.apache.sis.referencing.crs.AbstractCRS; + import org.apache.sis.referencing.cs.AxesConvention; + import org.apache.sis.referencing.factory.CommonAuthorityFactory; + import org.apache.sis.referencing.factory.MultiAuthoritiesFactory; + import org.apache.sis.referencing.factory.sql.EPSGFactory; + import org.apache.sis.util.internal.shared.URLs; + import org.apache.sis.util.iso.DefaultNameSpace; + import org.apache.sis.util.logging.Logging; + ++// Specific to the main branch: ++import org.opengis.referencing.ReferenceIdentifier; ++ + + /** + * Generates a list of supported Coordinate Reference Systems in the current directory. + * This class is for manual execution after the <abbr>EPSG</abbr> database has been updated, + * or after some implementations of operation methods changed. + * + * <h2>WARNING:</h2> + * this class implements heuristic rules for nicer sorting (e.g. of CRS having numbers as Roman letters). + * Those heuristic rules were determined specifically for the EPSG dataset expanded with WMS codes. + * This class is not likely to produce good results for any other authorities, and many need to be updated + * after any upgrade of the <abbr>EPSG</abbr> dataset. + * + * @author Martin Desruisseaux (Geomatys) + */ + public final class CoordinateReferenceSystems extends HTMLGenerator { + /** + * Generates the <abbr>HTML</abbr> report. + * + * @param args ignored. + * @throws FactoryException if an error occurred while fetching the CRS. + * @throws IOException if an error occurred while writing the HTML file. + */ + @SuppressWarnings("UseOfSystemOutOrSystemErr") + public static void main(final String[] args) throws FactoryException, IOException { + Locale.setDefault(LOCALE); // We have to use this hack for now because exceptions are formatted in the current locale. + try (var writer = new CoordinateReferenceSystems()) { + writer.write(); + } + } + + /** + * Words to ignore in a datum name in order to detect if a CRS name is the acronym of the datum name. + */ + private static final Set<String> DATUM_WORDS_TO_IGNORE = Set.of( + "of", // VIVD: Virgin Islands Vertical Datum of 2009 + "de", // RRAF: Reseau de Reference des Antilles Francaises + "des", // RGAF: Reseau Geodesique des Antilles Francaises + "la", // RGR: Reseau Geodesique de la Reunion + "et", // RGSPM: Reseau Geodesique de Saint Pierre et Miquelon + "para", // SIRGAS: Sistema de Referencia Geocentrico para America del Sur 1995 + "del", // SIRGAS: Sistema de Referencia Geocentrico para America del Sur 1995 + "las", // SIRGAS: Sistema de Referencia Geocentrico para las AmericaS 2000 + "Tides"); // MLWS: Mean Low Water Spring Tides + + /** + * The keywords before which to cut the CRS names when sorting by alphabetical order. + * The intent is to preserve the "far west", "west", "central west", "central", + * "central east", "east", "far east" order. + */ + private static final String[] CUT_BEFORE = { + " far west", // "MAGNA-SIRGAS / Colombia Far West zone" + " far east", + " west", // "Bogota 1975 / Colombia West zone" + " east", // "Bogota 1975 / Colombia East Central zone" + " central", // "Korean 1985 / Central Belt" (between "East Belt" and "West Belt") + " old central", // "NAD Michigan / Michigan Old Central" + " bogota zone", // "Bogota 1975 / Colombia Bogota zone" + // Do not declare "North" and "South" as it causes confusion with "WGS 84 / North Pole" and other cases. + }; + + /** + * The keywords after which to cut the CRS names when sorting by alphabetical order. + * + * Note: alphabetical sorting of Roman numbers work for zones from I to VIII inclusive. + * If there is more zones (for example with "JGD2000 / Japan Plane Rectangular"), then + * we need to cut before those numbers in order to use sorting by EPSG codes instead. + * + * Note 2: if alphabetical sorting is okay for Roman numbers, it is actually preferable + * because it give better position of names with height like "zone II + NGF-IGN69 height". + */ + private static final String[] CUT_AFTER = { + " cs ", // "JGD2000 / Japan Plane Rectangular CS IX" + " tm", // "ETRS89 / TM35FIN(E,N)" — we want to not interleave them between "TM35" and "TM36". + " dktm", // "ETRS89 / DKTM1 + DVR90 height" + "-gk", // "ETRS89 / ETRS-GK19FIN" + " philippines zone ", // "Luzon 1911 / Philippines zone IV" + " california zone ", // "NAD27 / California zone V" + " ngo zone ", // "NGO 1948 (Oslo) / NGO zone I" + " lambert zone ", // "NTF (Paris) / Lambert zone II + NGF-IGN69 height" + "fiji 1956 / utm zone " // Two zones: 60S and 1S with 60 before 1. + }; + + /** + * The symbol to write in front of <abbr>EPSG</abbr> code of <abbr>CRS</abbr> + * having an axis order different than the (longitude, latitude) order. + */ + private static final char YX_ORDER = '\u21B7'; + + /** + * The factory which create CRS instances. + */ + private final CRSAuthorityFactory factory; + + /** + * Version of the <abbr>EPSG</abbr> geodetic dataset. + */ + private final String versionEPSG; + + /** + * Creates a new instance. + */ + private CoordinateReferenceSystems() throws FactoryException, IOException { + super("CoordinateReferenceSystems.html", + "Coordinate Reference Systems recognized by Apache SIS™", + "crs-report.css"); + + final var ogc = new CommonAuthorityFactory(); + final var epsg = new EPSGFactory(Map.of("showDeprecated", Boolean.TRUE)); + final var asSet = Set.of(epsg); + factory = new MultiAuthoritiesFactory(List.of(ogc, epsg), asSet, asSet, asSet); + final GeographicCRS anyCRS = factory.createGeographicCRS("EPSG:4326"); - versionEPSG = IdentifiedObjects.getIdentifier(anyCRS, Citations.EPSG).getVersion(); ++ versionEPSG = ((ReferenceIdentifier) IdentifiedObjects.getIdentifier(anyCRS, Citations.EPSG)).getVersion(); + } + + /** + * Writes the report after all rows have been collected. + */ + private void write() throws IOException, FactoryException { + int numSupportedCRS = 0; + int numDeprecatedCRS = 0; + int numAnnotatedCRS = 0; + final var rows = new ArrayList<Row>(10000); + for (final String code : factory.getAuthorityCodes(CoordinateReferenceSystem.class)) { + final var row = new Row(); + row.code = escape(code).toString(); + try { + row.setValues(factory, factory.createCoordinateReferenceSystem(code)); + } catch (FactoryException exception) { + if (row.setValues(factory, exception, code)) { + continue; + } + } + rows.add(row); + if (!row.hasError) numSupportedCRS++; + if (row.annotation != 0) numAnnotatedCRS++; + if (row.isDeprecated) numDeprecatedCRS++; + } + final int numCRS = rows.size(); + sortRows(rows); // May add new rows as section separators. + println("h1", "Coordinate Reference Systems recognized by Apache <abbr title=\"Spatial Information System\">SIS</abbr>™"); + int item = openTag("p"); + println("This list is generated from the <abbr>EPSG</abbr> geodetic dataset version " + versionEPSG + ", together with other sources."); + println("Those Coordinate Reference Systems (<abbr>CRS</abbr>) are supported by the Apache <abbr>SIS</abbr>™ library version " + Version.SIS); + println("(provided that a <a href=\"" + URLs.EPSG_INSTALL + "\">connection to an <abbr>EPSG</abbr> database</a> exists),"); + println("except those with a red text in the last column."); + println("There are " + numCRS + " codes, " + (100 * numSupportedCRS / numCRS) + "% of them being supported."); + closeTags(item); + println("p", "<b>Notation:</b>"); + item = openTag("ul"); + openTag("li"); + println("The " + YX_ORDER + " symbol in front of authority codes (" + Math.round(100f * numAnnotatedCRS / numCRS) + "% of them) " + + "identifies left-handed coordinate systems (for example with <var>latitude</var> axis before <var>longitude</var>)."); + reopenTag("li"); + println("The <del>codes with a strike</del> (" + Math.round(100f * numDeprecatedCRS / numCRS) + "% of them) " + + "identify deprecated definitions. In some cases, the remarks column indicates the replacement."); + reopenTag("li"); + println("Coordinate Reference Systems are grouped by their reference frame or datum."); + closeTags(item); + item = openTag("table"); + printlnWithoutIndentation("<tr><th class=\"narrow\"></th><th class=\"left-align\">Code</th><th class=\"left-align\">Name</th><th class=\"left-align\">Remarks</th></tr>"); + final var buffer = new StringBuilder(); + int counterForHighlight = 0; + for (final Row row : rows) { + row.write(buffer, (counterForHighlight & 2) != 0); + printlnWithoutIndentation(buffer); + buffer.setLength(0); + counterForHighlight++; + if (row.isSectionHeader) { + counterForHighlight = 0; + } + } + closeTags(item); + } + + /** + * Creates the text to show in the "Remarks" column for the given CRS. + */ + private static String getRemark(final CoordinateReferenceSystem crs) { + if (crs instanceof GeographicCRS) { + return (crs.getCoordinateSystem().getDimension() == 3) ? "Geographic 3D" : "Geographic"; + } + if (crs instanceof DerivedCRS derived) { + final OperationMethod method = derived.getConversionFromBase().getMethod(); + final Identifier identifier = IdentifiedObjects.getIdentifier(method, Citations.EPSG); + if (identifier != null) { + return "<a href=\"CoordinateOperationMethods.html#" + identifier.getCode() + + "\">" + method.getName().getCode().replace('_', ' ') + "</a>"; + } + } + if (crs instanceof GeodeticCRS) { + final CoordinateSystem cs = crs.getCoordinateSystem(); + if (cs instanceof CartesianCS) { + return "Geocentric (Cartesian coordinate system)"; + } else if (cs instanceof SphericalCS) { + return "Geocentric (spherical coordinate system)"; + } + return "Geodetic"; + } - if (crs instanceof VerticalCRS vertical) { - VerticalDatum datum = vertical.getDatum(); - if (datum != null) { - Optional<RealizationMethod> method = datum.getRealizationMethod(); - if (method.isPresent()) { - String name = method.get().name().toLowerCase(LOCALE); - return CharSequences.camelCaseToSentence(name) + " realization method"; - } - } - } + if (crs instanceof CompoundCRS compound) { + final var buffer = new StringBuilder(); + for (final CoordinateReferenceSystem component : compound.getComponents()) { + if (buffer.length() != 0) { + buffer.append(" + "); + } + buffer.append(getRemark(component)); + } + return buffer.toString(); + } + if (crs instanceof EngineeringCRS) { + return "Engineering (" + crs.getCoordinateSystem().getName().getCode() + ')'; + } + return ""; + } + + /** + * Omits the trailing number, if any. + * For example if the given name is "Abidjan 1987", then this method returns "Abidjan". + */ + private static String omitTrailingNumber(String name) { + int i = CharSequences.skipTrailingWhitespaces(name, 0, name.length()); + while (i != 0) { + final char c = name.charAt(--i); + if (c < '0' || c > '9') { + name = name.substring(0, CharSequences.skipTrailingWhitespaces(name, 0, i+1)); + break; + } + } + return name; + } + + /** + * If the first word of the CRS name seems to be an acronym of the datum name, + * puts that acronym in a {@code <abbr title="datum name">...</abbr>} element. + */ + private static String insertAbbreviationTitle(final String crsName, final String datumName) { + int s = crsName.indexOf(' '); + if (s < 0) s = crsName.length(); + int p = crsName.indexOf('('); + if (p >= 0 && p < s) s = p; + p = datumName.indexOf('('); + if (p < 0) p = datumName.length(); + final String acronym = crsName.substring(0, s); + final String ar = omitTrailingNumber(acronym); + final String dr = omitTrailingNumber(datumName.substring(0, p)); + if (dr.startsWith(ar)) { + return crsName; // Avoid redudancy between CRS name and datum name. + } + /* + * If the first CRS word does not seem to be an acronym of the datum name, verify + * if there is some words that we should ignore in the datum name and try again. + */ + if (!CharSequences.isAcronymForWords(ar, dr)) { + final String[] words = (String[]) CharSequences.split(dr, ' '); + int n = 0; + for (final String word : words) { + if (!DATUM_WORDS_TO_IGNORE.contains(word)) { + words[n++] = word; + } + } + if (n == words.length || n < 2) { + return crsName; + } + final StringBuilder b = new StringBuilder(); + for (int i=0; i<n; i++) { + if (i != 0) b.append(' '); + b.append(words[i]); + } + if (!CharSequences.isAcronymForWords(ar, b)) { + return crsName; + } + } + return "<abbr title=\"" + datumName + "\">" + acronym + "</abbr>" + crsName.substring(s); + } + + + + + /** + * A row with a natural ordering that use the first part of the name before to use the authority code. + * Every {@link String} fields in this class must be valid HTML. If some text is expected to print + * {@code <} or {@code >} characters, then those characters need to be escaped to their HTML entities. + * + * <p>Content of each {@code Row} instance is written in the following order:</p> + * <ol> + * <li>{@link #annotation} if explicitly set (the default is none).</li> + * <li>{@link #code}</li> + * <li>{@link #name}</li> + * <li>{@link #remark}</li> + * </ol> + * + * <p>Other attributes ({@link #isSectionHeader}, {@link #isDeprecated} and {@link #hasError}) + * are not directly written in the table, but affect their styling.</p> + * + * <h2>Rules for sorting the rows</h2> + * We use only the part of the name prior some keywords (e.g. {@code "zone"}). + * For example in the following codes: + * + * <pre class="text"> + * EPSG:32609 WGS 84 / UTM zone 9N + * EPSG:32610 WGS 84 / UTM zone 10N</pre> + * + * We compare only the "WGS 84 / UTM" string, then the code. This is a reasonably easy way to keep a more + * natural ordering ("9" sorted before "10", "UTM North" projections kept together and same for South). + */ + private static final class Row implements Comparable<Row> { + /** + * {@code true} if this row should actually be used as a section header. + * We insert rows with this flag set to {@code true} for splitting the large table is smaller sections. + */ + boolean isSectionHeader; + + /** + * The datum name, or {@code null} if unknown. + * If non-null, this is used for grouping CRS names by sections. + */ + String section; + + /** + * The authority code in HTML. + */ + String code; + + /** + * The object name in HTML, or {@code null} if none. By default, this field is set to the value of + * <code>{@linkplain IdentifiedObject#getName()}.{@linkplain Identifier#getCode() getCode()}</code>. + */ + String name; + + /** + * A remark in HTML to display after the name, or {@code null} if none. + */ + private String remark; + + /** + * A string derived from the {@link #name} to use for sorting. + */ + private String reducedName; + + /** + * A small symbol to put before the {@linkplain #code} and {@linkplain #name}, or 0 (the default) if none. + * For example, it can indicate a <abbr>CRS</abbr> having unusual axes order. + */ + char annotation; + + /** + * {@code true} if this authority code is deprecated, or {@code false} otherwise. + */ + boolean isDeprecated; + + /** + * {@code true} if an exception occurred while creating the identified object. + * If {@code true}, then the {@link #remark} field will contains the exception localized message. + */ + boolean hasError; + + /** + * Creates a new row. + */ + Row() { + } + + /** + * Invoked when the <abbr>CRS</abbr> cannot be constructed because of the given error. + * + * @param factory the factory which created the <abbr>CRS</abbr>. + * @param cause the reason why the <abbr>CRS</abbr> cannot be constructed. + * @param code the authority code without <abbr>HTML</abbr> escapes. + * @return whether to ignore this row. + */ + final boolean setValues(final CRSAuthorityFactory factory, final FactoryException cause, final String code) { + if (code.startsWith(Constants.PROJ4 + DefaultNameSpace.DEFAULT_SEPARATOR)) { + return true; + } + String message = cause.getMessage(); + if (message == null) { + message = cause.toString(); + } + remark = escape(message).toString(); + hasError = true; + try { - name = toLocalizedString(factory.getDescriptionText(CoordinateReferenceSystem.class, code).get()); ++ name = toLocalizedString(factory.getDescriptionText(code)); + } catch (FactoryException e) { + Logging.unexpectedException(null, CoordinateReferenceSystems.class, "createRow", e); + } + if (code.startsWith("AUTO2:")) { + // It is normal to be unable to instantiate an "AUTO" CRS, + // because those authority codes need parameters. + hasError = false; + remark = "Projected"; + setSection(CommonCRS.WGS84.datum(true)); + } else { + if (cause instanceof NoSuchIdentifierException e) { + remark = '“' + e.getIdentifierCode() + "” operation method is not yet supported."; + } else { + remark = cause.getLocalizedMessage(); + } + setSection(null); + } + return false; + } + + /** + * Invoked when a <abbr>CRS</abbr> has been successfully created. + * + * @param factory the factory which created the <abbr>CRS</abbr>. + * @param crs the object created from the authority code. + * @return the created row, or {@code null} if the row should be ignored. + */ + final void setValues(final CRSAuthorityFactory factory, CoordinateReferenceSystem crs) { + name = escape(crs.getName().getCode()).toString(); + final CoordinateReferenceSystem crsXY = AbstractCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED); + if (!Utilities.deepEquals(crs.getCoordinateSystem(), crsXY.getCoordinateSystem(), ComparisonMode.IGNORE_METADATA)) { + annotation = YX_ORDER; + } + remark = getRemark(crs); + /* + * If the object is deprecated, find the replacement. + * We do not take the whole comment because it may be pretty long. + */ + if (crs instanceof Deprecable dep) { + isDeprecated = dep.isDeprecated(); + if (isDeprecated) { + String replacedBy = null; + InternationalString i18n = crs.getRemarks(); + for (final Identifier id : crs.getIdentifiers()) { + if (id instanceof Deprecable did && did.isDeprecated()) { + i18n = did.getRemarks(); + if (id instanceof DeprecatedCode dc) { + replacedBy = dc.replacedBy; + } + break; + } + } + remark = toLocalizedString(i18n); + /* + * If a replacement exists for a deprecated CRS, use the datum of the replacement instead of + * the datum of the deprecated CRS for determining in which section to put the CRS. The reason + * is that some CRS are deprecated because they were associated to the wrong datum, in which + * case the deprecated CRS would appear in the wrong section if we do not apply this correction. + */ + if (replacedBy != null) try { + crs = factory.createCoordinateReferenceSystem("EPSG:" + replacedBy); + } catch (FactoryException e) { + // Ignore - keep the datum of the deprecated object. + } + } + } + setSection(CRS.getSingleComponents(crs).get(0).getDatum()); + } + + /** + * Computes the {@link #reducedName} field value. + * It determines the section where the <abbr>CRS</abbr> will be placed. + */ + private void setSection(final Datum datum) { + /* + * Get a copy of the name in all lower case. + */ + final StringBuilder b = new StringBuilder(name); + for (int i=0; i<b.length(); i++) { + b.setCharAt(i, Character.toLowerCase(b.charAt(i))); + } + /* + * Cut the string to a shorter length if we find a keyword. + * This will result in many string equals, which will then be sorted by EPSG codes. + * This is useful when the EPSG codes give a better ordering than the alphabetic one + * (for example with Roman numbers). + */ + int s = 0; + for (final String keyword : CUT_BEFORE) { + int i = b.lastIndexOf(keyword); + if (i > 0 && (s == 0 || i < s)) s = i; + } + for (final String keyword : CUT_AFTER) { + int i = b.lastIndexOf(keyword); + if (i >= 0) { + i += keyword.length(); + if (i > s) s = i; + } + } + if (s != 0) b.setLength(s); + uniformizeZoneNumber(b); + reducedName = b.toString(); + if (datum != null) { + section = datum.getName().getCode().replace('_', ' '); + name = insertAbbreviationTitle(name, section); + } + } + + /** + * If the string ends with a number optionally followed by "N" or "S", replaces the hemisphere + * symbol by a sign and makes sure that the number uses at least 3 digits (e.g. "2N" → "+002"). + * This string will be used for better sorting order. + */ + private static void uniformizeZoneNumber(final StringBuilder b) { + if (b.indexOf("/") < 0) { + /* + * Do not process names like "WGS 84". We want to process only names like "WGS 84 / UTM zone 2N", + * otherwise the replacement of "WGS 84" by "WGS 084" causes unexpected sorting. + */ + return; + } + int i = b.length(); + char c = b.charAt(i - 1); + if (c == ')') { + // Ignore suffix like " (ftUS)". + i = b.lastIndexOf(" ("); + if (i < 0) return; + c = b.charAt(i - 1); + } + char sign; + switch (c) { + default: sign = 0; break; + case 'e': case 'n': sign = '+'; i--; break; + case 'w': case 's': sign = '-'; i--; break; + } + int upper = i; + do { + if (i == 0) return; + c = b.charAt(--i); + } while (c >= '0' && c <= '9'); + switch (upper - ++i) { + case 2: b.insert(i, '0'); upper++; break; // Found 2 digits. + case 1: b.insert(i, "00"); upper+=2; break; // Only one digit found. + case 0: return; // No digit. + } + if (sign != 0) { + b.insert(i, sign); + upper++; + } + b.setLength(upper); + } + + /** + * Writes this row to the given stream. + * + * @param out where to write this row. + * @param highlight whether to highlight this row. + * @throws IOException if an error occurred while writing this row. + */ + final void write(final StringBuilder out, final boolean highlight) { + if (isSectionHeader) { + out.append("<tr class=\"separator\"><td colspan=\"4\">").append(name).append("</td></tr>"); + return; + } + out.append("<tr"); + if (highlight) out.append(" class=\"HL\""); + out.append("><td class=\"narrow\">"); + if (annotation != 0) out.append(annotation); + out.append("</td><td>"); + if (code != null) { + out.append("<code>"); + if (isDeprecated) out.append("<del>"); + out.append(code); + if (isDeprecated) out.append("</del>"); + out.append("</code>"); + } + out.append("</td><td>"); + if (name != null) out.append(name); + out.append("</td><td"); + if (hasError) out.append(" class=\"error\""); + else if (isDeprecated) out.append(" class=\"warning\""); + out.append('>'); + if (remark != null) out.append(remark); + out.append("</td></tr>"); + } + + /** + * Compares this row with the given row for ordering by name and authority code. + */ + @Override + public int compareTo(final Row o) { + int n = reducedName.compareTo(o.reducedName); + if (n == 0) { + try { + n = Integer.compare(Integer.parseInt(code), Integer.parseInt(o.code)); + } catch (NumberFormatException e) { + n = code.compareTo(o.code); + } + } + return n; + } + } + + /** + * Sorts the rows, then inserts sections between CRS instances that use different datums. + */ + private static void sortRows(final List<Row> rows) { + Collections.sort(rows); + @SuppressWarnings("SuspiciousToArrayCall") + final Row[] data = rows.toArray(Row[]::new); + final var sections = new TreeMap<String,String>(); + for (final Row row : data) { + final String section = row.section; + if (section != null) { + sections.put(CharSequences.toASCII(section).toString().toLowerCase(), section); + } + } + rows.clear(); + /* + * Recopy the rows, but section-by-section. We do this sorting here instead of in the Row.compareTo(Row) + * method in order to preserve the alphabetical order of rows with unknown datum. + * Algorithm below is inefficient, but this class should be rarely used anyway and only by site maintainer. + */ + for (final String section : sections.values()) { + final Row separator = new Row(); + separator.isSectionHeader = true; + separator.name = section; + rows.add(separator); + boolean found = false; + for (int i=0; i<data.length; i++) { + final Row row = data[i]; + if (row != null) { + if (row.section != null) { + found = section.equals(row.section); + } + if (found) { + rows.add(row); + data[i] = null; + found = true; + } + } + } + } + boolean found = false; + for (final Row row : data) { + if (row != null) { + if (!found) { + final Row separator = new Row(); + separator.isSectionHeader = true; + separator.name = "Unknown"; + rows.add(separator); + } + rows.add(row); + found = true; + } + } + } + } diff --cc optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java index 04f7a4c421,49df55c576..6ba0e5cc66 --- a/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java +++ b/optional/src/org.apache.sis.referencing.database/test/org/apache/sis/resources/embedded/EmbeddedResourcesTest.java @@@ -23,6 -24,6 +24,7 @@@ import java.util.Map import javax.sql.DataSource; import java.util.ServiceLoader; import org.opengis.util.FactoryException; ++import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.setup.InstallationResources; import org.apache.sis.metadata.sql.internal.shared.Initializer; import org.apache.sis.system.DataDirectory; @@@ -33,10 -35,10 +36,6 @@@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; --import org.apache.sis.test.TestUtilities; -- - // Specific to the main branch: - import org.apache.sis.referencing.crs.AbstractCRS; -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.referencing.crs.CoordinateReferenceSystem; /** @@@ -130,8 -141,16 +138,16 @@@ public final strictfp class EmbeddedRes @Test public void testCrsforCode() throws FactoryException { assumeContainsEPSG(); - var crs = assertInstanceOf(AbstractCRS.class, CRS.forCode("EPSG:6676")); - String area = TestUtilities.getSingleton(crs.getDomains()).getDomainOfValidity().getDescription().toString(); + final String dir = DataDirectory.getenv(); + assumeTrue((dir == null) || dir.isEmpty(), "The SIS_DATA environment variable must be unset for enabling this test."); + verifyEPSG_6676(CRS.forCode("EPSG:6676")); + } + + /** + * Verifies the <abbr>CRS</abbr> created from code EPSG:6676 + */ + private static void verifyEPSG_6676(final CoordinateReferenceSystem crs) { - String area = TestUtilities.getSingleton(crs.getDomains()).getDomainOfValidity().getDescription().toString(); ++ String area = crs.getDomainOfValidity().getDescription().toString(); assertTrue(area.contains("Japan"), area); } }
