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 a00f7dd Fix an axis order problem when a linearization (currently a
UTM map projection) is applied on the localization grid of a netCDF file.
Before this fix, geographic bounding box were wrong because map projection
applied on coordinates in the wrong order.
a00f7dd is described below
commit a00f7dd2ef67f0ec781a825feae3123d7eeabe3a
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Mar 9 23:51:44 2021 +0100
Fix an axis order problem when a linearization (currently a UTM map
projection) is applied on the localization grid of a netCDF file.
Before this fix, geographic bounding box were wrong because map projection
applied on coordinates in the wrong order.
---
.../internal/referencing/ReferencingUtilities.java | 14 ++
.../operation/builder/LinearTransformBuilder.java | 8 +
.../operation/builder/LocalizationGridBuilder.java | 185 +++++++++++++--------
.../apache/sis/internal/earth/netcdf/GCOM_C.java | 9 +-
.../org/apache/sis/internal/netcdf/CRSBuilder.java | 34 ++--
.../java/org/apache/sis/internal/netcdf/Grid.java | 32 ++--
.../apache/sis/internal/netcdf/GridCacheKey.java | 5 +-
.../apache/sis/internal/netcdf/GridCacheValue.java | 45 ++---
.../org/apache/sis/internal/netcdf/Linearizer.java | 60 +++++--
9 files changed, 254 insertions(+), 138 deletions(-)
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
index 03c02a2..c174b14 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
@@ -365,6 +365,20 @@ public final class ReferencingUtilities extends Static {
}
/**
+ * Returns {@code true} if the given coordinate system has at least 2
dimensions and
+ * the 2 first axis have (North, East) directions. This method is used for
assertions.
+ *
+ * @param cs the coordinate system to verify.
+ * @return whether the coordinate system starts with (North, East)
direction.
+ */
+ public static boolean startsWithNorthEast(final CoordinateSystem cs) {
+ final int dimension = cs.getDimension();
+ return (dimension >= 2)
+ && AxisDirection.NORTH.equals(cs.getAxis(0).getDirection())
+ && AxisDirection.EAST .equals(cs.getAxis(1).getDirection());
+ }
+
+ /**
* Returns the properties of the given object but potentially with a
modified name.
* Current implement truncates the name at the first non-white character
which is not
* a valid Unicode identifier part, with the following exception:
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
index 1f8a882..46377c4 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LinearTransformBuilder.java
@@ -1545,6 +1545,14 @@ search: for (int j=domain(); --j >= 0;) {
}
/**
+ * Returns the transform computed by {@link #create(MathTransformFactory)},
+ * of {@code null} if that method has not yet been invoked.
+ */
+ final LinearTransform transform() {
+ return transform;
+ }
+
+ /**
* Returns a string representation of this builder for debugging purpose.
* Current implementation shows the following information:
*
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
index fca7409..6c5b6d2 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilder.java
@@ -103,13 +103,13 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* The transform for the linear part.
* Always created with a grid size specified to the constructor.
*/
- private final LinearTransformBuilder linear;
+ private final LinearTransformBuilder linearBuilder;
/**
* A temporary array for two-dimensional source coordinates.
* Used for reducing object allocations.
*/
- private final int[] tmp = new int[SOURCE_DIMENSION];
+ private final int[] gridCoordinates = new int[SOURCE_DIMENSION];
/**
* Conversions from source real-world coordinates to grid indices before
interpolation.
@@ -141,7 +141,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
/**
* If the coordinates in some dimensions are cyclic, their periods.
Otherwise {@code null}.
* Values are in units of the target CRS. For longitude wraparounds, the
period is typically 360°.
- * Array length shall be {@code linear.getTargetDimensions()} and
non-cyclic dimensions shall have
+ * Array length shall be {@code linearBuilder.getTargetDimensions()} and
non-cyclic dimensions shall have
* a period of zero (not {@link Double#NaN}, because we will use this
array as a displacement vector).
*
* @see #resolveWraparoundAxis(int, int, double)
@@ -156,8 +156,8 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* @param height the number of rows in the grid of target positions.
*/
public LocalizationGridBuilder(final int width, final int height) {
- linear = new LinearTransformBuilder(width, height);
- sourceToGrid = MathTransforms.identity(SOURCE_DIMENSION);
+ linearBuilder = new LinearTransformBuilder(width, height);
+ sourceToGrid = MathTransforms.identity(SOURCE_DIMENSION);
}
/**
@@ -180,7 +180,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
final Matrix fromGrid = new Matrix3();
final int width = infer(sourceX, fromGrid, 0);
final int height = infer(sourceY, fromGrid, 1);
- linear = new LinearTransformBuilder(width, height);
+ linearBuilder = new LinearTransformBuilder(width, height);
try {
sourceToGrid = MathTransforms.linear(fromGrid).inverse();
} catch (NoninvertibleTransformException e) {
@@ -218,8 +218,8 @@ public class LocalizationGridBuilder extends
TransformBuilder {
ArgumentChecks.ensureNonNull("localizations", localizations);
int n = localizations.getGridDimensions();
if (n == SOURCE_DIMENSION) {
- linear = localizations;
- sourceToGrid = MathTransforms.identity(SOURCE_DIMENSION);
+ linearBuilder = localizations;
+ sourceToGrid = MathTransforms.identity(SOURCE_DIMENSION);
} else {
if (n < 0) {
final Vector[] sources = localizations.sources();
@@ -228,14 +228,14 @@ public class LocalizationGridBuilder extends
TransformBuilder {
final Matrix fromGrid = new Matrix3();
final int width = infer(sources[0], fromGrid, 0);
final int height = infer(sources[1], fromGrid, 1);
- linear = new LinearTransformBuilder(width, height);
- linear.setControlPoints(localizations.getControlPoints());
+ linearBuilder = new LinearTransformBuilder(width, height);
+
linearBuilder.setControlPoints(localizations.getControlPoints());
try {
sourceToGrid =
MathTransforms.linear(fromGrid).inverse();
} catch (NoninvertibleTransformException e) {
throw (ArithmeticException) new
ArithmeticException(e.getLocalizedMessage()).initCause(e);
}
- linear.setLinearizers(localizations);
+ linearBuilder.setLinearizers(localizations);
return;
}
}
@@ -296,7 +296,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* Throws {@link IllegalStateException} if this builder can not be
modified anymore.
*/
private void ensureModifiable() throws IllegalStateException {
- if (!linear.isModifiable()) {
+ if (!linearBuilder.isModifiable()) {
throw new
IllegalStateException(Errors.format(Errors.Keys.UnmodifiableObject_1,
LocalizationGridBuilder.class));
}
}
@@ -428,7 +428,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* @since 1.0
*/
public Envelope getSourceEnvelope(final boolean fullArea) throws
TransformException {
- Envelope envelope = linear.getSourceEnvelope();
+ Envelope envelope = linearBuilder.getSourceEnvelope();
if (fullArea) {
for (int i = envelope.getDimension(); --i >= 0;) {
final GeneralEnvelope ge =
GeneralEnvelope.castOrCopy(envelope);
@@ -453,7 +453,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
public void setControlPoints(final Vector... coordinates) {
ensureModifiable();
ArgumentChecks.ensureNonNull("coordinates", coordinates);
- linear.setControlPoints(coordinates);
+ linearBuilder.setControlPoints(coordinates);
}
/**
@@ -472,9 +472,9 @@ public class LocalizationGridBuilder extends
TransformBuilder {
*/
public void setControlPoint(final int gridX, final int gridY, final
double... target) {
ensureModifiable();
- tmp[0] = gridX;
- tmp[1] = gridY;
- linear.setControlPoint(tmp, target);
+ gridCoordinates[0] = gridX;
+ gridCoordinates[1] = gridY;
+ linearBuilder.setControlPoint(gridCoordinates, target);
}
/**
@@ -488,9 +488,9 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* @throws IllegalArgumentException if the {@code x} or {@code y}
coordinate value is out of grid range.
*/
public double[] getControlPoint(final int gridX, final int gridY) {
- tmp[0] = gridX;
- tmp[1] = gridY;
- return linear.getControlPoint(tmp);
+ gridCoordinates[0] = gridX;
+ gridCoordinates[1] = gridY;
+ return linearBuilder.getControlPoint(gridCoordinates);
}
/**
@@ -504,9 +504,9 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* @since 1.0
*/
public Vector getRow(final int dimension, final int row) {
- tmp[0] = 0;
- tmp[1] = row;
- return linear.getTransect(dimension, tmp, 0);
+ gridCoordinates[0] = 0;
+ gridCoordinates[1] = row;
+ return linearBuilder.getTransect(dimension, gridCoordinates, 0);
}
/**
@@ -520,9 +520,9 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* @since 1.0
*/
public Vector getColumn(final int dimension, final int column) {
- tmp[0] = column;
- tmp[1] = 0;
- return linear.getTransect(dimension, tmp, 1);
+ gridCoordinates[0] = column;
+ gridCoordinates[1] = 0;
+ return linearBuilder.getTransect(dimension, gridCoordinates, 1);
}
/**
@@ -570,18 +570,18 @@ public class LocalizationGridBuilder extends
TransformBuilder {
*/
public NumberRange<Double> resolveWraparoundAxis(final int dimension,
final int direction, final double period) {
ensureModifiable();
- ArgumentChecks.ensureBetween("dimension", 0,
linear.getTargetDimensions() - 1, dimension);
- ArgumentChecks.ensureBetween("direction", 0,
linear.getSourceDimensions() - 1, direction);
+ ArgumentChecks.ensureBetween("dimension", 0,
linearBuilder.getTargetDimensions() - 1, dimension);
+ ArgumentChecks.ensureBetween("direction", 0,
linearBuilder.getSourceDimensions() - 1, direction);
ArgumentChecks.ensureStrictlyPositive("period", period);
if (periods == null) {
- periods = new double[linear.getTargetDimensions()];
+ periods = new double[linearBuilder.getTargetDimensions()];
}
if (periods[dimension] != 0) {
throw new IllegalStateException(Errors.format(
Errors.Keys.ValueAlreadyDefined_1,
Strings.bracket("periods", dimension)));
}
periods[dimension] = period;
- return linear.resolveWraparoundAxis(dimension, direction, period);
+ return linearBuilder.resolveWraparoundAxis(dimension, direction,
period);
}
/**
@@ -622,7 +622,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
public void addLinearizers(final Map<String,MathTransform> projections,
final boolean compensate, final int... projToGrid) {
ArgumentChecks.ensureNonNull("projections", projections);
ensureModifiable();
- linear.addLinearizers(projections, compensate, projToGrid);
+ linearBuilder.addLinearizers(projections, compensate, projToGrid);
}
/**
@@ -657,14 +657,14 @@ public class LocalizationGridBuilder extends
TransformBuilder {
@Override
public MathTransform create(final MathTransformFactory factory) throws
FactoryException {
if (transform == null) {
- final LinearTransform gridToCoord = linear.create(factory);
+ final LinearTransform gridToCoord = linearBuilder.create(factory);
/*
* Make a first check about whether the result of above
LinearTransformBuilder.create() call
* can be considered a good fit. If true, then we may return the
linear transform directly.
*/
boolean isExact = true;
boolean isLinear = true;
- for (final double c : linear.correlation()) {
+ for (final double c : linearBuilder.correlation()) {
isExact &= (c == 1);
if (!(c >= 0.9999)) { // Empirical
threshold (may need to be revisited).
isLinear = false;
@@ -675,9 +675,9 @@ public class LocalizationGridBuilder extends
TransformBuilder {
if (isExact) {
step = MathTransforms.concatenate(sourceToGrid, gridToCoord);
} else {
- final int width = linear.gridSize(0);
- final int height = linear.gridSize(1);
- final float[] residual = new float [SOURCE_DIMENSION *
linear.gridLength];
+ final int width = linearBuilder.gridSize(0);
+ final int height = linearBuilder.gridSize(1);
+ final float[] residual = new float [SOURCE_DIMENSION *
linearBuilder.gridLength];
final double[] grid = new double[SOURCE_DIMENSION * width];
double gridPrecision = precision;
try {
@@ -706,9 +706,9 @@ public class LocalizationGridBuilder extends
TransformBuilder {
*/
final MathTransform coordToGrid = gridToCoord.inverse();
for (int k=0,y=0; y<height; y++) {
- tmp[0] = 0;
- tmp[1] = y;
- linear.getControlRow(tmp, grid);
// Expected positions.
+ gridCoordinates[0] = 0;
+ gridCoordinates[1] = y;
+ linearBuilder.getControlRow(gridCoordinates, grid);
// Expected positions.
coordToGrid.transform(grid, 0, grid, 0, width);
// As grid coordinate.
for (int i=0,x=0; x<width; x++) {
final double dx = grid[i++] - x;
@@ -722,10 +722,10 @@ public class LocalizationGridBuilder extends
TransformBuilder {
if (isLinear) {
step = MathTransforms.concatenate(sourceToGrid,
gridToCoord);
} else {
- step =
InterpolatedTransform.createGeodeticTransformation(nonNull(factory),
- new ResidualGrid(sourceToGrid, gridToCoord,
width, height, residual,
- (gridPrecision > 0) ? gridPrecision :
DEFAULT_PRECISION, periods,
- linear.appliedLinearizer()));
+ final ResidualGrid shifts = new
ResidualGrid(sourceToGrid, gridToCoord, width, height,
+ residual, (gridPrecision > 0) ? gridPrecision
: DEFAULT_PRECISION, periods,
+ linearBuilder.appliedLinearizer());
+ step =
InterpolatedTransform.createGeodeticTransformation(nonNull(factory), shifts);
}
} catch (TransformException e) {
throw new FactoryException(e);
// Should never happen.
@@ -736,7 +736,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* If those target coordinates have been modified in order to make
that step more
* linear, apply the inverse transformation after the step.
*/
- final ProjectedTransformTry linearizer =
linear.appliedLinearizer();
+ final ProjectedTransformTry linearizer =
linearBuilder.appliedLinearizer();
if (linearizer != null && linearizer.reverseAfterLinearization)
try {
step = factory.createConcatenatedTransform(step,
linearizer.getValue().inverse());
} catch (NoninvertibleTransformException e) {
@@ -780,7 +780,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* @since 1.1
*/
public Optional<Map.Entry<String,MathTransform>> linearizer(final boolean
ifNotCompensated) {
- ProjectedTransformTry linearizer = linear.appliedLinearizer();
+ ProjectedTransformTry linearizer = linearBuilder.appliedLinearizer();
if (ifNotCompensated && linearizer != null &&
linearizer.reverseAfterLinearization) {
linearizer = null;
}
@@ -788,46 +788,54 @@ public class LocalizationGridBuilder extends
TransformBuilder {
}
/**
- * Returns statistics of differences between values calculated by the
given transform and actual values.
- * The given math transform is typically the transform computed by {@link
#create(MathTransformFactory)},
- * but not necessarily. The returned statistics are:
+ * Returns statistics of differences between values calculated by the
transform and actual values.
+ * The tested transform is
+ * the one computed by {@link #create(MathTransformFactory)} if the {@code
linear} argument is {@code false},
+ * or the transform computed by {@link LinearTransformBuilder} if the
{@code linear} argument is {@code true}.
+ * The returned statistics are:
*
* <ol class="verbose">
* <li>One {@code Statistics} instance for each target dimension,
containing statistics about the differences between
* coordinates computed by the given transform and expected
coordinates. For each (<var>i</var>,<var>j</var>) indices
* in this grid, the indices are transformed by a call to {@code
mt.transform(…)} and the result is compared with the
* coordinates given by <code>{@linkplain #getControlPoint(int, int)
getControlPoint}(i,j)</code>.
- * Those statistics are identified by labels like “P → x” and “P → y”
where <var>P</var> stands for pixel coordinates.</li>
+ * Those statistics are identified by labels like “Δx” and “Δy”.</li>
* <li>One {@code Statistics} instance for each source dimension,
containing statistics about the differences between
* coordinates computed by the <em>inverse</em> of the transform and
expected coordinates.
* For each (<var>x</var>,<var>y</var>) control point in this grid,
the points are transformed by a call
* to {@code mt.inverse().transform(…)} and the result is compared
with the pixel indices of that point.
- * Those statistics are identified by labels like “i ← P′” and “j ←
P′” where <var>P′</var> stands for
- * the control point.</li>
+ * Those statistics are identified by labels like "Δi" and "Δj".</li>
* </ol>
*
- * @param mt the transform to test.
+ * @param linear {@code false} for computing errors using the complete
transform,
+ * or {@code true} for using only the linear part.
* @return statistics of difference between computed values and expected
values for each target dimension.
- * @throws NoninvertibleTransformException if an error occurred while
inverting a transform.
+ * @throws IllegalStateException if {@link #create(MathTransformFactory)}
has not yet been invoked.
*
- * @since 1.0
+ * @see #toString(boolean, Locale)
+ *
+ * @since 1.1
*/
- public Statistics[] error(final MathTransform mt) throws
NoninvertibleTransformException {
+ public Statistics[] errors(final boolean linear) {
+ final MathTransform mt = linear ? linearBuilder.transform() :
transform;
+ if (mt == null) {
+ throw new
IllegalStateException(Errors.format(Errors.Keys.Uninitialized_1,
getClass().getSimpleName()));
+ }
final int tgtDim = mt.getTargetDimensions();
final double[] point = new double[Math.max(tgtDim,
SOURCE_DIMENSION)];
final Statistics[] stats = new Statistics[tgtDim + SOURCE_DIMENSION];
final StringBuilder buffer = new StringBuilder();
for (int i=0; i<stats.length; i++) {
buffer.setLength(0);
+ buffer.append('Δ');
if (i < tgtDim) {
- buffer.append("P → ");
if (i < 3) {
buffer.append((char) ('x' + i));
} else {
buffer.append('z').append(i - 1); // After (x,y,z)
continue with z2, z3, z4, etc.
}
} else {
- buffer.append((char) ('i' + (i - tgtDim))).append(" ← P′");
+ buffer.append((char) ('i' + (i - tgtDim)));
}
stats[i] = new Statistics(buffer.toString());
}
@@ -837,19 +845,24 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* The way that we get the transform below should be the same way than
in `create(…)`, except that we
* apply the inverse transform unconditionally.
*/
- final ProjectedTransformTry linearizer = linear.appliedLinearizer();
- final MathTransform complete = (linearizer != null) ?
linearizer.getValue().inverse() : null;
- final MathTransform inverse = mt.inverse();
- final int width = linear.gridSize(0);
- final int height = linear.gridSize(1);
+ final MathTransform complete, inverse;
+ try {
+ final ProjectedTransformTry linearizer =
linearBuilder.appliedLinearizer();
+ complete = (linearizer != null &&
linearizer.reverseAfterLinearization) ? linearizer.getValue().inverse() : null;
+ inverse = mt.inverse();
+ } catch (NoninvertibleTransformException e) {
+ throw new IllegalStateException(e);
+ }
+ final int width = linearBuilder.gridSize(0);
+ final int height = linearBuilder.gridSize(1);
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
- point[0] = tmp[0] = x;
- point[1] = tmp[1] = y;
+ point[0] = gridCoordinates[0] = x;
+ point[1] = gridCoordinates[1] = y;
final double[] expected;
try {
mt.transform(point, 0, point, 0, 1);
- expected = linear.getControlPoint(tmp);
+ expected = linearBuilder.getControlPoint(gridCoordinates);
if (complete != null) {
complete.transform(expected, 0, expected, 0, 1);
}
@@ -868,7 +881,7 @@ public class LocalizationGridBuilder extends
TransformBuilder {
continue; // Ignore the points
that we fail to transform.
}
for (int i=0; i<SOURCE_DIMENSION; i++) {
- stats[tgtDim + i].accept(expected[i] - tmp[i]);
+ stats[tgtDim + i].accept(expected[i] - gridCoordinates[i]);
}
}
}
@@ -876,6 +889,18 @@ public class LocalizationGridBuilder extends
TransformBuilder {
}
/**
+ * @deprecated Replaced by {@link #errors(boolean)}.
+ *
+ * @since 1.0
+ */
+ @Deprecated
+ public Statistics[] error(final MathTransform mt) throws
NoninvertibleTransformException {
+ if (mt == transform) return errors(false);
+ if (mt == linearBuilder.transform()) return errors(true);
+ throw new IllegalArgumentException();
+ }
+
+ /**
* Returns a string representation of this builder in the given locale.
* Current implementation shows the following information:
*
@@ -883,21 +908,25 @@ public class LocalizationGridBuilder extends
TransformBuilder {
* <li>Number of points.</li>
* <li>Linearizers and their correlation coefficients (if
available).</li>
* <li>The linear component of the transform.</li>
- * <li>Error statistics, as documented in the {@link
#error(MathTransform)} method.</li>
+ * <li>Error statistics, as documented in the {@link #errors(boolean)}
method.</li>
* </ul>
*
* The string representation may change in any future version.
*
+ * @param linear {@code false} for errors using the complete transform,
+ * or {@code true} for using only the linear part.
* @param locale the locale for formatting messages and some numbers, or
{@code null} for the default.
* @return a string representation of this builder.
*
- * @since 1.0
+ * @see #errors(boolean)
+ *
+ * @since 1.1
*/
- public String toString(final Locale locale) {
+ public String toString(final boolean linear, final Locale locale) {
final StringBuilder buffer = new StringBuilder(400);
String lineSeparator = null;
try {
- lineSeparator = linear.appendTo(buffer, getClass(), locale,
Vocabulary.Keys.LinearTransformation);
+ lineSeparator = linearBuilder.appendTo(buffer, getClass(), locale,
Vocabulary.Keys.LinearTransformation);
if (transform != null) {
buffer.append(Strings.CONTINUATION_ITEM);
final Vocabulary vocabulary = Vocabulary.getResources(locale);
@@ -909,11 +938,11 @@ public class LocalizationGridBuilder extends
TransformBuilder {
} else {
sf = StatisticsFormat.getInstance();
}
- sf.format(error(transform), buffer);
+ sf.format(errors(linear), buffer);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
- } catch (NoninvertibleTransformException e) {
+ } catch (IllegalStateException e) {
// Ignore - we will not report error statistics.
}
Strings.insertLineInLeftMargin(buffer, lineSeparator);
@@ -921,6 +950,16 @@ public class LocalizationGridBuilder extends
TransformBuilder {
}
/**
+ * @deprecated Replaced by {@link #toString(boolean, Locale)}.
+ *
+ * @since 1.0
+ */
+ @Deprecated
+ public String toString(final Locale locale) {
+ return toString(false, locale);
+ }
+
+ /**
* Returns a string representation of this builder for debugging purpose.
* The string representation is for debugging purpose and may change in
any future version.
* The default implementation delegates to {@link #toString(Locale)} with
a null locale.
@@ -931,6 +970,6 @@ public class LocalizationGridBuilder extends
TransformBuilder {
*/
@Override
public String toString() {
- return toString(null);
+ return toString(false, null);
}
}
diff --git
a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
index d4e1dd3..af3265b 100644
---
a/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
+++
b/profiles/sis-japan-profile/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
@@ -129,10 +129,17 @@ public final class GCOM_C extends Convention {
/**
* Mapping from ACDD or CF-Convention attribute names to names of
attributes used by GCOM-C.
+ * This map does not include attributes for geographic extent because the
"Lower_left_latitude",
+ * "Lower_left_longitude", "Lower_right_latitude", <i>etc.</i> attributes
are difficult to use.
+ * They are corners in the grid with no clear relationship with "real
world" West and East bounds.
+ * We have no way to detect anti-meridian spanning (the {@code left >
right} test is useless) and
+ * the minimal latitude may be in the middle of a border. Consequently a
bounding box made from
+ * the corner minimal and maximal coordinates is not guaranteed to
encompass the whole data,
+ * and may even contain no data at all.
*/
private static final Map<String,String> ATTRIBUTES;
static {
- final Map<String,String> m = new HashMap<>();
+ final Map<String,String> m = new HashMap<>(16);
m.put(AttributeNames.TITLE, "Product_name");
// identificationInfo / citation / title
m.put(AttributeNames.PRODUCT_VERSION, "Product_version");
// identificationInfo / citation / edition
m.put(AttributeNames.IDENTIFIER.TEXT, "Product_file_name");
// identificationInfo / citation / identifier / code
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
index e62c53e..439a592 100644
---
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
+++
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
@@ -57,8 +57,8 @@ import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArraysExt;
+import org.apache.sis.measure.NumberRange;
import org.apache.sis.measure.Units;
-import org.apache.sis.math.Vector;
/**
@@ -171,20 +171,20 @@ abstract class CRSBuilder<D extends Datum, CS extends
CoordinateSystem> {
* Infers a new CRS for a {@link Grid}.
*
* <h4>CRS replacements</h4>
- * The {@code linearizationTargets} argument allows to replace some CRSs
inferred by this method by hard-coded CRSs.
- * This is non-empty only when reading a netCDF file for a specific
profile, i.e. a file decoded with a subclass of
- * {@link Convention}. The CRS to be replaced is inferred from the axis
directions.
+ * The {@code linearizations} argument allows to replace some CRSs
inferred by this method by hard-coded CRSs.
+ * This is non-empty only when reading a netCDF file for a specific
profile, i.e. a file decoded with a subclass
+ * of {@link Convention}. The CRS to be replaced is inferred from the axis
directions.
*
- * @param decoder the decoder of the netCDF from which the
CRS are constructed.
- * @param grid the grid for which the CRS are
constructed.
- * @param linearizationTargets CRS to use instead of CRS inferred by
this method, or null or empty if none.
- * @param reorderGridToCRS an affine transform doing a final step in
a "grid to CRS" transform for ordering axes.
+ * @param decoder the decoder of the netCDF from which the CRS
are constructed.
+ * @param grid the grid for which the CRS are constructed.
+ * @param linearizations contains CRS to use instead of CRS inferred
by this method, or null or empty if none.
+ * @param reorderGridToCRS an affine transform doing a final step in a
"grid to CRS" transform for ordering axes.
* Not used by this method, but may be modified for taking in
account axis order changes caused by replacements
- * defined in {@code linearizationTargets}. Ignored (can be null)
if {@code linearizationTargets} is null.
+ * defined in {@code linearizations}. Ignored (can be null) if
{@code linearizations} is null.
* @return coordinate reference system from the given axes, or {@code
null}.
*/
public static CoordinateReferenceSystem assemble(final Decoder decoder,
final Grid grid,
- final List<CoordinateReferenceSystem> linearizationTargets, final
Matrix reorderGridToCRS)
+ final List<GridCacheValue> linearizations, final Matrix
reorderGridToCRS)
throws DataStoreException, FactoryException, IOException
{
final List<CRSBuilder<?,?>> builders = new ArrayList<>(4);
@@ -200,8 +200,8 @@ abstract class CRSBuilder<D extends Datum, CS extends
CoordinateSystem> {
* We do not verify the datum; we assume that the linearizer that
built the CRS
* was consistent with `Convention.defaultHorizontalCRS(false)`.
*/
- if ((linearizationTargets != null) && !linearizationTargets.isEmpty())
{
- Linearizer.replaceInCompoundCRS(components, linearizationTargets,
reorderGridToCRS);
+ if ((linearizations != null) && !linearizations.isEmpty()) {
+ Linearizer.replaceInCompoundCRS(components, linearizations,
reorderGridToCRS);
}
switch (components.length) {
case 0: return null;
@@ -426,12 +426,10 @@ previous: for (int i=components.size(); --i >= 0;) {
for (int i=cs.getDimension(); --i >= 0;) {
final CoordinateSystemAxis axis = cs.getAxis(i);
if (RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) {
- final Vector coordinates = axes[i].read();
// Typically a cached vector.
- final int length = coordinates.size();
- if (length != 0) {
- final double first = coordinates.doubleValue(0);
- final double last = coordinates.doubleValue(length -
1);
- if (Math.min(first, last) >= 0 && Math.max(first,
last) > axis.getMaximumValue()) {
+ final NumberRange<?> range = axes[i].read().range();
// Vector is cached.
+ if (range != null) {
+ // Note: minimum/maximum are not necessarily first and
last values in the vector.
+ if (range.getMinDouble() >= 0 && range.getMaxDouble()
> axis.getMaximumValue()) {
referenceSystem = (SingleCRS)
AbstractCRS.castOrCopy(referenceSystem)
.forConvention(AxesConvention.POSITIVE_RANGE);
break;
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
index fe64f24..5471b56 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Grid.java
@@ -289,27 +289,27 @@ public abstract class Grid extends NamedElement {
* this method because this CRS will be used for adjusting axis order or
for completion if grid mapping does not include
* information for all dimensions.</p>
*
- * @param decoder the decoder for which CRS are constructed.
- * @param warnings previous warnings, for avoiding to log
the same message twice. Can be null.
- * @param linearizationTargets CRS to use instead of CRS inferred by
this method, or null or empty if none.
- * @param reorderGridToCRS an affine transform doing a final step in
a "grid to CRS" transform for ordering axes.
+ * @param decoder the decoder for which CRS are constructed.
+ * @param warnings previous warnings, for avoiding to log the
same message twice. Can be null.
+ * @param linearizations contains CRS to use instead of CRS inferred
by this method, or null or empty if none.
+ * @param reorderGridToCRS an affine transform doing a final step in a
"grid to CRS" transform for ordering axes.
* Not used by this method, but may be modified for taking in
account axis order changes caused by replacements
- * defined in {@code linearizationTargets}. Ignored (can be null)
if {@code linearizationTargets} is null.
+ * defined in {@code linearizations}. Ignored (can be null) if
{@code linearizations} is null.
* @return the CRS for this grid geometry, or {@code null}.
* @throws IOException if an I/O operation was necessary but failed.
* @throws DataStoreException if the CRS can not be constructed.
*/
final CoordinateReferenceSystem getCoordinateReferenceSystem(final Decoder
decoder, final List<Exception> warnings,
- final List<CoordinateReferenceSystem> linearizationTargets, final
Matrix reorderGridToCRS)
+ final List<GridCacheValue> linearizations, final Matrix
reorderGridToCRS)
throws IOException, DataStoreException
{
- final boolean isCached = (linearizationTargets == null) ||
linearizationTargets.isEmpty();
- if (isCached & isCRSDetermined) {
+ final boolean useCache = (linearizations == null) ||
linearizations.isEmpty();
+ if (useCache & isCRSDetermined) {
return crs;
} else try {
- if (isCached) isCRSDetermined = true; // Set now for
avoiding new attempts if creation fail.
- final CoordinateReferenceSystem result =
CRSBuilder.assemble(decoder, this, linearizationTargets, reorderGridToCRS);
- if (isCached) crs = result;
+ if (useCache) isCRSDetermined = true; // Set now for
avoiding new attempts if creation fail.
+ final CoordinateReferenceSystem result =
CRSBuilder.assemble(decoder, this, linearizations, reorderGridToCRS);
+ if (useCache) crs = result;
return result;
} catch (FactoryException | NullArgumentException ex) {
if (isNewWarning(ex, warnings)) {
@@ -448,7 +448,7 @@ 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<CoordinateReferenceSystem> linearizationTargets = new
ArrayList<>();
+ final List<GridCacheValue> linearizations = new ArrayList<>();
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();) {
@@ -476,7 +476,7 @@ findFree: for (int srcDim :
axis.gridDimensionIndices) {
* Replace the first transform by the
two-dimensional localization grid and
* remove the other transform. Removals need
to be done in arrays too.
*/
- nonLinears.set(i, grid.transform);
+ nonLinears.set(i, grid.gridToCRS);
nonLinears.remove(j);
final int n = nonLinears.size() - j;
System.arraycopy(deferred, j+1,
deferred, j, n);
@@ -484,7 +484,9 @@ findFree: for (int srcDim :
axis.gridDimensionIndices) {
if (otherDim < srcDim) {
gridDimensionIndices[i] = otherDim; //
Index of the first dimension.
}
-
grid.getLinearizationTarget(linearizationTargets);
+ if (grid.linearizationTarget != null) {
+ linearizations.add(grid);
+ }
break; //
Continue the 'i' loop.
}
}
@@ -506,7 +508,7 @@ 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, linearizationTargets, affine);
+ final CoordinateReferenceSystem crs =
getCoordinateReferenceSystem(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.
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheKey.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheKey.java
index fae9e03..6412ddc 100644
---
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheKey.java
+++
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheKey.java
@@ -37,6 +37,7 @@ import org.apache.sis.math.Vector;
* </ul>
*
* The base class if for local cache. The inner class is for the global cache.
+ * {@code GridCacheKey}s are associated to {@link GridCacheValue}s in a hash
map.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
@@ -67,7 +68,9 @@ class GridCacheKey {
}
/**
- * Creates a global key from the given local key.
+ * Creates a global key from the given local key. This constructor is for
{@link Global} construction only,
+ * because the information stored by this constructor are not sufficient
for testing if two grids are equal.
+ * The {@link Global} subclass will add a MD5 checksum.
*/
private GridCacheKey(final GridCacheKey keyLocal) {
width = keyLocal.width;
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheValue.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheValue.java
index cd3be6d..3f08c59 100644
---
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheValue.java
+++
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/GridCacheValue.java
@@ -16,10 +16,10 @@
*/
package org.apache.sis.internal.netcdf;
+import java.util.Map;
import java.util.Set;
-import java.util.List;
+import java.util.Optional;
import org.opengis.util.FactoryException;
-import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -29,6 +29,7 @@ import
org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
/**
* A value cached in {@link GridCacheKey.Global#CACHE}.
* This is used for sharing common localization grids between different netCDF
files.
+ * {@code GridCacheValue}s are associated to {@link GridCacheKey}s in a hash
map.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
@@ -39,13 +40,23 @@ final class GridCacheValue {
/**
* The transform from grid coordinates to geographic or projected
coordinates.
*/
- final MathTransform transform;
+ final MathTransform gridToCRS;
/**
- * The target CRS of {@link #transform} if different than the CRS inferred
by {@link CRSBuilder}.
+ * The target CRS of {@link #gridToCRS} if different than the CRS inferred
by {@link CRSBuilder}.
* This field is non-null if the target CRS has been changed by
application of a linearizer.
*/
- private CoordinateReferenceSystem targetCRS;
+ final CoordinateReferenceSystem linearizationTarget;
+
+ /**
+ * Whether axes need to be swapped in order to have the same direction
before and after linearization.
+ * For example if input coordinates stored in the localization grid have
(east, north) directions, then
+ * {@link #linearizationTarget} coordinates shall have (east, north)
directions as well.
+ * This flag specifies whether input coordinates must be swapped for
making above condition true.
+ *
+ * <p>This flag assumes that {@link #linearizationTarget} has two
dimensions.</p>
+ */
+ final boolean axisSwap;
/**
* Creates a new "grid to CRS" together with target CRS.
@@ -53,25 +64,19 @@ final class GridCacheValue {
GridCacheValue(final Set<Linearizer> linearizers, final
LocalizationGridBuilder grid,
final MathTransformFactory factory) throws FactoryException
{
- transform = grid.create(factory);
- grid.linearizer(true).ifPresent((e) -> {
- final String name = e.getKey();
+ gridToCRS = grid.create(factory);
+ final Optional<Map.Entry<String,MathTransform>> e =
grid.linearizer(true);
+ if (e.isPresent()) {
+ final String name = e.get().getKey();
for (final Linearizer linearizer : linearizers) {
if (name.equals(linearizer.name())) {
- targetCRS = linearizer.getTargetCRS();
- break;
+ linearizationTarget = linearizer.getTargetCRS();
+ axisSwap = linearizer.axisSwap();
+ return;
}
}
- });
- }
-
- /**
- * Adds the target CRS to the given list if that CRS is different than the
CRS inferred by {@link CRSBuilder}.
- * This is an element of the list to provide to {@link
CRSBuilder#assemble(Decoder, Grid, List, Matrix)}.
- */
- final void getLinearizationTarget(final List<CoordinateReferenceSystem>
list) {
- if (targetCRS != null) {
- list.add(targetCRS);
}
+ linearizationTarget = null;
+ axisSwap = false;
}
}
diff --git
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
index 5a0f50f..7f46913 100644
---
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
+++
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Linearizer.java
@@ -34,7 +34,9 @@ import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.internal.util.Strings;
import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.util.ArraysExt;
/**
@@ -42,6 +44,11 @@ import org.apache.sis.storage.DataStoreReferencingException;
* Non-linear transforms are tested in "trials and errors" and the one
resulting in best correlation
* coefficients is selected.
*
+ * <p>Before linearization, source coordinates may be in (latitude, longitude)
or (longitude, latitude) order
+ * depending on the order of dimensions in netCDF variable. But after
linearization, axes will be in a fixed
+ * order determined by {@link #targetCRS}. In other words, netCDF dimension
order shall be ignored if a
+ * linearization is applied.</p>
+ *
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
*
@@ -84,10 +91,19 @@ public final class Linearizer {
/**
* The target coordinate reference system after application of the
non-linear transform.
+ * May depend on the netCDF file being read (for example for choosing a
UTM zone).
*/
private CoordinateReferenceSystem targetCRS;
/**
+ * Whether axes need to be swapped in order to have the same direction
before and after the transform.
+ * For example if input coordinates have (east, north) directions, then
output coordinates shall have
+ * (east, north) directions as well. This flag specifies whether input
coordinates must be swapped for
+ * making above condition true.
+ */
+ private boolean axisSwap;
+
+ /**
* Creates a new linearizer working on the specified datum.
*
* @param datum the datum to use. Should be consistent with {@link
Convention#defaultHorizontalCRS(boolean)}.
@@ -113,6 +129,17 @@ public final class Linearizer {
}
/**
+ * Returns whether axes need to be swapped in order to have the same
direction before and after the transform.
+ * For example if input coordinates have (east, north) directions, then
output coordinates shall have (east, north)
+ * directions as well. This flag specifies whether input coordinates must
be swapped for making above condition true.
+ *
+ * @see GridCacheValue#axisSwap
+ */
+ final boolean axisSwap() {
+ return axisSwap;
+ }
+
+ /**
* Returns a string representation for debugging purposes.
*/
@Override
@@ -141,25 +168,28 @@ public final class Linearizer {
throw new AssertionError(type);
}
/*
- * Create a Universal Transverse Mercator (UTM) projection
- * for the zone containing a point in the middle of the grid.
+ * Create a Universal Transverse Mercator (UTM) projection for the
zone containing a point in
+ * the middle of the grid. We apply `Math.signum(…)` on the
latitude for avoiding stereographic
+ * projections near poles and for avoiding Norway and Svalbard
special cases.
*/
case UTM: {
final Envelope bounds = grid.getSourceEnvelope(false);
final double[] median = grid.getControlPoint(
(int) Math.round(bounds.getMedian(0)),
(int) Math.round(bounds.getMedian(1)));
- ProjectedCRS crs = datum.universal(median[ydim], median[xdim]);
+ final ProjectedCRS crs =
datum.universal(Math.signum(median[ydim]), median[xdim]);
+ assert
ReferencingUtilities.startsWithNorthEast(crs.getBaseCRS().getCoordinateSystem());
transform = crs.getConversionFromBase().getMathTransform();
targetCRS = crs;
break;
}
}
/*
- * Above transform expects (latitude, longitude) inputs. If grid
coordinates
- * are in (longitude, latitude) order, we need to swap input
coordinates.
+ * Above transform expects (latitude, longitude) inputs (verified by
assertion).
+ * If grid coordinates are in (longitude, latitude) order, we must
swap inputs.
*/
- if (xdim < ydim) {
+ axisSwap = ydim < xdim;
+ if (!axisSwap) {
final Matrix3 m = new Matrix3();
m.m00 = m.m11 = 0;
m.m01 = m.m10 = 1;
@@ -199,6 +229,11 @@ public final class Linearizer {
final MathTransform transform =
linearizer.gridToTargetCRS(grid, xdim, ydim);
projections.put(linearizer.name(), transform);
}
+ /*
+ * Axis order before linearization was taken in account by above
`gridToTargetCRS(…, xdim, ydim)`.
+ * Consequently arguments below shall specify only the dimensions
to select without reordering axes.
+ * Note that after linearization, axes will be in a fixed order
determined by the CRS.
+ */
grid.addLinearizers(projections, false, Math.min(xdim, ydim),
Math.max(xdim, ydim));
}
}
@@ -207,22 +242,27 @@ public final class Linearizer {
* Given CRS components inferred by {@link CRSBuilder}, replaces CRS
components in the dimensions
* where linearization has been applied. The CRS components to replace are
inferred from axis directions.
*
+ * <p>This static method is defined here for keeping in a single class all
codes related to linearization.</p>
+ *
* @param components the components of the compound CRS that
{@link CRSBuilder} inferred.
* @param replacements the {@link #targetCRS} of linearizations.
* @param reorderGridToCRS an affine transform doing a final step in a
"grid to CRS" transform for ordering axes.
* Not used by this method, but modified for taking in account
axis order changes caused by replacements.
*/
- static void replaceInCompoundCRS(final SingleCRS[] components,
- final List<CoordinateReferenceSystem> replacements, final Matrix
reorderGridToCRS)
- throws DataStoreReferencingException
+ static void replaceInCompoundCRS(final SingleCRS[] components, final
List<GridCacheValue> replacements,
+ final Matrix reorderGridToCRS) throws
DataStoreReferencingException
{
Matrix original = null;
-search: for (final CoordinateReferenceSystem targetCRS : replacements) {
+search: for (final GridCacheValue cache : replacements) {
+ final CoordinateReferenceSystem targetCRS =
cache.linearizationTarget;
int firstDimension = 0;
for (int i=0; i < components.length; i++) {
final SingleCRS sourceCRS = components[i];
final int[] r =
AxisDirections.indicesOfColinear(sourceCRS.getCoordinateSystem(),
targetCRS.getCoordinateSystem());
if (r != null) {
+ if (cache.axisSwap) {
+ ArraysExt.swap(r, 0, 1);
+ }
for (int j=0; j<r.length; j++) {
if (r[j] != j) {
final int oldRow = r[j] + firstDimension;