Index: trunk/src/org/openstreetmap/josm/Main.java
===================================================================
--- trunk/src/org/openstreetmap/josm/Main.java	(revision 12776)
+++ trunk/src/org/openstreetmap/josm/Main.java	(revision 12777)
@@ -17,5 +17,4 @@
 import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
@@ -57,5 +56,4 @@
 import org.openstreetmap.josm.tools.PlatformHook;
 import org.openstreetmap.josm.tools.PlatformHookOsx;
-import org.openstreetmap.josm.tools.PlatformHookUnixoid;
 import org.openstreetmap.josm.tools.PlatformHookWindows;
 import org.openstreetmap.josm.tools.Shortcut;
Index: trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileSource.java	(revision 12777)
+++ trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileSource.java	(revision 12777)
@@ -0,0 +1,19 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.projection.datum;
+
+import java.io.InputStream;
+
+/**
+ * Source of NTV2 grid shift files (local directory, download, etc.).
+ * @since 12777
+ */
+public interface NTV2GridShiftFileSource {
+
+    /**
+     * Locate grid file with given name.
+     * @param gridFileName the name of the grid file
+     * @return an input stream for the file data
+     */
+    InputStream getNTV2GridShiftFile(String gridFileName);
+
+}
Index: trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileWrapper.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileWrapper.java	(revision 12776)
+++ trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2GridShiftFileWrapper.java	(revision 12777)
@@ -8,8 +8,7 @@
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
 
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.io.CachedFile;
-import org.openstreetmap.josm.tools.Platform;
 import org.openstreetmap.josm.tools.PlatformVisitor;
 
@@ -24,4 +23,22 @@
     private NTV2GridShiftFile instance;
     private final String gridFileName;
