This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 7b052340a2bf67de70f319d7bcdeff49b9b6ecc9
Merge: 57b84010bf f78936afd0
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Oct 28 15:00:09 2026 +0100

    Merge branch 'geoapi-3.1' (bug fixes).

 .../org/apache/sis/coverage/RegionOfInterest.java  |   8 +-
 .../coverage/grid/CoordinateOperationFinder.java   |   5 +-
 .../apache/sis/coverage/grid/DefaultEvaluator.java |   3 +-
 .../apache/sis/coverage/grid/DomainLinearizer.java |   3 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |   3 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |   5 +-
 .../sis/coverage/grid/ResampledGridCoverage.java   |   2 +-
 .../sis/geometry/wrapper/GeometryWrapper.java      |   7 +-
 .../apache/sis/geometry/wrapper/jts/Wrapper.java   |   7 +-
 .../apache/sis/profile/japan/netcdf/GCOM_C.java    |  20 +-
 .../main/org/apache/sis/geometry/Envelopes.java    |   7 +-
 .../apache/sis/geometry/WraparoundAdjustment.java  |   5 +-
 .../main/org/apache/sis/referencing/CRS.java       | 328 +++++++--
 .../referencing/internal/PositionTransformer.java  |   7 +-
 .../apache/sis/referencing/internal/Resources.java |   5 +
 .../sis/referencing/internal/Resources.properties  |   1 +
 .../referencing/internal/Resources_fr.properties   |   1 +
 .../referencing/internal/ServicesForMetadata.java  |   3 +-
 .../internal/shared/ExtentSelector.java            |   2 +-
 .../operation/CoordinateOperationFinder.java       |   8 +-
 .../operation/CoordinateOperationRegistry.java     |   4 +-
 .../operation/DefaultConcatenatedOperation.java    |  33 +-
 .../DefaultCoordinateOperationFactory.java         |  35 +-
 .../referencing/operation/SubOperationInfo.java    |  32 +-
 .../operation/TransformedCoordinateSet.java        |   7 +-
 .../sis/referencing/operation/matrix/Matrices.java |  37 +-
 .../referencing/operation/matrix/package-info.java |   2 +-
 .../referencing/operation/projection/Mercator.java |   4 +-
 .../operation/transform/MathTransforms.java        |   4 +-
 .../transform/TransformFactoryException.java       |  68 ++
 .../operation/transform/package-info.java          |   2 +-
 .../org/apache/sis/referencing/package-info.java   |   2 +-
 .../test/org/apache/sis/referencing/CRSTest.java   |  70 +-
 .../operation/CoordinateOperationRegistryTest.java |  85 ++-
 .../DefaultCoordinateOperationFactoryTest.java     |   3 +-
 .../main/module-info.java                          |   2 +-
 .../apache/sis/storage/netcdf/base/Convention.java |  33 +-
 .../apache/sis/storage/netcdf/base/Decoder.java    |   2 +
 .../sis/storage/netcdf/base/GridMapping.java       | 742 ++++++++++++---------
 .../sis/storage/netcdf/internal/Resources.java     |   6 +-
 .../storage/netcdf/internal/Resources.properties   |   2 +-
 .../netcdf/internal/Resources_fr.properties        |   2 +-
 .../apache/sis/storage/netcdf/package-info.java    |   2 +-
 .../apache/sis/storage/netcdf/NetcdfStoreTest.java |   4 +-
 .../org/apache/sis/storage/esri/WritableStore.java |   2 +-
 .../apache/sis/storage/tiling/TileReadEvent.java   |   3 +-
 .../main/org/apache/sis/math/Vector.java           |   4 +
 .../main/org/apache/sis/util/Classes.java          |  22 +-
 .../main/org/apache/sis/util/ComparisonMode.java   |   8 +-
 .../main/org/apache/sis/util/Utilities.java        |   2 +
 .../test/org/apache/sis/test/LoggingWatcher.java   |  17 +
 .../apache/sis/storage/shapefile/dbf/DBFField.java |  13 +-
 .../shapefile/shp/ShapeGeometryEncoder.java        |   2 +-
 .../sis/referencing/factory/sql/epsg/Finish.sql    |   1 +
 54 files changed, 1146 insertions(+), 541 deletions(-)

diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
index 500becdf21,47a7fd010e..6b46e04271
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
@@@ -44,9 -44,10 +44,10 @@@ import org.opengis.util.FactoryExceptio
  import org.opengis.geometry.DirectPosition;
  import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.opengis.referencing.operation.CoordinateOperation;
 -import org.opengis.referencing.operation.TransformException;
  import org.opengis.referencing.operation.MathTransform;
 +import org.opengis.referencing.operation.TransformException;
  import org.apache.sis.referencing.CRS;
