You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/01/14 16:34:23 UTC

[sis] branch geoapi-4.0 updated (ae61bb0a20 -> 8991c55d92)

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

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


    from ae61bb0a20 Add missing `equals` and `hashCode` method on a legacy metadata property.
     new 8881e88be3 Disable some GIGS tests that are known to fail. https://gigs.iogp.org/
     new 0f46e213f3 More advanced test for rounding errors.
     new 8991c55d92 Replace `Path` parameter value by `URI` in operation methods using datum shift grids.

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/java/org/apache/sis/gui/RecentFiles.java  |   2 +-
 .../referencing/provider/DatumShiftGridFile.java   |  65 ++--
 .../referencing/provider/DatumShiftGridGroup.java  |   6 +-
 .../referencing/provider/DatumShiftGridLoader.java |  53 ++-
 .../provider/FranceGeocentricInterpolation.java    | 359 +++++++++++----------
 .../provider/MolodenskyInterpolation.java          |   1 +
 .../sis/internal/referencing/provider/NADCON.java  |  93 +++---
 .../sis/internal/referencing/provider/NTv2.java    |  54 ++--
 .../src/main/resources/META-INF/GIGS.properties    |  80 +++++
 .../referencing/provider/DatumShiftTestCase.java   |  23 +-
 .../FranceGeocentricInterpolationTest.java         |  34 +-
 .../internal/referencing/provider/NADCONTest.java  |  11 +-
 .../internal/referencing/provider/NTv2Test.java    |  19 +-
 .../transform/ProjectiveTransformTest.java         |   7 +-
 .../sis/test/integration/DatumShiftTest.java       |  10 +-
 .../apache/sis/internal/system/DataDirectory.java  |  23 +-
 .../src/test/java/org/apache/sis/test/Assume.java  |   7 +-
 .../java/org/apache/sis/test/package-info.java     |   2 +-
 18 files changed, 489 insertions(+), 360 deletions(-)
 create mode 100644 core/sis-referencing/src/main/resources/META-INF/GIGS.properties


[sis] 01/03: Disable some GIGS tests that are known to fail. https://gigs.iogp.org/

Posted by de...@apache.org.
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

commit 8881e88be30b10c50e7eb84a2be0d334dee963df
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 14 12:26:25 2023 +0100

    Disable some GIGS tests that are known to fail.
    https://gigs.iogp.org/
---
 .../src/main/resources/META-INF/GIGS.properties    | 80 ++++++++++++++++++++++
 1 file changed, 80 insertions(+)

diff --git a/core/sis-referencing/src/main/resources/META-INF/GIGS.properties b/core/sis-referencing/src/main/resources/META-INF/GIGS.properties
new file mode 100644
index 0000000000..53b50f829a
--- /dev/null
+++ b/core/sis-referencing/src/main/resources/META-INF/GIGS.properties
@@ -0,0 +1,80 @@
+# Configuration file for Geospatial Integrity of Geoscience Software (GIGS) tests
+# https://gigs.iogp.org/
+#
+# This file disables some GIGS tests that are known to fail.
+
+Test2203.EPSG_8913.isStandardAliasSupported=false
+Test2204.EPSG_6123.isStandardAliasSupported=false
+Test2204.EPSG_6142.isStandardAliasSupported=false
+Test2204.EPSG_6143.isStandardAliasSupported=false
+Test2204.EPSG_6152.isStandardNameSupported=false
+Test2204.EPSG_6156.isStandardAliasSupported=false
+Test2204.EPSG_6160.isStandardAliasSupported=false
+Test2204.EPSG_6171.isStandardAliasSupported=false
+Test2204.EPSG_6190.isStandardAliasSupported=false
+Test2204.EPSG_6258.isStandardAliasSupported=false
+Test2204.EPSG_6277.isStandardNameSupported=false
+Test2204.EPSG_6326.isStandardAliasSupported=false
+Test2204.EPSG_6659.isStandardNameSupported=false
+Test2204.EPSG_6674.isStandardNameSupported=false
+Test2204.EPSG_6694.isStandardAliasSupported=false
+Test2204.EPSG_6728.isStandardNameSupported=false
+Test2204.EPSG_6740.isStandardNameSupported=false
+Test2204.EPSG_6818.isStandardAliasSupported=false
+Test2205.EPSG_4143.isStandardAliasSupported=false
+Test2205.EPSG_4277.isStandardNameSupported=false
+Test2205.EPSG_4723.isStandardNameSupported=false
+Test2205.EPSG_4932.isStandardNameSupported=false
+Test2205.EPSG_4933.isStandardNameSupported=false
+Test2205.EPSG_4935.isStandardNameSupported=false
+Test2205.EPSG_4936.isStandardAliasSupported=false
+Test2205.EPSG_4937.isStandardAliasSupported=false
+Test2205.EPSG_4938.isStandardNameSupported=false
+Test2205.EPSG_4939.isStandardNameSupported=false
+Test2205.EPSG_4942.isStandardNameSupported=false
+Test2205.EPSG_4943.isStandardNameSupported=false
+Test2205.EPSG_4946.isStandardNameSupported=false
+Test2205.EPSG_4947.isStandardNameSupported=false
+Test2205.EPSG_4950.isStandardNameSupported=false
+Test2205.EPSG_4951.isStandardNameSupported=false
+Test2205.EPSG_4964.isStandardNameSupported=false
+Test2205.EPSG_4965.isStandardNameSupported=false
+Test2207.EPSG_2041.isStandardAliasSupported=false
+Test2207.EPSG_2043.isStandardAliasSupported=false
+Test2207.EPSG_22032.isDependencyIdentificationSupported=false
+Test2207.EPSG_22033.isDependencyIdentificationSupported=false
+Test2207.EPSG_22091.isDependencyIdentificationSupported=false
+Test2207.EPSG_22092.isDependencyIdentificationSupported=false
+Test2207.EPSG_27700.isStandardNameSupported=false
+Test2207.EPSG_29871.isStandardAliasSupported=false
+Test2208.EPSG_1311.isStandardNameSupported=false
+Test2208.EPSG_1327.isStandardNameSupported=false
+Test2208.EPSG_15715.isStandardNameSupported=false
+Test2208.EPSG_15934.isStandardNameSupported=false
+Test2208.EPSG_1612.isStandardNameSupported=false
+Test2208.EPSG_1613.isStandardNameSupported=false
+Test2208.EPSG_1753.isStandardNameSupported=false
+Test2208.EPSG_1998.isStandardNameSupported=false
+Test2208.EPSG_1999.isStandardNameSupported=false
+Test2209.EPSG_5181.isStandardAliasSupported=false
+Test2209.EPSG_5181.isStandardNameSupported=false
+Test2209.EPSG_5182.isStandardAliasSupported=false
+Test2209.EPSG_5182.isStandardNameSupported=false
+Test2210.EPSG_5612.isStandardAliasSupported=false
+Test2210.EPSG_5614.isStandardAliasSupported=false
+Test2210.EPSG_5621.isStandardAliasSupported=false
+Test2210.EPSG_5701.isStandardAliasSupported=false
+Test2210.EPSG_5702.isStandardAliasSupported=false
+Test2210.EPSG_5706.isStandardAliasSupported=false
+Test2210.EPSG_5709.isStandardAliasSupported=false
+Test2210.EPSG_5712.isStandardAliasSupported=false
+Test2210.EPSG_5714.isStandardAliasSupported=false
+Test2210.EPSG_5715.isStandardAliasSupported=false
+Test2210.EPSG_5719.isStandardAliasSupported=false
+Test2210.EPSG_5724.isStandardAliasSupported=false
+Test2210.EPSG_5734.isStandardAliasSupported=false
+Test2210.EPSG_5736.isStandardAliasSupported=false
+Test2210.EPSG_5786.isStandardAliasSupported=false
+Test2210.EPSG_5796.isStandardAliasSupported=false
+Test2211.EPSG_5443.isStandardNameSupported=false
+Test2211.EPSG_5445.isStandardNameSupported=false


[sis] 02/03: More advanced test for rounding errors.

Posted by de...@apache.org.
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

commit 0f46e213f39d4d26ff37dcbc5b90ea4ffd8faf4f
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 14 13:00:30 2023 +0100

    More advanced test for rounding errors.
---
 .../referencing/operation/transform/ProjectiveTransformTest.java   | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ProjectiveTransformTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ProjectiveTransformTest.java
