Repository: OSGeo/PROJ (C++)

Issue:
COMPOUNDCRS+BOUNDCRS with derived VERTCRS loses ABRIDGEDTRANSFORMATION
Thanks @rouault for the fix for https://github.com/OSGeo/PROJ/issues/4576 - I have tested this and can confirm the ABRIDGEDTRANSFORMATION is applied and works as expected when the target is `EPSG:7855+EPSG:5711`.  The resulting transformation pipeline is:

```
+proj=pipeline
	+step +inv +proj=utm +zone=55 +south +ellps=aust_SA
	+step +proj=push +v_3
	+step +proj=cart +ellps=aust_SA
	+step +proj=helmert +x=-153.306840928477 +y=92.4132660160741 +z=-76.481435572882 +rx=-11.9644937558611 +ry=13.9284887447329 +rz=-3.44390727325759 +s=-34.2275583388174 +convention=position_vector
	+step +inv +proj=cart +ellps=GRS80
	+step +proj=pop +v_3
	+step +proj=utm +zone=55 +south +ellps=GRS80
```

However, I now need to correct for the residual 1.055m vertical offset that arises due to the difference between the ellipsoids.

#### Problem:

I tried creating a source WKT with a DERIVINGCONVERSION on the VERTCRS to correct for the vertical difference:

```
COMPOUNDCRS["AGD84 / AMG zone 55 + AHD height",
    BOUNDCRS[
        SOURCECRS[
            PROJCRS["AGD84 / AMG zone 55 + AHD height",
                BASEGEOGCRS["AGD84",
                    DATUM["User datum (no grid)",
                        ELLIPSOID["Australian National Spheroid",6378160,298.25,
                            LENGTHUNIT["metre",1]]],
                    PRIMEM["Greenwich",0,
                        ANGLEUNIT["degree",0.0174532925199433]]],
                CONVERSION["Australian Map Grid zone 55",
                    METHOD["Transverse Mercator"],
                    PARAMETER["Latitude of natural origin",0,
                        ANGLEUNIT["degree",0.0174532925199433]],
                    PARAMETER["Longitude of natural origin",147,
                        ANGLEUNIT["degree",0.0174532925199433]],
                    PARAMETER["Scale factor at natural origin",0.9996,
                        SCALEUNIT["unity",1]],
                    PARAMETER["False easting",500000,
                        LENGTHUNIT["metre",1]],
                    PARAMETER["False northing",10000000,
                        LENGTHUNIT["metre",1]]],
                CS[Cartesian,2],
                    AXIS["(E)",east,
                        ORDER[1],
                        LENGTHUNIT["metre",1]],
                    AXIS["(N)",north,
                        ORDER[2],
                        LENGTHUNIT["metre",1]],
                USAGE[
                    SCOPE["Engineering survey, topographic mapping."],
                    AREA["Australia - Queensland between 144°E and 150°E."],
                    BBOX[-29.01,144,-14.01,150]]]],
        TARGETCRS[
            GEOGCRS["GDA2020",
                DATUM["Geocentric Datum of Australia 2020",
                    ELLIPSOID["GRS 1980",6378137,298.257222101,
                        LENGTHUNIT["metre",1]]],
                PRIMEM["Greenwich",0,
                    ANGLEUNIT["degree",0.0174532925199433]],
                CS[ellipsoidal,2],
                    AXIS["geodetic latitude (Lat)",north,
                        ORDER[1],
                        ANGLEUNIT["degree",0.0174532925199433]],
                    AXIS["geodetic longitude (Lon)",east,
                        ORDER[2],
                        ANGLEUNIT["degree",0.0174532925199433]]]],
        ABRIDGEDTRANSFORMATION["User-defined Bursa-Wolf Transform",
            METHOD["Position Vector transformation (geog2D domain)"],
            PARAMETER["X-axis translation",-153.306840928477,
                ID["EPSG",8605]],
            PARAMETER["Y-axis translation",92.4132660160741,
                ID["EPSG",8606]],
            PARAMETER["Z-axis translation",-76.481435572882,
                ID["EPSG",8607]],
            PARAMETER["X-axis rotation",-11.9644937558611,
                ID["EPSG",8608]],
            PARAMETER["Y-axis rotation",13.9284887447329,
                ID["EPSG",8609]],
            PARAMETER["Z-axis rotation",-3.44390727325759,
                ID["EPSG",8610]],
            PARAMETER["Scale difference",0.999965772441661,
                ID["EPSG",8611]]]],
    VERTCRS["AHD height + local offset",
        BASEVERTCRS["AHD height",
            VDATUM["Australian Height Datum"]],
        DERIVINGCONVERSION["vertical offset",
            METHOD["PROJ affine"],
            PARAMETER["zoff",1.055,
                LENGTHUNIT["metre",1]]],
        CS[vertical,1],
            AXIS["gravity-related height (H)",up,
                LENGTHUNIT["metre",1]],
        USAGE[
            SCOPE["Cadastre, engineering surveying applications over distances up to 10km."],
            AREA["Australia - Australian Capital Territory, New South Wales, Northern Territory, Queensland, South Australia, Tasmania, Western Australia and Victoria - onshore. Christmas Island - onshore. Cocos and Keeling Islands - onshore."],
            BBOX[-43.7,96.76,-9.86,153.69]],
        ID["EPSG",5711]]]
```