+ import 
org.apache.sis.referencing.operation.transform.TransformFactoryException;
  import org.apache.sis.geometry.DirectPosition2D;
  import org.apache.sis.geometry.GeneralDirectPosition;
  import org.apache.sis.geometry.GeneralEnvelope;
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index 714b32cf95,8811113bc8..ed7dfcb8ac
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@@ -1181,15 -1196,15 +1198,15 @@@ public final class CRS 
       * {@linkplain org.apache.sis.metadata.iso.extent.DefaultExtent#intersect 
intersection}
       * of the domain of validity of all components.
       *
-      * <h4>Ellipsoidal height</h4>
+      * <h4>Augmenting a two-dimensional <abbr>CRS</abbr> with an ellipsoidal 
height</h4>
       * If a two-dimensional geographic or projected CRS is followed or 
preceded by a vertical CRS with ellipsoidal
 -     * {@linkplain 
org.apache.sis.referencing.datum.DefaultVerticalDatum#getRealizationMethod() 
realization method},
 +     * {@linkplain 
org.apache.sis.referencing.datum.DefaultVerticalDatum#getVerticalDatumType() 
datum type},
-      * this method combines them in a single three-dimensional geographic or 
projected CRS.  Note that standalone
-      * ellipsoidal heights are not allowed according ISO 19111. But if such 
situation is nevertheless found, then
-      * the action described here fixes the issue. This is the reverse of 
<code>{@linkplain #getVerticalComponent
-      * getVerticalComponent}(crs, true)</code>.
+      * this method combines them in a single three-dimensional geographic or 
projected <abbr>CRS</abbr>.
+      * Note that standalone ellipsoidal heights are not allowed according 
<abbr>ISO</abbr> 19111.
+      * But if such situation is nevertheless found, then the action described 
here fixes the standard violation.
+      * This is the reverse of <code>{@linkplain #getVerticalComponent 
getVerticalComponent}(crs, true)</code>.
       *
-      * <h4>Components order</h4>
+      * <h4>{@code SingleCRS} component order in {@code CompoundCRS}</h4>
       * Apache SIS is permissive on the order of components that can be used 
in a compound CRS.
       * However for better inter-operability, users are encouraged to follow 
the order mandated by ISO 19162:
       *
