This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new c0da944894 When a CRS is specified in a netCDF file both as attributes
and by WKT, and when the attributes do not specify object names, get the names
from the WKT.
c0da944894 is described below
commit c0da944894e6b459f49d87ce77768d2adb749bb9
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Nov 24 16:20:13 2025 +0100
When a CRS is specified in a netCDF file both as attributes and by WKT, and
when the attributes do not specify object names, get the names from the WKT.
Add a missing compensation for axis swapping when the CRS is specified by
WKT.
Fix the "pixel center versus corner" convention when using GDAL's
GeoTransform.
---
.../apache/sis/profile/japan/netcdf/GCOM_C.java | 2 +-
.../main/org/apache/sis/referencing/CRS.java | 32 ++-
.../sis/referencing/datum/DatumOrEnsemble.java | 3 +
.../internal/shared/ReferencingUtilities.java | 17 +-
.../operation/transform/MathTransforms.java | 4 +-
.../operation/transform/TransformSeparator.java | 43 ++-
.../apache/sis/storage/netcdf/base/CRSBuilder.java | 2 +-
.../apache/sis/storage/netcdf/base/CRSMerger.java | 5 +-
.../apache/sis/storage/netcdf/base/Convention.java | 2 +-
.../apache/sis/storage/netcdf/base/Decoder.java | 2 +-
.../org/apache/sis/storage/netcdf/base/Grid.java | 50 ++--
.../sis/storage/netcdf/base/GridAdjustment.java | 8 +-
.../sis/storage/netcdf/base/GridMapping.java | 314 ++++++++++++++-------
.../apache/sis/storage/netcdf/base/Variable.java | 6 +-
.../sis/storage/netcdf/internal/Resources.java | 6 +
.../storage/netcdf/internal/Resources.properties | 1 +
.../netcdf/internal/Resources_fr.properties | 1 +
17 files changed, 348 insertions(+), 150 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.profile.japan/main/org/apache/sis/profile/japan/netcdf/GCOM_C.java
b/endorsed/src/org.apache.sis.profile.japan/main/org/apache/sis/profile/japan/netcdf/GCOM_C.java
index 746eb93493..c31e0d4ea4 100644
---
a/endorsed/src/org.apache.sis.profile.japan/main/org/apache/sis/profile/japan/netcdf/GCOM_C.java
+++
b/endorsed/src/org.apache.sis.profile.japan/main/org/apache/sis/profile/japan/netcdf/GCOM_C.java
@@ -412,7 +412,7 @@ public final class GCOM_C extends Convention {
};
/**
- * Returns the <i>grid to CRS</i> transform for the given node.
+ * Returns the <i>grid corners to CRS</i> transform for the given node.
* This method is invoked after call to {@link #projection(Node)} resulted
in creation of a projected CRS.
* The {@linkplain ProjectedCRS#getBaseCRS() base CRS} shall have
(latitude, longitude) axes in degrees.
*
diff --git
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
index d6889ba187..7fe88aff7f 100644
---
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
@@ -46,6 +46,7 @@ import org.opengis.referencing.crs.TemporalCRS;
import org.opengis.referencing.crs.VerticalCRS;
import org.opengis.referencing.crs.EngineeringCRS;
import org.opengis.referencing.datum.Datum;
+import org.opengis.referencing.datum.GeodeticDatum;
import org.opengis.referencing.operation.Conversion;
import org.opengis.referencing.operation.OperationNotFoundException;
import org.opengis.referencing.operation.CoordinateOperation;
@@ -148,7 +149,7 @@ import org.opengis.coordinate.CoordinateMetadata;
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.5
+ * @version 1.6
* @since 0.3
*/
public final class CRS {
@@ -1016,6 +1017,35 @@ public final class CRS {
return Optional.ofNullable(epoch);
}
+ /**
+ * Returns the geodetic reference frame used by the given coordinate
reference system.
+ * If the given <abbr>CRS</abbr> is an instance of {@link GeodeticCRS},
then this method returns the
+ * <abbr>CRS</abbr>'s datum. Otherwise, if the given <abbr>CRS</abbr> is
an instance of {@link CompoundCRS},
+ * then this method searches for the first geodetic component. Otherwise,
this method returns an empty value.
+ *
+ * @param crs the coordinate reference system for which to get the
geodetic reference frame, or {@code null}.
+ * @return the geodetic reference frame, or an empty value if none.
+ *
+ * @see DatumOrEnsemble#getEllipsoid(CoordinateReferenceSystem)
+ * @see DatumOrEnsemble#getPrimeMeridian(CoordinateReferenceSystem)
+ * @see #getGreenwichLongitude(GeodeticCRS)
+ *
+ * @since 1.6
+ */
+ public static Optional<GeodeticDatum> getGeodeticReferenceFrame(final
CoordinateReferenceSystem crs) {
+ if (crs instanceof GeodeticCRS) {
+ return Optional.ofNullable(DatumOrEnsemble.asDatum((GeodeticCRS)
crs));
+ } else if (crs instanceof CompoundCRS) {
+ for (CoordinateReferenceSystem component : ((CompoundCRS)
crs).getComponents()) {
+ final Optional<GeodeticDatum> datum =
getGeodeticReferenceFrame(component);
+ if (datum.isPresent()) {
+ return datum;
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
/**
* Creates a compound coordinate reference system from an ordered list of
CRS components.
* A CRS is inferred from the given components and the domain of validity
is set to the
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
index 2ec5a4c69d..a2cd7ff376 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/datum/DatumOrEnsemble.java
@@ -544,6 +544,8 @@ public final class DatumOrEnsemble {
*
* @param crs the coordinate reference system for which to get the
ellipsoid.
* @return the ellipsoid, or an empty value if none or not equivalent for
all members of the ensemble.
+ *
+ * @see
org.apache.sis.referencing.CRS#getGeodeticReferenceFrame(CoordinateReferenceSystem)
*/
public static Optional<Ellipsoid> getEllipsoid(final
CoordinateReferenceSystem crs) {
return Optional.ofNullable(getProperty(crs, GeodeticDatum.class,
GeodeticDatum::getEllipsoid, Objects::nonNull));
@@ -556,6 +558,7 @@ public final class DatumOrEnsemble {
* @param crs the coordinate reference system for which to get the prime
meridian.
* @return the prime meridian, or an empty value if none or not equivalent
for all members of the ensemble.
*
+ * @see
org.apache.sis.referencing.CRS#getGeodeticReferenceFrame(CoordinateReferenceSystem)
* @see org.apache.sis.referencing.CRS#getGreenwichLongitude(GeodeticCRS)
*/
public static Optional<PrimeMeridian> getPrimeMeridian(final
CoordinateReferenceSystem crs) {
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
index 948a53b505..0e9e469553 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/shared/ReferencingUtilities.java
@@ -18,6 +18,7 @@ package org.apache.sis.referencing.internal.shared;
import java.util.Map;
import java.util.HashMap;
+import java.util.OptionalInt;
import javax.measure.Unit;
import javax.measure.quantity.Angle;
import org.opengis.annotation.UML;
@@ -143,13 +144,23 @@ public final class ReferencingUtilities {
* @return the number of dimensions, or 0 if the given CRS or its
coordinate system is null.
*/
public static int getDimension(final CoordinateReferenceSystem crs) {
+ return getOptionalDimension(crs).orElse(0);
+ }
+
+ /**
+ * Returns the number of dimensions of the given <abbr>CRS</abbr>.
+ *
+ * @param crs the <abbr>CRS</abbr> from which to get the number of
dimensions, or {@code null}.
+ * @return the number of dimensions, or empty if the given CRS or its
coordinate system is null.
+ */
+ public static OptionalInt getOptionalDimension(final
CoordinateReferenceSystem crs) {
if (crs != null) {
final CoordinateSystem cs = crs.getCoordinateSystem();
- if (cs != null) { //
Paranoiac check.
- return cs.getDimension();
+ if (cs != null) { // Should never be null, but let be safe.
+ return OptionalInt.of(cs.getDimension());
}
}
- return 0;
+ return OptionalInt.empty();
}
/**
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
index 3a764936a9..5d776ca4a3 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/MathTransforms.java
@@ -203,8 +203,8 @@ public final class MathTransforms {
case 1: {
final MatrixSIS m = MatrixSIS.castOrCopy(matrix);
return LinearTransform1D.create(
- DoubleDouble.of(m.getNumber(0,0), true),
- DoubleDouble.of(m.getNumber(0,1), true));
+ DoubleDouble.of(m.getNumber(0, 0), true),
+ DoubleDouble.of(m.getNumber(0, 1), true));
}
case 2: {
return AffineTransform2D.create(matrix);
diff --git
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
index f011c92cff..2d631b87b4 100644
---
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
+++
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/TransformSeparator.java
@@ -42,7 +42,7 @@ import org.apache.sis.util.resources.Errors;
* and (<var>λ</var>,<var>φ</var>,<var>h</var>) outputs, then the following
code:
*
* {@snippet lang="java" :
- * TransformSeparator s = new TransformSeparator(theTransform);
+ * var s = new TransformSeparator(theTransform);
* s.addSourceDimensionRange(0, 2);
* MathTransform mt = s.separate();
* }
@@ -51,7 +51,7 @@ import org.apache.sis.util.resources.Errors;
* The output dimensions can be verified with a call to {@link
#getTargetDimensions()}.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.6
* @since 0.7
*/
public class TransformSeparator {
@@ -496,6 +496,45 @@ public class TransformSeparator {
throw new
FactoryException(Resources.format(Resources.Keys.CanNotSeparateTransform_3,
side, expected, actual));
}
+ /**
+ * Replaces the transform in the given range of coordinate indexes.
+ * The coordinates at indexes in the range from {@code lower} inclusive to
{@code upper} exclusive
+ * will be computed by the given {@code replacement}. Coordinates at other
indexes are computed by
+ * the transform given at construction time.
+ *
+ * @param lower index of the first coordinate to compute with the
given replacement.
+ * @param upper index after the last coordinate to compute with
the given replacement.
+ * @param replacement the transform to use for the given range of
coordinate indexes.
+ * @return a transform with the replacement, or the transform specified at
construction time if no change.
+ * @throws FactoryException if the transform cannot be separated.
+ *
+ * @since 1.6
+ */
+ public MathTransform replace(final int lower, final int upper, final
MathTransform replacement) throws FactoryException {
+ final int limit = transform.getTargetDimensions();
+ ArgumentChecks.ensureBetween("lower", 0, limit, lower);
+ ArgumentChecks.ensureBetween("upper", lower, limit, upper);
+ ArgumentChecks.ensureNonNull("replacement", replacement);
+ clear();
+ int count = 0;
+ var components = new MathTransform[3];
+ if (lower != 0) {
+ addTargetDimensionRange(0, lower);
+ components[count++] = separate();
+ clear();
+ }
+ components[count++] = replacement;
+ if (upper != limit) {
+ addTargetDimensionRange(upper, limit);
+ components[count++] = separate();
+ clear();
+ }
+ components = ArraysExt.resize(components, count);
+ // Note: we should use `factory` below, but it may not be worth to
duplicate the `compound` code for now.
+ final MathTransform result = MathTransforms.compound(components);
+ return transform.equals(result) ? transform : result;
+ }
+
/**
* Creates a transform for the same mathematic as the given {@code step}
* but expecting only the given dimensions as inputs.
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
index 532da2a730..55cad0d6f9 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSBuilder.java
@@ -81,7 +81,7 @@ import org.apache.sis.measure.Units;
* which is a {@linkplain Axis#abbreviation controlled vocabulary} for this
implementation.
*
* <h2>Exception handling</h2>
- * {@link FactoryException} is handled as a warning by {@linkplain the caller
Grid#getCoordinateReferenceSystem},
+ * {@link FactoryException} is handled as a warning by {@linkplain
Grid#getCRSFromAxes the caller},
* while {@link DataStoreException} is handled as a fatal error. Warnings are
stored in {@link #warnings} field.
*
* @author Martin Desruisseaux (Geomatys)
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSMerger.java
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSMerger.java
index a5300adb54..b6f09f6e92 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSMerger.java
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/CRSMerger.java
@@ -67,7 +67,8 @@ final class CRSMerger extends GeodeticObjectBuilder {
explicit =
AbstractCRS.castOrCopy(explicit).forConvention(AxesConvention.POSITIVE_RANGE);
}
}
- final CoordinateReferenceSystem result =
super.replaceComponent(implicit, firstDimension, explicit);
- return CRS.equivalent(implicit, result) ? implicit : result;
+ CoordinateReferenceSystem result = super.replaceComponent(implicit,
firstDimension, explicit);
+ if (CRS.equivalent(implicit, result)) result = implicit;
+ return result;
}
}
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
index fd5f608530..a36eb9dcb0 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Convention.java
@@ -535,7 +535,7 @@ public class Convention {
}
/**
- * Returns the <i>grid to CRS</i> transform for the given node. This
method is invoked after call
+ * Returns the <i>grid corner to CRS</i> transform for the given node.
This method is invoked after call
* to {@link #projection(Node)} method resulted in creation of a projected
coordinate reference system.
* The {@linkplain ProjectedCRS#getBaseCRS() base CRS} is fixed to
(latitude, longitude) axes in degrees,
* but the projected CRS axes may have any order and units. In the
particular case of "latitude_longitude"
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
index ae58fee855..24cf3f3707 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Decoder.java
@@ -462,7 +462,7 @@ public abstract class Decoder extends
ReferencingFactoryContainer {
if (list.isEmpty()) {
final var warnings = new ArrayList<Exception>(); // For
internal usage by Grid.
for (final Grid grid : getGridCandidates()) {
- addIfNotPresent(list, grid.getCoordinateReferenceSystem(this,
warnings, null, null));
+ addIfNotPresent(list, grid.getCRSFromAxes(this, warnings,
null, null));
}
}
return list;
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Grid.java
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Grid.java
index 917d296380..d5163847a4 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Grid.java
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Grid.java
@@ -74,7 +74,7 @@ public abstract class Grid extends NamedElement {
* The coordinate reference system, created when first needed.
* May be {@code null} even after we attempted to create it.
*
- * @see #getCoordinateReferenceSystem(Decoder, List, List, Matrix)
+ * @see #getCRSFromAxes(Decoder, List, List, Matrix)
*/
private CoordinateReferenceSystem crs;
@@ -290,7 +290,7 @@ public abstract class Grid extends NamedElement {
* @throws IOException if an I/O operation was necessary but failed.
* @throws DataStoreException if the CRS cannot be constructed.
*/
- final CoordinateReferenceSystem getCoordinateReferenceSystem(final Decoder
decoder, final List<Exception> warnings,
+ final CoordinateReferenceSystem getCRSFromAxes(final Decoder decoder,
final List<Exception> warnings,
final List<GridCacheValue> linearizations, final Matrix
reorderGridToCRS)
throws IOException, DataStoreException
{
@@ -299,12 +299,12 @@ public abstract class Grid extends NamedElement {
return crs;
} else try {
if (useCache) isCRSDetermined = true; // Set now for
avoiding new attempts if creation fail.
- final CoordinateReferenceSystem result =
CRSBuilder.assemble(decoder, this, linearizations, reorderGridToCRS);
+ CoordinateReferenceSystem result = CRSBuilder.assemble(decoder,
this, linearizations, reorderGridToCRS);
if (useCache) crs = result;
return result;
} catch (FactoryException | NullPointerException ex) {
if (isNewWarning(ex, warnings)) {
- canNotCreate(decoder, "getCoordinateReferenceSystem",
Resources.Keys.CanNotCreateCRS_3, ex);
+ canNotCreate(decoder, "getCRSFromAxes",
Resources.Keys.CanNotCreateCRS_3, ex);
}
return null;
}
@@ -343,7 +343,7 @@ public abstract class Grid extends NamedElement {
if (length <= 0) return null;
high[(n-1) - i] = length;
}
- final DimensionNameType[] names = new DimensionNameType[n];
+ final var names = new DimensionNameType[n];
switch (n) {
default: names[1] = DimensionNameType.ROW; // Fall through
case 1: names[0] = DimensionNameType.COLUMN; // Fall through
@@ -381,18 +381,18 @@ public abstract class Grid extends NamedElement {
final GridGeometry getGridGeometry(final Decoder decoder) throws
IOException, DataStoreException {
if (!isGeometryDetermined) try {
isGeometryDetermined = true; // Set now for
avoiding new attempts if creation fail.
- final Axis[] axes = getAxes(decoder); // In CRS order
(reverse of netCDF order).
/*
* Creates the "grid to CRS" transform. The number of columns is
the number of dimensions in the grid
* (the source) +1, and the number of rows is the number of
dimensions in the CRS (the target) +1.
* The order of dimensions in the transform is the reverse of the
netCDF dimension order.
*/
- int lastSrcDim = getSourceDimensions(); // Will be
decremented later, then kept final.
- int lastTgtDim = axes.length; // Should be
`getTargetDimensions()` but some axes may have been excluded.
- final int[] deferred = new int[axes.length]; // Indices of axes
that have been deferred.
- final List<MathTransform> nonLinears = new
ArrayList<>(axes.length);
- final Matrix affine = Matrices.createZero(lastTgtDim + 1,
lastSrcDim + 1);
- affine.setElement(lastTgtDim--, lastSrcDim--, 1);
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ final Axis[] axes = getAxes(decoder); // In CRS
order (reverse of netCDF order).
+ final int[] deferred = new int[axes.length]; // Indices
of axes that have been deferred.
+ final var nonLinears = new
ArrayList<MathTransform>(axes.length);
+ final int lastSrcDim = getSourceDimensions() - 1;
+ final Matrix affine = Matrices.createZero(axes.length + 1,
lastSrcDim + 2);
+ affine.setElement(axes.length, lastSrcDim + 1, 1);
for (int tgtDim=0; tgtDim < axes.length; tgtDim++) {
if (!axes[tgtDim].trySetTransform(affine, lastSrcDim, tgtDim,
nonLinears)) {
deferred[nonLinears.size() - 1] = tgtDim;
@@ -439,8 +439,8 @@ findFree: for (int srcDim :
axis.gridDimensionIndices) {
* two-dimensional localization grid. Those transforms require two
variables, i.e. "two-dimensional"
* axes come in pairs.
*/
- final List<GridCacheValue> linearizations = new ArrayList<>();
- for (int i=0; i<nonLinears.size(); i++) { // Length of
`nonLinears` may change in this loop.
+ final var linearizations = new ArrayList<GridCacheValue>();
+ for (int i=0; i < nonLinears.size(); i++) { // Length of
`nonLinears` may change in this loop.
if (nonLinears.get(i) == null) {
for (int j=i; ++j < nonLinears.size();) {
if (nonLinears.get(j) == null) {
@@ -499,25 +499,21 @@ findFree: for (int srcDim :
axis.gridDimensionIndices) {
* This modification happens only if `Convention.linearizers()`
specified transforms to apply on the
* localization grid for making it more linear. This is a
profile-dependent feature.
*/
- final CoordinateReferenceSystem crs =
getCoordinateReferenceSystem(decoder, null, linearizations, affine);
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ final CoordinateReferenceSystem crs = getCRSFromAxes(decoder,
null, linearizations, affine);
/*
* Final transform, as the concatenation of the non-linear
transforms followed by the affine transform.
* We concatenate the affine transform last because it may change
axis order.
*/
- MathTransform gridToCRS = null;
- final int nonLinearCount = nonLinears.size();
final MathTransformFactory factory =
decoder.getMathTransformFactory();
- // Not a non-linear transform, but we abuse this list for
convenience.
- nonLinears.add(factory.createAffineTransform(affine));
- for (int i=0; i <= nonLinearCount; i++) {
+ MathTransform gridToCRS = factory.createAffineTransform(affine);
+ for (int i = nonLinears.size(); --i >= 0;) {
MathTransform tr = nonLinears.get(i);
if (tr != null) {
- if (i < nonLinearCount) {
- final int srcDim = gridDimensionIndices[i];
- tr = factory.createPassThroughTransform(srcDim, tr,
- (lastSrcDim + 1) - (srcDim +
tr.getSourceDimensions()));
- }
- gridToCRS = (gridToCRS == null) ? tr :
factory.createConcatenatedTransform(gridToCRS, tr);
+ int firstAffectedCoordinate = gridDimensionIndices[i];
+ int numTrailingCoordinates = (lastSrcDim + 1) -
(firstAffectedCoordinate + tr.getSourceDimensions());
+ tr =
factory.createPassThroughTransform(firstAffectedCoordinate, tr,
numTrailingCoordinates);
+ gridToCRS = factory.createConcatenatedTransform(tr,
gridToCRS);
}
}
/*
@@ -550,7 +546,7 @@ findFree: for (int srcDim :
axis.gridDimensionIndices) {
/**
* Logs a warning about a CRS or grid geometry that cannot be created.
*
- * @param caller one of {@code "getCoordinateReferenceSystem"} or {@code
"getGridGeometry"}.
+ * @param caller one of {@code "getCRSFromAxes"} or {@code
"getGridGeometry"}.
* @param key one of {@link Resources.Keys#CanNotCreateCRS_3} or
{@link Resources.Keys#CanNotCreateGridGeometry_3}.
*/
private void canNotCreate(final Decoder decoder, final String caller,
final short key, final Exception ex) {
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridAdjustment.java
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridAdjustment.java
index 7c60e3e333..1f4b6ea9a6 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridAdjustment.java
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/GridAdjustment.java
@@ -184,19 +184,19 @@ public final class GridAdjustment {
/**
* Creates a new grid geometry with a scale factor applied in grid
coordinates before the "grid to CRS" conversion.
*
- * @param grid the grid geometry to scale.
+ * @param geometry the grid geometry to scale.
* @param extent the extent to allocate to the new grid
geometry.
* @param anchor the transform to adjust: "center to CRS" or
"corner to CRS".
* @param dataToGridIndices value of {@link #dataToGridIndices()}.
* @return scaled grid geometry.
*/
- static GridGeometry scale(final GridGeometry grid, final GridExtent
extent, final PixelInCell anchor,
+ static GridGeometry scale(final GridGeometry geometry, final GridExtent
extent, final PixelInCell anchor,
final double[] dataToGridIndices)
{
- MathTransform gridToCRS = grid.getGridToCRS(anchor);
+ MathTransform gridToCRS = geometry.getGridToCRS(anchor);
final LinearTransform scale = MathTransforms.scale(dataToGridIndices);
gridToCRS = MathTransforms.concatenate(scale, gridToCRS);
return new GridGeometry(extent, anchor, gridToCRS,
- grid.isDefined(GridGeometry.CRS) ?
grid.getCoordinateReferenceSystem() : null);
+ geometry.isDefined(GridGeometry.CRS) ?
geometry.getCoordinateReferenceSystem() : null);
}
}
diff --git
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
index 0aed626745..355f2077ab 100644
---
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
@@ -32,12 +32,13 @@ import ucar.nc2.constants.CF; // String constants are
copied by the compil
import ucar.nc2.constants.ACDD; // idem
import javax.measure.Unit;
import javax.measure.quantity.Length;
+import javax.measure.IncommensurableException;
import org.opengis.util.FactoryException;
+import org.opengis.metadata.Identifier;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.referencing.IdentifiedObject;
-import org.opengis.referencing.cs.CartesianCS;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.GeographicCRS;
@@ -46,7 +47,6 @@ import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.Conversion;
-import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.datum.DatumFactory;
import org.opengis.referencing.datum.GeodeticDatum;
import org.opengis.referencing.datum.PrimeMeridian;
@@ -56,10 +56,13 @@ import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.crs.AbstractCRS;
import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.cs.CoordinateSystems;
+import org.apache.sis.referencing.datum.DatumOrEnsemble;
import org.apache.sis.referencing.datum.BursaWolfParameters;
import org.apache.sis.referencing.datum.DefaultGeodeticDatum;
import org.apache.sis.referencing.operation.matrix.Matrix3;
import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.TransformSeparator;
import org.apache.sis.referencing.operation.provider.PseudoPlateCarree;
@@ -105,9 +108,29 @@ final class GridMapping {
/**
* Names of some (not all) attributes where the <abbr>CRS</abbr> may be
encoded in <abbr>WKT</abbr> format.
* Values must be in lower-cases because {@link
Convention#projection(Node)} converts names to lower cases.
+ * {@code "crs_wkt"} is defined by the <abbr>CF</abbr> convention, while
{@code "spatial_ref"} was used in
+ * old versions of <abbr>GDAL</abbr>.
*/
private static final String CRS_WKT = "crs_wkt", SPATIAL_REF =
"spatial_ref";
+ /**
+ * Name of attributes where the "grid to <abbr>CRS</abbr> transform may be
encoded as an affine transform.
+ * The {@code GeoTransform} attribute is specific to <abbr>GDAL</abbr>. It
uses pixel-corner convention and
+ * interprets data as if it was an image (as if the row shown on the top
had index 0), ignoring the netCDF
+ * cell indices (where row 0 is often in the bottom).
+ *
+ * @see #gridToCRS
+ * @see #SOURCE_AXIS_TO_FLIP
+ */
+ private static final String GEOTRANSFORM = "GeoTransform";
+
+ /**
+ * Index of the source axis to flip in a "grid to <abbr>CRS</abbr>"
transform. This is for flipping the
+ * <var>y</var> axis for switching from an image coordinate system to an
arithmetic coordinate system.
+ * The flip requires the number of cells (rows in the case of <var>y</var>
axis) along the axis to flip.
+ */
+ private static final int SOURCE_AXIS_TO_FLIP = 1;
+
/**
* The variable on which projection parameters are defined as attributes.
* This is typically an empty variable referenced by the value of the
@@ -131,9 +154,18 @@ final class GridMapping {
private CoordinateReferenceSystem crs;
/**
- * The <i>grid to CRS</i> transform, or {@code null} if none.
+ * The <i>grid corner to CRS</i> transform, or {@code null} if none.
* This information is usually not specified except when using
<abbr>GDAL</abbr> conventions.
* If {@code null}, then the transform should be inferred by {@link Grid}.
+ *
+ * <h4>Image flip</h4>
+ * If the {@code gridToCRS} transform has been specified by the
<abbr>GDAL</abbr>'s {@value #GEOTRANSFORM}
+ * attribute, then it needs to have the <var>y</var> axis flipped. This is
because <abbr>GDAL</abbr> seems
+ * to ignore the netCDF cell coordinate system and to handle the data has
if it was an image with the last
+ * row (in netCDF order) shown on the top.
+ *
+ * @see #SOURCE_AXIS_TO_FLIP
+ * @see #gridToCRS(Variable)
*/
private MathTransform gridToCRS;
@@ -214,8 +246,12 @@ final class GridMapping {
* @see <a
href="http://cfconventions.org/cf-conventions/cf-conventions.html#grid-mappings-and-projections">CF-conventions</a>
*/
private boolean parseProjectionParameters() {
- final Map<String,Object> definition =
mapping.decoder.convention().projection(mapping);
+ final Map<String, Object> definition =
mapping.decoder.convention().projection(mapping);
if (definition != null) try {
+ // Take only one WKT for now. We will parse the other one later.
+ final var alreadyParsedWKT = new ArrayList<String>(2);
+ if (crs == null) setOrVerifyWKT(definition, CRS_WKT,
alreadyParsedWKT);
+ if (crs == null) setOrVerifyWKT(definition, SPATIAL_REF,
alreadyParsedWKT);
/*
* Fetch now numerical values that are not map projection
parameters.
* This step needs to be done before to try to set parameter
values.
@@ -223,7 +259,7 @@ final class GridMapping {
final Object greenwichLongitude =
definition.remove(Convention.LONGITUDE_OF_PRIME_MERIDIAN);
/*
* Prepare the block of projection parameters. The set of legal
parameter depends on the map projection.
- * We assume that all numerical values are map projection
parameters; character sequences (assumed to be
+ * We assume that all numerical values are map projection
parameters. Character sequences (assumed to be
* component names) are handled later. The CF-conventions use
parameter names that are slightly different
* than OGC names, but Apache SIS implementations of map
projections know how to handle them, including
* the redundant parameters like "inverse_flattening" and
"earth_radius".
@@ -248,7 +284,7 @@ final class GridMapping {
case CRS_WKT:
case SPATIAL_REF: continue; // Will be parsed
after this loop.
case "geotransform": { // "GeoTransform"
made lower-case.
- if (parseGeoTransform(null, text)) {
+ if (parseGeoTransform(null, text)) {
it.remove();
}
continue;
@@ -291,31 +327,39 @@ final class GridMapping {
* But if those information are provided, then we use them for
building the geodetic reference frame.
* Otherwise a default reference frame will be used.
*/
+ final CoordinateReferenceSystem fromWKT = crs;
final boolean geographic = (method instanceof PseudoPlateCarree);
- final GeographicCRS baseCRS = createBaseCRS(mapping.decoder,
parameters, definition, greenwichLongitude, geographic);
+ final GeographicCRS baseCRS = createBaseCRS(mapping.decoder,
fromWKT, parameters, definition, greenwichLongitude, geographic);
final MathTransform baseToCRS;
if (geographic) {
// Only swap axis order from (latitude, longitude) to
(longitude, latitude).
baseToCRS = MathTransforms.linear(new Matrix3(0, 1, 0, 1, 0,
0, 0, 0, 1));
crs = baseCRS;
} else {
- final CoordinateOperationFactory opFactory =
mapping.decoder.getCoordinateOperationFactory();
- Map<String,?> properties = properties(definition,
Convention.CONVERSION_NAME, false, mapping.getName());
- final Conversion conversion =
opFactory.createDefiningConversion(properties, method, parameters);
- final CartesianCS cs =
mapping.decoder.getStandardProjectedCS();
- properties = properties(definition,
Convention.PROJECTED_CRS_NAME, true, conversion);
- final ProjectedCRS p =
mapping.decoder.getCRSFactory().createProjectedCRS(properties, baseCRS,
conversion, cs);
- baseToCRS = p.getConversionFromBase().getMathTransform();
- crs = p;
+ // Create a projected CRS.
+ Supplier<Object> nameFallback = () -> (fromWKT instanceof
ProjectedCRS) ?
+ ((ProjectedCRS)
fromWKT).getConversionFromBase().getName() : mapping.getName();
+ Map<String,?> properties = properties(definition,
Convention.CONVERSION_NAME, nameFallback, false);
+ final Decoder decoder = mapping.decoder;
+ final Conversion conversion =
decoder.getCoordinateOperationFactory()
+ .createDefiningConversion(properties, method,
parameters);
+
+ nameFallback = () -> (fromWKT != null) ? fromWKT.getName() :
conversion.getName();
+ properties = properties(definition,
Convention.PROJECTED_CRS_NAME, nameFallback, true);
+ final ProjectedCRS projected = decoder.getCRSFactory()
+ .createProjectedCRS(properties, baseCRS, conversion,
decoder.getStandardProjectedCS());
+
+ baseToCRS =
projected.getConversionFromBase().getMathTransform();
+ crs = projected;
}
/*
* The CF-Convention said that even if a WKT definition is
provided, other attributes shall be present
* and have precedence over the WKT definition. Consequently, the
purpose of WKT in netCDF files is not
* obvious (except for CompoundCRS).
*/
- final var done = new ArrayList<String>(2);
- setOrVerifyWKT(definition, CRS_WKT, done);
- setOrVerifyWKT(definition, SPATIAL_REF, done);
+ if (fromWKT != null) verifyCRS(fromWKT);
+ setOrVerifyWKT(definition, CRS_WKT, alreadyParsedWKT);
+ setOrVerifyWKT(definition, SPATIAL_REF, alreadyParsedWKT);
/*
* Report all projection parameters that have not been used. If
the map is not rendered
* at expected location, it may be because we have ignored some
important parameters.
@@ -331,12 +375,13 @@ final class GridMapping {
*/
if (gridToCRS == null) {
gridToCRS = mapping.decoder.convention().gridToCRS(mapping,
baseToCRS);
+ // Map pixel corners by `convention().gridToCRS(…)` contract.
} else {
gridToCRS = MathTransforms.concatenate(gridToCRS, baseToCRS);
}
return true;
} catch (ClassCastException | IllegalArgumentException |
FactoryException | TransformException e) {
- warningInMapping(mapping, e, Resources.Keys.CanNotCreateCRS_3,
null);
+ cannotCreateGridOrCRS(mapping, e, false);
}
return false;
}
@@ -345,12 +390,18 @@ final class GridMapping {
* Creates the geographic CRS from axis length specified in the given map
projection parameters.
* The returned CRS will always have (latitude, longitude) axes in that
order and in degrees.
*
+ * @param decoder the decoder from which to get factories,
conventions and listeners.
+ * @param fromWKT the CRS parsed from WKT if any. Used for fetching
default object names.
* @param parameters parameters from which to get ellipsoid axis
lengths. Will not be modified.
* @param definition map from which to get element names. Elements used
will be removed.
- * @param main whether the returned <abbr>CRS</abbr> will be the
main one.
+ * @param isMainCRS whether the returned <abbr>CRS</abbr> will be the
main one.
*/
- private static GeographicCRS createBaseCRS(final Decoder decoder, final
ParameterValueGroup parameters,
- final Map<String,Object> definition, final Object
greenwichLongitude, final boolean main)
+ private static GeographicCRS createBaseCRS(final Decoder
decoder,
+ final CoordinateReferenceSystem
fromWKT,
+ final ParameterValueGroup
parameters,
+ final Map<String,Object>
definition,
+ final Object
greenwichLongitude,
+ final boolean
isMainCRS)
throws FactoryException
{
final DatumFactory datumFactory = decoder.getDatumFactory();
@@ -362,8 +413,9 @@ final class GridMapping {
final PrimeMeridian meridian;
if (greenwichLongitude instanceof Number) {
final double longitude = ((Number)
greenwichLongitude).doubleValue();
- final String name = (longitude == 0) ? "Greenwich" : null;
- Map<String,?> properties = properties(definition,
Convention.PRIME_MERIDIAN_NAME, false, name);
+ Supplier<Object> nameFallback = () ->
DatumOrEnsemble.getPrimeMeridian(fromWKT)
+ .<Object>map(PrimeMeridian::getName).orElse(longitude == 0
? "Greenwich" : null);
+ Map<String,?> properties = properties(definition,
Convention.PRIME_MERIDIAN_NAME, nameFallback, false);
meridian = datumFactory.createPrimeMeridian(properties, longitude,
Units.DEGREE);
isSpecified = true;
} else {
@@ -390,17 +442,19 @@ final class GridMapping {
secondDefiningParameter =
parameters.parameter(Constants.SEMI_MINOR).doubleValue(axisUnit);
isSphere = secondDefiningParameter == semiMajor;
}
- final Supplier<Object> fallback = () -> { // Default
ellipsoid name if not specified.
- final Locale locale = decoder.listeners.getLocale();
- final NumberFormat f = NumberFormat.getNumberInstance(locale);
- f.setMaximumFractionDigits(5); // Centimetric precision.
- final double km =
axisUnit.getConverterTo(Units.KILOMETRE).convert(semiMajor);
- final StringBuffer b = new StringBuffer()
-
.append(Vocabulary.forLocale(locale).getString(isSphere ?
Vocabulary.Keys.Sphere : Vocabulary.Keys.Ellipsoid))
- .append(isSphere ? " R=" : " a=");
- return f.format(km, b, new FieldPosition(0)).append("
km").toString();
+ final Supplier<Object> nameFallback = () -> { // Default
ellipsoid name if not specified.
+ return
DatumOrEnsemble.getEllipsoid(fromWKT).<Object>map(Ellipsoid::getName).orElseGet(()
-> {
+ final Locale locale = decoder.getLocale();
+ final String name =
Vocabulary.forLocale(locale).getString(isSphere ? Vocabulary.Keys.Sphere :
Vocabulary.Keys.Ellipsoid);
+ final NumberFormat f =
NumberFormat.getNumberInstance(locale);
+ f.setMaximumFractionDigits(5); // Centimetric
precision.
+ return
f.format(axisUnit.getConverterTo(Units.KILOMETRE).convert(semiMajor),
+ new StringBuffer(name).append(isSphere ? "
R=" : " a="),
+ new FieldPosition(0))
+ .append(" km").toString();
+ });
};
- final Map<String,?> properties = properties(definition,
Convention.ELLIPSOID_NAME, false, fallback);
+ final Map<String,?> properties = properties(definition,
Convention.ELLIPSOID_NAME, nameFallback, false);
if (isIvfDefinitive) {
ellipsoid = datumFactory.createFlattenedSphere(properties,
semiMajor, secondDefiningParameter, axisUnit);
} else {
@@ -417,8 +471,9 @@ final class GridMapping {
final Object bursaWolf = definition.remove(Convention.TOWGS84);
final GeodeticDatum datum;
DatumEnsemble<GeodeticDatum> ensemble = null;
- if (isSpecified | bursaWolf != null) {
- Map<String,Object> properties = properties(definition,
Convention.GEODETIC_DATUM_NAME, false, ellipsoid);
+ if (isSpecified || bursaWolf != null) {
+ Supplier<Object> nameFallback = () ->
CRS.getGeodeticReferenceFrame(fromWKT).map(GeodeticDatum::getName).orElse(null);
+ Map<String,Object> properties = properties(definition,
Convention.GEODETIC_DATUM_NAME, nameFallback, false);
if (bursaWolf instanceof BursaWolfParameters) {
properties = new HashMap<>(properties);
properties.put(DefaultGeodeticDatum.BURSA_WOLF_KEY, bursaWolf);
@@ -435,7 +490,8 @@ final class GridMapping {
* Geographic CRS from all above properties.
*/
if (isSpecified) {
- final Map<String,?> properties = properties(definition,
Convention.GEOGRAPHIC_CRS_NAME, main, datum);
+ Supplier<Object> nameFallback = () -> (fromWKT != null ? fromWKT :
datum).getName();
+ Map<String,?> properties = properties(definition,
Convention.GEOGRAPHIC_CRS_NAME, nameFallback, isMainCRS);
return decoder.getCRSFactory().createGeographicCRS(
properties,
datum,
@@ -453,30 +509,27 @@ final class GridMapping {
*
* @param definition map containing the attribute values.
* @param nameAttribute name of the attribute from which to get the name.
+ * @param nameFallback can return {@link String}, {@link Identifier} or
{@code null}.
* @param takeComment whether to consume the {@code comment} attribute.
- * @param fallback fallback as an {@link IdentifiedObject} (from
which the name will be copied),
- * or a character sequence, or {@code null} for
"Unnamed" localized string.
*/
- private static Map<String,Object> properties(final Map<String,Object>
definition, final String nameAttribute,
- final boolean takeComment,
final Object fallback)
+ private static Map<String,Object> properties(final Map<String,Object>
definition,
+ final String
nameAttribute,
+ final Supplier<?>
nameFallback,
+ final boolean
takeComment)
{
Object name = definition.remove(nameAttribute);
if (name == null) {
- if (fallback == null) {
+ name = nameFallback.get();
+ if (name == null) {
// Note: IdentifiedObject.name does not accept
InternationalString.
name = Vocabulary.format(Vocabulary.Keys.Unnamed);
- } else if (fallback instanceof IdentifiedObject) {
- name = ((IdentifiedObject) fallback).getName();
- } else if (fallback instanceof Supplier<?>) {
- name = ((Supplier<?>) fallback).get();
- } else {
- name = fallback.toString();
}
}
if (takeComment) {
Object comment = definition.remove(ACDD.comment);
if (comment != null) {
- return Map.of(IdentifiedObject.NAME_KEY, name,
IdentifiedObject.REMARKS_KEY, comment.toString());
+ return Map.of(IdentifiedObject.NAME_KEY, name,
+ IdentifiedObject.REMARKS_KEY, comment);
}
}
return Map.of(IdentifiedObject.NAME_KEY, name);
@@ -491,6 +544,7 @@ final class GridMapping {
* @param attributeName name of the attribute to consume in the
definition map.
* @param done <abbr>WKT</abbr> already parsed, for avoiding
repetition.
*/
+ @SuppressWarnings("UseSpecificCatch")
private void setOrVerifyWKT(final Map<String,Object> definition, final
String attributeName, final List<String> done) {
Object value = definition.remove(attributeName);
if (value instanceof String) {
@@ -501,26 +555,38 @@ final class GridMapping {
}
}
done.add(wkt);
- CoordinateReferenceSystem check;
+ CoordinateReferenceSystem fromWKT;
try {
- check = createFromWKT((String) value);
+ fromWKT = createFromWKT((String) value);
} catch (Exception e) {
warning(mapping, e, mapping.errors(),
Errors.Keys.CanNotParseCRS_1, attributeName);
return;
}
if (crs == null) {
- crs = check;
- } else if (!Utilities.deepEquals(crs, check,
ComparisonMode.ALLOW_VARIANT)) {
- warning(mapping, // Node
- null, // Exception
- null, // Resources
- Resources.Keys.InconsistentCRS_2,
- mapping.decoder.getFilename(),
- mapping.getName());
+ crs = fromWKT;
+ } else {
+ verifyCRS(fromWKT);
}
}
}
+ /**
+ * Verifies that the given <abbr>CRS</abbr> is consistent with the {@link
#crs} attribute.
+ * If not, a warning will be logger. This method does not change the state
of this object.
+ *
+ * @param fromWKT the object parsed from <abbr>WKT</abbr>.
+ */
+ private void verifyCRS(final CoordinateReferenceSystem fromWKT) {
+ if (!Utilities.deepEquals(crs, fromWKT, ComparisonMode.ALLOW_VARIANT))
{
+ warning(mapping, // Node
+ null, // Exception
+ null, // Resources
+ Resources.Keys.InconsistentCRS_2,
+ mapping.decoder.getFilename(),
+ mapping.getName());
+ }
+ }
+
/**
* Tries to parse a CRS and affine transform from GDAL GeoTransform
coefficients.
* Those coefficients are not in the usual order expected by matrix, affine
@@ -536,14 +602,17 @@ final class GridMapping {
*/
private boolean parseGeoTransform() {
return parseGeoTransform(mapping.getAttributeAsString(SPATIAL_REF),
- mapping.getAttributeAsString("GeoTransform"));
+ mapping.getAttributeAsString(GEOTRANSFORM));
}
/**
* Implementation of {@link #parseGeoTransform()} with given attribute
values.
+ * Used for parsing the <abbr>GDAL</abbr>'s {@value #GEOTRANSFORM}
attribute.
+ * Results is stored in {@link #gridToCRS}.
*/
+ @SuppressWarnings("UseSpecificCatch")
private boolean parseGeoTransform(final String wkt, final String gtr) {
- short message = Resources.Keys.CanNotCreateCRS_3;
+ boolean grid = false;
boolean done = false;
try {
if (wkt != null) {
@@ -552,16 +621,21 @@ final class GridMapping {
done = true;
}
if (gtr != null) {
- message = Resources.Keys.CanNotCreateGridGeometry_3;
+ grid = true;
final double[] c = parseDoubles(gtr);
if (c.length != 6) {
throw new
DataStoreContentException(mapping.errors().getString(Errors.Keys.UnexpectedArrayLength_2,
6, c.length));
}
- gridToCRS = new AffineTransform2D(c[1], c[4], c[2], c[5],
c[0], c[3]); // X_DIMENSION, Y_DIMENSION
+ /*
+ * GDAL convention maps pixel corners and see the data as if
it was an image.
+ * The row which is visually on the top is handled as if its
index was zero,
+ * ignoring the fact that this is usually the last row in a
netCDF variable.
+ */
+ gridToCRS = new AffineTransform2D(c[1], c[4], c[2], c[5],
c[0], c[3]); // X_DIMENSION, Y_DIMENSION
done = true;
}
} catch (Exception e) {
- warningInMapping(mapping, e, message, null);
+ cannotCreateGridOrCRS(mapping, e, grid);
}
return done;
}
@@ -581,6 +655,7 @@ final class GridMapping {
*
* @return whether this method found grid geometry attributes.
*/
+ @SuppressWarnings("UseSpecificCatch")
private boolean parseESRI() {
String code = mapping.getAttributeAsString("ESRI_pe_string");
isWKT = (code != null);
@@ -604,7 +679,7 @@ final class GridMapping {
crs = CRS.forCode(Constants.EPSG + ':' + code);
}
} catch (Exception e) {
- warningInMapping(mapping, e, Resources.Keys.CanNotCreateCRS_3,
null);
+ cannotCreateGridOrCRS(mapping, e, false);
return false;
}
return true;
@@ -629,6 +704,19 @@ final class GridMapping {
return parsed;
}
+ /**
+ * Logs a warning with a message saying that we cannot create the grid or
the <abbr>CRS</abbr>.
+ *
+ * @param mapping the variable on which the warning applies.
+ * @param ex the exception that occurred while creating the CRS or
grid geometry.
+ * @param grid {@code grid} if creating the whole grid, or {@code
false} for only the <abbr>CRS</abbr>.
+ */
+ private static void cannotCreateGridOrCRS(final Node mapping, final
Exception ex, final boolean grid) {
+ warningInMapping(mapping, ex,
+ grid ? Resources.Keys.CanNotCreateGridGeometry_3 :
Resources.Keys.CanNotCreateCRS_3,
+ ex.getLocalizedMessage());
+ }
+
/**
* Logs a warning with a message that contains the netCDF file name and
the mapping variable, in that order.
* This method presumes that {@link GridMapping} are invoked (indirectly)
from {@link Variable#getGridGeometry()}.
@@ -636,12 +724,9 @@ final class GridMapping {
* @param mapping the variable on which the warning applies.
* @param ex the exception that occurred while creating the CRS or
grid geometry, or {@code null} if none.
* @param key {@link Resources.Keys#CanNotCreateCRS_3} or {@link
Resources.Keys#CanNotCreateGridGeometry_3}.
- * @param more an additional argument for localization, or {@code
null} for the exception message.
+ * @param more an additional argument for localization, or {@code
null}.
*/
private static void warningInMapping(final Node mapping, final Exception
ex, final short key, String more) {
- if (more == null) {
- more = ex.getLocalizedMessage();
- }
warning(mapping, ex, null, key, mapping.decoder.getFilename(),
mapping.getName(), more);
}
@@ -665,11 +750,35 @@ final class GridMapping {
return crs;
}
+ /**
+ * Returns the "grid to CRS", handling the reversal of <var>y</var> axis
direction.
+ *
+ * @param variable the variable for which to obtain the transform.
+ * @return the transform for the given variable.
+ */
+ private MathTransform gridToCRS(final Variable variable) {
+ MathTransform implicitG2C = gridToCRS;
+ if (implicitG2C != null) {
+ final int yDim = variable.getNumDimensions() - (1 +
SOURCE_AXIS_TO_FLIP);
+ if (yDim >= 0) {
+ final long height =
variable.getGridDimensions().get(yDim).length();
+ if (height >= 0) { // Negative if undetermined length.
+ final int srcDim = gridToCRS.getSourceDimensions();
+ final MatrixSIS m = Matrices.createIdentity(srcDim + 1);
+ m.setElement(SOURCE_AXIS_TO_FLIP, SOURCE_AXIS_TO_FLIP, -1);
+ m.setElement(SOURCE_AXIS_TO_FLIP, srcDim, height);
+ implicitG2C =
MathTransforms.concatenate(MathTransforms.linear(m), implicitG2C);
+ }
+ }
+ }
+ return implicitG2C;
+ }
+
/**
* Creates a new grid geometry with the extent of the given variable and a
potentially null <abbr>CRS</abbr>.
* This method should be invoked only as a fallback when no existing
{@link GridGeometry} can be used.
* The CRS and "grid to CRS" transform are null, unless some partial
information was found for example
- * as WKT string.
+ * as <abbr>WKT</abbr> string.
*/
final GridGeometry createGridCRS(final Variable variable) {
final List<Dimension> dimensions = variable.getGridDimensions();
@@ -679,25 +788,25 @@ final class GridMapping {
final int d = (srcDim - 1) - i; // Convert CRS dimension
to netCDF dimension.
upper[i] = dimensions.get(d).length();
}
- MathTransform implicitG2C = gridToCRS;
+ MathTransform implicitG2C = gridToCRS(variable);
CoordinateReferenceSystem implicitCRS = crs;
if (implicitG2C != null) {
- implicitG2C = MathTransforms.concatenate(
- changeOfDimension(srcDim,
implicitG2C.getSourceDimensions()),
- implicitG2C,
- changeOfDimension(implicitG2C.getTargetDimensions(),
-
ReferencingUtilities.getDimension(implicitCRS)));
+ final int tgtDim =
ReferencingUtilities.getOptionalDimension(implicitCRS).orElse(srcDim);
+ MathTransform step1 = changeOfDimension(srcDim,
implicitG2C.getSourceDimensions());
+ MathTransform step3 =
changeOfDimension(implicitG2C.getTargetDimensions(), tgtDim);
+ implicitG2C = MathTransforms.concatenate(step1, implicitG2C,
step3);
}
final var extent = new GridExtent(null, null, upper, false);
- return new GridGeometry(extent, PixelInCell.CELL_CENTER, implicitG2C,
implicitCRS);
+ return new GridGeometry(extent, PixelInCell.CELL_CORNER, implicitG2C,
implicitCRS);
}
/**
* Returns a transform for changing the number of dimensions of a math
transform.
- * For convenience, a target number of dimensions of 0 means no change.
+ * If the number of dimensions is increased, new coordinates are
initialized to zero.
+ * If the number of dimensions is decreased, the last coordinates are
dropped.
*/
private static MathTransform changeOfDimension(final int srcDim, final int
tgtDim) {
- if (tgtDim == srcDim || tgtDim == 0) {
+ if (tgtDim == srcDim) {
return MathTransforms.identity(srcDim);
}
return MathTransforms.linear(Matrices.createDimensionSelect(srcDim,
ArraysExt.range(0, tgtDim)));
@@ -714,12 +823,17 @@ final class GridMapping {
* @param anchor whether we computed "grid to CRS" transform relative
to pixel center or pixel corner.
* @return the grid geometry with modified CRS and "grid to CRS"
transform, or {@code null} in case of failure.
*/
- final GridGeometry adaptGridCRS(final Variable variable, final
GridGeometry implicit, final PixelInCell anchor) {
+ final GridGeometry adaptGridCRS(final Variable variable, final
GridGeometry implicit, PixelInCell anchor) {
/*
* The CRS and grid geometry built from grid mapping attributes are
called "explicit" in this method.
* This is by contrast with CRS derived from coordinate variables,
which is only implicit.
*/
CoordinateReferenceSystem explicitCRS = crs;
+ MathTransform explicitG2C = gridToCRS(variable);
+ if (explicitG2C != null) {
+ // GDAL "GeoTransform" uses pixel corner convention.
+ anchor = PixelInCell.CELL_CORNER;
+ }
int firstAffectedCoordinate = 0;
boolean isSameGrid = true;
if (implicit.isDefined(GridGeometry.CRS)) {
@@ -757,7 +871,7 @@ final class GridMapping {
explicitCRS = new CRSMerger(variable.decoder)
.replaceComponent(implicitCRS,
firstAffectedCoordinate, explicitCRS);
} catch (FactoryException e) {
- warningInMapping(variable, e,
Resources.Keys.CanNotCreateCRS_3, null);
+ cannotCreateGridOrCRS(variable, e, false);
return null;
}
isSameGrid = implicitCRS.equals(explicitCRS);
@@ -765,6 +879,17 @@ final class GridMapping {
explicitCRS = implicitCRS; // Keep existing
instance if appropriate.
}
}
+ /*
+ * If we have run the `AbstractCRS.castOrCopy(…).forConvention(…)`
code above, the axis order of the CRS
+ * may be different than the axis order which was assumed when the
"grid to CRS" transform was built.
+ */
+ if (explicitCRS != crs && explicitG2C != null) try {
+ var swap =
CoordinateSystems.swapAndScaleAxes(crs.getCoordinateSystem(),
explicitCRS.getCoordinateSystem());
+ explicitG2C = MathTransforms.concatenate(explicitG2C,
MathTransforms.linear(swap));
+ } catch (IllegalArgumentException | IncommensurableException e) {
+ cannotCreateGridOrCRS(variable, e, false);
+ return null;
+ }
}
/*
* Perform the same substitution as above, but in the "grid to CRS"
transform. Note that the "grid to CRS"
@@ -772,36 +897,21 @@ final class GridMapping {
* then we need to perform selection in target dimensions (not source
dimensions) because the first affected
* coordinate computed above is in CRS dimension, which is the target
of "grid to CRS" transform.
*/
- MathTransform explicitG2C = gridToCRS;
if (implicit.isDefined(GridGeometry.GRID_TO_CRS)) {
final MathTransform implicitG2C = implicit.getGridToCRS(anchor);
if (explicitG2C == null) {
explicitG2C = implicitG2C;
} else try {
- int count = 0;
- var components = new MathTransform[3];
final var sep = new TransformSeparator(implicitG2C,
variable.decoder.getMathTransformFactory());
- if (firstAffectedCoordinate != 0) {
- sep.addTargetDimensionRange(0, firstAffectedCoordinate);
- components[count++] = sep.separate();
- sep.clear();
- }
- components[count++] = explicitG2C;
- final int next = firstAffectedCoordinate +
explicitG2C.getTargetDimensions();
- final int upper = implicitG2C.getTargetDimensions();
- if (next != upper) {
- sep.addTargetDimensionRange(next, upper);
- components[count++] = sep.separate();
- }
- components = ArraysExt.resize(components, count);
- explicitG2C = MathTransforms.compound(components);
- if (implicitG2C.equals(explicitG2C)) {
- explicitG2C = implicitG2C; // Keep using existing
instance if appropriate.
- } else {
+ final int end = firstAffectedCoordinate +
explicitG2C.getTargetDimensions();
+ explicitG2C = sep.replace(firstAffectedCoordinate, end,
explicitG2C);
+ if (explicitG2C != implicitG2C) {
isSameGrid = false;
+ warningInMapping(variable, null,
Resources.Keys.InconsistentTransform_3, GEOTRANSFORM);
+ // In current version, GDAL's GeoTransform is the only
supported attribute.
}
} catch (FactoryException e) {
- warningInMapping(variable, e,
Resources.Keys.CanNotCreateGridGeometry_3, null);
+ cannotCreateGridOrCRS(variable, e, true);
return null;
}
}
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
index 3c00b4eb30..0ca276a300 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/Variable.java
@@ -574,8 +574,8 @@ public abstract class Variable extends Node {
* the variable has a vertical or temporal axis which has not been
decimated contrarily
* to longitude and latitude axes. Note that this map is recycled
later for other use.
*/
- final List<Variable> axes = new ArrayList<>();
- final Map<Object,Dimension> domain = new HashMap<>();
+ final var axes = new ArrayList<Variable>();
+ final var domain = new HashMap<Object, Dimension>();
for (final Variable candidate : decoder.getVariables()) {
if (candidate.getRole() == VariableRole.AXIS) {
axes.add(candidate);
@@ -750,7 +750,7 @@ public abstract class Variable extends Node {
GridExtent extent = grid.getExtent();
final var sizes = new long[extent.getDimension()];
boolean needsResize = false;
- for (int i=sizes.length; --i >= 0;) {
+ for (int i = sizes.length; --i >= 0;) {
final int d = (sizes.length - 1) - i;
// Convert "natural order" index into netCDF index.
sizes[i] = dimensions.get(d).length();
if (!needsResize) {
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java
index 4d665bedea..5c37f894fb 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.java
@@ -148,6 +148,12 @@ public class Resources extends IndexedResourceBundle {
*/
public static final short InconsistentCRS_2 = 29;
+ /**
+ * The “{2}” attribute does not match the transform inferred from the
axes of “{1}” in the
+ * “{0}” netCDF file.
+ */
+ public static final short InconsistentTransform_3 = 30;
+
/**
* Attributes “{1}” and “{2}” on variable “{0}” have different
lengths: {3} and {4}
* respectively.
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
index 46408eec38..2c64238ed4 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources.properties
@@ -36,6 +36,7 @@ DuplicatedAxisType_4 = Axes \u201c{2}\u201d and
\u201c{3}\u201d hav
IllegalAttributeValue_3 = Illegal value \u201c{2}\u201d for
attribute \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
IllegalValueRange_4 = Illegal value range {2,number} \u2026
{3,number} for variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
InconsistentCRS_2 = The CRS declared by WKT is inconsistent
with the attributes of \u201c{1}\u201d in the \u201c{0}\u201d netCDF file.
+InconsistentTransform_3 = The \u201c{2}\u201d attribute does not
match the transform inferred from the axes of \u201c{1}\u201d in the
\u201c{0}\u201d netCDF file.
GridLongitudeSpanTooWide_2 = The grid spans {0}\u00b0 of longitude,
which may be too wide for the \u201c{1}\u201d domain.
MismatchedAttributeLength_5 = Attributes \u201c{1}\u201d and
\u201c{2}\u201d on variable \u201c{0}\u201d have different lengths: {3} and {4}
respectively.
MismatchedVariableSize_3 = The declared size of variable
\u201c{1}\u201d in netCDF file \u201c{0}\u201d is {2,number} bytes greater than
expected.
diff --git
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties
index 4d6ea7da30..6e94aba15d 100644
---
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties
+++
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/internal/Resources_fr.properties
@@ -41,6 +41,7 @@ DuplicatedAxisType_4 = Les axes
\u00ab\u202f{2}\u202f\u00bb et \u00
IllegalAttributeValue_3 = La valeur \u00ab\u202f{2}\u202f\u00bb est
ill\u00e9gale pour l\u2019attribut \u00ab\u202f{1}\u202f\u00bb dans le fichier
netCDF \u00ab\u202f{0}\u202f\u00bb.
IllegalValueRange_4 = Plage de valeurs {2,number} \u2026
{3,number} ill\u00e9gale pour la variable \u00ab\u202f{1}\u202f\u00bb dans le
fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
InconsistentCRS_2 = Le syst\u00e8me de r\u00e9f\u00e9rence
d\u00e9clar\u00e9 par WKT est incoh\u00e9rent avec les attributs de
\u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
+InconsistentTransform_3 = L\u2019attribut
\u00ab\u202f{2}\u202f\u00bb ne correspond pas \u00e0 la transformation
d\u00e9riv\u00e9e des axes de la variable \u00ab\u202f{1}\u202f\u00bb du
fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
GridLongitudeSpanTooWide_2 = La grille s\u2019\u00e9tend sur {0}\u00b0
de longitude, ce qui peut \u00eatre trop pour le domaine de
\u00ab\u202f{1}\u202f\u00bb.
MismatchedAttributeLength_5 = Les attributs \u201c{1}\u201d et
\u201c{2}\u201d de la variable \u201c{0}\u201d ont des longueurs
diff\u00e9rentes\u00a0: {3} et {4} respectivement.
MismatchedVariableSize_3 = La longueur d\u00e9clar\u00e9e de la
variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF
\u00ab\u202f{0}\u202f\u00bb d\u00e9passe de {2,number} octets la valeur
attendue.