sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Add a "Satellite ground track" transform (internal only) for trying to make GCOM data a little bit more linear before to let LocalizationGridBuilder to its job.
Date Mon, 15 Apr 2019 20:10:21 GMT
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 6fd90af  Add a "Satellite ground track" transform (internal only) for trying to make GCOM data a little bit more linear before to let LocalizationGridBuilder to its job.
6fd90af is described below

commit 6fd90afa3e5d7053bbf9d79d844646b4f71f4d8c
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Apr 15 22:09:14 2019 +0200

    Add a "Satellite ground track" transform (internal only) for trying to make GCOM data a little bit more linear before to let LocalizationGridBuilder to its job.
---
 .../sis/referencing/datum/DatumShiftGrid.java      |  28 ++-
 .../operation/builder/LinearTransformBuilder.java  |  23 ++
 .../operation/builder/LocalizationGridBuilder.java |  94 +++++---
 .../operation/transform/ContextualParameters.java  |  25 ++-
 .../transform/CoordinateSystemTransform.java       |   8 +-
 .../operation/transform/DatumShiftTransform.java   |   6 +-
 .../transform/EllipsoidToCentricTransform.java     |   2 +-
 .../operation/transform/InterpolatedTransform.java |   4 +-
 .../builder/LocalizationGridBuilderTest.java       |   5 +
 .../transform/ContextualParametersTest.java        |   8 +-
 .../src/main/java/org/apache/sis/math/Vector.java  |   8 +-
 .../apache/sis/internal/earth/netcdf/GCOM_C.java   |  15 ++
 .../apache/sis/internal/earth/netcdf/GCOM_W.java   |  15 ++
 storage/sis-netcdf/pom.xml                         |  14 ++
 .../java/org/apache/sis/internal/netcdf/Axis.java  |  33 ++-
 .../java/org/apache/sis/internal/netcdf/Grid.java  |   2 +-
 .../org/apache/sis/internal/netcdf/Linearizer.java |  57 ++++-
 .../sis/internal/netcdf/SatelliteGroundTrack.java  | 246 +++++++++++++++++++++
 .../internal/netcdf/SatelliteGroundTrackTest.java  |  81 +++++++
 .../org/apache/sis/test/suite/NetcdfTestSuite.java |   1 +
 20 files changed, 590 insertions(+), 85 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