@@@ -1221,17 -1236,188 +1238,186 @@@
      }
  
      /**
-      * Returns a mask of the dimensions where a {@code subCRS} element is 
found in the given <abbr>CRS</abbr>.
-      * If the given {@code crs} is {@linkplain #equivalent equivalent} to an 
element of the {@code subCRS} array,
-      * then this method returns a {@link BitSet} in which all bits are set to 
{@code true} in the range from 0 to
-      * <var>n</var>−1, where <var>n</var> is the number of dimensions of 
{@code crs}.
-      * Otherwise, if {@code crs} is an instance of {@link CompoundCRS}, then 
the {@code crs} components are traversed
-      * recursively and compared in the same way as described above, except 
that the range of bits set to {@code true}
-      * does not start at index 0. Instead, the range starts at the index of 
the first dimension of the matched
-      * {@code crs} component. If no match is found, then the returned {@link 
BitSet} is empty.
+      * Identification of a property that defines when two <abbr>CRS</abbr>s 
are considered equivalent.
+      * A comparison between two {@link SingleCRS} instances can take in 
account all properties relevant
+      * to coordinate operations, or only the datum, or only the datum 
<em>type</em>.
+      * These properties are compared in {@linkplain 
ComparisonMode#COMPATIBILITY compatibility mode}.
       *
-      * <h4>Example</h4>
-      * The following snippet gets the dimensions of the temporal and vertical 
components of a <abbr>CRS</abbr>:
+      * <p>Enumeration values are ordered from strictest criterion to most 
lenient criterion.</p>
+      *
+      * @author  Martin Desruisseaux (Geomatys)
+      * @version 1.8
+      *
+      * @see ComparisonMode#COMPATIBILITY
+      *
+      * @since 1.8
+      */
+     public enum EquivalenceProperty implements BiPredicate<SingleCRS, 
SingleCRS> {
+         /**
+          * Compares all {@code SingleCRS} properties relevant to coordinate 
operations.
+          * Invoking <code>COMPONENT.{@link #test test}(<var>a</var>, 
<var>b</var>)</code> is equivalent
+          * to invoking <code>{@link #equivalent(CoordinateReferenceSystem, 
CoordinateReferenceSystem)
+          * CRS.equivalent}(<var>a</var>, <var>b</var>)</code>.
+          *
+          * @see #equivalent(CoordinateReferenceSystem, 
CoordinateReferenceSystem)
+          */
+         COMPONENT,
+ 
+         /**
+          * Compares only the {@code SingleCRS} datum properties; coordinate 
systems are ignored.
+          * Therefore, the <abbr>CRS</abbr> comparisons are insensitive to 
coordinate system type,
+          * axis order, axis directions and axis units of measurements.
+          * Datum properties are compared in {@linkplain 
ComparisonMode#COMPATIBILITY compatibility mode}.
+          *
+          * <p>The type of a <abbr>CRS</abbr> is closely related to the type 
of its datum.
+          * Since this criterion matches <abbr>CRS</abbr> with the same datum,
+          * it indirectly requires the matched <abbr>CRS</abbr>s to be of the 
same type ignoring specializations
+          * (<i>e.g.</i>, the difference between {@link GeographicCRS} and 
geocentric <abbr>CRS</abbr> is ignored).
+          * Furthermore, a {@link DerivedCRS} is considered equivalent to its 
base <abbr>CRS</abbr>.</p>
+          *
+          * @see SingleCRS#getDatum()
+          */
+         DATUM {
+             @Override Predicate<SingleCRS> comparator(final SingleCRS crs) {
+                 final IdentifiedObject datum = DatumOrEnsemble.of(crs);
+                 return (other) -> Utilities.deepEquals(datum, 
DatumOrEnsemble.of(other), ComparisonMode.COMPATIBILITY);
+             }
+         },
+ 
+         /**
+          * Compares only the {@code SingleCRS} datum <em>types</em>.
+          * Not only coordinate systems are ignored, but datum changes are 
also ignored.
+          * Datum ensembles are considered equivalent to ordinary datums,
+          * but dynamic datums are not considered equivalent to static datum
+          * (because dynamic datums require more information: a data epoch).
+          *
+          * <p><b>Example:</b>
+          * <q><abbr>WGS</abbr> 1972</q> and <q><abbr>WGS</abbr> 1984</q> are 
considered equivalent
+          * even if they are different datums requiring a coordinate 
transformation between them.</p>
+          */
+         DATUM_TYPE {
+             @Override Predicate<SingleCRS> comparator(final SingleCRS crs) {
+                 final Set<Class<? extends Datum>> types = getDatumType(crs);
+                 return (other) -> types.equals(getDatumType(other));
+             }
+         };
+ 
+         /**
+          * Returns the datum types implemented by the datum of the given 
<abbr>CRS</abbr>.
+          * The return value should contain exactly one element, but a 
collection is used for robustness.
+          * If the {@code crs} is {@code null} or has no datum, an empty set 
is returned (not {@code null}).
+          *
+          * @param  crs  the <abbr>CRS</abbr> component from which to get the 
datum type, or {@code null}.
+          * @return the datum type(s) of the given <abbr>CRS</abbr> component, 
or an empty set if none.
+          */
+         private static Set<Class<? extends Datum>> getDatumType(final 
SingleCRS crs) {
+             final var types = new HashSet<Class<? extends Datum>>(4);
+             for (Class<? extends Datum> type : 
Classes.getLeafInterfaces(Classes.getClass(DatumOrEnsemble.of(crs)), 
Datum.class)) {
 -                if (!DatumEnsemble.class.isAssignableFrom(type)) {
 -                    types.add(type);
 -                }
++                types.add(type);
+             }
+             return types;
+         }
+ 
+         /**
+          * Creates a comparator for comparing the given <abbr>CRS</abbr> 
component with other components.
+          * The {@code crs} argument may be {@code null} and the returned 
predicate must accept {@code null}.
+          *
+          * @param  crs  the <abbr>CRS</abbr> component to compare, or {@code 
null}.
+          * @return a predicate for comparing the given component with other 
components.
+          */
+         Predicate<SingleCRS> comparator(final SingleCRS crs) {
+             return (other) -> equivalent(crs, other);
+         }
+ 
+         /**
+          * Compares two <abbr>CRS</abbr> components for equivalence.
+          * If this method is invoked on the {@link #COMPONENT} enumeration 
value,
+          * replacing one <abbr>CRS</abbr> by the other does not change the 
result of coordinate operations.
+          * When invoked on other enumeration values, more lenient comparisons 
are performed which can accept
+          * changes in coordinate system types, axis orders, directions, units 
of measurement or datum properties.
+          *
+          * @param  crs1  the first <abbr>CRS</abbr> to compare (can be null).
+          * @param  crs2  the second <abbr>CRS</abbr> to compare (can be null).
+          * @return whether the properties identified by this enumeration 
value are equivalent.
+          */
+         @Override
+         public final boolean test(final SingleCRS crs1, final SingleCRS crs2) 
{
+             return comparator(crs1).test(crs2);
+         }
+ 
+         /**
+          * Returns a mask of the dimensions where a component matches one of 
the {@code search} elements.
+          * The search is lenient, with "lenient" defined by this enumeration 
value.
+          *
+          * <p>If {@code overwrite} is true, this method overwrites the {@code 
search} array.
+          * The {@code overwrite} flag should be {@code false} for 
user-supplied arrays.</p>
+          *
+          * @param  components  value of {@code getSingleComponents(crs)}.
+          * @param  search      the elements to search in the {@code 
components} list.
+          * @param  overwrite   whether to overwrite the {@code search} array 
for matching elements only once.
+          * @return mask with indexes of dimensions where a {@code search} 
element is leniently found.
+          */
+         final BitSet locateDimensions(final List<SingleCRS> components, final 
SingleCRS[] search, final boolean overwrite) {
+             final var mask = new BitSet();
+             if (search != null) {
+                 int count = search.length;
+                 for (final EquivalenceProperty criterion : values()) {
+                     /*
+                      * Try more specific criteria before this criterion. For 
example, if `this` is `DATUM_TYPE`,
+                      * matches will first be attempted using the `COMPONENT` 
and `DATUM` criteria in that order.
+                      * More restrictive criteria are tried first because 
datum comparisons may be ambiguous.
+                      * For example, some meteorological data have two 
temporal axes, which can result in two
+                      * equivalent temporal datums.
+                      */
+                     int lower = 0;
+                     for (final SingleCRS component : components) {
+                         final Predicate<SingleCRS> comparator = 
criterion.comparator(component);
+                         final int upper = lower + 
getDimensionOrZero(component);
+                         for (int i=0; i<count; i++) {
+                             if (comparator.test(search[i])) {
+                                 mask.set(lower, upper);
+                                 if (overwrite) {
+                                     // Remove this component from the 
elements to test.
+                                     if (--count == 0) return mask;  // 
Short-circuit.
+                                     System.arraycopy(search, i+1, search, i, 
count - i);
+                                 }
+                                 break;
+                             }
+                         }
+                         lower = upper;
+                     }
+                     if (criterion == this) {
+                         // Finished execution with the user requested 
criterion.
+                         break;
+                     }
+                 }
+             }
+             return mask;
+         }
+     }
+ 
+     /**
+      * Returns a mask of the {@code crs} dimensions where a component is 
equivalent to a {@code search} elements.
+      * The returned {@code BitSet} contains bits that are set to {@code true} 
at indexes equal to all {@code crs}
+      * dimensions where a {@linkplain 
#getSingleComponents(CoordinateReferenceSystem) single component} of
+      * {@code crs} is {@linkplain #equivalent equivalent} to an element of 
the given {@code search} array.
+      * In other words, if <var>i</var> is the first dimension of a single 
{@code crs} component and <var>n</var>
+      * is its number of dimensions, and if that component is equivalent to an 
element of the {@code search} array,
+      * then bits in the range from <var>i</var> to 
<var>i</var>+<var>n</var>−1 inclusive are set to {@code true}.
+      *
+      * <p>The order of elements in the {@code search} array is not 
significant,
+      * but the axis order and units of measurement inside each {@link 
SingleCRS} is significant.
+      * For more lenient methods ignoring also axis order, axis units and (to 
some extent) to <abbr>CRS</abbr> type,
+      * see the {@link #locateDimensions(CoordinateReferenceSystem, 
CoordinateReferenceSystem, EquivalenceProperty)}
+      * method.</p>
+      *
+      * <p>This method is null-safe: if {@code crs} or {@code search} is 
{@code null}, this method returns an empty set.
+      * If {@code search} contains {@code null} elements, these elements are 
ignored. The latter tolerance makes easy to
+      * use directly the return value of methods such as {@link 
#getTemporalComponent(CoordinateReferenceSystem)}.</p>
+      *
+      * <p><b>Example:</b>
+      * the following snippet gets the dimensions in {@code crs} of the 
temporal and vertical components,
+      * if these components are present and have the same axis order and units 
of measurement:</p>
       *
       * {@snippet lang="java" :
       *     CoordinateReferenceSystem crs = ...;
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/PositionTransformer.java
index 83827aff5d,e7da88e555..22dbd18b60
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/PositionTransformer.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/PositionTransformer.java
@@@ -28,10 -28,12 +28,11 @@@ import org.opengis.referencing.operatio
  import org.apache.sis.referencing.CRS;
  import org.apache.sis.referencing.MultiRegisterOperations;
  import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import 
org.apache.sis.referencing.operation.transform.TransformFactoryException;
  import org.apache.sis.geometry.GeneralDirectPosition;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.referencing.RegisterOperations;
 -import org.opengis.coordinate.MismatchedDimensionException;
 +// Specific to the main branch:
 +import org.opengis.geometry.MismatchedDimensionException;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
index ccf91fd960,536dcc0f7e..b22751d688
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/SubOperationInfo.java
@@@ -278,27 -278,30 +279,30 @@@ searchSrc:  while (sourceComponentInde
              final DirectPosition coordinates = 
context.getConstantCoordinates();
              if (coordinates != null) {
                  /*
-                  * Finds the index of the first coordinate to use among the 
constant coordinates.
+                  * Finds the indexes of the coordinates to use among the 
constant coordinates.
                   * The default CRS of `coordinates` is the full target CRS 
(with all dimensions).
-                  * If a different CRS is specified, search the index of this 
target CRS component.
+                  * If a different CRS is specified, search the indexes of 
that target component.
                   * No coordinate transformation is perfomed in this method, 
only selection.
                   */