However, while the resulting transformation pipeline now includes the vertical offset as expected, it again loses the helmert transform steps for the horizontal shift: 

```
+proj=pipeline
	+step +inv +proj=utm +zone=55 +south +ellps=aust_SA
	+step +inv +proj=affine +zoff=1.055
	+step +proj=utm +zone=55 +south +ellps=GRS80
```

#### Expected output:

If I manually combine elements of both pipelines and construct a pipeline string by including the affine offset step, then the coordinates align exactly as expected:

```
+proj=pipeline
	+step +inv +proj=utm +zone=55 +south +ellps=aust_SA
	+step +proj=push +v_3
	+step +proj=cart +ellps=aust_SA
	+step +proj=helmert +x=-153.306840928477 +y=92.4132660160741 +z=-76.481435572882 +rx=-11.9644937558611 +ry=13.9284887447329 +rz=-3.44390727325759 +s=-34.2275583388174 +convention=position_vector
	+step +inv +proj=cart +ellps=GRS80
	+step +proj=pop +v_3
	+step +inv +proj=affine +zoff=1.055
	+step +proj=utm +zone=55 +south +ellps=GRS80
```

Is this a bug? Should it possible to construct a Compound WKT2 that includes both horizontal and vertical shift components?

Thanks again for looking into this!


#### Environment Information

- PROJ branch `backport-4577-to-9.7`, commit `9c381624cd3615064f9f09b9fd504feaca884040`
- Operation System Information: Ubuntu 20.04

#### Installation method

- Built from source



Discussion:
I don't know if the Bursa-Wolf is a must. If not this paper may help you. It applies the rotation and translation on the surface, not at the centre of the earth
https://isprs-archives.copernicus.org/articles/XLVIII-4-W7-2023/75/2023/



