Repository: OSGeo/PROJ (C++)

Issue:
Grid Shift been requested before it is used.
PROJ version: 9.3


   ```
 OGRSpatialReference src;
    OGRErr error = src.importFromWkt("GEOGCS[\"WGS84 Coordinate System\",DATUM[\"WGS 1984\",SPHEROID[\"WGS 1984\",6378137,298.257223563],TOWGS84[0,0,0,0,0,0,0],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"],AUTHORITY[\"SBMG\",\"LAT-LONG,LAT-LONG,WGS84,METERS\"]]");

    OGRSpatialReference dest;
    error = dest.importFromWkt("GEOGCS[\"NAD27\",DATUM[\"North_American_Datum_1927\",SPHEROID[\"Clarke 1866\",6378206.4,294.978698213898,AUTHORITY[\"EPSG\",\"7008\"]],AUTHORITY[\"EPSG\",\"6267\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AXIS[\"Latitude\",NORTH],AXIS[\"Longitude\",EAST],AUTHORITY[\"EPSG\",\"4267\"]]");

// this line make a call  to proj_network_open_cbk_type
    OGRCoordinateTransformation* ddd = OGRCreateCoordinateTransformation(&src, &dest);
```

two question about that code:

1. the function `OGRCreateCoordinateTransformation` cause a calling to `proj_network_open_cbk_type` which I think it to early 
because any transformation still not done yet. 

2. suppose that transformation request will be only in X,Y (without Z). in this case vertical grid shift is not necessary, isn't it ?