index f7c03e26d8..7ed92c62a3 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ProjectiveTransformTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/transform/ProjectiveTransformTest.java
@@ -186,6 +186,11 @@ public class ProjectiveTransformTest extends AffineTransformTest {
     public void testRoundingErrors() throws FactoryException, TransformException {
         final Matrix4 num = new Matrix4(); num.m00 =  2; num.m11 = 3.25; num.m22 = -17;
         final Matrix4 den = new Matrix4(); den.m00 = 37; den.m11 = 1000; den.m22 = 127;
+
+        // Add translation terms.
+        num.m03 =  4*37; num.m13 = 17; num.m23 = -2*127;
+        den.m03 = -3*37; den.m13 = 65; den.m23 =  7*127;
+
         transform = TransformResultComparator.concatenate(
                 mtFactory.createAffineTransform(num),
                 mtFactory.createAffineTransform(den).inverse(),
@@ -203,7 +208,7 @@ public class ProjectiveTransformTest extends AffineTransformTest {
          * Test a transformation, expecting exact result.
          */
         verifyTransform(new double[] {2645.5,  19500, 2222.5},
-                        new double[] {   143, 63.375, -297.5});
+                        new double[] {   150, 63.327, -306.5});
     }
 
     /**


[sis] 03/03: Replace `Path` parameter value by `URI` in operation methods using datum shift grids.

Posted by de...@apache.org.
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

commit 8991c55d92826eb9fcda9f3d0570adfedbc33a77
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 14 17:31:56 2023 +0100

    Replace `Path` parameter value by `URI` in operation methods using datum shift grids.
    
    https://issues.apache.org/jira/browse/SIS-569
---
 .../main/java/org/apache/sis/gui/RecentFiles.java  |   2 +-
 .../referencing/provider/DatumShiftGridFile.java   |  65 ++--
 .../referencing/provider/DatumShiftGridGroup.java  |   6 +-
 .../referencing/provider/DatumShiftGridLoader.java |  53 ++-
 .../provider/FranceGeocentricInterpolation.java    | 359 +++++++++++----------
 .../provider/MolodenskyInterpolation.java          |   1 +
 .../sis/internal/referencing/provider/NADCON.java  |  93 +++---
 .../sis/internal/referencing/provider/NTv2.java    |  54 ++--
 .../referencing/provider/DatumShiftTestCase.java   |  23 +-
 .../FranceGeocentricInterpolationTest.java         |  34 +-
 .../internal/referencing/provider/NADCONTest.java  |  11 +-
 .../internal/referencing/provider/NTv2Test.java    |  19 +-
 .../sis/test/integration/DatumShiftTest.java       |  10 +-
 .../apache/sis/internal/system/DataDirectory.java  |  23 +-
 .../src/test/java/org/apache/sis/test/Assume.java  |   7 +-
 .../java/org/apache/sis/test/package-info.java     |   2 +-
 16 files changed, 403 insertions(+), 359 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/RecentFiles.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/RecentFiles.java
index 5ea3439497..7e20c151c2 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/RecentFiles.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/RecentFiles.java
@@ -121,7 +121,7 @@ final class RecentFiles implements EventHandler<ActionEvent> {
         try {
             file = path.toFile();
         } catch (UnsupportedOperationException e) {
-            // Future version may have an "recently used URI" section. We don't do that for now.
+            // Recently used URIs are not saved here.
             return;
         }
         final int size = items.size();
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
index 70510c524d..5b5cf52834 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
@@ -18,12 +18,14 @@ package org.apache.sis.internal.referencing.provider;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.AbstractMap;
 import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.logging.Level;
+import java.util.concurrent.Callable;
 import java.lang.reflect.Array;
-import java.nio.file.Path;
+import java.net.URI;
 import javax.measure.Unit;
 import javax.measure.Quantity;
 import javax.measure.quantity.Angle;
@@ -47,10 +49,11 @@ import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.referencing.datum.DatumShiftGrid;
-import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
+import org.apache.sis.referencing.factory.FactoryDataException;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
+import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
 
 
 /**
@@ -62,7 +65,7 @@ import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
  * sharing data and for {@link #equals(Object)} and {@link #hashCode()} implementations.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  *
  * @param <C>  dimension of the coordinate unit (usually {@link Angle}).
  * @param <T>  dimension of the translation unit. Usually {@link Angle},
@@ -76,10 +79,10 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = -5801692909082130314L;
+    private static final long serialVersionUID = -1690433946781367085L;
 
     /**
-     * Cache of grids loaded so far. The keys are typically {@link java.nio.file.Path}s or a tuple of paths.
+     * Cache of grids loaded so far. The keys are typically {@link URI}s or a tuple of URIs.
      * Values are grids stored by hard references until the amount of data exceed 32768 (about 128 kilobytes
      * if the values use the {@code float} type), in which case the oldest grids will be replaced by soft references.
      *
@@ -87,8 +90,10 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
      * The use of soft references instead of weak references is on the assumption that users typically use
      * the same few Coordinate Reference Systems for their work. Consequently, we presume that users will not
      * load a lot of grids and are likely to reuse the already loaded grids.
+     *
+     * @see #getOrLoad(URI, URI, Callable)
      */
-    static final Cache<Object, DatumShiftGridFile<?,?>> CACHE = new Cache<Object, DatumShiftGridFile<?,?>>(4, 32*1024, true) {
+    private static final Cache<Object, DatumShiftGridFile<?,?>> CACHE = new Cache<Object, DatumShiftGridFile<?,?>>(4, 32*1024, true) {
         @Override protected int cost(final DatumShiftGridFile<?,?> grid) {
             int p = 1;
             for (final Object data : grid.getData()) {
@@ -112,10 +117,8 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
      * The files from which the grid has been loaded. This is not used directly by this class
      * (except for {@link #equals(Object)} and {@link #hashCode()}), but can be used by math
      * transform for setting the parameter values. Shall never be null and never empty.
-     *
-     * @todo We have a serialization problem here. Possible workaround may be to replace by URI.
      */
-    private final Path[] files;
+    private final URI[] files;
 
     /**
      * Number of cells between the start of adjacent rows in the grid. This is usually {@code getGridSize(0)},
@@ -150,14 +153,14 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
      * in the domain of validity of this grid. Children do not change the way this {@code DatumShiftGrid}
      * performs its calculation; this list is used only at the time of building {@link MathTransform} tree.
      *
-     * <div class="note"><b>Design note:</b>
+     * <h4>Design note</h4>
      * we do not provide sub-grids functionality in the {@link DatumShiftGrid} parent class because
      * the {@link MathTransform} tree will depend on assumptions about {@link #getCoordinateToGrid()},
      * in particular that it contains only translations and scales (no rotation, no shear).
-     * Those assumptions are enforced by the {@link DatumShiftGridFile} constructor.</div>
+     * Those assumptions are enforced by the {@link DatumShiftGridFile} constructor.
      *
-     * This field has protected access for usage by {@link DatumShiftGridGroup} subclass only.
-     * No access to this field should be done except by subclasses.
+     * <p>This field has protected access for usage by {@link DatumShiftGridGroup} subclass only.
+     * No access to this field should be done except by subclasses.</p>
      *
      * @see #setSubGrids(Collection)
      */
@@ -186,7 +189,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
                        final double Δx, final double Δy,
                        final int    nx, final int    ny,
                        final ParameterDescriptorGroup descriptor,
-                       final Path... files) throws NoninvertibleTransformException
+                       final URI... files) throws NoninvertibleTransformException
     {
         super(coordinateUnit, new AffineTransform2D(Δx, 0, 0, Δy, x0, y0).inverse(),
               new int[] {nx, ny}, isCellValueRatio, translationUnit);
@@ -246,6 +249,28 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
         // Accuracy to be set by caller. Initial value needs to be zero.
     }
 
+    /**
+     * Gets the grid from the cache if available, or loads it.
+     *
+     * @param  f1      the main file to load.
+     * @param  f2      a second file to load, or {@code null} if none.
+     * @param  loader  the loader to execute if the grid is not in the cache.
+     * @return the cached or loaded grid.
+     * @throws FactoryException if an error occurred while loading the grid.
+     */
+    static DatumShiftGridFile<?,?> getOrLoad(final URI f1, final URI f2, final Callable<DatumShiftGridFile<?,?>> loader)
+            throws FactoryException
+    {
+        final Object key = (f2 != null) ? new AbstractMap.SimpleImmutableEntry<>(f1, f2) : f1;
+        try {
+            return CACHE.getOrCreate(key, loader);
+        } catch (FactoryException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new FactoryDataException(Errors.format(Errors.Keys.CanNotRead_1, f1), e);
+        }
+    }
+
     /**
      * Sets the sub-grids that are direct children of this grid.
      * This method can be invoked only once.
@@ -288,6 +313,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
 
     /**
      * Formats this grid as a tree with its children.
+     * Used for building a tree representation of children nodes.
      */
     private void toTree(final TreeTable.Node branch) {
         String label = super.toString();
@@ -420,8 +446,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
     }
 
     /**
-     * Sets all parameters for a value of type {@link Path} to the values given to the constructor.
-     * Subclasses may override for defining other kinds of parameters too.
+     * Sets all parameters for a value of type {@link URI} to the values given to the constructor.
      *
      * @param  parameters  the parameter group where to set the values.
      */
@@ -431,8 +456,8 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
         for (final GeneralParameterDescriptor gd : descriptor.descriptors()) {
             if (gd instanceof ParameterDescriptor<?>) {
                 final ParameterDescriptor<?> d = (ParameterDescriptor<?>) gd;
-                if (Path.class.isAssignableFrom(d.getValueClass())) {
-                    if (i >= files.length) break;                               // Safety in case of invalid parameters.
+                if (URI.class.isAssignableFrom(d.getValueClass())) {
+                    if (i >= files.length) break;                       // Safety in case of invalid parameters.
                     parameters.getOrCreate(d).setValue(files[i++]);
                 }
             }
@@ -519,7 +544,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
               final double Δx, final double Δy,
               final int    nx, final int    ny,
               final ParameterDescriptorGroup descriptor,
-              final Path... files) throws NoninvertibleTransformException
+              final URI... files) throws NoninvertibleTransformException
         {
             super(coordinateUnit, translationUnit, isCellValueRatio, x0, y0, Δx, Δy, nx, ny, descriptor, files);
             offsets = new float[dim][Math.multiplyExact(nx, ny)];
@@ -631,7 +656,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
                final double Δx, final double Δy,
                final int    nx, final int    ny,
                final ParameterDescriptorGroup descriptor,
-               final Path... files) throws NoninvertibleTransformException
+               final URI... files) throws NoninvertibleTransformException
         {
             super(coordinateUnit, translationUnit, isCellValueRatio, x0, y0, Δx, Δy, nx, ny, descriptor, files);
             offsets = new double[dim][Math.multiplyExact(nx, ny)];
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridGroup.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridGroup.java
index 7d98c5b849..7e86ebb069 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridGroup.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridGroup.java
@@ -18,12 +18,12 @@ package org.apache.sis.internal.referencing.provider;
 
 import java.util.Map;
 import java.util.List;
-import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.net.URI;
 import java.io.IOException;
 import java.awt.Dimension;
 import java.awt.Rectangle;
 import java.awt.geom.AffineTransform;
-import java.util.LinkedHashMap;
 import javax.measure.Quantity;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
@@ -155,7 +155,7 @@ final class DatumShiftGridGroup<C extends Quantity<C>, T extends Quantity<T>> ex
      * @throws IOException declared because {@link Tile#getRegion()} declares it, but should not happen.
      */
     static <C extends Quantity<C>, T extends Quantity<T>> DatumShiftGridGroup<C,T> create(
-            final Path file, final List<DatumShiftGridFile<C,T>> subgrids)
+            final URI file, final List<DatumShiftGridFile<C,T>> subgrids)
             throws IOException, FactoryException, NoninvertibleTransformException
     {
         final TileOrganizer mosaic = new TileOrganizer(null);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
index fdc1a11cc7..1db194ec33 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
@@ -17,18 +17,25 @@
 package org.apache.sis.internal.referencing.provider;
 
 import java.util.logging.Level;
+import java.util.logging.Logger;
 import java.util.logging.LogRecord;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.io.EOFException;
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
+import java.nio.file.FileSystemNotFoundException;
 import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.Channels;
 import org.opengis.util.FactoryException;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.referencing.factory.FactoryDataException;
@@ -37,12 +44,13 @@ import org.apache.sis.referencing.factory.MissingFactoryResourceException;
 
 /**
  * Base class of datum shift grid loaders.
+ * This loader uses {@link ReadableByteChannel}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  * @since   0.7
  */
-class DatumShiftGridLoader {
+abstract class DatumShiftGridLoader {
     /**
      * Conversion factor from degrees to seconds.
      */
@@ -68,7 +76,7 @@ class DatumShiftGridLoader {
     /**
      * The file to load, used for parameter declaration and if we have errors to report.
      */
-    final Path file;
+    final URI file;
 
     /**
      * The channel opened on the file.
@@ -93,7 +101,7 @@ class DatumShiftGridLoader {
      * @param  buffer   the buffer to use.
      * @param  file     path to the longitude or latitude difference file. Used for parameter declaration and error reporting.
      */
-    DatumShiftGridLoader(final ReadableByteChannel channel, final ByteBuffer buffer, final Path file) throws IOException {
+    DatumShiftGridLoader(final ReadableByteChannel channel, final ByteBuffer buffer, final URI file) throws IOException {
         this.file    = file;
         this.buffer  = buffer;
         this.channel = channel;
@@ -143,12 +151,45 @@ class DatumShiftGridLoader {
         buffer.position(p);
     }
 
+    /**
+     * If the given URI is not absolute, tries to make it absolute
+     * with a path to the common directory of datum shift grid files.
+     *
+     * @param  path  the URI to make absolute.
+     * @return an absolute (if possible) URI to the data.
+     */
+    static URI toAbsolutePath(final URI path) {
+        if (!path.isAbsolute() && !path.isOpaque()) {
+            final Path dir = DataDirectory.DATUM_CHANGES.getDirectory();
+            if (dir != null) {
+                return dir.resolve(path.getPath()).toUri();
+            }
+        }
+        return path;
+    }
+
+    /**
+     * Creates a channel for reading bytes from the file at the specified path.
+     *
+     * @param  path  the path from where to read bytes.
+     * @return a channel for reading bytes from the given path.
+     * @throws IOException if the channel can not be created.
+     */
+    static ReadableByteChannel newByteChannel(final URI path) throws IOException {
+        try {
+            return Files.newByteChannel(Paths.get(path));
+        } catch (FileSystemNotFoundException e) {
+            Logging.ignorableException(Logger.getLogger(Modules.REFERENCING), DatumShiftGridLoader.class, "newByteChannel", e);
+        }
+        return Channels.newChannel(path.toURL().openStream());
+    }
+
     /**
      * Logs a message about a grid which is about to be loaded.
      *
      * @param  caller  the provider to logs as the source class.
      *                 the source method will be set to {@code "createMathTransform"}.
-     * @param  file    the grid file, as a {@link String} or a {@link Path}.
+     * @param  file    the grid file, as a {@link String} or a {@link URI}.
      */
     static void startLoading(final Class<?> caller, final Object file) {
         log(caller, Resources.forLocale(null).getLogRecord(Level.FINE, Resources.Keys.LoadingDatumShiftFile_1, file));
@@ -173,7 +214,7 @@ class DatumShiftGridLoader {
      * @param  file    the grid file that the subclass tried to load.
      * @param  cause   the cause of the failure to load the grid file.
      */
-    static FactoryException canNotLoad(final String format, final Path file, final Exception cause) {
+    static FactoryException canNotLoad(final String format, final URI file, final Exception cause) {
         if (!datumDirectoryLogged.get()) {
             final Path directory = DataDirectory.DATUM_CHANGES.getDirectory();
             if (directory != null && !datumDirectoryLogged.getAndSet(true)) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
index ddb61882f5..2e7b64259e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.internal.referencing.provider;
 
+import java.net.URI;
 import java.util.Map;
 import java.util.Arrays;
 import java.util.Locale;
@@ -23,12 +24,11 @@ import java.util.NoSuchElementException;
 import java.util.StringTokenizer;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.util.concurrent.Callable;
 import java.io.BufferedReader;
 import java.io.EOFException;
 import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.Files;
+import java.io.InputStreamReader;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.measure.quantity.Angle;
 import javax.measure.quantity.Length;
@@ -45,7 +45,6 @@ import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.internal.system.Loggers;
-import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.internal.referencing.NilReferencingObject;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.parameter.ParameterBuilder;
@@ -54,7 +53,6 @@ import org.apache.sis.measure.Units;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.collection.Cache;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.datum.DefaultEllipsoid;
 import org.apache.sis.referencing.operation.transform.InterpolatedGeocentricTransform;
@@ -87,7 +85,7 @@ import static org.apache.sis.internal.util.Constants.DIM;
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   0.7
  */
 @XmlTransient
@@ -95,7 +93,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = -4707304160205218546L;
+    private static final long serialVersionUID = -298193260915837911L;
 
     /**
      * Geocentric translation parameters to use as a first guess before to use the grid in France.
@@ -137,7 +135,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      * Name of the default grid file, as mentioned in the NTG_88 document.
      * We use the 5 first characters ({@code "gr3df"}) as a sentinel value for French grid file.
      *
-     * @see #isRecognized(Path)
+     * @see #isRecognized(URI)
      */
     private static final String DEFAULT = "gr3df97a.txt";
 
@@ -154,7 +152,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      *   <li>Default value: {@code gr3df97a.txt}</li>
      * </ul>
      */
-    public static final ParameterDescriptor<Path> FILE;
+    public static final ParameterDescriptor<URI> FILE;
 
     /**
      * The operation parameter descriptor for the <cite>EPSG code for Interpolation CRS</cite> parameter value.
@@ -197,7 +195,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
         FILE = builder
                 .addIdentifier("8727")
                 .addName("Geocentric translation file")
-                .create(Path.class, Paths.get(DEFAULT));
+                .create(URI.class, URI.create(DEFAULT));
         INTERPOLATION_CRS = builder
                 .addIdentifier("1048")
                 .addName("EPSG code for Interpolation CRS")
@@ -258,8 +256,10 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      * @param  file  the grid file.
      * @return {@code true} if the given file looks like a fie from the French mapping agency.
      */
-    public static boolean isRecognized(final Path file) {
-        return file.getFileName().toString().regionMatches(true, 0, DEFAULT, 0, 5);
+    public static boolean isRecognized(final URI file) {
+        final String filename = file.getPath();
+        final int s = filename.lastIndexOf('/') + 1;
+        return filename.regionMatches(true, s, DEFAULT, 0, 5);
     }
 
     /**
@@ -326,7 +326,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
             default: throw new InvalidParameterValueException(Errors.format(
                             Errors.Keys.IllegalArgumentValue_2, DIM, dim), DIM, dim);
         }
-        final Path file = pg.getMandatoryValue(FILE);
+        final URI file = pg.getMandatoryValue(FILE);
         final DatumShiftGridFile<Angle,Length> grid = getOrLoad(file,
                 isRecognized(file) ? new double[] {TX, TY, TZ} : null, PRECISION);
 
@@ -361,176 +361,205 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      * Returns the grid of the given name. This method returns the cached instance if it still exists,
      * or load the grid otherwise.
      *
-     * @param  file      name of the datum shift grid file to load.
+     * @param  file      an absolute or relative reference to the datum shift grid file to load.
      * @param  averages  an "average" value for the offset in each dimension, or {@code null} if unknown.
      * @param  scale     the factor by which to multiply each compressed value before to add to the average value.
      */
-    static DatumShiftGridFile<Angle,Length> getOrLoad(final Path file, final double[] averages, final double scale)
+    static DatumShiftGridFile<Angle,Length> getOrLoad(final URI file, final double[] averages, final double scale)
             throws FactoryException
     {
-        final Path resolved = DataDirectory.DATUM_CHANGES.resolve(file).toAbsolutePath();
-        DatumShiftGridFile<?,?> grid = DatumShiftGridFile.CACHE.peek(resolved);
-        if (grid == null) {
-            final Cache.Handler<DatumShiftGridFile<?,?>> handler = DatumShiftGridFile.CACHE.lock(resolved);
-            try {
-                grid = handler.peek();
-                if (grid == null) {
-                    try (BufferedReader in = Files.newBufferedReader(resolved)) {
-                        DatumShiftGridLoader.startLoading(FranceGeocentricInterpolation.class, file);
-                        final DatumShiftGridFile.Float<Angle,Length> g = load(in, file);
-                        grid = DatumShiftGridCompressed.compress(g, averages, scale);
-                    } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
-                        // NumberFormatException, ArithmeticException, NoSuchElementException, possibly other.
-                        throw DatumShiftGridLoader.canNotLoad(HEADER, file, e);
-                    }
-                    grid = grid.useSharedData();
-                }
-            } finally {
-                handler.putAndUnlock(grid);
-            }
-        }
-        return grid.castTo(Angle.class, Length.class);
+        final URI resolved = DatumShiftGridLoader.toAbsolutePath(file);
+        return DatumShiftGridFile.getOrLoad(resolved, null, new Loader(resolved, averages, scale))
+                                 .castTo(Angle.class, Length.class);
     }
 
     /**
-     * Unconditionally loads the grid for the given file without in-memory compression.
-     *
-     * @param  in    reader of the RGF93 datum shift file.
-     * @param  file  path to the file being read, used for parameter declaration and error reporting.
-     * @throws IOException if an I/O error occurred.
-     * @throws NumberFormatException if a number cannot be parsed.
-     * @throws NoSuchElementException if a data line is missing a value.
-     * @throws FactoryException if an problem is found with the file content.
-     * @throws ArithmeticException if the width or the height exceed the integer capacity.
+     * Temporary object created for loading gridded data if not present in the cache.
+     * The data are provided in a text file, which is read with {@link BufferedReader}.
      */
-    static DatumShiftGridFile.Float<Angle,Length> load(final BufferedReader in, final Path file)
-            throws IOException, FactoryException, NoninvertibleTransformException
-    {
-        DatumShiftGridFile.Float<Angle,Length> grid = null;
-        double x0 = 0;
-        double xf = 0;
-        double y0 = 0;
-        double yf = 0;
-        double Δx = 0;
-        double Δy = 0;
-        int    nx = 0;
-        int    ny = 0;
-        /*
-         * The header should be like below, but the only essential line for this class is the one
-         * starting with "GR3D1". We also check that "GR3D2" declares the expected interpolation.
+    static final class Loader implements Callable<DatumShiftGridFile<?,?>> {
+        /** The file to load. */
+        private final URI file;
+
+        /** An "average" value for the offset in each dimension, or {@code null} if unknown. */
+        private final double[] averages;
+
+        /** The factor by which to multiply each compressed value before to add to the average value. */
+        private final double scale;
+
+        /** Creates a new loader for the given file. */
+        Loader(final URI file, final double[] averages, final double scale) {
+            this.file     = file;
+            this.averages = averages;
+            this.scale    = scale;
+        }
+
+        /** Returns the reader for the specified URI. */
+        static BufferedReader newBufferedReader(final URI file) throws IOException {
+            return new BufferedReader(new InputStreamReader(file.toURL().openStream()));
+        }
+
+        /**
+         * Invoked when the gridded data are not in the cache.
+         * This method load grid data from the file specified at construction time.
          *
-         *     GR3D  002024 024 20370201
-         *     GR3D1   -5.5000  10.0000  41.0000  52.0000    .1000    .1000
-         *     GR3D2 INTERPOLATION BILINEAIRE
-         *     GR3D3 PREC CM 01:5 02:10 03:20 04:50 99>100
+         * @return the loaded grid data.
+         * @throws FactoryException if an error occurred while loading the grid data.
          */
-        String line;
-        while (true) {
-            line = in.readLine();
-            if (line == null) {
-                throw new EOFException(Errors.format(Errors.Keys.UnexpectedEndOfFile_1, file));
-            }
-            final int length = CharSequences.skipTrailingWhitespaces(line, 0, line.length());
-            if (length <= 0) {
-                continue;   // Skip empty lines.
+        @Override
+        public DatumShiftGridFile<?,?> call() throws FactoryException {
+            final DatumShiftGridFile<?,?> grid;
+            try (BufferedReader in = newBufferedReader(file)) {
+                DatumShiftGridLoader.startLoading(FranceGeocentricInterpolation.class, file);
+                final DatumShiftGridFile.Float<Angle,Length> g = load(in, file);
+                grid = DatumShiftGridCompressed.compress(g, averages, scale);
+            } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
+                // NumberFormatException, ArithmeticException, NoSuchElementException, possibly other.
+                throw DatumShiftGridLoader.canNotLoad(HEADER, file, e);
             }
-            int p = CharSequences.skipLeadingWhitespaces(line, 0, length);
-            if (line.charAt(p) == '#') {
-                continue;   // Skip comment lines (not officially part of the format).
-            }
-            if (!line.regionMatches(true, p, HEADER, 0, HEADER.length())) {
-                break;      // End of header.
-            }
-            if ((p += HEADER.length()) < length) {
-                final char c = line.charAt(p);
-                p = CharSequences.skipLeadingWhitespaces(line, p+1, length);
-                switch (c) {
-                    case '1': {
-                        if (grid != null) {
-                            throw new FactoryException(Errors.format(Errors.Keys.DuplicatedElement_1, HEADER));
-                        }
-                        final double[] gridGeometry = CharSequences.parseDoubles(line.substring(p, length), ' ');
-                        if (gridGeometry.length == 6) {
-                            x0 = gridGeometry[0];
-                            xf = gridGeometry[1];
-                            y0 = gridGeometry[2];
-                            yf = gridGeometry[3];
-                            Δx = gridGeometry[4];
-                            Δy = gridGeometry[5];
-                            nx = Math.toIntExact(Math.round((xf - x0) / Δx + 1));
-                            ny = Math.toIntExact(Math.round((yf - y0) / Δy + 1));
-                            grid = new DatumShiftGridFile.Float<>(3,
-                                    Units.DEGREE, Units.METRE, false,
-                                    x0, y0, Δx, Δy, nx, ny, PARAMETERS, file);
-                            grid.accuracy = Double.NaN;
-                            for (final float[] data : grid.offsets) {
-                                Arrays.fill(data, Float.NaN);
+            return grid.useSharedData();
+        }
+
+        /**
+         * Unconditionally loads the grid for the given file without in-memory compression.
+         *
+         * @param  in    reader of the RGF93 datum shift file.
+         * @param  file  path to the file being read, used for parameter declaration and error reporting.
+         * @throws IOException if an I/O error occurred.
+         * @throws NumberFormatException if a number cannot be parsed.
+         * @throws NoSuchElementException if a data line is missing a value.
+         * @throws FactoryException if an problem is found with the file content.
+         * @throws ArithmeticException if the width or the height exceed the integer capacity.
+         */
+        static DatumShiftGridFile.Float<Angle,Length> load(final BufferedReader in, final URI file)
+                throws IOException, FactoryException, NoninvertibleTransformException
+        {
+            DatumShiftGridFile.Float<Angle,Length> grid = null;
+            double x0 = 0;
+            double xf = 0;
+            double y0 = 0;
+            double yf = 0;
+            double Δx = 0;
+            double Δy = 0;
+            int    nx = 0;
+            int    ny = 0;
+            /*
+             * The header should be like below, but the only essential line for this class is the one
+             * starting with "GR3D1". We also check that "GR3D2" declares the expected interpolation.
+             *
+             *     GR3D  002024 024 20370201
+             *     GR3D1   -5.5000  10.0000  41.0000  52.0000    .1000    .1000
+             *     GR3D2 INTERPOLATION BILINEAIRE
+             *     GR3D3 PREC CM 01:5 02:10 03:20 04:50 99>100
+             */
+            String line;
+            while (true) {
+                line = in.readLine();
+                if (line == null) {
+                    throw new EOFException(Errors.format(Errors.Keys.UnexpectedEndOfFile_1, file));
+                }
+                final int length = CharSequences.skipTrailingWhitespaces(line, 0, line.length());
+                if (length <= 0) {
+                    continue;   // Skip empty lines.
+                }
+                int p = CharSequences.skipLeadingWhitespaces(line, 0, length);
+                if (line.charAt(p) == '#') {
+                    continue;   // Skip comment lines (not officially part of the format).
+                }
+                if (!line.regionMatches(true, p, HEADER, 0, HEADER.length())) {
+                    break;      // End of header.
+                }
+                if ((p += HEADER.length()) < length) {
+                    final char c = line.charAt(p);
+                    p = CharSequences.skipLeadingWhitespaces(line, p+1, length);
+                    switch (c) {
+                        case '1': {
+                            if (grid != null) {
+                                throw new FactoryException(Errors.format(Errors.Keys.DuplicatedElement_1, HEADER));
+                            }
+                            final double[] gridGeometry = CharSequences.parseDoubles(line.substring(p, length), ' ');
+                            if (gridGeometry.length == 6) {
+                                x0 = gridGeometry[0];
+                                xf = gridGeometry[1];
+                                y0 = gridGeometry[2];
+                                yf = gridGeometry[3];
+                                Δx = gridGeometry[4];
+                                Δy = gridGeometry[5];
+                                nx = Math.toIntExact(Math.round((xf - x0) / Δx + 1));
+                                ny = Math.toIntExact(Math.round((yf - y0) / Δy + 1));
+                                grid = new DatumShiftGridFile.Float<>(3,
+                                        Units.DEGREE, Units.METRE, false,
+                                        x0, y0, Δx, Δy, nx, ny, PARAMETERS, file);
+                                grid.accuracy = Double.NaN;
+                                for (final float[] data : grid.offsets) {
+                                    Arrays.fill(data, Float.NaN);
+                                }
                             }
+                            break;
                         }
-                        break;
-                    }
-                    case '2': {
-                        final String interp = line.substring(p, length);
-                        if (!interp.matches("(?i)INTERPOLATION[^A-Z]+BILINEAIRE")) {
-                            final LogRecord record = Errors.getResources((Locale) null).getLogRecord(
-                                    Level.WARNING, Errors.Keys.UnsupportedInterpolation_1, interp);
-                            record.setLoggerName(Loggers.COORDINATE_OPERATION);
-                            Logging.log(FranceGeocentricInterpolation.class, "createMathTransform", record);
-                            // We declare `createMathTransform(…)` method because it is closer to public API.
+                        case '2': {
+                            final String interp = line.substring(p, length);
+                            if (!interp.matches("(?i)INTERPOLATION[^A-Z]+BILINEAIRE")) {
+                                final LogRecord record = Errors.getResources((Locale) null).getLogRecord(
+                                        Level.WARNING, Errors.Keys.UnsupportedInterpolation_1, interp);
+                                record.setLoggerName(Loggers.COORDINATE_OPERATION);
+                                Logging.log(FranceGeocentricInterpolation.class, "createMathTransform", record);
+                                // We declare `createMathTransform(…)` method because it is closer to public API.
+                            }
+                            break;
                         }
-                        break;
                     }
                 }
             }
-        }
-        if (grid == null) {
-            throw new FactoryException(Resources.format(Resources.Keys.FileNotFound_2, HEADER, file));
-        }
-        /*
-         * Loads the data with the sign of all offsets reversed. Data columns are
-         *
-         *     (unknown), longitude, latitude, tX, tY, tZ, accuracy code, data sheet (ignored)
-         *
-         * where the longitude and latitude values are in RGF93 system.
-         * Example:
-         *
-         *     00002   -5.500000000   41.000000000  -165.027   -67.100   315.813  99  -0158
-         *     00002   -5.500000000   41.100000000  -165.169   -66.948   316.007  99  -0157
-         *     00002   -5.500000000   41.200000000  -165.312   -66.796   316.200  99  -0157
-         *
-         * Translation values in the IGN file are from NTF to RGF93, but Apache SIS implementation needs
-         * the opposite direction (from RGF93 to NTF). The reason is that SIS expect the source datum to
-         * be the datum in which longitude and latitude values are expressed.
-         */
-        final float[] tX = grid.offsets[0];
-        final float[] tY = grid.offsets[1];
-        final float[] tZ = grid.offsets[2];
-        do {
-            final StringTokenizer t = new StringTokenizer(line.trim());
-            t.nextToken();                                                      // Ignored
-            final double x = Double.parseDouble(t.nextToken());                 // Longitude in degrees
-            final double y = Double.parseDouble(t.nextToken());                 // Latitude in degrees
-            final int    i = Math.toIntExact(Math.round((x - x0) / Δx));        // Column index
-            final int    j = Math.toIntExact(Math.round((y - y0) / Δy));        // Row index
-            if (i < 0 || i >= nx) {
-                throw new FactoryException(Errors.format(Errors.Keys.ValueOutOfRange_4, "x", x, x0, xf));
-            }
-            if (j < 0 || j >= ny) {
-                throw new FactoryException(Errors.format(Errors.Keys.ValueOutOfRange_4, "y", y, y0, yf));
-            }
-            final int p = j*nx + i;
-            if (!Double.isNaN(tX[p]) || !Double.isNaN(tY[p]) || !Double.isNaN(tZ[p])) {
-                throw new FactoryException(Errors.format(Errors.Keys.ValueAlreadyDefined_1, x + ", " + y));
-            }
-            tX[p] = -parseFloat(t.nextToken());     // See javadoc for the reason why we reverse the sign.
-            tY[p] = -parseFloat(t.nextToken());
-            tZ[p] = -parseFloat(t.nextToken());
-            final double accuracy = ACCURACY[Math.min(ACCURACY.length - 1,
-                    Math.max(0, Integer.parseInt(t.nextToken()) - 1))];
-            if (!(accuracy >= grid.accuracy)) {     // Use `!` for replacing the initial NaN.
-                grid.accuracy = accuracy;
+            if (grid == null) {
+                throw new FactoryException(Resources.format(Resources.Keys.FileNotFound_2, HEADER, file));
             }
-        } while ((line = in.readLine()) != null);
-        return grid;
+            /*
+             * Loads the data with the sign of all offsets reversed. Data columns are
+             *
+             *     (unknown), longitude, latitude, tX, tY, tZ, accuracy code, data sheet (ignored)
+             *
+             * where the longitude and latitude values are in RGF93 system.
+             * Example:
+             *
+             *     00002   -5.500000000   41.000000000  -165.027   -67.100   315.813  99  -0158
+             *     00002   -5.500000000   41.100000000  -165.169   -66.948   316.007  99  -0157
+             *     00002   -5.500000000   41.200000000  -165.312   -66.796   316.200  99  -0157
+             *
+             * Translation values in the IGN file are from NTF to RGF93, but Apache SIS implementation needs
+             * the opposite direction (from RGF93 to NTF). The reason is that SIS expect the source datum to
+             * be the datum in which longitude and latitude values are expressed.
+             */
+            final float[] tX = grid.offsets[0];
+            final float[] tY = grid.offsets[1];
+            final float[] tZ = grid.offsets[2];
+            do {
+                final StringTokenizer t = new StringTokenizer(line.trim());
+                t.nextToken();                                                      // Ignored
+                final double x = Double.parseDouble(t.nextToken());                 // Longitude in degrees
+                final double y = Double.parseDouble(t.nextToken());                 // Latitude in degrees
+                final int    i = Math.toIntExact(Math.round((x - x0) / Δx));        // Column index
+                final int    j = Math.toIntExact(Math.round((y - y0) / Δy));        // Row index
+                if (i < 0 || i >= nx) {
+                    throw new FactoryException(Errors.format(Errors.Keys.ValueOutOfRange_4, "x", x, x0, xf));
+                }
+                if (j < 0 || j >= ny) {
+                    throw new FactoryException(Errors.format(Errors.Keys.ValueOutOfRange_4, "y", y, y0, yf));
+                }
+                final int p = j*nx + i;
+                if (!Double.isNaN(tX[p]) || !Double.isNaN(tY[p]) || !Double.isNaN(tZ[p])) {
+                    throw new FactoryException(Errors.format(Errors.Keys.ValueAlreadyDefined_1, x + ", " + y));
+                }
+                tX[p] = -parseFloat(t.nextToken());     // See javadoc for the reason why we reverse the sign.
+                tY[p] = -parseFloat(t.nextToken());
+                tZ[p] = -parseFloat(t.nextToken());
+                final double accuracy = ACCURACY[Math.min(ACCURACY.length - 1,
+                        Math.max(0, Integer.parseInt(t.nextToken()) - 1))];
+                if (!(accuracy >= grid.accuracy)) {     // Use `!` for replacing the initial NaN.
+                    grid.accuracy = accuracy;
+                }
+            } while ((line = in.readLine()) != null);
+            return grid;
+        }
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MolodenskyInterpolation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MolodenskyInterpolation.java
index 33f3eb118f..e74f6274fe 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MolodenskyInterpolation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MolodenskyInterpolation.java
@@ -47,6 +47,7 @@ import org.apache.sis.referencing.operation.transform.InterpolatedMolodenskyTran
  */
 @XmlTransient
 @Deprecated(since="1.4", forRemoval=true)
+// Note: after removal, delete overrideable method in parent class.
 public final class MolodenskyInterpolation extends FranceGeocentricInterpolation {
     /**
      * Serial number for inter-operability with different versions.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
index 0828872fd7..e50e10723d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
@@ -16,14 +16,11 @@
  */
 package org.apache.sis.internal.referencing.provider;
 
-import java.util.AbstractMap;
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteOrder;
 import java.nio.ByteBuffer;
 import java.nio.FloatBuffer;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.Files;
 import java.nio.channels.ReadableByteChannel;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.measure.quantity.Angle;
@@ -40,9 +37,7 @@ import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.collection.Cache;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.measure.Units;
 
 
@@ -56,7 +51,7 @@ import org.apache.sis.measure.Units;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rueben Schulz (UBC)
- * @version 1.3
+ * @version 1.4
  *
  * @see <a href="http://www.ngs.noaa.gov/cgi-bin/nadcon.prl">NADCON on-line computation</a>
  *
@@ -83,7 +78,7 @@ public final class NADCON extends AbstractProvider {
      *   <li>Default value: {@code conus.las}</li>
      * </ul>
      */
-    private static final ParameterDescriptor<Path> LATITUDE;
+    private static final ParameterDescriptor<URI> LATITUDE;
 
     /**
      * The operation parameter descriptor for the <cite>"Longitude difference file"</cite> parameter value.
@@ -99,7 +94,7 @@ public final class NADCON extends AbstractProvider {
      *   <li>Default value: {@code conus.los}</li>
      * </ul>
      */
-    private static final ParameterDescriptor<Path> LONGITUDE;
+    private static final ParameterDescriptor<URI> LONGITUDE;
 
     /**
      * The group of all parameters expected by this coordinate operation.
@@ -110,11 +105,11 @@ public final class NADCON extends AbstractProvider {
         LATITUDE = builder
                 .addIdentifier("8657")
                 .addName("Latitude difference file")
-                .create(Path.class, Paths.get("conus.las"));
+                .create(URI.class, URI.create("conus.las"));
         LONGITUDE = builder
                 .addIdentifier("8658")
                 .addName("Longitude difference file")
-                .create(Path.class, Paths.get("conus.los"));
+                .create(URI.class, URI.create("conus.los"));
         PARAMETERS = builder
                 .addIdentifier("9613")
                 .addName("NADCON")
@@ -149,52 +144,42 @@ public final class NADCON extends AbstractProvider {
     }
 
     /**
-     * Returns the grid of the given name. This method returns the cached instance if it still exists,
-     * or load the grid otherwise.
+     * Returns the grid of the given name.
+     * This method returns the cached instance if it still exists, or load the grid otherwise.
      *
-     * @param latitudeShifts   name of the grid file for latitude shifts.
-     * @param longitudeShifts  name of the grid file for longitude shifts.
+     * @param latitudeShifts   relative or absolute path of the grid file for latitude shifts.
+     * @param longitudeShifts  relative or absolute path name of the grid file for longitude shifts.
      */
-    static DatumShiftGridFile<Angle,Angle> getOrLoad(final Path latitudeShifts, final Path longitudeShifts)
+    static DatumShiftGridFile<Angle,Angle> getOrLoad(final URI latitudeShifts, final URI longitudeShifts)
             throws FactoryException
     {
-        final Path rlat = DataDirectory.DATUM_CHANGES.resolve(latitudeShifts).toAbsolutePath();
-        final Path rlon = DataDirectory.DATUM_CHANGES.resolve(longitudeShifts).toAbsolutePath();
-        final Object key = new AbstractMap.SimpleImmutableEntry<>(rlat, rlon);
-        DatumShiftGridFile<?,?> grid = DatumShiftGridFile.CACHE.peek(key);
-        if (grid == null) {
-            final Cache.Handler<DatumShiftGridFile<?,?>> handler = DatumShiftGridFile.CACHE.lock(key);
+        final URI rlat = Loader.toAbsolutePath(latitudeShifts);
+        final URI rlon = Loader.toAbsolutePath(longitudeShifts);
+        return DatumShiftGridFile.getOrLoad(rlat, rlon, () -> {
+            final Loader loader;
+            URI file = latitudeShifts;
+            final DatumShiftGridFile<?,?> grid;
             try {
-                grid = handler.peek();
-                if (grid == null) {
-                    final Loader loader;
-                    Path file = latitudeShifts;
-                    try {
-                        // Note: buffer size must be divisible by the size of `float` data type.
-                        final ByteBuffer buffer = ByteBuffer.allocate(4096).order(ByteOrder.LITTLE_ENDIAN);
-                        final FloatBuffer fb = buffer.asFloatBuffer();
-                        try (ReadableByteChannel in = Files.newByteChannel(rlat)) {
-                            DatumShiftGridLoader.startLoading(NADCON.class, CharSequences.commonPrefix(
-                                    latitudeShifts.toString(), longitudeShifts.toString()).toString() + '…');
-                            loader = new Loader(in, buffer, file);
-                            loader.readGrid(fb, null, longitudeShifts);
-                        }
-                        buffer.clear();
-                        file = longitudeShifts;
-                        try (ReadableByteChannel in = Files.newByteChannel(rlon)) {
-                            new Loader(in, buffer, file).readGrid(fb, loader, null);
-                        }
-                    } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
-                        throw DatumShiftGridLoader.canNotLoad("NADCON", file, e);
-                    }
-                    grid = DatumShiftGridCompressed.compress(loader.grid, null, loader.grid.accuracy);
-                    grid = grid.useSharedData();
+                // Note: buffer size must be divisible by the size of `float` data type.
+                final ByteBuffer buffer = ByteBuffer.allocate(4096).order(ByteOrder.LITTLE_ENDIAN);
+                final FloatBuffer fb = buffer.asFloatBuffer();
+                try (ReadableByteChannel in = Loader.newByteChannel(rlat)) {
+                    DatumShiftGridLoader.startLoading(NADCON.class, CharSequences.commonPrefix(
+                            latitudeShifts.toString(), longitudeShifts.toString()).toString() + '…');
+                    loader = new Loader(in, buffer, file);
+                    loader.readGrid(fb, null, longitudeShifts);
                 }
-            } finally {
-                handler.putAndUnlock(grid);
+                buffer.clear();
+                file = longitudeShifts;
+                try (ReadableByteChannel in = Loader.newByteChannel(rlon)) {
+                    new Loader(in, buffer, file).readGrid(fb, loader, null);
+                }
+            } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
+                throw DatumShiftGridLoader.canNotLoad("NADCON", file, e);
             }
-        }
-        return grid.castTo(Angle.class, Angle.class);
+            grid = DatumShiftGridCompressed.compress(loader.grid, null, loader.grid.accuracy);
+            return grid.useSharedData();
+        }).castTo(Angle.class, Angle.class);
     }
 
 
@@ -260,7 +245,7 @@ public final class NADCON extends AbstractProvider {
         private final StringBuilder ascii;
 
         /**
-         * The grid created by {@link #readGrid(FloatBuffer, Loader, Path)}.
+         * The grid created by {@link #readGrid(FloatBuffer, Loader, URI)}.
          */
         DatumShiftGridFile.Float<Angle,Angle> grid;
 
@@ -273,7 +258,7 @@ public final class NADCON extends AbstractProvider {
          *                 and have a capacity divisible by the size of the {@code float} type.
          * @param file     path to the longitude or latitude difference file. Used for parameter declaration and error reporting.
          */
-        Loader(final ReadableByteChannel channel, final ByteBuffer buffer, final Path file)
+        Loader(final ReadableByteChannel channel, final ByteBuffer buffer, final URI file)
                 throws IOException, FactoryException
         {
             super(channel, buffer, file);
@@ -387,7 +372,7 @@ public final class NADCON extends AbstractProvider {
          * @param latitudeShifts   the previously loaded latitude shifts, or {@code null} if not yet loaded.
          * @param longitudeShifts  the file for the longitude grid.
          */
-        final void readGrid(final FloatBuffer fb, final Loader latitudeShifts, final Path longitudeShifts)
+        final void readGrid(final FloatBuffer fb, final Loader latitudeShifts, final URI longitudeShifts)
                 throws IOException, FactoryException, NoninvertibleTransformException
         {
             final int dim;
@@ -403,7 +388,7 @@ public final class NADCON extends AbstractProvider {
                     y0 != latitudeShifts.y0 || Δy != latitudeShifts.Δy || ny != latitudeShifts.ny || nz != latitudeShifts.nz)
                 {
                     throw new FactoryException(Errors.format(Errors.Keys.MismatchedGridGeometry_2,
-                            latitudeShifts.file.getFileName(), file.getFileName()));
+                            latitudeShifts.file.getPath(), file.getPath()));
                 }
                 dim   = 0;                                              // Dimension of longitudes
                 scale = -DEGREES_TO_SECONDS * Δx;                       // NADCON shifts are positive west.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
index 108d96fe9e..a44fb3a054 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
@@ -25,10 +25,9 @@ import java.util.Arrays;
 import java.util.Locale;
 import java.util.logging.Level;
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteOrder;
 import java.nio.ByteBuffer;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.charset.StandardCharsets;
 import javax.xml.bind.annotation.XmlTransient;
@@ -44,13 +43,11 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Transformation;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
-import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
-import org.apache.sis.util.collection.Cache;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Messages;
@@ -63,7 +60,7 @@ import org.apache.sis.measure.Units;
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   0.7
  */
 @XmlTransient
@@ -87,7 +84,7 @@ public final class NTv2 extends AbstractProvider {
      *   <li>No default value</li>
      * </ul>
      */
-    static final ParameterDescriptor<Path> FILE;
+    static final ParameterDescriptor<URI> FILE;
 
     /**
      * The group of all parameters expected by this coordinate operation.
@@ -98,7 +95,7 @@ public final class NTv2 extends AbstractProvider {
         FILE = builder
                 .addIdentifier("8656")
                 .addName("Latitude and longitude difference file")
-                .create(Path.class, null);
+                .create(URI.class, null);
         PARAMETERS = builder
                 .addIdentifier("9615")
                 .addName("NTv2")
@@ -151,38 +148,29 @@ public final class NTv2 extends AbstractProvider {
     }
 
     /**
-     * Returns the grid of the given name. This method returns the cached instance if it still exists,
-     * or load the grid otherwise.
+     * Returns the grid of the given name.
+     * This method returns the cached instance if it still exists, or load the grid otherwise.
      *
      * @param  provider  the provider which is creating a transform.
-     * @param  file      name of the datum shift grid file to load.
+     * @param  file      relative or absolute path of the datum shift grid file to load.
      * @param  version   the expected version (1 or 2).
      */
     static DatumShiftGridFile<Angle,Angle> getOrLoad(final Class<? extends AbstractProvider> provider,
-            final Path file, final int version) throws FactoryException
+            final URI file, final int version) throws FactoryException
     {
-        final Path resolved = DataDirectory.DATUM_CHANGES.resolve(file).toAbsolutePath();
-        DatumShiftGridFile<?,?> grid = DatumShiftGridFile.CACHE.peek(resolved);
-        if (grid == null) {
-            final Cache.Handler<DatumShiftGridFile<?,?>> handler = DatumShiftGridFile.CACHE.lock(resolved);
-            try {
-                grid = handler.peek();
-                if (grid == null) {
-                    try (ReadableByteChannel in = Files.newByteChannel(resolved)) {
-                        DatumShiftGridLoader.startLoading(provider, file);
-                        final Loader loader = new Loader(in, file, version);
-                        grid = loader.readAllGrids();
-                        loader.report(provider);
-                    } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
-                        throw DatumShiftGridLoader.canNotLoad(provider.getSimpleName(), file, e);
-                    }
-                    grid = grid.useSharedData();
-                }
-            } finally {
-                handler.putAndUnlock(grid);
+        final URI resolved = Loader.toAbsolutePath(file);
+        return DatumShiftGridFile.getOrLoad(resolved, null, () -> {
+            final DatumShiftGridFile<?,?> grid;
+            try (ReadableByteChannel in = Loader.newByteChannel(resolved)) {
+                DatumShiftGridLoader.startLoading(provider, file);
+                final Loader loader = new Loader(in, file, version);
+                grid = loader.readAllGrids();
+                loader.report(provider);
+            } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
+                throw DatumShiftGridLoader.canNotLoad(provider.getSimpleName(), file, e);
             }
-        }
-        return grid.castTo(Angle.class, Angle.class);
+            return grid.useSharedData();
+        }).castTo(Angle.class, Angle.class);
     }
 
 
@@ -321,7 +309,7 @@ public final class NTv2 extends AbstractProvider {
          * @param  version  the expected version (1 or 2).
          * @throws FactoryException if a data record cannot be parsed.
          */
-        Loader(final ReadableByteChannel channel, final Path file, int version) throws IOException, FactoryException {
+        Loader(final ReadableByteChannel channel, final URI file, int version) throws IOException, FactoryException {
             super(channel, ByteBuffer.allocate(4096), file);
             header = new LinkedHashMap<>();
             ensureBufferContains(RECORD_LENGTH);
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/DatumShiftTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/DatumShiftTestCase.java
index 4139f4b352..13cb9d581d 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/DatumShiftTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/DatumShiftTestCase.java
@@ -16,11 +16,9 @@
  */
 package org.apache.sis.internal.referencing.provider;
 
+import java.net.URI;
 import java.net.URL;
 import java.net.URISyntaxException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.FileSystemNotFoundException;
 import org.apache.sis.test.TestCase;
 
 import static org.junit.Assert.*;
@@ -30,10 +28,10 @@ import static org.junit.Assume.assumeFalse;
 /**
  * Base class of tests that need to load a datum shift grid. This base class provides a
  * {@link #getResourceAsConvertibleURL(String)} method for fetching the data in a form
- * convertible to {@link Path}.
+ * convertible to {@link URI}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.4
  * @since   0.8
  */
 public abstract class DatumShiftTestCase extends TestCase {
@@ -44,7 +42,7 @@ public abstract class DatumShiftTestCase extends TestCase {
     }
 
     /**
-     * Finds resource of the given name as a URL convertible to a {@link Path}.
+     * Finds resource of the given name as a URL convertible to a {@link URI}.
      * If the URL is not convertible, then this method declares the test as ignored.
      *
      * @param  name  name of the resource to get.
@@ -61,22 +59,17 @@ public abstract class DatumShiftTestCase extends TestCase {
     }
 
     /**
-     * Finds resource of the given name as a path. If the resource cannot be obtained because
+     * Finds resource of the given name as an URI. If the resource cannot be obtained because
      * the grid file is inside a JAR file, declares the test as ignored instead of failed.
      *
      * @param  name  name of the resource to get.
      * @return the requested resources.
      */
-    static Path getResource(final String name) throws URISyntaxException {
-        final URL file = DatumShiftTestCase.class.getResource(name);
+    static URI getResource(final String name) throws URISyntaxException {
+        final URL file = getResourceAsConvertibleURL(name);
         if (file == null) {
-            fail("Test file \"" + name + "\" not found.");
-        } else try {
-            return Paths.get(file.toURI());
-        } catch (FileSystemNotFoundException e) {
             assumeFalse("Cannot read grid data in a JAR file.", "jar".equals(file.getProtocol()));
-            throw e;
         }
-        return null;
+        return file.toURI();
     }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolationTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolationTest.java
index 41917c679b..2dc409a8d5 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolationTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolationTest.java
@@ -19,9 +19,7 @@ package org.apache.sis.internal.referencing.provider;
 import java.net.URISyntaxException;
 import java.io.BufferedReader;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.nio.file.Path;
+import java.net.URI;
 import javax.measure.quantity.Angle;
 import javax.measure.quantity.Length;
 import org.opengis.geometry.Envelope;
@@ -38,7 +36,7 @@ import static org.opengis.test.Assert.*;
  * Tests {@link FranceGeocentricInterpolation}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.4
  *
  * @see GeocentricTranslationTest#testFranceGeocentricInterpolationPoint()
  * @see org.apache.sis.referencing.operation.transform.MolodenskyTransformTest#testFranceGeocentricInterpolationPoint()
@@ -90,14 +88,16 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
     public static final double ANGULAR_TOLERANCE = (0.0001 / 60 / 60) / 2;
 
     /**
-     * Tests {@link FranceGeocentricInterpolation#isRecognized(Path)}.
+     * Tests {@link FranceGeocentricInterpolation#isRecognized(URI)}.
+     *
+     * @throws URISyntaxException if the URL to the test file is not valid.
      */
     @Test
-    public void testIsRecognized() {
-        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get("GR3DF97A.txt")));
-        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get("gr3df")));
-        assertFalse(FranceGeocentricInterpolation.isRecognized(Paths.get("gr3d")));
-        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get(TEST_FILE)));
+    public void testIsRecognized() throws URISyntaxException {
+        assertTrue (FranceGeocentricInterpolation.isRecognized(new URI("GR3DF97A.txt")));
+        assertTrue (FranceGeocentricInterpolation.isRecognized(new URI("gr3df")));
+        assertFalse(FranceGeocentricInterpolation.isRecognized(new URI("gr3d")));
+        assertTrue (FranceGeocentricInterpolation.isRecognized(new URI(TEST_FILE)));
     }
 
     /**
@@ -111,7 +111,7 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
     /**
      * Tests a small grid file with interpolations in geocentric coordinates.
      *
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws IOException if an error occurred while loading the grid.
      * @throws FactoryException if an error occurred while computing the grid.
      * @throws TransformException if an error occurred while computing the envelope.
@@ -128,7 +128,7 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
      * The next method is {@link #testGridAsShorts(DatumShiftGridFile)}.</p>
      *
      * @return the loaded grid with values as {@code float}.
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws IOException if an error occurred while loading the grid.
      * @throws FactoryException if an error occurred while computing the grid.
      * @throws TransformException if an error occurred while computing the envelope.
@@ -137,10 +137,10 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
     private static DatumShiftGridFile<Angle,Length> testGridAsFloats()
             throws URISyntaxException, IOException, FactoryException, TransformException
     {
-        final Path file = getResource(TEST_FILE);
+        final URI file = getResource(TEST_FILE);
         final DatumShiftGridFile.Float<Angle,Length> grid;
-        try (BufferedReader in = Files.newBufferedReader(file)) {
-            grid = FranceGeocentricInterpolation.load(in, file);
+        try (BufferedReader in = FranceGeocentricInterpolation.Loader.newBufferedReader(file)) {
+            grid = FranceGeocentricInterpolation.Loader.load(in, file);
         }
         assertEquals("cellPrecision",   0.005, grid.getCellPrecision(), STRICT);
         assertEquals("getCellMean",  168.2587, grid.getCellMean(0), 0.0001);
@@ -219,9 +219,9 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
     }
 
     /**
-     * Tests the {@link FranceGeocentricInterpolation#getOrLoad(Path, double[], double)} method and its cache.
+     * Tests the {@link FranceGeocentricInterpolation#getOrLoad(URI, double[], double)} method and its cache.
      *
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws FactoryException if an error occurred while computing the grid.
      * @throws TransformException if an error occurred while computing the envelope.
      */
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NADCONTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NADCONTest.java
index 7a03e51bc3..c159b4e9f4 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NADCONTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NADCONTest.java
@@ -19,6 +19,7 @@ package org.apache.sis.internal.referencing.provider;
 import java.util.Locale;
 import java.io.BufferedWriter;
 import java.io.IOException;
+import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.nio.file.Files;
@@ -40,7 +41,7 @@ import static org.opengis.test.Assert.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Simon Reynard (Geomatys)
- * @version 0.8
+ * @version 1.4
  * @since   0.7
  */
 public final class NADCONTest extends DatumShiftTestCase {
@@ -91,7 +92,7 @@ public final class NADCONTest extends DatumShiftTestCase {
      * Tests loading a grid file and interpolating a sample point.
      * The point used for this test is given by {@link #samplePoint(int)}.
      *
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws FactoryException if an error occurred while loading or computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
      */
@@ -113,21 +114,21 @@ public final class NADCONTest extends DatumShiftTestCase {
      * @throws FactoryException if an error occurred while loading or computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
      */
-    public static void testNADCON(final Path latitudeShifts, final Path longitudeShifts)
+    public static void testNADCON(final URI latitudeShifts, final URI longitudeShifts)
             throws FactoryException, TransformException
     {
         testNADCON(latitudeShifts, longitudeShifts, -131, -63, 20, 50);
     }
 
     /**
-     * Implementation of {@link #testLoader()} and {@link #testNADCON(Path, Path)}.
+     * Implementation of {@link #testLoader()} and {@link #testNADCON(URI, URI)}.
      *
      * @param  xmin  westmost longitude.
      * @param  xmax  eastmost longitude.
      * @param  ymin  southmost latitude.
      * @param  ymax  northmost latitude.
      */
-    private static void testNADCON(final Path latitudeShifts, final Path longitudeShifts,
+    private static void testNADCON(final URI latitudeShifts, final URI longitudeShifts,
             final double xmin, final double xmax, final double ymin, final double ymax)
             throws FactoryException, TransformException
     {
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NTv2Test.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NTv2Test.java
index c3e6299551..82a2b93f1a 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NTv2Test.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NTv2Test.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.internal.referencing.provider;
 
+import java.net.URI;
 import java.net.URISyntaxException;
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -35,7 +36,6 @@ import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.measure.Units;
 import org.apache.sis.internal.referencing.Formulas;
-import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.test.DependsOn;
 import org.junit.Test;
 
@@ -49,7 +49,7 @@ import static org.apache.sis.internal.referencing.provider.DatumShiftGridLoader.
  * It will also indirectly tests {@link DatumShiftGridGroup} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  *
  * @see GeocentricTranslationTest#testFranceGeocentricInterpolationPoint()
  * @see org.apache.sis.referencing.operation.transform.MolodenskyTransformTest#testFranceGeocentricInterpolationPoint()
@@ -80,7 +80,7 @@ public final class NTv2Test extends DatumShiftTestCase {
      * Tests loading a grid file and interpolating a sample point. The point used for
      * this test is given by {@link FranceGeocentricInterpolationTest#samplePoint(int)}.
      *
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws IOException if an error occurred while loading the grid.
      * @throws FactoryException if an error occurred while computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
@@ -104,19 +104,19 @@ public final class NTv2Test extends DatumShiftTestCase {
      * @throws FactoryException if an error occurred while loading or computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
      */
-    public static void testRGF93(final Path file) throws FactoryException, TransformException {
+    public static void testRGF93(final URI file) throws FactoryException, TransformException {
         testRGF93(file, -19800, 36000, 147600, 187200);
     }
 
     /**
-     * Implementation of {@link #testLoader()} and {@link #testRGF93(Path)}.
+     * Implementation of {@link #testLoader()} and {@link #testRGF93(URI)}.
      *
      * @param  xmin  negative of value of {@code "W_LONG"} record.
      * @param  xmax  negative of value of {@code "E_LONG"} record.
      * @param  ymin  value of the {@code "S_LAT"} record.
      * @param  ymax  value of the {@code "N_LAT"} record.
      */
-    private static void testRGF93(final Path file, final double xmin, final double xmax,
+    private static void testRGF93(final URI file, final double xmin, final double xmax,
             final double ymin, final double ymax) throws FactoryException, TransformException
     {
         final double cellSize = 360;
@@ -177,14 +177,15 @@ public final class NTv2Test extends DatumShiftTestCase {
      * to be present in the {@code $SIS_DATA/DatumChanges} directory. This test is executed only if the
      * {@link #RUN_EXTENSIVE_TESTS} flag is set.
      *
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws FactoryException if an error occurred while loading or computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
      */
     @Test
-    public void testMultiGrids() throws FactoryException, TransformException {
+    public void testMultiGrids() throws URISyntaxException, FactoryException, TransformException {
         assumeTrue(RUN_EXTENSIVE_TESTS);
-        final Path file = DataDirectory.DATUM_CHANGES.resolve(Paths.get(MULTIGRID_TEST_FILE));
-        assumeTrue(Files.exists(file));
+        final URI file = DatumShiftGridLoader.toAbsolutePath(new URI(MULTIGRID_TEST_FILE));
+        assumeTrue(Files.exists(Paths.get(file)));
         final DatumShiftGridFile<Angle,Angle> grid = NTv2.getOrLoad(NTv2.class, file, 2);
         assertInstanceOf("Should contain many grids.", DatumShiftGridGroup.class, grid);
         assertEquals("coordinateUnit",  Units.ARC_SECOND, grid.getCoordinateUnit());
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/DatumShiftTest.java b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/DatumShiftTest.java
index 304ff3dcdb..38b116d3e8 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/DatumShiftTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/DatumShiftTest.java
@@ -16,7 +16,7 @@
  */
 package org.apache.sis.test.integration;
 
-import java.nio.file.Path;
+import java.net.URI;
 import java.io.IOException;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.TransformException;
@@ -37,7 +37,7 @@ import static org.apache.sis.test.Assume.*;
  * directory.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.4
  * @since   0.7
  */
 @DependsOn({
@@ -56,7 +56,7 @@ public final class DatumShiftTest extends TestCase {
      */
     @Test
     public void testRGF93() throws IOException, FactoryException, TransformException {
-        final Path file = assumeDataExists(DataDirectory.DATUM_CHANGES, "ntf_r93.gsb");
+        final URI file = assumeDataExists(DataDirectory.DATUM_CHANGES, "ntf_r93.gsb");
         NTv2Test.testRGF93(file);
     }
 
@@ -70,8 +70,8 @@ public final class DatumShiftTest extends TestCase {
      */
     @Test
     public void testNADCON() throws IOException, FactoryException, TransformException {
-        final Path latitudeShifts  = assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.las");
-        final Path longitudeShifts = assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.los");
+        final URI latitudeShifts  = assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.las");
+        final URI longitudeShifts = assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.los");
         NADCONTest.testNADCON(latitudeShifts, longitudeShifts);
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java b/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
index 32746aa757..641bd5b7ac 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
@@ -33,7 +33,7 @@ import static java.util.logging.Logger.getLogger;
  * Sub-directories of {@code SIS_DATA} where SIS looks for EPSG database, datum shift grids and other resources.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.4
  * @since   0.7
  */
 public enum DataDirectory {
@@ -228,25 +228,4 @@ public enum DataDirectory {
         }
         return directory;
     }
-
-    /**
-     * If the given path is relative, returns the path as a child of the directory represented by this enum.
-     * If no valid directory is configured by the {@code SIS_DATA} environment variable, then the relative
-     * path is returned as-is.
-     *
-     * <p>This method is invoked for files that may be user-specified, for example datum shift file specified
-     * in {@link org.opengis.parameter.ParameterValue}.</p>
-     *
-     * @param  file  the path to resolve, or {@code null}.
-     * @return the path to use, or {@code null} if the given path was null.
-     */
-    public Path resolve(Path file) {
-        if (file != null && !file.isAbsolute()) {
-            final Path dir = getDirectory();
-            if (dir != null) {
-                return dir.resolve(file);
-            }
-        }
-        return file;
-    }
 }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/Assume.java b/core/sis-utility/src/test/java/org/apache/sis/test/Assume.java
index 6b3db4f30c..efe64d3fcd 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/Assume.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/Assume.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.test;
 
+import java.net.URI;
 import java.nio.file.Path;
 import java.nio.file.Files;
 import org.apache.sis.internal.system.DataDirectory;
@@ -25,7 +26,7 @@ import org.apache.sis.internal.system.DataDirectory;
  * Assumption methods used by the SIS project in addition of the JUnit ones.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.4
  * @since   0.7
  */
 public final class Assume extends org.junit.Assume {
@@ -48,13 +49,13 @@ public final class Assume extends org.junit.Assume {
      * @param  file  the file that needs to exist.
      * @return the path to the given file.
      */
-    public static Path assumeDataExists(final DataDirectory type, final String file) {
+    public static URI assumeDataExists(final DataDirectory type, final String file) {
         assumeNotNull("$SIS_DATA environment variable not set.", System.getenv(DataDirectory.ENV));
         Path path = type.getDirectory();
         assumeNotNull("$SIS_DATA/" + type + " directory not found.", path);
         path = path.resolve(file);
         assumeTrue("Specified file or directory not found.", Files.exists(path));
         assumeTrue("Specified directory not readable.", Files.isReadable(path));
-        return path;
+        return path.toUri();
     }
 }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/package-info.java b/core/sis-utility/src/test/java/org/apache/sis/test/package-info.java
index 537edf9e95..8bf3aab850 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/package-info.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/package-info.java
@@ -41,7 +41,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  * @since   0.3
  */
 package org.apache.sis.test;