Patch:
diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp
index 95bc15278f..62b3d17ed9 100644
--- a/src/iso19111/operation/coordinateoperationfactory.cpp
+++ b/src/iso19111/operation/coordinateoperationfactory.cpp
@@ -6955,8 +6955,7 @@ void CoordinateOperationFactory::Private::createOperationsCompoundToCompound(
 
         auto interpolationCRS =
             NN_NO_CHECK(std::static_pointer_cast<crs::SingleCRS>(srcGeog));
-        auto interpTransformCRS = verticalTransform->interpolationCRS();
-        if (interpTransformCRS) {
+        if (auto interpTransformCRS = verticalTransform->interpolationCRS()) {
             auto interpTransformSingleCRS =
                 std::static_pointer_cast<crs::SingleCRS>(interpTransformCRS);
             if (interpTransformSingleCRS) {
@@ -6976,6 +6975,12 @@ void CoordinateOperationFactory::Private::createOperationsCompoundToCompound(
                 interpolationCRS =
                     NN_NO_CHECK(util::nn_dynamic_pointer_cast<crs::SingleCRS>(
                         compSrc0BoundCrs->hubCRS()));
+            } else if (compSrc0BoundCrs && !compDst0BoundCrs &&
+                       compSrc0BoundCrs->hubCRS()->_isEquivalentTo(
+                           dstGeog.get(),
+                           util::IComparable::Criterion::EQUIVALENT)) {
+                interpolationCRS = NN_NO_CHECK(
+                    std::static_pointer_cast<crs::SingleCRS>(dstGeog));
             }
         }
 


Test patch:
diff --git a/test/unit/test_operationfactory.cpp b/test/unit/test_operationfactory.cpp
index f7897590b6..f4f8f71fb8 100644
--- a/test/unit/test_operationfactory.cpp
+++ b/test/unit/test_operationfactory.cpp
@@ -6159,6 +6159,145 @@ TEST(operation,
 
 // ---------------------------------------------------------------------------
 
+TEST(
+    operation,
+    compoundCRS_with_horizontal_boundCRS_to_compoundCRS_with_derived_vertical) {
+
+    auto dbContext = DatabaseContext::create();
+    auto objSrc = WKTParser().attachDatabaseContext(dbContext).createFromWKT(
+        "COMPOUNDCRS[\"AGD84 / AMG zone 55 + AHD height + local offset\",\n"
+        "    BOUNDCRS[\n"
+        "        SOURCECRS[\n"
+        "            PROJCRS[\"AGD84 / AMG zone 55\",\n"
+        "                BASEGEOGCRS[\"AGD84\",\n"
+        "                    DATUM[\"User datum (no grid)\",\n"
+        "                        ELLIPSOID[\"Australian National Spheroid\","
+        "6378160,298.25,\n"
+        "                            LENGTHUNIT[\"metre\",1]]],\n"
+        "                    PRIMEM[\"Greenwich\",0,\n"
+        "                        ANGLEUNIT[\"degree\",0.0174532925199433]]],\n"
+        "                CONVERSION[\"Australian Map Grid zone 55\",\n"
+        "                    METHOD[\"Transverse Mercator\"],\n"
+        "                    PARAMETER[\"Latitude of natural origin\",0,\n"
+        "                        ANGLEUNIT[\"degree\",0.0174532925199433]],\n"
+        "                    PARAMETER[\"Longitude of natural origin\",147,\n"
+        "                        ANGLEUNIT[\"degree\",0.0174532925199433]],\n"
+        "                    PARAMETER[\"Scale factor at natural origin\","
+        "0.9996,\n"
+        "                        SCALEUNIT[\"unity\",1]],\n"
+        "                    PARAMETER[\"False easting\",500000,\n"
+        "                        LENGTHUNIT[\"metre\",1]],\n"
+        "                    PARAMETER[\"False northing\",10000000,\n"
+        "                        LENGTHUNIT[\"metre\",1]]],\n"
+        "                CS[Cartesian,2],\n"
+        "                    AXIS[\"(E)\",east,\n"
+        "                        ORDER[1],\n"
+        "                        LENGTHUNIT[\"metre\",1]],\n"
+        "                    AXIS[\"(N)\",north,\n"
+        "                        ORDER[2],\n"
+        "                        LENGTHUNIT[\"metre\",1]]]],\n"
+        "        TARGETCRS[\n"
+        "            GEOGCRS[\"GDA2020\",\n"
+        "                DATUM[\"Geocentric Datum of Australia 2020\",\n"
+        "                    ELLIPSOID[\"GRS 1980\",6378137,298.257222101,\n"
+        "                        LENGTHUNIT[\"metre\",1]]],\n"
+        "                PRIMEM[\"Greenwich\",0,\n"
+        "                    ANGLEUNIT[\"degree\",0.0174532925199433]],\n"
+        "                CS[ellipsoidal,2],\n"
+        "                    AXIS[\"geodetic latitude (Lat)\",north,\n"
+        "                        ORDER[1],\n"
+        "                        ANGLEUNIT[\"degree\",0.0174532925199433]],\n"
+        "                    AXIS[\"geodetic longitude (Lon)\",east,\n"
+        "                        ORDER[2],\n"
+        "                        ANGLEUNIT[\"degree\",0.0174532925199433]]]],\n"
+        "        ABRIDGEDTRANSFORMATION[\"User-defined Bursa-Wolf "
+        "Transform\",\n"
+        "            METHOD[\"Position Vector transformation (geog2D "
+        "domain)\"],\n"
+        "            PARAMETER[\"X-axis translation\",-153.306840928477,\n"
+        "                ID[\"EPSG\",8605]],\n"
+        "            PARAMETER[\"Y-axis translation\",92.4132660160741,\n"
+        "                ID[\"EPSG\",8606]],\n"
+        "            PARAMETER[\"Z-axis translation\",-76.481435572882,\n"
+        "                ID[\"EPSG\",8607]],\n"
+        "            PARAMETER[\"X-axis rotation\",-11.9644937558611,\n"
+        "                ID[\"EPSG\",8608]],\n"
+        "            PARAMETER[\"Y-axis rotation\",13.9284887447329,\n"
+        "                ID[\"EPSG\",8609]],\n"
+        "            PARAMETER[\"Z-axis rotation\",-3.44390727325759,\n"
+        "                ID[\"EPSG\",8610]],\n"
+        "            PARAMETER[\"Scale difference\",0.999965772441661,\n"
+        "                ID[\"EPSG\",8611]]]],\n"
+        "    VERTCRS[\"AHD height + local offset\",\n"
+        "        BASEVERTCRS[\"AHD height\",\n"
+        "            VDATUM[\"Australian Height Datum\"]],\n"
+        "        DERIVINGCONVERSION[\"vertical offset\",\n"
+        "            METHOD[\"PROJ affine\"],\n"
+        "            PARAMETER[\"zoff\",1.055,\n"
+        "                LENGTHUNIT[\"metre\",1]]],\n"
+        "        CS[vertical,1],\n"
+        "            AXIS[\"gravity-related height (H)\",up,\n"
+        "                LENGTHUNIT[\"metre\",1]]]]");
+    auto src = nn_dynamic_pointer_cast<CRS>(objSrc);
+    ASSERT_TRUE(src != nullptr);
+
+    // GDA2020 / MGA zone 55 + AHD height
+    auto authFactoryEPSG = AuthorityFactory::create(dbContext, "EPSG");
+    auto dstObj = createFromUserInput("EPSG:7855+5711", dbContext, false);
+    auto dst = nn_dynamic_pointer_cast<CRS>(dstObj);
+    ASSERT_TRUE(dst != nullptr);
+
+    auto ctxt = CoordinateOperationContext::create(
+        AuthorityFactory::create(dbContext, std::string()), nullptr, 0.0);
+    ctxt->setSpatialCriterion(
+        CoordinateOperationContext::SpatialCriterion::PARTIAL_INTERSECTION);
+    {
+        auto list = CoordinateOperationFactory::create()->createOperations(
+            NN_NO_CHECK(src), NN_NO_CHECK(dst), ctxt);
+        ASSERT_GE(list.size(), 1U);
+
+        EXPECT_EQ(
+            list[0]->exportToPROJString(PROJStringFormatter::create().get()),
+            "+proj=pipeline "
+            "+step +inv +proj=utm +zone=55 +south +ellps=aust_SA "
+            "+step +proj=push +v_3 "
+            "+step +proj=cart +ellps=aust_SA "
+            "+step +proj=helmert +x=-153.306840928477 "
+            "+y=92.4132660160741 "
+            "+z=-76.481435572882 +rx=-11.9644937558611 "
+            "+ry=13.9284887447329 "
+            "+rz=-3.44390727325759 +s=-34.2275583390395 "
+            "+convention=position_vector "
+            "+step +inv +proj=cart +ellps=GRS80 "
+            "+step +proj=pop +v_3 "
+            "+step +inv +proj=affine +zoff=1.055 "
+            "+step +proj=utm +zone=55 +south +ellps=GRS80");
+    }
+    {
+        auto list = CoordinateOperationFactory::create()->createOperations(
+            NN_NO_CHECK(dst), NN_NO_CHECK(src), ctxt);
+        ASSERT_GE(list.size(), 1U);
+
+        EXPECT_EQ(
+            list[0]->exportToPROJString(PROJStringFormatter::create().get()),
+            "+proj=pipeline "
+            "+step +inv +proj=utm +zone=55 +south +ellps=GRS80 "
+            "+step +proj=affine +zoff=1.055 "
+            "+step +proj=push +v_3 "
+            "+step +proj=cart +ellps=GRS80 "
+            "+step +inv +proj=helmert +x=-153.306840928477 "
+            "+y=92.4132660160741 +z=-76.481435572882 "
+            "+rx=-11.9644937558611 +ry=13.9284887447329 "
+            "+rz=-3.44390727325759 +s=-34.2275583390395 "
+            "+convention=position_vector "
+            "+step +inv +proj=cart +ellps=aust_SA "
+            "+step +proj=pop +v_3 "
+            "+step +proj=utm +zone=55 +south +ellps=aust_SA");
+    }
+}
+
+// ---------------------------------------------------------------------------
+
 TEST(
     operation,
     compoundCRS_to_compoundCRS_concatenated_operation_with_two_vert_transformation) {