index e9efe3c..15d8d5d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/datum/DatumShiftGrid.java
@@ -417,13 +417,13 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
      * If the length of the given array is at least <var>n</var> + 4 where <var>n</var> = {@link #getTranslationDimensions()},
      * then this method appends the derivative (approximated) at the given grid indices. This is the same derivative than the
      * one computed by {@link #derivativeInCell(double, double)}, opportunistically computed here for performance reasons.
-     * The matrix layout is as below, where <var>u</var> and <var>v</var> are the coordinates after translation.
+     * The matrix layout is as below, where <var>t₀</var> and <var>t₁</var> are the coordinates after translation.
      *
      * {@preformat math
-     *   ┌                 ┐         ┌                             ┐
-     *   │  ∂u/∂x   ∂u/∂y  │    =    │  vector[n+0]   vector[n+1]  │
-     *   │  ∂v/∂x   ∂v/∂y  │         │  vector[n+2]   vector[n+3]  │
-     *   └                 ┘         └                             ┘
+     *   ┌                   ┐         ┌                             ┐
+     *   │  ∂t₀/∂x   ∂t₀/∂y  │    =    │  vector[n+0]   vector[n+1]  │
+     *   │  ∂t₁/∂x   ∂t₁/∂y  │         │  vector[n+2]   vector[n+3]  │
+     *   └                   ┘         └                             ┘
      * }
      *
      * <div class="section">Default implementation</div>
@@ -504,10 +504,24 @@ public abstract class DatumShiftGrid<C extends Quantity<C>, T extends Quantity<T
     }
 
     /**
-     * Estimates the derivative at the given grid indices.
+     * Estimates the derivative at the given grid indices. Derivatives must be consistent with values given by
+     * {@link #interpolateInCell(double, double, double[])} at adjacent positions. For a two-dimensional grid,
+     * {@code tₐ(x,y)} an abbreviation for {@code interpolateInCell(gridX, gridY, …)[a]} and for <var>x</var>
+     * and <var>y</var> integers, the derivative is:
+     *
+     * {@preformat math
+     *   ┌                   ┐         ┌                                                        ┐
+     *   │  ∂t₀/∂x   ∂t₀/∂y  │    =    │  t₀(x+1,y) - t₀(x,y) + 1      t₀(x,y+1) - t₀(x,y)      │
+     *   │  ∂t₁/∂x   ∂t₁/∂y  │         │  t₁(x+1,y) - t₁(x,y)          t₁(x,y+1) - t₁(x,y) + 1  │
+     *   └                   ┘         └                                                        ┘
+     * }
      *
      * <div class="section">Extrapolations</div>
-     * If the given coordinates is outside the grid, then the derivative will have some columns set to identity.
+     * Derivatives must be consistent with {@link #interpolateInCell(double, double, double[])} even when the
+     * given coordinates are outside the grid. The {@code interpolateInCell(…)} contract in such cases is to
+     * compute the translation vector at the closest position in the grid. A consequence of this contract is
+     * that translation vectors stay constant when moving along at least one direction outside the grid.
+     * Consequences on the derivative matrix are as below:
      *
      * <ul>
      *   <li>If both {@code gridX} and {@code gridY} are outside the grid, then the derivative is the identity matrix.</li>
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 2bb4b0d..9883a30 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
@@ -1052,6 +1052,29 @@ search:         for (int j=domain(); --j >= 0;) {
     }
 
     /**
+     * Returns the coordinates of a single row or column in the given dimension. This method can be invoked
+     * only when this {@code LinearTransformBuilder} is known to have been built for grid source coordinates.
+     *
+     * <div class="note"><b>Note:</b>
+     * while this method is primarily for row and columns, it can be generalized to more dimensions.</div>
+     *
+     * The returned vector is a view; changes in the returned vector will be reflected in this builder.
+     *
+     * @param  dimension  the dimension of source point for which to get coordinate values.
+     * @param  start      index of the first coordinate value to get.
+     * @param  direction  0 for getting a row, 1 for getting a column.
+     * @return coordinate values for specified row or column in the given dimension.
+     */
+    final Vector getTransect(final int dimension, final int[] start, final int direction) {
+        final int first = flatIndex(start);
+        int step = 1;
+        for (int i=0; i<direction; i++) {
+            step *= gridSize[i];
+        }
+        return Vector.create(targets[dimension]).subSampling(first, step, gridSize[direction] - start[direction]);
+    }
+
+    /**
      * Tries to remove discontinuities in coordinates values caused by anti-meridian crossing. This is the implementation of
      * {@link LocalizationGridBuilder#resolveWraparoundAxis(int, int, double)} public method. See that method for javadoc.
      *
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 adda0d8..aca8b7e 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
@@ -387,6 +387,46 @@ public class LocalizationGridBuilder extends TransformBuilder {
     }
 
     /**
+     * Returns the envelope of source coordinates. The {@code fullArea} argument control whether
+     * the returned envelope shall encompass full surface of every cells or only their centers:
+     * <ul>
+     *   <li>If {@code true}, then the returned envelope encompasses full cell surfaces,
+     *       from lower border to upper border. In other words, the returned envelope encompasses all
+     *       {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CORNER cell corners}.</li>
+     *   <li>If {@code false}, then the returned envelope encompasses only
+     *       {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CENTER cell centers}, inclusive.</li>
+     * </ul>
+     *
+     * This is the envelope of the grid domain (i.e. the ranges of valid {@code gridX} and {@code gridY} argument
+     * values in calls to {@code get/setControlPoint(…)} methods) transformed as below:
+     * <ol>
+     *   <li>expanded by ½ cell on each side if {@code fullArea} is {@code true}</li>
+     *   <li>transformed by the inverse of {@linkplain #getSourceToGrid() source to grid} transform.</li>
+     * </ol>
+     *
+     * @param  fullArea  whether the the envelope shall encompass the full cell surfaces instead than only their centers.
+     * @return the envelope of grid points, from lower corner to upper corner.
+     * @throws IllegalStateException if the grid points are not yet known.
+     * @throws TransformException if the envelope can not be calculated.
+     *
+     * @see LinearTransformBuilder#getSourceEnvelope()
+     *
+     * @since 1.0
+     */
+    public Envelope getSourceEnvelope(final boolean fullArea) throws TransformException {
+        Envelope envelope = linear.getSourceEnvelope();
+        if (fullArea) {
+            for (int i = envelope.getDimension(); --i >= 0;) {
+                final GeneralEnvelope ge = GeneralEnvelope.castOrCopy(envelope);
+                ge.setRange(i, ge.getLower(i) - 0.5,
+                               ge.getUpper(i) + 0.5);
+                envelope = ge;
+            }
+        }
+        return Envelopes.transform(sourceToGrid.inverse(), envelope);
+    }
+
+    /**
      * Sets all control points. The length of given vectors must be equal to the total number of cells in the grid.
      * The first vector provides the <var>x</var> coordinates; the second vector provides the <var>y</var> coordinates,
      * <i>etc.</i>. Coordinates are stored in row-major order (column index varies faster, followed by row index).
@@ -440,43 +480,35 @@ public class LocalizationGridBuilder extends TransformBuilder {
     }
 
     /**
-     * Returns the envelope of source coordinates. The {@code fullArea} argument control whether
-     * the returned envelope shall encompass full surface of every cells or only their centers:
-     * <ul>
-     *   <li>If {@code true}, then the returned envelope encompasses full cell surfaces,
-     *       from lower border to upper border. In other words, the returned envelope encompasses all
-     *       {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CORNER cell corners}.</li>
-     *   <li>If {@code false}, then the returned envelope encompasses only
-     *       {@linkplain org.opengis.referencing.datum.PixelInCell#CELL_CENTER cell centers}, inclusive.</li>
-     * </ul>
+     * Returns a row of coordinate values in the given dimension.
+     * The returned vector is a view; changes in the returned vector will be reflected in this builder.
      *
-     * This is the envelope of the grid domain (i.e. the ranges of valid {@code gridX} and {@code gridY} argument
-     * values in calls to {@code get/setControlPoint(…)} methods) transformed as below:
-     * <ol>
-     *   <li>expanded by ½ cell on each side if {@code fullArea} is {@code true}</li>
-     *   <li>transformed by the inverse of {@linkplain #getSourceToGrid() source to grid} transform.</li>
-     * </ol>
+     * @param  dimension  the target dimension for which to get coordinate values.
+     * @param  row        index of the row to get.
+     * @return coordinate values of the specified row in the specified dimension.
      *
-     * @param  fullArea  whether the the envelope shall encompass the full cell surfaces instead than only their centers.
-     * @return the envelope of grid points, from lower corner to upper corner.
-     * @throws IllegalStateException if the grid points are not yet known.
-     * @throws TransformException if the envelope can not be calculated.
+     * @since 1.0
+     */
+    public Vector getRow(final int dimension, final int row) {
+        tmp[0] = 0;
+        tmp[1] = row;
+        return linear.getTransect(dimension, tmp, 0);
+    }
+
+    /**
+     * Returns a column of coordinate values in the given dimension.
+     * The returned vector is a view; changes in the returned vector will be reflected in this builder.
      *
-     * @see LinearTransformBuilder#getSourceEnvelope()
+     * @param  dimension  the target dimension for which to get coordinate values.
+     * @param  column     index of the column to get.
+     * @return coordinate values of the specified column in the specified dimension.
      *
      * @since 1.0
      */
-    public Envelope getSourceEnvelope(final boolean fullArea) throws TransformException {
-        Envelope envelope = linear.getSourceEnvelope();
-        if (fullArea) {
-            for (int i = envelope.getDimension(); --i >= 0;) {
-                final GeneralEnvelope ge = GeneralEnvelope.castOrCopy(envelope);
-                ge.setRange(i, ge.getLower(i) - 0.5,
-                               ge.getUpper(i) + 0.5);
-                envelope = ge;
-            }
-        }
-        return Envelopes.transform(sourceToGrid.inverse(), envelope);
+    public Vector getColumn(final int dimension, final int column) {
+        tmp[0] = column;
+        tmp[1] = 0;
+        return linear.getTransect(dimension, tmp, 1);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ContextualParameters.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ContextualParameters.java
index f7a62cd..99e9d3c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ContextualParameters.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/ContextualParameters.java
@@ -53,8 +53,7 @@ import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
-
-import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -126,7 +125,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  * Serialization should be used only for short term storage or RMI between applications running the same SIS version.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see org.apache.sis.referencing.operation.projection.NormalizedProjection
  * @see AbstractMathTransform#getContextualParameters()
@@ -247,7 +246,7 @@ public class ContextualParameters extends Parameters implements Serializable {
      * @param  method  the non-linear operation method for which to define the parameter values.
      */
     public ContextualParameters(final OperationMethod method) {
-        ensureNonNull("method", method);
+        ArgumentChecks.ensureNonNull("method", method);
         descriptor  = method.getParameters();
         normalize   = linear("sourceDimensions", method.getSourceDimensions());
         denormalize = linear("targetDimensions", method.getTargetDimensions());
@@ -255,16 +254,22 @@ public class ContextualParameters extends Parameters implements Serializable {
     }
 
     /**
-     * Equivalent to the public constructor, but avoid the need for an {@link OperationMethod} instance.
+     * Creates a new group of parameters with the given descriptor. This constructor performs the same construction than
+     * {@link #ContextualParameters(OperationMethod)} but without the need to specify an {@code OperationMethod} instance.
      *
      * @param  descriptor  the parameter descriptor.
-     * @param  srcSize     size of the normalization matrix: source dimensions + 1.
-     * @param  tgtSize     size of the denormalization matrix: target dimensions + 1.
+     * @param  srcDim      number of source dimensions.
+     * @param  tgtDim      number of target dimensions.
+     *
+     * @since 1.0
      */
-    ContextualParameters(final ParameterDescriptorGroup descriptor, final int srcSize, final int tgtSize) {
+    public ContextualParameters(final ParameterDescriptorGroup descriptor, int srcDim, int tgtDim) {
+        ArgumentChecks.ensureNonNull("descriptor", descriptor);
+        ArgumentChecks.ensureStrictlyPositive("srcDim", srcDim);
+        ArgumentChecks.ensureStrictlyPositive("tgtDim", tgtDim);
         this.descriptor  = descriptor;
-        this.normalize   = Matrices.create(srcSize, srcSize, ExtendedPrecisionMatrix.IDENTITY);
-        this.denormalize = Matrices.create(tgtSize, tgtSize, ExtendedPrecisionMatrix.IDENTITY);
+        this.normalize   = Matrices.create(++srcDim, srcDim, ExtendedPrecisionMatrix.IDENTITY);
+        this.denormalize = Matrices.create(++tgtDim, tgtDim, ExtendedPrecisionMatrix.IDENTITY);
         this.values      = new ParameterValue<?>[descriptor.descriptors().size()];
     }
 
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/CoordinateSystemTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/CoordinateSystemTransform.java
index a773c3f..f363e2e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/CoordinateSystemTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/CoordinateSystemTransform.java
@@ -38,7 +38,6 @@ import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.parameter.DefaultParameterDescriptorGroup;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.cs.CoordinateSystems;
-import org.apache.sis.referencing.operation.DefaultOperationMethod;
 
 
 /**
@@ -46,7 +45,7 @@ import org.apache.sis.referencing.operation.DefaultOperationMethod;
  * Each subclasses should have a singleton instance.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.7
  * @module
  */
@@ -83,10 +82,9 @@ abstract class CoordinateSystemTransform extends AbstractMathTransform {
      */
     CoordinateSystemTransform(final String method, final int dimension) {
         this.dimension = dimension;
-        final Map<String,?> properties = Collections.singletonMap(DefaultOperationMethod.NAME_KEY,
+        final Map<String,?> properties = Collections.singletonMap(DefaultParameterDescriptorGroup.NAME_KEY,
                 new ImmutableIdentifier(Citations.SIS, Constants.SIS, method));
-        context = new ContextualParameters(new DefaultOperationMethod(properties, dimension, dimension,
-                new DefaultParameterDescriptorGroup(properties, 1, 1)));
+        context = new ContextualParameters(new DefaultParameterDescriptorGroup(properties, 1, 1), dimension, dimension);
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
index 74cfb4e..c027c9c 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DatumShiftTransform.java
@@ -121,8 +121,8 @@ public abstract class DatumShiftTransform extends AbstractMathTransform implemen
      * @param grid        interpolation grid.
      */
     DatumShiftTransform(ParameterDescriptorGroup descriptor, final DatumShiftGrid<?,?> grid) {
-        final int size = grid.getTranslationDimensions() + 1;
-        context = new ContextualParameters(descriptor, size, size);
+        final int dim = grid.getTranslationDimensions();
+        context = new ContextualParameters(descriptor, dim, dim);
         this.grid = grid;
         computeConversionFactors();
     }
@@ -139,7 +139,7 @@ public abstract class DatumShiftTransform extends AbstractMathTransform implemen
     DatumShiftTransform(final ParameterDescriptorGroup descriptor,
             final boolean isSource3D, final boolean isTarget3D, final DatumShiftGrid<?,?> grid)
     {
-        context = new ContextualParameters(descriptor, isSource3D ? 4 : 3, isTarget3D ? 4 : 3);
+        context = new ContextualParameters(descriptor, isSource3D ? 3 : 2, isTarget3D ? 3 : 2);
         this.grid = grid;
         computeConversionFactors();
     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
index 845f02a..d3ac78b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/EllipsoidToCentricTransform.java
@@ -279,7 +279,7 @@ public class EllipsoidToCentricTransform extends AbstractMathTransform implement
          * Copy parameters to the ContextualParameter. Those parameters are not used directly by
          * EllipsoidToCentricTransform, but we need to store them in case the user asks for them.
          */
-        context = new ContextualParameters(GeographicToGeocentric.PARAMETERS, withHeight ? 4 : 3, 4);
+        context = new ContextualParameters(GeographicToGeocentric.PARAMETERS, withHeight ? 3 : 2, 3);
         context.getOrCreate(SEMI_MAJOR).setValue(semiMajor, unit);
         context.getOrCreate(SEMI_MINOR).setValue(semiMinor, unit);
         /*
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedTransform.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedTransform.java
index 0f3d4cd..2044551 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedTransform.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/InterpolatedTransform.java
@@ -164,8 +164,8 @@ public class InterpolatedTransform extends DatumShiftTransform {
             Number offset = 0.0;
             final Number[] coefficients = Units.coefficients(normalized.getConverterTo(unit));
             switch (coefficients != null ? coefficients.length : -1) {
-                case 2:  scale  = coefficients[1];       // Fall through
-                case 1:  offset = coefficients[0];       // Fall through
+                case 2:  scale  = coefficients[1];                                      // Fall through
+                case 1:  offset = coefficients[0];                                      // Fall through
                 case 0:  break;
                 default: throw new IllegalArgumentException(Resources.format(Resources.Keys.NonLinearUnitConversion_2, normalized, unit));
             }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
index 11a3201..158d283 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/builder/LocalizationGridBuilderTest.java
@@ -138,5 +138,10 @@ public final strictfp class LocalizationGridBuilderTest extends TransformTestCas
         assertArrayEquals(new double[] {  0.4,  -21.7}, builder.getControlPoint(1, 0), STRICT);
         assertArrayEquals(new double[] {  1.3,   -8.5}, builder.getControlPoint(0, 2), STRICT);
         assertArrayEquals(new double[] { 87.7, -123.7}, builder.getControlPoint(1, 2), STRICT);
+        /*
+         * Verify getting a row and a column.
+         */
+        assertArrayEquals(new double[] {-8.5, -123.7}, builder.getRow(1, 2).doubleValues(), STRICT);
+        assertArrayEquals(new double[] {-21.7, -26.2, -123.7}, builder.getColumn(1, 1).doubleValues(), STRICT);
     }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ContextualParametersTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ContextualParametersTest.java
index 2336cf3..eac37f4 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ContextualParametersTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ContextualParametersTest.java
@@ -16,14 +16,12 @@
  */
 package org.apache.sis.referencing.operation.transform;
 
-import java.util.Collections;
 import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValue;
 import org.opengis.parameter.ParameterNotFoundException;
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
-import org.apache.sis.referencing.operation.DefaultOperationMethod;
 import org.apache.sis.parameter.DefaultParameterDescriptorGroupTest;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.test.DependsOnMethod;
@@ -40,7 +38,7 @@ import static org.opengis.test.Assert.*;
  * Tests {@link ContextualParameters}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.6
+ * @version 1.0
  * @since   0.6
  * @module
  */
@@ -50,9 +48,7 @@ public final strictfp class ContextualParametersTest extends TestCase {
      * Creates an instance to use for testing purpose.
      */
     private static ContextualParameters create(final int srcDim, final int dstDim) {
-        return new ContextualParameters(new DefaultOperationMethod(
-                Collections.singletonMap(DefaultOperationMethod.NAME_KEY, "Test method"),
-                srcDim, dstDim, DefaultParameterDescriptorGroupTest.M1_M1_O1_O2));
+        return new ContextualParameters(DefaultParameterDescriptorGroupTest.M1_M1_O1_O2, srcDim, dstDim);
     }
 
     /**
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java b/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
index 3c035ea..8f9dad0 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/Vector.java
@@ -842,10 +842,10 @@ search:     for (;;) {
      * <p>This method does not copy the values. Consequently any modification to the
      * values of this vector will be reflected in the returned view and vice-versa.</p>
      *
-     * @param  first   index of the first value to be included in the returned view.
-     * @param  step    the index increment in this vector between two consecutive values
-     *                 in the returned vector. Can be positive, zero or negative.
-     * @param  length  the length of the vector to be returned. Can not be greater than
+     * @param  first   index of the first value in this vector to be included in the returned view.
+     * @param  step    the index increment between values in this vector to be included in the returned view.
+     *                 Can be positive, zero or negative.
+     * @param  length  the length of the view to be returned. Can not be greater than
      *                 the length of this vector, except if the {@code step} is zero.
      * @return a view of this vector containing values in the given index range.
      * @throws IndexOutOfBoundsException if {@code first} or {@code first + step*(length-1)}
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
index a10fda0..a17d736 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_C.java
@@ -16,14 +16,17 @@
  */
 package org.apache.sis.internal.earth.netcdf;
 
+import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
+import java.util.Collections;
 import java.util.regex.Pattern;
 import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.internal.netcdf.Convention;
 import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.netcdf.VariableRole;
+import org.apache.sis.internal.netcdf.Linearizer;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
 import org.apache.sis.measure.NumberRange;
 
@@ -306,4 +309,16 @@ public final class GCOM_C extends Convention {
         }
         return tr;
     }
+
+    /**
+     * Returns an enumeration of two-dimensional non-linear transforms that may be tried in attempts to make
+     * localization grid more linear.
+     *
+     * @param  decoder  the netCDF file for which to determine linearizers that may possibly apply.
+     * @return enumeration of two-dimensional non-linear transforms to try.
+     */
+    @Override
+    public Set<Linearizer> linearizers(final Decoder decoder) {
+        return Collections.singleton(Linearizer.GROUND_TRACK);
+    }
 }
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
index 90177bb..9c53ad6 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/internal/earth/netcdf/GCOM_W.java
@@ -16,14 +16,17 @@
  */
 package org.apache.sis.internal.earth.netcdf;
 
+import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
+import java.util.Collections;
 import java.util.regex.Pattern;
 import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.internal.netcdf.Convention;
 import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.Variable;
 import org.apache.sis.internal.netcdf.VariableRole;
+import org.apache.sis.internal.netcdf.Linearizer;
 import org.apache.sis.referencing.operation.transform.TransferFunction;
 
 
@@ -204,4 +207,16 @@ public final class GCOM_W extends Convention {
         }
         return tr;
     }
+
+    /**
+     * Returns an enumeration of two-dimensional non-linear transforms that may be tried in attempts to make
+     * localization grid more linear.
+     *
+     * @param  decoder  the netCDF file for which to determine linearizers that may possibly apply.
+     * @return enumeration of two-dimensional non-linear transforms to try.
+     */
+    @Override
+    public Set<Linearizer> linearizers(final Decoder decoder) {
+        return Collections.singleton(Linearizer.GROUND_TRACK);
+    }
 }
diff --git a/storage/sis-netcdf/pom.xml b/storage/sis-netcdf/pom.xml
index 583d3bc..34dd5b5 100644
--- a/storage/sis-netcdf/pom.xml
+++ b/storage/sis-netcdf/pom.xml
@@ -122,6 +122,20 @@
 
     <!-- Leverage GeoAPI tests. -->
     <dependency>
+      <groupId>org.apache.sis.core</groupId>
+      <artifactId>sis-referencing</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sis.core</groupId>
+      <artifactId>sis-metadata</artifactId>
+      <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-jdk14</artifactId>
       <scope>test</scope>
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
index 2b8438a..2678593 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Axis.java
@@ -109,6 +109,8 @@ public final class Axis extends NamedElement {
      * <p>A given {@link Grid} should not have two {@code Axis} instances with equal {@code sourceDimensions} array.
      * When {@code sourceDimensions.length} ≧ 2 we may have two {@code Axis} instances with the same indices in their
      * {@code sourceDimensions} arrays, but those indices should be in different order.</p>
+     *
+     * @see #getMainDirection()
      */
     final int[] sourceDimensions;
 
@@ -245,7 +247,7 @@ public final class Axis extends NamedElement {
      * @throws DataStoreException if a logical error occurred.
      * @throws ArithmeticException if the size of an axis exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
      *
-     * @see #getDimension()
+     * @see #getMainDirection()
      */
     final void mainDimensionFirst(final Axis[] axes, final int count) throws IOException, DataStoreException {
         final int d0 = sourceDimensions[0];
@@ -313,6 +315,26 @@ public final class Axis extends NamedElement {
     }
 
     /**
+     * Returns the fastest varying dimension of this "two-dimensional" axis.
+     *
+     * <ul>
+     *   <li>If this method returns 0, then axis coordinates vary mostly in columns.
+     *       Or to be more accurate, in first grid dimension (in netCDF order) associated to this axis.</li>
+     *   <li>If this method returns 1, then axis coordinates vary mostly in rows.
+     *       Or to be more accurate, in second grid dimension (in netCDF order) associated to this axis.</li>
+     * </ul>
+     *
+     * If a grid has <var>n</var> dimensions but we copy in an array of length 2 the dimensions used by this
+     * {@code Axis} instance, while preserving the dimension order as declared in the netCDF file, then the
+     * value returned by this method is the index of the "main" dimension in this array of length 2.
+     *
+     * @return 0 or 1, depending on whether coordinates vary mostly on columns or on rows respectively.
+     */
+    final int getMainDirection() {
+        return (sourceDimensions.length < 2 || sourceDimensions[0] <= sourceDimensions[1]) ? 0 : 1;
+    }
+
+    /**
      * Returns the number of dimension of the localization grid used by this axis.
      * This method returns 2 if this axis if backed by a localization grid having 2 or more dimensions.
      * In the netCDF UCAR library, such axes are handled by a {@link ucar.nc2.dataset.CoordinateAxis2D}.
@@ -344,6 +366,7 @@ public final class Axis extends NamedElement {
      * Returns the {@link #sourceSizes} value at the given index, making sure it is representable as a
      * signed integer value. This method is invoked by operations not designed for unsigned integers.
      *
+     * @param  i  index of the desired dimension, in the same order than {@link #sourceDimensions}.
      * @throws ArithmeticException if the size can not be represented as a signed 32 bits integer.
      */
     private int getSize(final int i) {
@@ -564,7 +587,7 @@ public final class Axis extends NamedElement {
     final boolean trySetTransform(final Matrix gridToCRS, final int lastSrcDim, final int tgtDim,
             final List<MathTransform> nonLinears) throws IOException, DataStoreException
     {
-main:   switch (getDimension()) {
+        switch (getDimension()) {
             /*
              * Defined as a matter of principle, but should never happen.
              */
@@ -609,7 +632,7 @@ main:   switch (getDimension()) {
                 for (int r : repetitions) {
                     repetitionLength = Math.multiplyExact(repetitionLength, r);
                 }
-                final int ri = (sourceDimensions[0] <= sourceDimensions[1]) ? 0 : 1;
+                final int ri = getMainDirection();
                 for (int i=0; i<=1; i++) {
                     final int width  = getSize(ri ^ i    );
                     final int height = getSize(ri ^ i ^ 1);
@@ -672,8 +695,8 @@ main:   switch (getDimension()) {
                  */
                 final int ri = (xd <= yd) ? 0 : 1;      // Take in account that mainDimensionFirst(…) may have reordered values.
                 final int ro = (xo <= yo) ? 0 : 1;
-                final int width  = getSize(ri ^ 1);     // Fastest varying is right-most dimension.
-                final int height = getSize(ri    );     // Slowest varying if left-most dimension.
+                final int width  = getSize(ri ^ 1);     // Fastest varying is right-most dimension (when in netCDF order).
+                final int height = getSize(ri    );     // Slowest varying is left-most dimension (when in netCDF order).
                 if (other.sourceSizes[ro ^ 1] == width &&
                     other.sourceSizes[ro    ] == height)
                 {
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 461cdf8..ac5301c 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
@@ -436,7 +436,7 @@ findFree:       for (int srcDim : axis.sourceDimensions) {
                             if (grid != null) {
                                 final Set<Linearizer> linearizers = decoder.convention().linearizers(decoder);
                                 if (!linearizers.isEmpty()) {
-                                    Linearizer.applyTo(linearizers, grid, gridAxes);
+                                    Linearizer.applyTo(linearizers, factory, grid, gridAxes);
                                 }
                                 /*
                                  * Replace the first transform by the two-dimensional localization grid and
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 d59bf99..3917825 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
@@ -23,6 +23,7 @@ import org.opengis.util.FactoryException;
 import org.opengis.parameter.ParameterValueGroup;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
 import org.apache.sis.internal.metadata.ReferencingServices;
 import org.apache.sis.internal.system.DefaultFactories;
@@ -54,7 +55,12 @@ public enum Linearizer {
      * Mercator (Spherical) projection. Inputs are latitude and longitude in any order (axis order will
      * be detected by inspection of {@link Axis} elements). Outputs are projected coordinates.
      */
-    MERCATOR("Mercator (Spherical)");
+    MERCATOR("Mercator (Spherical)"),
+
+    /**
+     * Satellite ground track.
+     */
+    GROUND_TRACK(null);
 
     /**
      * The map projection method to use for constructing {@link #transform}, or {@code null} if the operation
@@ -79,6 +85,8 @@ public enum Linearizer {
     /**
      * Returns the hard-coded transform represented by this enumeration that may help to make a localization grid
      * more linear.
+     *
+     * @return the transform, or {@code null} if it can not be built.
      */
     private synchronized MathTransform transform() {
         final String p = projection;
@@ -96,12 +104,7 @@ public enum Linearizer {
                 pg.parameter(Constants.SEMI_MINOR).setValue(ReferencingServices.AUTHALIC_RADIUS);
                 transform = factory.createParameterizedTransform(pg);
             } catch (FactoryException e) {
-                /*
-                 * Should never happen. But if it happens anyway, do not cause the whole netCDF reader
-                 * to fail for all files because of this error. Declare this error as originating from
-                 * Variable.getGridGeometry() because it is the caller (indirectly) for this class.
-                 */
-                Logging.unexpectedException(Logging.getLogger(Modules.NETCDF), Variable.class, "getGridGeometry", e);
+                warning(e);
             }
         }
         return transform;
@@ -110,11 +113,14 @@ public enum Linearizer {
     /**
      * Applies non-linear transform candidates to the given localization grid.
      *
+     * @param  factory      the factory to use for creating transforms.
      * @param  linearizers  the linearizers to apply.
      * @param  grid         the grid on which to add non-linear transform candidates.
      * @param  axes         coordinate system axes in CRS order.
      */
-    static void applyTo(final Set<Linearizer> linearizers, final LocalizationGridBuilder grid, final Axis[] axes) {
+    static void applyTo(final Set<Linearizer> linearizers, final MathTransformFactory factory,
+                        final LocalizationGridBuilder grid, final Axis[] axes)
+    {
         int xdim = -1, ydim = -1;
         for (int i=axes.length; --i >= 0;) {
             switch (axes[i].abbreviation) {
@@ -125,11 +131,42 @@ public enum Linearizer {
         if (xdim >= 0 && ydim >= 0) {
             final Map<String,MathTransform> projections = new HashMap<>();
             for (final Linearizer linearizer : linearizers) {
-                if (linearizer.transform != null) {
-                    projections.put(linearizer.name(), linearizer.transform());
+                MathTransform transform;
+                switch (linearizer) {
+                    default: {
+                        transform = linearizer.transform();
+                        break;
+                    }
+                    /*
+                     * Some special cases require information about the particular grid we have at hand.
+                     * Only one case for now, but more cases may be added here in the future.
+                     */
+                    case GROUND_TRACK: {
+                        int direction = axes[ydim].getMainDirection();  // Fastest varying dimension (in netCDF order) of latitude.
+                        direction ^= 1;                                 // Convert netCDF order to CRS order.
+                        try {
+                            transform = SatelliteGroundTrack.create(factory, grid, xdim, direction);
+                        } catch (FactoryException | TransformException e) {
+                            transform = null;
+                            warning(e);
+                        }
+                        break;
+                    }
+                }
+                if (transform != null) {
+                    projections.put(linearizer.name(), transform);
                 }
             }
             grid.addLinearizers(projections, xdim, ydim);
         }
     }
+
+    /**
+     * Reports a warning as originating from {@link Variable#getGridGeometry()}, because it is the caller
+     * (indirectly) for this class. The warning reported to this method should never happen. But if one
+     * happens anyway, do not cause the whole netCDF reader to fail for all files because of this error.
+     */
+    private static void warning(final Exception e) {
+        Logging.unexpectedException(Logging.getLogger(Modules.NETCDF), Variable.class, "getGridGeometry", e);
+    }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/SatelliteGroundTrack.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/SatelliteGroundTrack.java
new file mode 100644
index 0000000..d6eddf0
--- /dev/null
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/SatelliteGroundTrack.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.netcdf;
+
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransform2D;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.AbstractMathTransform2D;
+import org.apache.sis.referencing.operation.transform.ContextualParameters;
+import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.matrix.Matrix2;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.geometry.DirectPosition2D;
+import org.apache.sis.internal.util.DoubleDouble;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.math.Vector;
+import org.apache.sis.math.Line;
+
+
+/**
+ * An estimation of the position of the satellite for given row and column indices.
+ * The calculation done in this class is very rough; the intent is not to give an exact answer,
+ * but to convert grid indices to something roughly proportional to latitudes and longitudes
+ * in order to make {@link LocalizationGridBuilder} work easier.
+ *
+ * <p>Current implementation is similar to a <a href="https://en.wikipedia.org/wiki/Sinusoidal_projection">sinusoidal projection</a>
+ * in which the central meridian is oblique. That "oblique central meridian" is fitted (by linear regression) to the presumed
+ * satellite trajectory. This model may change in any future SIS version.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class SatelliteGroundTrack extends AbstractMathTransform2D {
+    /**
+     * Parameter descriptor for this transform.
+     */
+    private static final ParameterDescriptorGroup PARAMETERS;
+    static {
+        final ParameterBuilder builder = new ParameterBuilder().setRequired(true);
+        final ParameterDescriptor<?>[] grids = new ParameterDescriptor<?>[] {
+            builder.addName(Constants.CENTRAL_MERIDIAN + "_start").create(DirectPosition.class, null),
+            builder.addName(Constants.CENTRAL_MERIDIAN + "_end")  .create(DirectPosition.class, null),
+        };
+        PARAMETERS = builder.addName("Satellite ground track").createGroup(grids);
+    }
+
+    /**
+     * Parameters describing (at least partially) this transform.
+     * They are used for formatting <cite>Well Known Text</cite> (WKT).
+     *
+     * @see #getContextualParameters()
+     */
+    private final ContextualParameters context;
+
+    /**
+     * Terms of the λ = <var>slope</var>⋅φ + λ₀ equation estimating the satellite longitude λ for a latitude φ.
+     */
+    private final double λ0, slope;
+
+    /**
+     * The inverse of this transform.
+     */
+    private final MathTransform2D inverse;
+
+    /**
+     * Creates a new instance of this transform.
+     *
+     * @param grid       localization grid containing longitude and latitude coordinates.
+     * @param lonDim     dimension of the longitude coordinates in the given grid.
+     * @param direction  0 if the ground track is on rows, of 1 if it is on columns.
+     */
+    private SatelliteGroundTrack(final LocalizationGridBuilder grid, final int lonDim, final int direction) throws TransformException {
+        /*
+         * We presume that the row or column in the middle of the localization grid give the
+         * coordinates that are closest to coordinates of the actual satellite ground track.
+         */
+        final int median = (int) grid.getSourceEnvelope(false).getMedian(direction ^ 1);
+        final Vector longitudes, latitudes;
+        if (direction == 0) {
+            longitudes = grid.getRow(lonDim,     median);
+            latitudes  = grid.getRow(lonDim ^ 1, median);
+        } else {
+            longitudes = grid.getColumn(lonDim,     median);
+            latitudes  = grid.getColumn(lonDim ^ 1, median);
+        }
+        final Line line = new Line();
+        line.fit(latitudes, longitudes);
+        λ0      = line.y0();                                // Longitude in degrees.
+        slope   = Math.toRadians(line.slope());             // Take the slope as if all latitudes were given in radians.
+        inverse = new Inverse();
+        context = new ContextualParameters(PARAMETERS, 2, 2);
+        final MatrixSIS normalize = context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
+        normalize.convertAfter(1, DoubleDouble.createDegreesToRadians(), null);
+        setPositionParameter(Constants.CENTRAL_MERIDIAN + "_start", line, latitudes.doubleValue(0));
+        setPositionParameter(Constants.CENTRAL_MERIDIAN + "_end",   line, latitudes.doubleValue(latitudes.size() - 1));
+    }
+
+    /**
+     * Sets the {@link DirectPosition} value of a parameter.
+     */
+    private void setPositionParameter(final String name, final Line line, final double φ) {
+        final DirectPosition2D pos = new DirectPosition2D(φ, line.y(φ));
+        context.parameter(name).setValue(pos);
+    }
+
+    /**
+     * Creates a new instance of this transform, or returns {@code null} if no instance can be created.
+     * The grid is presumed to contains latitude and longitude coordinates in decimal degrees.
+     *
+     * @param factory    the factory to use for creating transforms.
+     * @param grid       localization grid containing longitude and latitude coordinates.
+     * @param lonDim     dimension of the longitude coordinates in the given grid.
+     * @param direction  0 if the ground track is on rows, of 1 if it is on columns.
+     */
+    static MathTransform create(final MathTransformFactory factory, final LocalizationGridBuilder grid,
+            final int lonDim, final int direction) throws TransformException, FactoryException
+    {
+        final SatelliteGroundTrack tr = new SatelliteGroundTrack(grid, lonDim, direction);
+        return tr.context.completeTransform(factory, tr);
+    }
+
+    /**
+     * Returns the parameter values for this math transform.
+     */
+    @Override
+    public ParameterValueGroup getParameterValues() {
+        return context;
+    }
+
+    /**
+     * Returns the parameters used for creating the complete transformation. Those parameters describe a sequence
+     * of <cite>normalize</cite> → {@code this} → <cite>denormalize</cite> transforms.
+     */
+    @Override
+    protected ContextualParameters getContextualParameters() {
+        return context;
+    }
+
+    /**
+     * Converts a single geographic coordinates into something hopefully more proportional to grid indices.
+     * For each coordinate tuple in {@code srcPts}, the first coordinate value is longitude in degrees and
+     * the second value is latitude in <strong>radians</strong>. The conversion from degrees to radians is
+     * done by the concatenated transform.
+     */
+    @Override
+    public Matrix transform(final double[] srcPts, final int srcOff,
+                            final double[] dstPts, final int dstOff,
+                            final boolean derivate) throws TransformException
+    {
+        final double λ    = srcPts[srcOff  ];
+        final double φ    = srcPts[srcOff+1];
+        final double cosφ = Math.cos(φ);
+        final double m    = φ * slope + λ0;                 // Central meridian at the given latitude.
+        final double Δλ   = λ - m;
+        if (dstPts != null) {
+            dstPts[dstOff  ] = Δλ * cosφ + m;               // TODO: use Math.fma with JDK9.
+            dstPts[dstOff+1] = φ;
+        }
+        if (!derivate) {
+            return null;
+        }
+        final Matrix2 d = new Matrix2();
+        d.m00 = cosφ;
+        d.m01 = slope * (1 - cosφ) - Δλ * Math.sin(φ);
+        return d;
+    }
+
+    /**
+     * Returns the inverse of this transform.
+     */
+    @Override
+    public MathTransform2D inverse() {
+        return inverse;
+    }
+
+    /**
+     * The inverse of {@link SatelliteGroundTrack} transform.
+     */
+    private final class Inverse extends AbstractMathTransform2D.Inverse {
+        /**
+         * Creates a new instance of the inverse transform.
+         */
+        Inverse() {
+        }
+
+        /**
+         * Returns the inverse of this transform, which is the enclosing {@link SatelliteGroundTrack} transform.
+         */
+        @Override
+        public MathTransform2D inverse() {
+            return SatelliteGroundTrack.this;
+        }
+
+        /**
+         * Converts grid indices to geographic coordinates (not necessarily in degrees units).
+         * See {@link SatelliteGroundTrack#transform(double[], int, double[], int, boolean)}
+         * for the units of measurement.
+         */
+        @Override
+        public Matrix transform(final double[] srcPts, final int srcOff,
+                                final double[] dstPts, final int dstOff,
+                                final boolean derivate) throws TransformException
+        {
+            final double x    = srcPts[srcOff  ];
+            final double φ    = srcPts[srcOff+1];
+            final double cosφ = Math.cos(φ);
+            final double m    = φ * slope + λ0;                 // Central meridian at the given latitude.
+            final double Δx   = x - m;
+            if (dstPts != null) {
+                dstPts[dstOff  ] = Δx / cosφ + m;
+                dstPts[dstOff+1] = φ;
+            }
+            if (!derivate) {
+                return null;
+            }
+            final Matrix2 d = new Matrix2();
+            d.m00 = 1 / cosφ;
+            d.m01 = (Δx * Math.sin(φ) / cosφ - slope) / cosφ + slope;
+            return d;
+        }
+    }
+}
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/SatelliteGroundTrackTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/SatelliteGroundTrackTest.java
new file mode 100644
index 0000000..c2fdad6
--- /dev/null
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/SatelliteGroundTrackTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.netcdf;
+
+import java.awt.geom.AffineTransform;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.operation.MathTransformFactory;
+import org.apache.sis.referencing.operation.builder.LocalizationGridBuilder;
+import org.apache.sis.referencing.operation.transform.MathTransformTestCase;
+import org.apache.sis.referencing.operation.transform.CoordinateDomain;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.junit.Test;
+
+
+/**
+ * Tests {@link SatelliteGroundTrack}. There is no external data that we can use as a reference.
+ * Consequently this test merely verifies that {@link SatelliteGroundTrack} is self-consistent.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class SatelliteGroundTrackTest extends MathTransformTestCase {
+    /**
+     * Size of the grid created for testing purpose.
+     */
+    private static final int WIDTH = 20, HEIGHT = 20;
+
+    /**
+     * Creates a transform for a grid of fixed size in a geographic domain.
+     */
+    private void createTransform() throws TransformException, FactoryException {
+        final LocalizationGridBuilder grid = new LocalizationGridBuilder(WIDTH, HEIGHT);
+        final AffineTransform tr = AffineTransform.getRotateInstance(StrictMath.random()/2 + 0.25);     // Between 14 and 43°.
+        tr.translate(-WIDTH / 2, -HEIGHT / 2);
+        final double[] point = new double[2];
+        for (int y=0; y<HEIGHT; y++) {
+            for (int x=0; x<WIDTH; x++) {
+                point[0] = x;
+                point[1] = y;
+                tr.transform(point, 0, point, 0, 1);
+                grid.setControlPoint(x, y, point);
+            }
+        }
+        transform = SatelliteGroundTrack.create(DefaultFactories.forBuildin(MathTransformFactory.class), grid, 1, 1);
+        tolerance = 1E-12;
+        derivativeDeltas = new double[] {0.1, 0.1};
+    }
+
+    /**
+     * Tests self-consistency at random points.
+     *
+     * @throws FactoryException if an error occurred while creating the transform.
+     * @throws TransformException if an error occurred while transforming a point.
+     */
+    @Test
+    public void testConsistency() throws TransformException, FactoryException {
+        createTransform();
+        validate();
+        verifyInDomain(CoordinateDomain.RANGE_10, -979924465940961910L);
+        transform = transform.inverse();
+        validate();
+        verifyInDomain(CoordinateDomain.RANGE_10, -2122465178330330413L);
+    }
+}
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/test/suite/NetcdfTestSuite.java b/storage/sis-netcdf/src/test/java/org/apache/sis/test/suite/NetcdfTestSuite.java
index 5074f24..90537e9 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/test/suite/NetcdfTestSuite.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/test/suite/NetcdfTestSuite.java
@@ -34,6 +34,7 @@ import org.junit.BeforeClass;
     org.apache.sis.internal.netcdf.DecoderTest.class,
     org.apache.sis.internal.netcdf.VariableTest.class,
     org.apache.sis.internal.netcdf.GridTest.class,
+    org.apache.sis.internal.netcdf.SatelliteGroundTrackTest.class,
     org.apache.sis.internal.netcdf.impl.ChannelDecoderTest.class,
     org.apache.sis.internal.netcdf.impl.VariableInfoTest.class,
     org.apache.sis.internal.netcdf.impl.GridInfoTest.class,


Mime
View raw message