-                 int indexOfConstant = targetLowerDimension;     // Value for 
the default CRS.
+                 final BitSet indexOfConstant;
                  final CoordinateReferenceSystem crs = 
coordinates.getCoordinateReferenceSystem();
                  if (crs != null) {
-                     indexOfConstant = CRS.locateDimensions(crs, 
targetComponent).nextSetBit(0);
-                     if (indexOfConstant < 0) {
-                         return null;
-                     }
+                     indexOfConstant = CRS.locateDimensions(crs, 
targetComponent);
+                 } else {
+                     indexOfConstant = new BitSet();
+                     indexOfConstant.set(targetLowerDimension, 
targetUpperDimension);
                  }
-                 final int d = coordinates.getDimension();
-                 final var c = new double[targetUpperDimension - 
targetLowerDimension];
-                 for (int i=0; i<c.length; i++) {
-                     if (indexOfConstant >= d || Double.isNaN(c[i] = 
coordinates.getOrdinate(indexOfConstant++))) {
-                         return 
targetComponent.getCoordinateSystem().getAxis(i);
+                 if (!indexOfConstant.isEmpty()) {
+                     final var c = new double[targetUpperDimension - 
targetLowerDimension];
+                     for (int next = -1, i=0; i<c.length; i++) {
+                         next = indexOfConstant.nextSetBit(next + 1);
 -                        if (next < 0 || Double.isNaN(c[i] = 
coordinates.getCoordinate(next))) {
++                        if (next < 0 || Double.isNaN(c[i] = 
coordinates.getOrdinate(next))) {
+                             // Coordinate cannot be resolved. Return an 
object to use in error message.
+                             return 
targetComponent.getCoordinateSystem().getAxis(i);
+                         }
                      }
+                     constantCoordinates = c;
                  }
-                 constantCoordinates = c;
              }
          }
          return null;
diff --cc 
endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
index 7b646115a8,3fd14b8b1c..3143cb6df2
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridMapping.java
@@@ -502,10 -515,17 +515,17 @@@ final class GridMapping 
                  }
                  return base.getName();
              });
-             baseCRS = crsFactory.createGeographicCRS(properties, (datum != 
null) ? datum : (GeodeticDatum) ensemble, baseCRS.getCoordinateSystem());
+             if (wantGeographicCRS) {
+                 properties.put(GeographicCRS.REMARKS_KEY, 
definition.remove(ACDD.comment));
+             }
 -            baseCRS = crsFactory.createGeographicCRS(properties, datum, null, 
baseCRS.getCoordinateSystem());
++            baseCRS = crsFactory.createGeographicCRS(properties, datum, 
baseCRS.getCoordinateSystem());
          }
-         // Only swap axis order from (latitude, longitude) to (longitude, 
latitude).
-         MathTransform baseToCRS = MathTransforms.swapTwoFirstAxes(2);
+         /*
+          * Geographic or projected CRS built from above objects declared in 
the grid mapping variable.
+          * The geographic CRS will always have (latitude, longitude) axes in 
that order and in degrees.
+          * We use that order because it will be the order that users see by 
default in user interfaces.
+          * It also increases the chances that we can assign an EPSG code to 
the CRS.
+          */
          if (wantGeographicCRS) {
              crs = baseCRS;
          } else {

Reply via email to