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 {