+
+    public static float NTV2_SOURCE_PRIORITY_LOCAL = 10f;
+    public static float NTV2_SOURCE_PRIORITY_DOWNLOAD = 5f;
+
+    private static Map<Float, NTV2GridShiftFileSource> sources =
+            new TreeMap<Float, NTV2GridShiftFileSource>(Collections.reverseOrder());
+
+    /**
+     * Register a source for NTV2 grid files.
+     * @param priority the priority, sources with higher priority are checked first;
+     * use {@link #NTV2_SOURCE_PRIORITY_LOCAL} for local files and
+     * {@link #NTV2_SOURCE_PRIORITY_DOWNLOAD} for remote downloads
+     * @param source the NTV2 grid file source
+     * @since 12777
+     */
+    public static void registerNTV2GridShiftFileSource(float priority, NTV2GridShiftFileSource source) {
+        sources.put(priority, source);
+    }
 
     /**
@@ -63,34 +80,15 @@
     public synchronized NTV2GridShiftFile getShiftFile() throws IOException {
         if (instance == null) {
-            File grid = null;
-            // Check is the grid is installed in default PROJ.4 directories
-            for (File dir : Platform.determinePlatform().accept(DEFAULT_PROJ4_NTV2_SHIFT_DIRS)) {
-                File file = new File(dir, gridFileName);
-                if (file.exists() && file.isFile()) {
-                    grid = file;
-                    break;
-                }
-            }
-            // If not, search into PROJ_LIB directory
-            if (grid == null) {
-                String projLib = System.getProperty("PROJ_LIB");
-                if (projLib != null && !projLib.isEmpty()) {
-                    File dir = new File(projLib);
-                    if (dir.exists() && dir.isDirectory()) {
-                        File file = new File(dir, gridFileName);
-                        if (file.exists() && file.isFile()) {
-                            grid = file;
-                        }
-                    }
-                }
-            }
-            // If not, retrieve it from JOSM website
-            String location = grid != null ? grid.getAbsolutePath() : (Main.getJOSMWebsite() + "/proj/" + gridFileName);
-            // Try to load grid file
-            try (CachedFile cf = new CachedFile(location); InputStream is = cf.getInputStream()) {
-                NTV2GridShiftFile ntv2 = new NTV2GridShiftFile();
-                ntv2.loadGridShiftFile(is, false);
-                instance = ntv2;
-            }
+            for (Map.Entry<Float, NTV2GridShiftFileSource> entry : sources.entrySet()) {
+                NTV2GridShiftFileSource source = entry.getValue();
+                try (InputStream is = source.getNTV2GridShiftFile(gridFileName)) {
+                    if (is != null) {
+                        NTV2GridShiftFile ntv2 = new NTV2GridShiftFile();
+                        ntv2.loadGridShiftFile(is, false);
+                        instance = ntv2;
+                        break;
+                     }
+                 }
+             }
         }
         return instance;
Index: trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2Proj4DirGridShiftFileSource.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2Proj4DirGridShiftFileSource.java	(revision 12777)
+++ trunk/src/org/openstreetmap/josm/data/projection/datum/NTV2Proj4DirGridShiftFileSource.java	(revision 12777)
@@ -0,0 +1,87 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.projection.datum;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Platform;
+import org.openstreetmap.josm.tools.PlatformVisitor;
+
+/**
+ * Shift file source that scans the common data directories of the proj4 library.
+ * @since 12777
+ */
+public class NTV2Proj4DirGridShiftFileSource implements NTV2GridShiftFileSource, PlatformVisitor<List<File>> {
+
+    private NTV2Proj4DirGridShiftFileSource() {
+        // hide constructor
+    }
+
+    // lazy initialization
+    private static class InstanceHolder {
+        static final NTV2Proj4DirGridShiftFileSource INSTANCE = new NTV2Proj4DirGridShiftFileSource();
+    }
+
+    /**
+     * Get the singleton instance of this class.
+     * @return the singleton instance of this class
+     */
+    public static NTV2Proj4DirGridShiftFileSource getInstance() {
+        return InstanceHolder.INSTANCE;
+    }
+
+    @Override
+    public InputStream getNTV2GridShiftFile(String gridFileName) {
+        File grid = null;
+        // Check is the grid is installed in default PROJ.4 directories
+        for (File dir : Platform.determinePlatform().accept(this)) {
+            File file = new File(dir, gridFileName);
+            if (file.exists() && file.isFile()) {
+                grid = file;
+                break;
+            }
+        }
+        // If not, search into PROJ_LIB directory
+        if (grid == null) {
+            String projLib = System.getProperty("PROJ_LIB");
+            if (projLib != null && !projLib.isEmpty()) {
+                File dir = new File(projLib);
+                if (dir.exists() && dir.isDirectory()) {
+                    File file = new File(dir, gridFileName);
+                    if (file.exists() && file.isFile()) {
+                        grid = file;
+                    }
+                }
+            }
+        }
+        if (grid != null) {
+            try {
+                return new FileInputStream(grid.getAbsoluteFile());
+            } catch (FileNotFoundException ex) {
+                Logging.warn("NTV2 grid shift file not found: " + grid);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public List<File> visitUnixoid() {
+        return Arrays.asList(new File("/usr/local/share/proj"), new File("/usr/share/proj"));
+    }
+
+    @Override
+    public List<File> visitWindows() {
+        return Arrays.asList(new File("C:\\PROJ\\NAD"));
+    }
+
+    @Override
+    public List<File> visitOsx() {
+        return Collections.emptyList();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/gui/MainApplication.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 12776)
+++ trunk/src/org/openstreetmap/josm/gui/MainApplication.java	(revision 12777)
@@ -85,4 +85,7 @@
 import org.openstreetmap.josm.data.osm.UserInfo;
 import org.openstreetmap.josm.data.osm.search.SearchMode;
+import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileSource;
+import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
+import org.openstreetmap.josm.data.projection.datum.NTV2Proj4DirGridShiftFileSource;
 import org.openstreetmap.josm.data.validation.OsmValidator;
 import org.openstreetmap.josm.gui.ProgramArguments.Option;
@@ -113,4 +116,5 @@
 import org.openstreetmap.josm.gui.util.WindowGeometry;
 import org.openstreetmap.josm.gui.widgets.UrlLabel;
+import org.openstreetmap.josm.io.CachedFile;
 import org.openstreetmap.josm.io.CertificateAmendment;
 import org.openstreetmap.josm.io.DefaultProxySelector;
@@ -235,4 +239,20 @@
             menu.redo.setEnabled(redoSize > 0);
         };
+
+    /**
+     * Source of NTV2 shift files: Download from JOSM website.
+     * @since 12777
+     */
+    public static final NTV2GridShiftFileSource JOSM_WEBSITE_NTV2_SOURCE = gridFileName -> {
+        String location = Main.getJOSMWebsite() + "/proj/" + gridFileName;
+        // Try to load grid file
+        CachedFile cf = new CachedFile(location);
+        try {
+            return cf.getInputStream();
+        } catch (IOException ex) {
+            Logging.warn(ex);
+            return null;
+        }
+    };
 
     /**
@@ -902,4 +922,10 @@
         Main.toolbar = toolbar;
         ProjectionPreference.setProjection();
+        NTV2GridShiftFileWrapper.registerNTV2GridShiftFileSource(
+                NTV2GridShiftFileWrapper.NTV2_SOURCE_PRIORITY_LOCAL,
+                NTV2Proj4DirGridShiftFileSource.getInstance());
+        NTV2GridShiftFileWrapper.registerNTV2GridShiftFileSource(
+                NTV2GridShiftFileWrapper.NTV2_SOURCE_PRIORITY_DOWNLOAD,
+                JOSM_WEBSITE_NTV2_SOURCE);
         GuiHelper.translateJavaInternalMessages();
         preConstructorInit();
Index: trunk/src/org/openstreetmap/josm/tools/PlatformHook.java
===================================================================
--- trunk/src/org/openstreetmap/josm/tools/PlatformHook.java	(revision 12776)
+++ trunk/src/org/openstreetmap/josm/tools/PlatformHook.java	(revision 12777)
@@ -20,5 +20,5 @@
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.data.projection.datum.NTV2GridShiftFileWrapper;
+import org.openstreetmap.josm.data.projection.datum.NTV2Proj4DirGridShiftFileSource;
 import org.openstreetmap.josm.io.CertificateAmendment.CertAmend;
 import org.openstreetmap.josm.tools.date.DateUtils;
@@ -259,5 +259,5 @@
      */
     default List<File> getDefaultProj4NadshiftDirectories() {
-        return getPlatform().accept(NTV2GridShiftFileWrapper.DEFAULT_PROJ4_NTV2_SHIFT_DIRS);
+        return getPlatform().accept(NTV2Proj4DirGridShiftFileSource.getInstance());
     }
 
