Repository: OSGeo/PROJ (C++)

Issue:
Support for transformation methods that are new in EPSG
In the recent database update to EPSG v11.023 and related changes ([https://github.com/OSGeo/PROJ/commit/a33372b0cfb48a64d403a81d14e552f17782bf93](https://github.com/OSGeo/PROJ/commit/a33372b0cfb48a64d403a81d14e552f17782bf93)) the CRSs and transformations for the island Saba in the Caribbean Netherlands are added. However, 4 new transformations from EPSG are missing. Probably because these use the exact Helmert transformation, or a null grid for the geoid of the height transformation, which are new transformation methods in EPSG. Since PROJ already supports the exact Helmert transformation, I hope it won't be too difficult to add the missing transformations too.

Two missing **exact Helmert transformations**:
- https://epsg.org/transformation_10646/Saba-to-BES2020-Saba-1.html
- https://epsg.org/transformation_10676/Saba-to-WGS-84-1.html

both using both these two new methods:
-  https://epsg.org/coord-operation-method_1133/Coordinate-Frame-rotation-full-matrix-geog2D.html
-  https://epsg.org/coord-operation-method_1132/Coordinate-Frame-rotation-full-matrix-geocen.html

&nbsp;
Two missing **height transformations with a null grid for the geoid**:
- https://epsg.org/transformation_10657/Saba-to-Saba-height-1.html (3D to 1D)
- https://epsg.org/transformation_10658/Saba-to-Saba-Saba-height-1.html (3D to 2D+1D)

and two corresponding concatenated operations:
- https://epsg.org/concatenated-operation_10675/BES2020-to-Saba-height-1.html (3D to 1D)
- https://epsg.org/concatenated-operation_?????/BES2020-to-Saba-Saba-height-1.html (3D to 2D+1D), still waiting for addition in EPSG

using one of these two new methods:
- https://epsg.org/coord-operation-method_1136/Geographic3D-to-GravityRelatedHeight.html (3D to 1D)
- https://epsg.org/coord-operation-method_1131/Geog3D-to-Geog2D-GravityRelatedHeight.html (3D to 2D+1D)





Discussion:
Support for the Geographic3D to GravityRelated Height and Geog3D to Geog2D GravityRelated Height had already been added a few days ago in master

Support for the Coordinate Frame Rotation Full matrix added in https://github.com/OSGeo/PROJ/pull/4348

You'll have to get back to IOGP about how concatenated operation EPSG:10675 is defined. Currently it is too messy and PROJ rejects it due to its consistency checks: "Note that concatenated operation EPSG:10675 is not imported because https://epsg.org/concatenated-operation_10675/BES2020-to-Saba-height-1.html is ill defined in EPSG 11.023 due to first step referencing BES2020 Saba geographic 2D (EPSG:10639), but source CRS of concatenated operation referencing BES2020 Saba geographic 3D (EPSG:10638)"
That is very quick. Many thanks!

I'll contact EPSG about definition of the concatenated operations. 



Patch:
diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt
index 8641d896f6..5ad36107ff 100644
--- a/data/CMakeLists.txt
+++ b/data/CMakeLists.txt
@@ -45,7 +45,7 @@ set(ALL_SQL_IN "${CMAKE_CURRENT_BINARY_DIR}/all.sql.in")
 set(PROJ_DB "${CMAKE_CURRENT_BINARY_DIR}/proj.db")
 include(sql_filelist.cmake)
 
-set(PROJ_DB_SQL_EXPECTED_MD5 "3d51445a31ba6980332fa8e2348f7b16")
+set(PROJ_DB_SQL_EXPECTED_MD5 "3e0b3f2f65e9bb051e2f5fb6b40e1a42")
 
 add_custom_command(
   OUTPUT ${PROJ_DB}
diff --git a/data/sql/helmert_transformation.sql b/data/sql/helmert_transformation.sql
index 1ef76b464b..5f6f806460 100644
--- a/data/sql/helmert_transformation.sql
+++ b/data/sql/helmert_transformation.sql
@@ -2690,10 +2690,14 @@ INSERT INTO "helmert_transformation" VALUES('EPSG','10607','WGS 84 (G2139) to WG
 INSERT INTO "usage" VALUES('EPSG','21197','helmert_transformation','EPSG','10607','EPSG','1262','EPSG','1027');
 INSERT INTO "helmert_transformation" VALUES('EPSG','10608','WGS 84 (G2296) to ITRF2020 (1)','Scale difference in ppb where 1/billion = 1E-9 or nm/m. Accuracy of 1cm applies at epoch 2024.0. Due to subsequent drift between the reference frames it may reach 2cm at other epochs.','EPSG','1032','Coordinate Frame rotation (geocentric domain)','EPSG','10604','EPSG','9988',0.01,0.0,0.0,0.0,'EPSG','1025',0.0,0.0,0.0,'EPSG','1031',0.0,'EPSG','1028',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'NGA-Wld 2024.0',0);
 INSERT INTO "usage" VALUES('EPSG','21198','helmert_transformation','EPSG','10608','EPSG','1262','EPSG','1026');
+INSERT INTO "helmert_transformation" VALUES('EPSG','10646','Saba to BES2020 Saba (1)','For the reverse transformation from BES2020 Saba to Saba, method 1133 [used here] is reversible with the parameter values given here (see GN7-2); the BESTRANS software uses different parameter values with the forward formulas: see BESTRANS documentation.','EPSG','1133','Coordinate Frame rotation full matrix (geog2D)','EPSG','10636','EPSG','10639',0.05,1138.7432,-2064.4761,110.7016,'EPSG','9001',-214.615206,479.360036,-164.703951,'EPSG','9104',-402.32073,'EPSG','9202',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'NSGI-Bes Saba',0);
+INSERT INTO "usage" VALUES('EPSG','21861','helmert_transformation','EPSG','10646','EPSG','4757','EPSG','1144');
 INSERT INTO "helmert_transformation" VALUES('EPSG','10647','BES2020 Saba to ITRF2014 (1)','Time-dependent component of official transformation BESTRANS2020.','EPSG','1056','Time-dependent Coordinate Frame rotation (geocen)','EPSG','10637','EPSG','7789',0.05,0.0,0.0,0.0,'EPSG','9001',0.0,0.0,0.0,'EPSG','9104',0.0,'EPSG','9202',0.00726,0.00848,0.01353,'EPSG','1042',0.0,0.0,0.0,'EPSG','1043',0.0,'EPSG','1041',2020.0,'EPSG','1029',NULL,NULL,NULL,NULL,NULL,'NSGI-Bes Saba',0);
 INSERT INTO "usage" VALUES('EPSG','21765','helmert_transformation','EPSG','10647','EPSG','4757','EPSG','1079');
 INSERT INTO "helmert_transformation" VALUES('EPSG','10648','BES2020 Saba to WGS 84 (1)','','EPSG','9603','Geocentric translations (geog2D domain)','EPSG','10639','EPSG','4326',0.5,0.0,0.0,0.0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'NSGI-Bes Saba',0);
 INSERT INTO "usage" VALUES('EPSG','21743','helmert_transformation','EPSG','10648','EPSG','4757','EPSG','1252');
+INSERT INTO "helmert_transformation" VALUES('EPSG','10676','Saba to WGS 84 (1)','Parameter values taken from Saba to BES2020 Saba (1) (transformation code 10646) assuming that BES2020 Saba is coincident with WGS 84 within the accuracy of the transformation.','EPSG','1133','Coordinate Frame rotation full matrix (geog2D)','EPSG','10636','EPSG','4326',1.0,1138.7432,-2064.4761,110.7016,'EPSG','9001',-214.615206,479.360036,-164.703951,'EPSG','9104',-402.32073,'EPSG','9202',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'IOGP-Bes Saba',0);
+INSERT INTO "usage" VALUES('EPSG','21783','helmert_transformation','EPSG','10676','EPSG','4757','EPSG','1252');
 INSERT INTO "helmert_transformation" VALUES('EPSG','10682','RGM04 to RGM23 (1)','Approximation at the +/- 0.1m level. For more accurate official transformation see RGM04 to RGM23 (2) (code 10683).','EPSG','9603','Geocentric translations (geog2D domain)','EPSG','4470','EPSG','10671',0.1,-0.5377,0.3946,0.3608,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'IGN-Myt 10cm',0);
 INSERT INTO "usage" VALUES('EPSG','21834','helmert_transformation','EPSG','10682','EPSG','1159','EPSG','1189');
 INSERT INTO "helmert_transformation" VALUES('EPSG','10684','RGM23 to WGS 84 (1)','Approximation at the +/- 1m level assuming that RGM23 is equivalent to WGS 84.','EPSG','9603','Geocentric translations (geog2D domain)','EPSG','10671','EPSG','4326',1.0,0.0,0.0,0.0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,'IOGP-Myt',0);
diff --git a/scripts/build_db.py b/scripts/build_db.py
index 3e7b1d880c..dd798fbce9 100755
--- a/scripts/build_db.py
+++ b/scripts/build_db.py
@@ -585,7 +585,7 @@ def fill_compound_crs(proj_db_cursor):
             raise
 
 def fill_helmert_transformation(proj_db_cursor):
-    proj_db_cursor.execute("SELECT coord_op_code, coord_op_name, coord_op_method_code, coord_op_method_name, source_crs_code, target_crs_code, coord_op_accuracy, coord_tfm_version, epsg_coordoperation.deprecated, epsg_coordoperation.remarks FROM epsg.epsg_coordoperation LEFT JOIN epsg.epsg_coordoperationmethod USING (coord_op_method_code) WHERE coord_op_type = 'transformation' AND coord_op_method_code IN (1031, 1032, 1033, 1034, 1035, 1037, 1038, 1039, 1053, 1054, 1055, 1056, 1057, 1058, 1061, 1062, 1063, 1065, 1066, 9603, 9606, 9607, 9636) ")
+    proj_db_cursor.execute("SELECT coord_op_code, coord_op_name, coord_op_method_code, coord_op_method_name, source_crs_code, target_crs_code, coord_op_accuracy, coord_tfm_version, epsg_coordoperation.deprecated, epsg_coordoperation.remarks FROM epsg.epsg_coordoperation LEFT JOIN epsg.epsg_coordoperationmethod USING (coord_op_method_code) WHERE coord_op_type = 'transformation' AND coord_op_method_code IN (1031, 1032, 1033, 1034, 1035, 1037, 1038, 1039, 1053, 1054, 1055, 1056, 1057, 1058, 1061, 1062, 1063, 1065, 1066, 1132, 1133, 9603, 9606, 9607, 9636) ")
     for (code, name, method_code, method_name, source_crs_code, target_crs_code, coord_op_accuracy, coord_tfm_version, deprecated, remarks) in proj_db_cursor.fetchall():
         expected_order = 1
         max_n_params = 15
@@ -928,6 +928,13 @@ def fill_concatenated_operation(proj_db_cursor):
         expected_order = 1
         steps_code = []
 
+        # FIXME: https://epsg.org/concatenated-operation_10675/BES2020-to-Saba-height-1.html ill defined in EPSG 11.023
+        # due to first step referencing BES2020 Saba geographic 2D (EPSG:10639), but source CRS of concatenated
+        # operation referencing BES2020 Saba geographic 3D (EPSG:10638)
+        if code == 10675:
+            print("FIXME! Skipping EPSG:10675 'BES2020 to Saba height (1)' for now")
+            continue
+
         iterator = proj_db_cursor.execute("SELECT op_path_step, single_operation_code FROM epsg_coordoperationpath WHERE concat_operation_code = ? ORDER BY op_path_step", (code,))
         for (order, single_operation_code) in iterator:
             assert order == expected_order
diff --git a/src/iso19111/operation/coordinateoperationfactory.cpp b/src/iso19111/operation/coordinateoperationfactory.cpp
index 078a0196d3..fe17a6ebc1 100644
--- a/src/iso19111/operation/coordinateoperationfactory.cpp
+++ b/src/iso19111/operation/coordinateoperationfactory.cpp
@@ -2381,8 +2381,12 @@ struct MyPROJStringExportableHorizVerticalHorizPROJBased final
 
                 methodEPSGCode ==
                     EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC ||
+                methodEPSGCode ==
+                    EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC ||
                 methodEPSGCode ==
                     EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D ||
+                methodEPSGCode ==
+                    EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D ||
                 methodEPSGCode ==
                     EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D ||
                 methodEPSGCode ==
diff --git a/src/iso19111/operation/parammappings.cpp b/src/iso19111/operation/parammappings.cpp
index 2d4fc2c549..0abaa8b61a 100644
--- a/src/iso19111/operation/parammappings.cpp
+++ b/src/iso19111/operation/parammappings.cpp
@@ -1003,7 +1003,9 @@ const struct MethodNameCode methodNameCodesList[] = {
     METHOD_NAME_CODE(AFFINE_PARAMETRIC_TRANSFORMATION),
     METHOD_NAME_CODE(SIMILARITY_TRANSFORMATION),
     METHOD_NAME_CODE(COORDINATE_FRAME_GEOCENTRIC),
+    METHOD_NAME_CODE(COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC),
     METHOD_NAME_CODE(COORDINATE_FRAME_GEOGRAPHIC_2D),
+    METHOD_NAME_CODE(COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D),
     METHOD_NAME_CODE(COORDINATE_FRAME_GEOGRAPHIC_3D),
     METHOD_NAME_CODE(POSITION_VECTOR_GEOCENTRIC),
     METHOD_NAME_CODE(POSITION_VECTOR_GEOGRAPHIC_2D),
@@ -1534,9 +1536,15 @@ static const MethodMapping gOtherMethodMappings[] = {
     {EPSG_NAME_METHOD_COORDINATE_FRAME_GEOCENTRIC,
      EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC, nullptr, nullptr, nullptr,
      paramsHelmert7},
+    {EPSG_NAME_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC,
+     EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC, nullptr, nullptr,
+     nullptr, paramsHelmert7},
     {EPSG_NAME_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D,
      EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D, nullptr, nullptr, nullptr,
      paramsHelmert7},
+    {EPSG_NAME_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D,
+     EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D, nullptr,
+     nullptr, nullptr, paramsHelmert7},
     {EPSG_NAME_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D,
      EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D, nullptr, nullptr, nullptr,
      paramsHelmert7},
diff --git a/src/iso19111/operation/singleoperation.cpp b/src/iso19111/operation/singleoperation.cpp
index 779d922042..42ca9ffc63 100644
--- a/src/iso19111/operation/singleoperation.cpp
+++ b/src/iso19111/operation/singleoperation.cpp
@@ -3473,6 +3473,7 @@ bool SingleOperation::exportToPROJStringGeneric(
     bool sevenParamsTransform = false;
     bool threeParamsTransform = false;
     bool fifteenParamsTransform = false;
+    bool fullMatrix = false;
     const auto &l_method = method();
     const auto &methodName = l_method->nameStr();
     const bool isMethodInverseOf = starts_with(methodName, INVERSE_OF);
@@ -3484,10 +3485,19 @@ bool SingleOperation::exportToPROJStringGeneric(
     const bool isCoordinateFrame =
         ci_find(methodName, "Coordinate Frame") != std::string::npos ||
         ci_find(methodName, "CF") != std::string::npos;
-    if ((paramCount == 7 && isCoordinateFrame && !l_isTimeDependent) ||
-        methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC ||
-        methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D ||
-        methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) {
+    if (methodEPSGCode ==
+            EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC ||
+        methodEPSGCode ==
+            EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D) {
+        positionVectorConvention = false;
+        sevenParamsTransform = true;
+        fullMatrix = true;
+    } else if ((paramCount == 7 && isCoordinateFrame && !l_isTimeDependent) ||
+               methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC ||
+               methodEPSGCode ==
+                   EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D ||
+               methodEPSGCode ==
+                   EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) {
         positionVectorConvention = false;
         sevenParamsTransform = true;
     } else if (
@@ -3558,6 +3568,8 @@ bool SingleOperation::exportToPROJStringGeneric(
         }
 
         formatter->addStep("helmert");
+        if (fullMatrix)
+            formatter->addParam("exact");
         formatter->addParam("x", x);
         formatter->addParam("y", y);
         formatter->addParam("z", z);
diff --git a/src/iso19111/operation/transformation.cpp b/src/iso19111/operation/transformation.cpp
index ce53cf88bf..ab3af1f99b 100644
--- a/src/iso19111/operation/transformation.cpp
+++ b/src/iso19111/operation/transformation.cpp
@@ -206,7 +206,11 @@ std::vector<double> Transformation::getTOWGS84Parameters(
     if ((paramCount == 7 &&
          ci_find(methodName, "Coordinate Frame") != std::string::npos) ||
         methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC ||
+        methodEPSGCode ==
+            EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC ||
         methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D ||
+        methodEPSGCode ==
+            EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D ||
         methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) {
         sevenParamsTransform = true;
         invertRotSigns = true;
@@ -1407,7 +1411,11 @@ createApproximateInverseIfPossible(const Transformation *op) {
     if ((paramCount == 7 && isCoordinateFrame &&
          !isTimeDependent(methodName)) ||
         methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC ||
+        methodEPSGCode ==
+            EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC ||
         methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D ||
+        methodEPSGCode ==
+            EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D ||
         methodEPSGCode == EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D) {
         sevenParamsTransform = true;
     } else if (
diff --git a/src/proj_constants.h b/src/proj_constants.h
index dba70c8375..e64ec76540 100644
--- a/src/proj_constants.h
+++ b/src/proj_constants.h
@@ -410,10 +410,18 @@
     "Coordinate Frame rotation (geocentric domain)"
 #define EPSG_CODE_METHOD_COORDINATE_FRAME_GEOCENTRIC 1032
 
+#define EPSG_NAME_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC               \
+    "Coordinate Frame rotation full matrix (geocen)"
+#define EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOCENTRIC 1132
+
 #define EPSG_NAME_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D                        \
     "Coordinate Frame rotation (geog2D domain)"
 #define EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_2D 9607
 
+#define EPSG_NAME_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D            \
+    "Coordinate Frame rotation full matrix (geog2D)"
+#define EPSG_CODE_METHOD_COORDINATE_FRAME_FULL_MATRIX_GEOGRAPHIC_2D 1133
+
 #define EPSG_NAME_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D                        \
     "Coordinate Frame rotation (geog3D domain)"
 #define EPSG_CODE_METHOD_COORDINATE_FRAME_GEOGRAPHIC_3D 1038


Test patch:
diff --git a/test/unit/test_operation.cpp b/test/unit/test_operation.cpp
index 4d73c66165..f2326760ba 100644
--- a/test/unit/test_operation.cpp
+++ b/test/unit/test_operation.cpp
@@ -6059,3 +6059,108 @@ TEST(operation, inverse_of_Geographic3DToGravityRelatedHeight) {
                   PROJStringFormatter::create().get()),
               "+proj=pipeline +step +inv +proj=affine +zoff=10");
 }
+
+// ---------------------------------------------------------------------------
+
+TEST(operation, CoordinateFrameRotationFullMatrixGeog2D) {
+
+    auto wkt =
+        "COORDINATEOPERATION[\"Saba to WGS 84 (1)\",\n"
+        "    VERSION[\"IOGP-Bes Saba\"],\n"
+        "    SOURCECRS[\n"
+        "        GEOGCRS[\"Saba\",\n"
+        "            DATUM[\"Saba\",\n"
+        "                ELLIPSOID[\"International 1924\",6378388,297,\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"
+        "            ID[\"EPSG\",10636]]],\n"
+        "    TARGETCRS[\n"
+        "        GEOGCRS[\"WGS 84\",\n"
+        "            ENSEMBLE[\"World Geodetic System 1984 ensemble\",\n"
+        "                MEMBER[\"World Geodetic System 1984 (Transit)\"],\n"
+        "                MEMBER[\"World Geodetic System 1984 (G730)\"],\n"
+        "                MEMBER[\"World Geodetic System 1984 (G873)\"],\n"
+        "                MEMBER[\"World Geodetic System 1984 (G1150)\"],\n"
+        "                MEMBER[\"World Geodetic System 1984 (G1674)\"],\n"
+        "                MEMBER[\"World Geodetic System 1984 (G1762)\"],\n"
+        "                MEMBER[\"World Geodetic System 1984 (G2139)\"],\n"
+        "                MEMBER[\"World Geodetic System 1984 (G2296)\"],\n"
+        "                ELLIPSOID[\"WGS 84\",6378137,298.257223563,\n"
+        "                    LENGTHUNIT[\"metre\",1]],\n"
+        "                ENSEMBLEACCURACY[2.0]],\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"
+        "            ID[\"EPSG\",4326]]],\n"
+        "    METHOD[\"Coordinate Frame rotation full matrix (geog2D)\",\n"
+        "        ID[\"EPSG\",1133]],\n"
+        "    PARAMETER[\"X-axis translation\",1138.7432,\n"
+        "        LENGTHUNIT[\"metre\",1],\n"
+        "        ID[\"EPSG\",8605]],\n"
+        "    PARAMETER[\"Y-axis translation\",-2064.4761,\n"
+        "        LENGTHUNIT[\"metre\",1],\n"
+        "        ID[\"EPSG\",8606]],\n"
+        "    PARAMETER[\"Z-axis translation\",110.7016,\n"
+        "        LENGTHUNIT[\"metre\",1],\n"
+        "        ID[\"EPSG\",8607]],\n"
+        "    PARAMETER[\"X-axis rotation\",-214.615206,\n"
+        "        ANGLEUNIT[\"arc-second\",4.84813681109536E-06],\n"
+        "        ID[\"EPSG\",8608]],\n"
+        "    PARAMETER[\"Y-axis rotation\",479.360036,\n"
+        "        ANGLEUNIT[\"arc-second\",4.84813681109536E-06],\n"
+        "        ID[\"EPSG\",8609]],\n"
+        "    PARAMETER[\"Z-axis rotation\",-164.703951,\n"
+        "        ANGLEUNIT[\"arc-second\",4.84813681109536E-06],\n"
+        "        ID[\"EPSG\",8610]],\n"
+        "    PARAMETER[\"Scale difference\",-402.32073,\n"
+        "        SCALEUNIT[\"parts per million\",1E-06],\n"
+        "        ID[\"EPSG\",8611]],\n"
+        "    OPERATIONACCURACY[1.0]]";
+
+    auto obj = WKTParser().createFromWKT(wkt);
+    auto transf = nn_dynamic_pointer_cast<Transformation>(obj);
+    ASSERT_TRUE(transf != nullptr);
+    EXPECT_EQ(transf->exportToPROJString(PROJStringFormatter::create().get()),
+              "+proj=pipeline "
+              "+step +proj=axisswap +order=2,1 "
+              "+step +proj=unitconvert +xy_in=deg +xy_out=rad "
+              "+step +proj=push +v_3 "
+              "+step +proj=cart +ellps=intl "
+              "+step +proj=helmert +exact "
+              "+x=1138.7432 +y=-2064.4761 +z=110.7016 "
+              "+rx=-214.615206 +ry=479.360036 +rz=-164.703951 +s=-402.32073 "
+              "+convention=coordinate_frame "
+              "+step +inv +proj=cart +ellps=WGS84 "
+              "+step +proj=pop +v_3 "
+              "+step +proj=unitconvert +xy_in=rad +xy_out=deg "
+              "+step +proj=axisswap +order=2,1");
+    EXPECT_EQ(transf->inverse()->exportToPROJString(
+                  PROJStringFormatter::create().get()),
+              "+proj=pipeline "
+              "+step +proj=axisswap +order=2,1 "
+              "+step +proj=unitconvert +xy_in=deg +xy_out=rad "
+              "+step +proj=push +v_3 "
+              "+step +proj=cart +ellps=WGS84 "
+              "+step +inv +proj=helmert +exact "
+              "+x=1138.7432 +y=-2064.4761 +z=110.7016 "
+              "+rx=-214.615206 +ry=479.360036 +rz=-164.703951 +s=-402.32073 "
+              "+convention=coordinate_frame "
+              "+step +inv +proj=cart +ellps=intl "
+              "+step +proj=pop +v_3 "
+              "+step +proj=unitconvert +xy_in=rad +xy_out=deg "
+              "+step +proj=axisswap +order=2,1");
+}