Discussion:
Related call stack from preliminary analysis:
```
PROJ_DEBUG=3 PROJ_NETWORK=ON gdb --args bin/projinfo -s "GEOGCS[\"WGS84 Coordinate System\",DATUM[\"WGS 1984\",SPHEROID[\"WGS 1984\",6378137,298.257223563],TOWGS84[0,0,0,0,0,0,0],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AUTHORITY[\"EPSG\",\"4326\"],AUTHORITY[\"SBMG\",\"LAT-LONG,LAT-LONG,WGS84,METERS\"]]" -t "GEOGCS[\"NAD27\",DATUM[\"North_American_Datum_1927\",SPHEROID[\"Clarke 1866\",6378206.4,294.978698213898,AUTHORITY[\"EPSG\",\"7008\"]],AUTHORITY[\"EPSG\",\"6267\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AXIS[\"Latitude\",NORTH],AXIS[\"Longitude\",EAST],AUTHORITY[\"EPSG\",\"4267\"]]"

#0  0x00007ffff74f2bbf in __GI___poll (fds=0x7fffffffc3b0, nfds=2, timeout=1000) at ../sysdeps/unix/sysv/linux/poll.c:29
#1  0x00007ffff71d9c19 in ?? () from /lib/x86_64-linux-gnu/libcurl-gnutls.so.4
#2  0x00007ffff71d3832 in ?? () from /lib/x86_64-linux-gnu/libcurl-gnutls.so.4
#3  0x00007ffff71d3a26 in curl_multi_poll () from /lib/x86_64-linux-gnu/libcurl-gnutls.so.4
#4  0x00007ffff71cc4b8 in curl_easy_perform () from /lib/x86_64-linux-gnu/libcurl-gnutls.so.4
#5  0x00007ffff7d8f14c in osgeo::proj::CurlFileHandle::open (ctx=0x7ffff7fc68e0 <pj_get_default_ctx()::default_context>, url=0x5555565b3880 "https://cdn.proj.org/us_noaa_conus.tif", offset=0, 
    size_to_read=16384, buffer=0x5555565ab690, out_size_read=0x7fffffffc6b0, error_string_max_size=1024, out_error_string=0x555555eb37a0 "") at /home/even/proj/proj/src/networkfilemanager.cpp:1732
#6  0x00007ffff7d8d3fd in osgeo::proj::NetworkFile::open (ctx=0x7ffff7fc68e0 <pj_get_default_ctx()::default_context>, filename=0x5555565b3880 "https://cdn.proj.org/us_noaa_conus.tif")
    at /home/even/proj/proj/src/networkfilemanager.cpp:1300
#7  0x00007ffff7d8d8df in osgeo::proj::pj_network_file_open (ctx=0x7ffff7fc68e0 <pj_get_default_ctx()::default_context>, filename=0x5555565b3880 "https://cdn.proj.org/us_noaa_conus.tif")
    at /home/even/proj/proj/src/networkfilemanager.cpp:1326
#8  0x00007ffff7d84209 in osgeo::proj::FileManager::open (ctx=0x7ffff7fc68e0 <pj_get_default_ctx()::default_context>, filename=0x5555565b3880 "https://cdn.proj.org/us_noaa_conus.tif", 
    access=osgeo::proj::FileAccess::READ_ONLY) at /home/even/proj/proj/src/filemanager.cpp:903
#9  0x00007ffff7d873a0 in osgeo::proj::FileManager::open_resource_file (ctx=0x7ffff7fc68e0 <pj_get_default_ctx()::default_context>, name=0x5555559bb630 "us_noaa_conus.tif", 
    out_full_filename=0x555555dfa610 "", out_full_filename_size=2047) at /home/even/proj/proj/src/filemanager.cpp:1718
#10 0x00007ffff7aa71dc in osgeo::proj::io::DatabaseContext::lookForGridInfo (this=0x5555555b3960, projFilename="us_noaa_conus.tif", considerKnownGridsAsAvailable=true, 
    fullFilename='\000' <repeats 200 times>..., packageName="", url="", directDownload=@0x7fffffffcd40: false, openLicense=@0x7fffffffcd41: false, gridAvailable=@0x7fffffffcd42: false)
    at /home/even/proj/proj/src/iso19111/factory.cpp:3330
#11 0x00007ffff7a55fec in osgeo::proj::operation::SingleOperation::gridsNeeded (this=0x555555b2af20, 
    databaseContext=std::shared_ptr<class osgeo::proj::io::DatabaseContext> (use count 2, weak count 1) = {...}, considerKnownGridsAsAvailable=true)
    at /home/even/proj/proj/src/iso19111/operation/singleoperation.cpp:1642
#12 0x00007ffff7b0c1ce in osgeo::proj::operation::ConcatenatedOperation::gridsNeeded (this=0x555555eddc20, 
    databaseContext=std::shared_ptr<class osgeo::proj::io::DatabaseContext> (use count 2, weak count 1) = {...}, considerKnownGridsAsAvailable=true)
    at /home/even/proj/proj/src/iso19111/operation/concatenatedoperation.cpp:937
#13 0x00007ffff7b58849 in osgeo::proj::operation::FilterResults::sort (this=0x7fffffffd050) at /home/even/proj/proj/src/iso19111/operation/coordinateoperationfactory.cpp:1393
#14 0x00007ffff7b570f4 in osgeo::proj::operation::FilterResults::andSort (this=0x7fffffffd050) at /home/even/proj/proj/src/iso19111/operation/coordinateoperationfactory.cpp:1139
#15 0x00007ffff7b0d15f in osgeo::proj::operation::filterAndSort (sourceList=std::vector of length 79, capacity 128 = {...}, context=..., 
    extent1=std::shared_ptr<class osgeo::proj::metadata::Extent> (empty) = {...}, extent2=std::shared_ptr<class osgeo::proj::metadata::Extent> (use count 3, weak count 1) = {...})
    at /home/even/proj/proj/src/iso19111/operation/coordinateoperationfactory.cpp:1651
#16 0x00007ffff7b361c7 in osgeo::proj::operation::CoordinateOperationFactory::createOperations (this=0x5555569dad70, sourceCRS=..., targetCRS=..., context=...)
    at /home/even/proj/proj/src/iso19111/operation/coordinateoperationfactory.cpp:7018
#17 0x000055555557946f in <lambda()>::operator()(void) const (__closure=0x7fffffffd470) at /home/even/proj/proj/src/apps/projinfo.cpp:953
#18 0x000055555557a181 in outputOperations (dbContext=std::shared_ptr<class osgeo::proj::io::DatabaseContext> (use count 2, weak count 1) = {...}, 
    sourceCRSStr="GEOGCS[\"WGS84 Coordinate System\",DATUM[\"WGS 1984\",SPHEROID[\"WGS 1984\",6378137,298.257223563],TOWGS84[0,0,0,0,0,0,0],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433],AU"..., sourceEpoch="", 
    targetCRSStr="GEOGCS[\"NAD27\",DATUM[\"North_American_Datum_1927\",SPHEROID[\"Clarke 1866\",6378206.4,294.978698213898,AUTHORITY[\"EPSG\",\"7008\"]],AUTHORITY[\"EPSG\",\"6267\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UN"..., targetEpoch="", bboxFilter=std::shared_ptr<class osgeo::proj::metadata::Extent> (empty) = {...}, 
    spatialCriterion=osgeo::proj::operation::CoordinateOperationContext::SpatialCriterion::STRICT_CONTAINMENT, spatialCriterionExplicitlySpecified=false, 
    crsExtentUse=osgeo::proj::operation::CoordinateOperationContext::SourceTargetCRSExtentUse::SMALLEST, 
    gridAvailabilityUse=osgeo::proj::operation::CoordinateOperationContext::GridAvailabilityUse::KNOWN_AVAILABLE, 
    allowUseIntermediateCRS=osgeo::proj::operation::CoordinateOperationContext::IntermediateCRSUse::IF_NO_DIRECT_TRANSFORMATION, pivots=std::vector of length 0, capacity 0, authority="", 
    usePROJGridAlternatives=true, showSuperseded=false, promoteTo3D=false, normalizeAxisOrder=false, minimumAccuracy=-1, outputOpt=..., summary=false) at /home/even/proj/proj/src/apps/projinfo.cpp:976
#19 0x000055555557ea23 in main (argc=5, argv=0x7fffffffdbf8) at /home/even/proj/proj/src/apps/projinfo.cpp:1701
```

