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);
      }
  }

Reply via email to