We might need to convince DatabaseContext::lookForGridInfo() or its callers in the above call context to not call NS_PROJ::FileManager::open_resource_file() (in PROJ_NETWORK / PROJ_GRID_AVAILABILITY_KNOWN_AVAILABLE mode)



Patch:
diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp
index 370aa9783a..2de95d9eda 100644
--- a/src/iso19111/factory.cpp
+++ b/src/iso19111/factory.cpp
@@ -3324,14 +3324,18 @@ bool DatabaseContext::lookForGridInfo(
     url.clear();
     openLicense = false;
     directDownload = false;
-
-    fullFilename.resize(2048);
-    int errno_before = proj_context_errno(ctxt);
-    gridAvailable = NS_PROJ::FileManager::open_resource_file(
-                        ctxt, projFilename.c_str(), &fullFilename[0],
-                        fullFilename.size() - 1) != nullptr;
-    proj_context_errno_set(ctxt, errno_before);
-    fullFilename.resize(strlen(fullFilename.c_str()));
+    gridAvailable = false;
+
+    const auto resolveFullFilename = [ctxt, &fullFilename, &projFilename]() {
+        fullFilename.resize(2048);
+        const int errno_before = proj_context_errno(ctxt);
+        bool lGridAvailable = NS_PROJ::FileManager::open_resource_file(
+                                  ctxt, projFilename.c_str(), &fullFilename[0],
+                                  fullFilename.size() - 1) != nullptr;
+        proj_context_errno_set(ctxt, errno_before);
+        fullFilename.resize(strlen(fullFilename.c_str()));
+        return lGridAvailable;
+    };
 
     auto res =
         d->run("SELECT "
@@ -3363,7 +3367,7 @@ bool DatabaseContext::lookForGridInfo(
             old_proj_grid_name == projFilename) {
             std::string fullFilenameNewName;
             fullFilenameNewName.resize(2048);
-            errno_before = proj_context_errno(ctxt);
+            const int errno_before = proj_context_errno(ctxt);
             bool gridAvailableWithNewName =
                 pj_find_file(ctxt, proj_grid_name.c_str(),
                              &fullFilenameNewName[0],
@@ -3376,8 +3380,17 @@ bool DatabaseContext::lookForGridInfo(
             }
         }
 
-        if (considerKnownGridsAsAvailable &&
+        if (!gridAvailable && considerKnownGridsAsAvailable &&
             (!packageName.empty() || (!url.empty() && openLicense))) {
+
+            // In considerKnownGridsAsAvailable mode, try to fetch the local
+            // file name if it exists, but do not attempt network access.
+            const auto network_was_enabled =
+                proj_context_is_network_enabled(ctxt);
+            proj_context_set_enable_network(ctxt, false);
+            (void)resolveFullFilename();
+            proj_context_set_enable_network(ctxt, network_was_enabled);
+
             gridAvailable = true;
         }
 
@@ -3391,7 +3404,13 @@ bool DatabaseContext::lookForGridInfo(
         }
         info.directDownload = directDownload;
         info.openLicense = openLicense;
+
+        if (!gridAvailable) {
+            gridAvailable = resolveFullFilename();
+        }
     } else {
+        gridAvailable = resolveFullFilename();
+
         if (starts_with(fullFilename, "http://") ||
             starts_with(fullFilename, "https://")) {
             url = fullFilename;


Test patch:
diff --git a/test/unit/test_network.cpp b/test/unit/test_network.cpp
index e5a3946f62..8eaeb3da80 100644
--- a/test/unit/test_network.cpp
+++ b/test/unit/test_network.cpp
@@ -1264,26 +1264,28 @@ TEST(networking, network_endpoint_api) {
 
 static PROJ_NETWORK_HANDLE *dummy_open_cbk(PJ_CONTEXT *, const char *,
                                            unsigned long long, size_t, void *,
-                                           size_t *, size_t, char *, void *) {
-    assert(false);
+                                           size_t *, size_t, char *,
+                                           void *pUserData) {
+    *static_cast<bool *>(pUserData) = true;
     return nullptr;
 }
 
-static void dummy_close_cbk(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *, void *) {
-    assert(false);
+static void dummy_close_cbk(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *,
+                            void *pUserData) {
+    *static_cast<bool *>(pUserData) = true;
 }
 
 static const char *dummy_get_header_value_cbk(PJ_CONTEXT *,
                                               PROJ_NETWORK_HANDLE *,
-                                              const char *, void *) {
-    assert(false);
+                                              const char *, void *pUserData) {
+    *static_cast<bool *>(pUserData) = true;
     return nullptr;
 }
 
 static size_t dummy_read_range_cbk(PJ_CONTEXT *, PROJ_NETWORK_HANDLE *,
                                    unsigned long long, size_t, void *, size_t,
-                                   char *, void *) {
-    assert(false);
+                                   char *, void *pUserData) {
+    *static_cast<bool *>(pUserData) = true;
     return 0;
 }
 
@@ -1332,12 +1334,14 @@ TEST(networking, cache_basic) {
     proj_cleanup();
 
     // Check that a second access doesn't trigger any network activity
+    bool networkActivity = false;
     ASSERT_TRUE(proj_context_set_network_callbacks(
         ctx, dummy_open_cbk, dummy_close_cbk, dummy_get_header_value_cbk,
-        dummy_read_range_cbk, nullptr));
+        dummy_read_range_cbk, &networkActivity));
     P = proj_create(ctx, pipeline);
     ASSERT_NE(P, nullptr);
     proj_destroy(P);
+    EXPECT_FALSE(networkActivity);
 
     proj_context_destroy(ctx);
 }
@@ -2020,4 +2024,50 @@ TEST(networking, pyproj_issue_1192) {
 
 #endif
 
+// ---------------------------------------------------------------------------
+
+#ifdef CURL_ENABLED
+
+TEST(networking, do_not_attempt_network_access_known_available_network_on) {
+
+    // Check that proj_create_operations() itself does not trigger network
+    // activity in enable_network == true and
+    // PROJ_GRID_AVAILABILITY_KNOWN_AVAILABLE mode when all grids are known.
+
+    const auto doTest = [](PJ_CONTEXT *ctxt) {
+        auto factory_context =
+            proj_create_operation_factory_context(ctxt, nullptr);
+        proj_operation_factory_context_set_grid_availability_use(
+            ctxt, factory_context, PROJ_GRID_AVAILABILITY_KNOWN_AVAILABLE);
+        proj_operation_factory_context_set_spatial_criterion(
+            ctxt, factory_context, PROJ_SPATIAL_CRITERION_PARTIAL_INTERSECTION);
+        auto from = proj_create(ctxt, "EPSG:4326");
+        auto to = proj_create(ctxt, "EPSG:4267");
+        auto pj_operations =
+            proj_create_operations(ctxt, from, to, factory_context);
+        proj_destroy(from);
+        proj_destroy(to);
+        auto num_operations = proj_list_get_count(pj_operations);
+        EXPECT_GE(num_operations, 10);
+        proj_operation_factory_context_destroy(factory_context);
+        proj_list_destroy(pj_operations);
+    };
+
+    auto ctx = proj_context_create();
+    proj_context_set_enable_network(ctx, true);
+
+    // Check that we don't trigger any network activity
+    bool networkActivity = false;
+    ASSERT_TRUE(proj_context_set_network_callbacks(
+        ctx, dummy_open_cbk, dummy_close_cbk, dummy_get_header_value_cbk,
+        dummy_read_range_cbk, &networkActivity));
+
+    doTest(ctx);
+    EXPECT_FALSE(networkActivity);
+
+    proj_context_destroy(ctx);
+}
+
+#endif
+
 } // namespace