aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/windows.yml3
-rw-r--r--cmake/ProjTest.cmake1
-rw-r--r--cmake/ProjUtilities.cmake2
-rw-r--r--data/sql/alias_name.sql1
-rw-r--r--data/sql/conversion.sql2
-rw-r--r--data/sql/metadata.sql4
-rw-r--r--data/sql/projected_crs.sql2
-rw-r--r--docs/source/development/cmake.rst15
-rw-r--r--man/CMakeLists.txt2
-rw-r--r--src/iso19111/factory.cpp21
-rw-r--r--src/iso19111/io.cpp15
-rw-r--r--src/iso19111/operation/concatenatedoperation.cpp24
-rw-r--r--src/iso19111/operation/projbasedoperation.cpp16
-rw-r--r--src/projections/moll.cpp2
-rw-r--r--test/gie/builtins.gie12
-rw-r--r--test/gie/nkg.gie6
-rw-r--r--test/unit/test_io.cpp195
-rw-r--r--test/unit/test_operationfactory.cpp63
18 files changed, 339 insertions, 47 deletions
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index 10c83759..9600875c 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -45,7 +45,8 @@ jobs:
run: |
set VCPKG_INSTALLED=c:\vcpkg\installed\${{ env.ARCH }}-windows
dir %VCPKG_INSTALLED%\bin
- set PATH=%VCPKG_INSTALLED%\bin;%PATH%
+ dir %VCPKG_INSTALLED%\tools
+ set PATH=%VCPKG_INSTALLED%\bin;%VCPKG_INSTALLED%\tools;%PATH%
set PROJ_BUILD=%GITHUB_WORKSPACE%\build
mkdir %PROJ_BUILD%
cd %PROJ_BUILD%
diff --git a/cmake/ProjTest.cmake b/cmake/ProjTest.cmake
index c632d539..9801fda1 100644
--- a/cmake/ProjTest.cmake
+++ b/cmake/ProjTest.cmake
@@ -39,6 +39,7 @@ endfunction()
# Create user writable directory for tests
add_custom_target(create_tmp_user_writable_dir ALL
+ COMMAND ${CMAKE_COMMAND} -E remove_directory ${PROJ_BINARY_DIR}/tmp_user_writable_dir
COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJ_BINARY_DIR}/tmp_user_writable_dir)
function(proj_add_gie_network_dependent_test TESTNAME TESTCASE)
diff --git a/cmake/ProjUtilities.cmake b/cmake/ProjUtilities.cmake
index 690336b2..f0853fe3 100644
--- a/cmake/ProjUtilities.cmake
+++ b/cmake/ProjUtilities.cmake
@@ -98,7 +98,7 @@ function(configure_proj_pc)
list(APPEND EXTRA_LIBS -lole32 -lshell32)
else()
set(cxx_libs "${CMAKE_CXX_IMPLICIT_LINK_LIBRARIES}")
- list(REMOVE_ITEM cxx_libs ${CMAKE_C_IMPLICIT_LINK_LIBRARIES})
+ list(REMOVE_ITEM cxx_libs ${CMAKE_C_IMPLICIT_LINK_LIBRARIES} nonempty)
foreach(lib IN LISTS cxx_libs)
list(APPEND EXTRA_LIBS "-l${lib}")
endforeach()
diff --git a/data/sql/alias_name.sql b/data/sql/alias_name.sql
index 68aaf79a..1ad3048d 100644
--- a/data/sql/alias_name.sql
+++ b/data/sql/alias_name.sql
@@ -7515,3 +7515,4 @@ INSERT INTO "alias_name" VALUES('projected_crs','EPSG','9943','ETRS89 / EBBWV14
INSERT INTO "alias_name" VALUES('compound_crs','EPSG','9944','ETRS89 / EBBWV14 SnakeGrid + Newlyn height','EPSG');
INSERT INTO "alias_name" VALUES('compound_crs','EPSG','6190','BD72 / Belgian Lambert 72 + Oostende height','EPSG');
INSERT INTO "alias_name" VALUES('compound_crs','EPSG','9907','ETRS89 + Oostende height','EPSG');
+INSERT INTO "alias_name" VALUES('projected_crs','EPSG','32159','NAD83 / WyLam','EPSG');
diff --git a/data/sql/conversion.sql b/data/sql/conversion.sql
index bc7081fa..ac7cb142 100644
--- a/data/sql/conversion.sql
+++ b/data/sql/conversion.sql
@@ -2303,6 +2303,8 @@ INSERT INTO "conversion" VALUES('EPSG','14903','Wyoming CS27 West Central zone',
INSERT INTO "usage" VALUES('EPSG','11344','conversion','EPSG','14903','EPSG','2272','EPSG','1142');
INSERT INTO "conversion" VALUES('EPSG','14904','Wyoming CS27 West zone','','EPSG','9807','Transverse Mercator','EPSG','8801','Latitude of natural origin',40.4,'EPSG','9110','EPSG','8802','Longitude of natural origin',-110.05,'EPSG','9110','EPSG','8805','Scale factor at natural origin',0.999941177,'EPSG','9201','EPSG','8806','False easting',500000.0,'EPSG','9003','EPSG','8807','False northing',0.0,'EPSG','9003',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0);
INSERT INTO "usage" VALUES('EPSG','11345','conversion','EPSG','14904','EPSG','2271','EPSG','1142');
+INSERT INTO "conversion" VALUES('EPSG','14930','Wyoming Lambert','Source originally defining the projection is unclear - possibly the Wyoming GIS Center.','EPSG','9802','Lambert Conic Conformal (2SP)','EPSG','8821','Latitude of false origin',41.0,'EPSG','9110','EPSG','8822','Longitude of false origin',-107.3,'EPSG','9110','EPSG','8823','Latitude of 1st standard parallel',41.0,'EPSG','9110','EPSG','8824','Latitude of 2nd standard parallel',45.0,'EPSG','9110','EPSG','8826','Easting at false origin',500000.0,'EPSG','9001','EPSG','8827','Northing at false origin',200000.0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,0);
+INSERT INTO "usage" VALUES('EPSG','17408','conversion','EPSG','14930','EPSG','1419','EPSG','1135');
INSERT INTO "conversion" VALUES('EPSG','14931','SPCS83 Wyoming East zone (meters)','See code 14935 for equivalent non-metric definition.','EPSG','9807','Transverse Mercator','EPSG','8801','Latitude of natural origin',40.3,'EPSG','9110','EPSG','8802','Longitude of natural origin',-105.1,'EPSG','9110','EPSG','8805','Scale factor at natural origin',0.9999375,'EPSG','9201','EPSG','8806','False easting',200000.0,'EPSG','9001','EPSG','8807','False northing',0.0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0);
INSERT INTO "usage" VALUES('EPSG','11346','conversion','EPSG','14931','EPSG','2269','EPSG','1142');
INSERT INTO "conversion" VALUES('EPSG','14932','SPCS83 Wyoming East Central zone (meters)','See code 14936 for equivalent non-metric definition.','EPSG','9807','Transverse Mercator','EPSG','8801','Latitude of natural origin',40.3,'EPSG','9110','EPSG','8802','Longitude of natural origin',-107.2,'EPSG','9110','EPSG','8805','Scale factor at natural origin',0.9999375,'EPSG','9201','EPSG','8806','False easting',400000.0,'EPSG','9001','EPSG','8807','False northing',100000.0,'EPSG','9001',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,0);
diff --git a/data/sql/metadata.sql b/data/sql/metadata.sql
index 112d6a0c..e5f14cea 100644
--- a/data/sql/metadata.sql
+++ b/data/sql/metadata.sql
@@ -9,8 +9,8 @@
INSERT INTO "metadata" VALUES('DATABASE.LAYOUT.VERSION.MAJOR', 1);
INSERT INTO "metadata" VALUES('DATABASE.LAYOUT.VERSION.MINOR', 2);
-INSERT INTO "metadata" VALUES('EPSG.VERSION', 'v10.054');
-INSERT INTO "metadata" VALUES('EPSG.DATE', '2022-02-13');
+INSERT INTO "metadata" VALUES('EPSG.VERSION', 'v10.055');
+INSERT INTO "metadata" VALUES('EPSG.DATE', '2022-03-09');
-- The value of ${PROJ_VERSION} is substituted at build time by the actual
-- value.
diff --git a/data/sql/projected_crs.sql b/data/sql/projected_crs.sql
index 18d96dea..c436fef9 100644
--- a/data/sql/projected_crs.sql
+++ b/data/sql/projected_crs.sql
@@ -9925,6 +9925,8 @@ INSERT INTO "projected_crs" VALUES('EPSG','32157','NAD83 / Wyoming West Central'
INSERT INTO "usage" VALUES('EPSG','7244','projected_crs','EPSG','32157','EPSG','2272','EPSG','1142');
INSERT INTO "projected_crs" VALUES('EPSG','32158','NAD83 / Wyoming West',NULL,'EPSG','4499','EPSG','4269','EPSG','14934',NULL,0);
INSERT INTO "usage" VALUES('EPSG','7245','projected_crs','EPSG','32158','EPSG','2271','EPSG','1142');
+INSERT INTO "projected_crs" VALUES('EPSG','32159','NAD83 / Wyoming Lambert',NULL,'EPSG','4499','EPSG','4269','EPSG','14930',NULL,0);
+INSERT INTO "usage" VALUES('EPSG','17409','projected_crs','EPSG','32159','EPSG','1419','EPSG','1135');
INSERT INTO "projected_crs" VALUES('EPSG','32161','NAD83 / Puerto Rico & Virgin Is.',NULL,'EPSG','4499','EPSG','4269','EPSG','15230',NULL,0);
INSERT INTO "usage" VALUES('EPSG','7246','projected_crs','EPSG','32161','EPSG','2251','EPSG','1142');
INSERT INTO "projected_crs" VALUES('EPSG','32164','NAD83 / BLM 14N (ftUS)',NULL,'EPSG','4497','EPSG','4269','EPSG','15914',NULL,0);
diff --git a/docs/source/development/cmake.rst b/docs/source/development/cmake.rst
index 477b4f3b..d1bbf42c 100644
--- a/docs/source/development/cmake.rst
+++ b/docs/source/development/cmake.rst
@@ -5,23 +5,18 @@ Using PROJ in CMake projects
********************************************************************************
The recommended way to use the PROJ library in a CMake project is to
-link to the imported library target ``${PROJ_LIBRARIES}`` provided by
+link to the imported library target ``PROJ::proj`` provided by
the CMake configuration which comes with the library. Typical usage is:
.. code::
- find_package(PROJ)
+ find_package(PROJ CONFIG REQUIRED)
- target_link_libraries(MyApp PRIVATE ${PROJ_LIBRARIES})
+ target_link_libraries(MyApp PRIVATE PROJ::proj)
-By adding the imported library target ``${PROJ_LIBRARIES}`` to the
+By adding the imported library target ``PROJ::proj`` to the
target link libraries, CMake will also pass the include directories to
-the compiler. This requires that you use CMake version 2.8.11 or later.
-If you are using an older version of CMake, then add
-
-.. code::
-
- include_directories(${PROJ_INCLUDE_DIRS})
+the compiler.
The CMake command ``find_package`` will look for the configuration in a
number of places. The lookup can be adjusted for all packages by setting
diff --git a/man/CMakeLists.txt b/man/CMakeLists.txt
index 031cde04..a27429a4 100644
--- a/man/CMakeLists.txt
+++ b/man/CMakeLists.txt
@@ -6,4 +6,4 @@ install(FILES
man1/gie.1
man1/projinfo.1
man1/projsync.1
- DESTINATION share/man/man1)
+ DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
diff --git a/src/iso19111/factory.cpp b/src/iso19111/factory.cpp
index 52c3f82b..a5162ab2 100644
--- a/src/iso19111/factory.cpp
+++ b/src/iso19111/factory.cpp
@@ -252,7 +252,7 @@ class SQLiteHandle {
}
// cppcheck-suppress functionStatic
- void registerFunctions();
+ void initialize();
SQLResultSet run(const std::string &sql,
const ListOfParams &parameters = ListOfParams(),
@@ -344,7 +344,7 @@ std::shared_ptr<SQLiteHandle> SQLiteHandle::open(PJ_CONTEXT *ctx,
#ifdef ENABLE_CUSTOM_LOCKLESS_VFS
handle->vfs_ = std::move(vfs);
#endif
- handle->registerFunctions();
+ handle->initialize();
handle->checkDatabaseLayout(path, path, std::string());
return handle;
}
@@ -359,7 +359,7 @@ SQLiteHandle::initFromExisting(sqlite3 *sqlite_handle, bool close_handle,
new SQLiteHandle(sqlite_handle, close_handle));
handle->nLayoutVersionMajor_ = nLayoutVersionMajor;
handle->nLayoutVersionMinor_ = nLayoutVersionMinor;
- handle->registerFunctions();
+ handle->initialize();
return handle;
}
@@ -370,7 +370,7 @@ SQLiteHandle::initFromExistingUniquePtr(sqlite3 *sqlite_handle,
bool close_handle) {
auto handle = std::unique_ptr<SQLiteHandle>(
new SQLiteHandle(sqlite_handle, close_handle));
- handle->registerFunctions();
+ handle->initialize();
return handle;
}
@@ -547,7 +547,18 @@ void SQLiteHandle::checkDatabaseLayout(const std::string &mainDbPath,
#define SQLITE_DETERMINISTIC 0
#endif
-void SQLiteHandle::registerFunctions() {
+void SQLiteHandle::initialize() {
+
+ // There is a bug in sqlite 3.38.0 with some complex queries.
+ // Cf https://github.com/OSGeo/PROJ/issues/3077
+ // Disabling Bloom-filter pull-down optimization as suggested in
+ // https://sqlite.org/forum/forumpost/7d3a75438c
+ const int sqlite3VersionNumber = sqlite3_libversion_number();
+ if (sqlite3VersionNumber == 3 * 1000000 + 38 * 1000) {
+ sqlite3_test_control(SQLITE_TESTCTRL_OPTIMIZATIONS, sqlite_handle_,
+ 0x100000);
+ }
+
sqlite3_create_function(sqlite_handle_, "pseudo_area_from_swne", 4,
SQLITE_UTF8 | SQLITE_DETERMINISTIC, nullptr,
PROJ_SQLITE_pseudo_area_from_swne, nullptr,
diff --git a/src/iso19111/io.cpp b/src/iso19111/io.cpp
index b7f26fe5..d4c6aec1 100644
--- a/src/iso19111/io.cpp
+++ b/src/iso19111/io.cpp
@@ -4341,10 +4341,10 @@ createBoundCRSSourceTransformationCRS(const crs::CRSPtr &sourceCRS,
sourceCRS->extractGeographicCRS();
sourceTransformationCRS = sourceGeographicCRS;
if (sourceGeographicCRS) {
- if (sourceGeographicCRS->datum() != nullptr &&
- sourceGeographicCRS->primeMeridian()
- ->longitude()
- .getSIValue() != 0.0) {
+ const auto &sourceDatum = sourceGeographicCRS->datum();
+ if (sourceDatum != nullptr && sourceGeographicCRS->primeMeridian()
+ ->longitude()
+ .getSIValue() != 0.0) {
sourceTransformationCRS =
GeographicCRS::create(
util::PropertyMap().set(
@@ -4354,13 +4354,12 @@ createBoundCRSSourceTransformationCRS(const crs::CRSPtr &sourceCRS,
datum::GeodeticReferenceFrame::create(
util::PropertyMap().set(
common::IdentifiedObject::NAME_KEY,
- sourceGeographicCRS->datum()->nameStr() +
+ sourceDatum->nameStr() +
" (with Greenwich prime meridian)"),
- sourceGeographicCRS->datum()->ellipsoid(),
+ sourceDatum->ellipsoid(),
util::optional<std::string>(),
datum::PrimeMeridian::GREENWICH),
- cs::EllipsoidalCS::createLatitudeLongitude(
- common::UnitOfMeasure::DEGREE))
+ sourceGeographicCRS->coordinateSystem())
.as_nullable();
}
} else {
diff --git a/src/iso19111/operation/concatenatedoperation.cpp b/src/iso19111/operation/concatenatedoperation.cpp
index 7da561b4..e5728c4c 100644
--- a/src/iso19111/operation/concatenatedoperation.cpp
+++ b/src/iso19111/operation/concatenatedoperation.cpp
@@ -276,14 +276,28 @@ void ConcatenatedOperation::fixStepsDirection(
}
}
+ const auto extractDerivedCRS =
+ [](const crs::CRS *crs) -> const crs::DerivedCRS * {
+ auto derivedCRS = dynamic_cast<const crs::DerivedCRS *>(crs);
+ if (derivedCRS)
+ return derivedCRS;
+ auto compoundCRS = dynamic_cast<const crs::CompoundCRS *>(crs);
+ if (compoundCRS) {
+ derivedCRS = dynamic_cast<const crs::DerivedCRS *>(
+ compoundCRS->componentReferenceSystems().front().get());
+ if (derivedCRS)
+ return derivedCRS;
+ }
+ return nullptr;
+ };
+
for (size_t i = 0; i < operationsInOut.size(); ++i) {
auto &op = operationsInOut[i];
auto l_sourceCRS = op->sourceCRS();
auto l_targetCRS = op->targetCRS();
auto conv = dynamic_cast<const Conversion *>(op.get());
if (conv && i == 0 && !l_sourceCRS && !l_targetCRS) {
- if (auto derivedCRS = dynamic_cast<const crs::DerivedCRS *>(
- concatOpSourceCRS.get())) {
+ if (auto derivedCRS = extractDerivedCRS(concatOpSourceCRS.get())) {
if (i + 1 < operationsInOut.size()) {
// use the sourceCRS of the next operation as our target CRS
l_targetCRS = operationsInOut[i + 1]->sourceCRS();
@@ -323,8 +337,7 @@ void ConcatenatedOperation::fixStepsDirection(
}
} else if (conv && i + 1 == operationsInOut.size() && !l_sourceCRS &&
!l_targetCRS) {
- auto derivedCRS =
- dynamic_cast<const crs::DerivedCRS *>(concatOpTargetCRS.get());
+ auto derivedCRS = extractDerivedCRS(concatOpTargetCRS.get());
if (derivedCRS) {
if (i >= 1) {
// use the sourceCRS of the previous operation as our source
@@ -350,8 +363,7 @@ void ConcatenatedOperation::fixStepsDirection(
} else if (i >= 1) {
l_sourceCRS = operationsInOut[i - 1]->targetCRS();
if (l_sourceCRS) {
- derivedCRS = dynamic_cast<const crs::DerivedCRS *>(
- l_sourceCRS.get());
+ derivedCRS = extractDerivedCRS(l_sourceCRS.get());
if (derivedCRS &&
conv->isEquivalentTo(
derivedCRS->derivingConversion().get(),
diff --git a/src/iso19111/operation/projbasedoperation.cpp b/src/iso19111/operation/projbasedoperation.cpp
index 6e0fd109..fd03fc09 100644
--- a/src/iso19111/operation/projbasedoperation.cpp
+++ b/src/iso19111/operation/projbasedoperation.cpp
@@ -232,15 +232,13 @@ void PROJBasedOperation::_exportToJSON(
method()->_exportToJSON(formatter);
const auto &l_parameterValues = parameterValues();
- if (!l_parameterValues.empty()) {
- writer->AddObjKey("parameters");
- {
- auto parametersContext(writer->MakeArrayContext(false));
- for (const auto &genOpParamvalue : l_parameterValues) {
- formatter->setAllowIDInImmediateChild();
- formatter->setOmitTypeInImmediateChild();
- genOpParamvalue->_exportToJSON(formatter);
- }
+ writer->AddObjKey("parameters");
+ {
+ auto parametersContext(writer->MakeArrayContext(false));
+ for (const auto &genOpParamvalue : l_parameterValues) {
+ formatter->setAllowIDInImmediateChild();
+ formatter->setOmitTypeInImmediateChild();
+ genOpParamvalue->_exportToJSON(formatter);
}
}
}
diff --git a/src/projections/moll.cpp b/src/projections/moll.cpp
index 87b38336..f8b52a5f 100644
--- a/src/projections/moll.cpp
+++ b/src/projections/moll.cpp
@@ -10,7 +10,7 @@ PROJ_HEAD(moll, "Mollweide") "\n\tPCyl, Sph";
PROJ_HEAD(wag4, "Wagner IV") "\n\tPCyl, Sph";
PROJ_HEAD(wag5, "Wagner V") "\n\tPCyl, Sph";
-#define MAX_ITER 10
+#define MAX_ITER 30
#define LOOP_TOL 1e-7
namespace { // anonymous namespace
diff --git a/test/gie/builtins.gie b/test/gie/builtins.gie
index 1a290294..38dfc6a2 100644
--- a/test/gie/builtins.gie
+++ b/test/gie/builtins.gie
@@ -3599,6 +3599,18 @@ roundtrip 1
accept -2 -1
expect -201113.698641813 -124066.283433860
roundtrip 1
+accept 0.0 89.99
+expect 0.00 9050917.562466157600
+roundtrip 1
+accept 0.0 89.999
+expect 0.00 9050964.513822982088
+roundtrip 1
+accept 0.0 -89.99
+expect 0.0 -9050917.562466157600
+roundtrip 1
+accept 0.0 -89.999
+expect 0.00 -9050964.513822982088
+roundtrip 1
direction inverse
accept 200 100
diff --git a/test/gie/nkg.gie b/test/gie/nkg.gie
index 5ea476cf..678b4680 100644
--- a/test/gie/nkg.gie
+++ b/test/gie/nkg.gie
@@ -208,15 +208,15 @@ tolerance 0.1 mm
# STAS
accept 3275753.4135 321111.2481 5445042.2134 2020.0
-expect 3275753.9111 321110.8658 5445041.8822 2020.0
+expect 3275753.9094 321110.8626 5445041.8818 2020.0
# BOD3
accept 2391773.9918 615615.1837 5860966.1279 2020.0
-expect 2391774.5535 615614.9100 5860965.8138 2020.0
+expect 2391774.5481 615614.9063 5860965.8185 2020.0
# KAUS
accept 2107888.9134 895603.4769 5933242.6269 2020.0
-expect 2107889.5097 895603.2085 5933242.3152 2020.0
+expect 2107889.5014 895603.2055 5933242.3208 2020.0
# -------------------------------------------------------------------------------
diff --git a/test/unit/test_io.cpp b/test/unit/test_io.cpp
index 69cab65c..f883ddb7 100644
--- a/test/unit/test_io.cpp
+++ b/test/unit/test_io.cpp
@@ -3827,6 +3827,201 @@ TEST(
// ---------------------------------------------------------------------------
+TEST(wkt_parse, CONCATENATEDOPERATION_with_inverse_conversion_of_compound) {
+
+ auto wkt =
+ "CONCATENATEDOPERATION[\"Inverse of RD New + Amersfoort to ETRS89 (9) "
+ "+ Inverse of ETRS89 to NAP height (2) + ETRS89 to WGS 84 (1)\",\n"
+ " SOURCECRS[\n"
+ " COMPOUNDCRS[\"Amersfoort / RD New + NAP height\",\n"
+ " PROJCRS[\"Amersfoort / RD New\",\n"
+ " BASEGEOGCRS[\"Amersfoort\",\n"
+ " DATUM[\"Amersfoort\",\n"
+ " ELLIPSOID[\"Bessel "
+ "1841\",6377397.155,299.1528128,\n"
+ " LENGTHUNIT[\"metre\",1]]],\n"
+ " PRIMEM[\"Greenwich\",0,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433]],\n"
+ " ID[\"EPSG\",4289]],\n"
+ " CONVERSION[\"RD New\",\n"
+ " METHOD[\"Oblique Stereographic\",\n"
+ " ID[\"EPSG\",9809]],\n"
+ " PARAMETER[\"Latitude of natural "
+ "origin\",52.1561605555556,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433],\n"
+ " ID[\"EPSG\",8801]],\n"
+ " PARAMETER[\"Longitude of natural "
+ "origin\",5.38763888888889,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433],\n"
+ " ID[\"EPSG\",8802]],\n"
+ " PARAMETER[\"Scale factor at natural "
+ "origin\",0.9999079,\n"
+ " SCALEUNIT[\"unity\",1],\n"
+ " ID[\"EPSG\",8805]],\n"
+ " PARAMETER[\"False easting\",155000,\n"
+ " LENGTHUNIT[\"metre\",1],\n"
+ " ID[\"EPSG\",8806]],\n"
+ " PARAMETER[\"False northing\",463000,\n"
+ " LENGTHUNIT[\"metre\",1],\n"
+ " ID[\"EPSG\",8807]]],\n"
+ " CS[Cartesian,2],\n"
+ " AXIS[\"easting (X)\",east,\n"
+ " ORDER[1],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " AXIS[\"northing (Y)\",north,\n"
+ " ORDER[2],\n"
+ " LENGTHUNIT[\"metre\",1]]],\n"
+ " VERTCRS[\"NAP height\",\n"
+ " VDATUM[\"Normaal Amsterdams Peil\"],\n"
+ " CS[vertical,1],\n"
+ " AXIS[\"gravity-related height (H)\",up,\n"
+ " LENGTHUNIT[\"metre\",1]]],\n"
+ " ID[\"EPSG\",7415]]],\n"
+ " TARGETCRS[\n"
+ " GEOGCRS[\"WGS 84 (3D)\",\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"
+ " 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,3],\n"
+ " AXIS[\"geodetic latitude (Lat)\",north,\n"
+ " ORDER[1],\n"
+ " ANGLEUNIT[\"degree minute second "
+ "hemisphere\",0.0174532925199433]],\n"
+ " AXIS[\"geodetic longitude (Long)\",east,\n"
+ " ORDER[2],\n"
+ " ANGLEUNIT[\"degree minute second "
+ "hemisphere\",0.0174532925199433]],\n"
+ " AXIS[\"ellipsoidal height (h)\",up,\n"
+ " ORDER[3],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " ID[\"EPSG\",4329]]],\n"
+ " STEP[\n"
+ " CONVERSION[\"Inverse of RD New\",\n"
+ " METHOD[\"Inverse of Oblique Stereographic\",\n"
+ " ID[\"INVERSE(EPSG)\",9809]],\n"
+ " PARAMETER[\"Latitude of natural "
+ "origin\",52.1561605555556,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433],\n"
+ " ID[\"EPSG\",8801]],\n"
+ " PARAMETER[\"Longitude of natural "
+ "origin\",5.38763888888889,\n"
+ " ANGLEUNIT[\"degree\",0.0174532925199433],\n"
+ " ID[\"EPSG\",8802]],\n"
+ " PARAMETER[\"Scale factor at natural origin\",0.9999079,\n"
+ " SCALEUNIT[\"unity\",1],\n"
+ " ID[\"EPSG\",8805]],\n"
+ " PARAMETER[\"False easting\",155000,\n"
+ " LENGTHUNIT[\"metre\",1],\n"
+ " ID[\"EPSG\",8806]],\n"
+ " PARAMETER[\"False northing\",463000,\n"
+ " LENGTHUNIT[\"metre\",1],\n"
+ " ID[\"EPSG\",8807]],\n"
+ " ID[\"INVERSE(EPSG)\",19914]]],\n"
+ " STEP[\n"
+ " COORDINATEOPERATION[\"PROJ-based coordinate operation\",\n"
+ " SOURCECRS[\n"
+ " COMPOUNDCRS[\"Amersfoort + NAP height\",\n"
+ " GEOGCRS[\"Amersfoort\",\n"
+ " DATUM[\"Amersfoort\",\n"
+ " ELLIPSOID[\"Bessel "
+ "1841\",6377397.155,299.1528128,\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\",4289]],\n"
+ " VERTCRS[\"NAP height\",\n"
+ " VDATUM[\"Normaal Amsterdams Peil\"],\n"
+ " CS[vertical,1],\n"
+ " AXIS[\"gravity-related height (H)\",up,\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " ID[\"EPSG\",5709]]]],\n"
+ " TARGETCRS[\n"
+ " GEOGCRS[\"WGS 84 (3D)\",\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"
+ " 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,3],\n"
+ " AXIS[\"geodetic latitude (Lat)\",north,\n"
+ " ORDER[1],\n"
+ " ANGLEUNIT[\"degree minute second "
+ "hemisphere\",0.0174532925199433]],\n"
+ " AXIS[\"geodetic longitude (Long)\",east,\n"
+ " ORDER[2],\n"
+ " ANGLEUNIT[\"degree minute second "
+ "hemisphere\",0.0174532925199433]],\n"
+ " AXIS[\"ellipsoidal height (h)\",up,\n"
+ " ORDER[3],\n"
+ " LENGTHUNIT[\"metre\",1]],\n"
+ " ID[\"EPSG\",4329]]],\n"
+ " METHOD[\"PROJ-based operation method: +proj=pipeline "
+ "+step +proj=axisswap +order=2,1 +step +proj=unitconvert +xy_in=deg "
+ "+xy_out=rad +step +proj=hgridshift +grids=nl_nsgi_rdtrans2018.tif "
+ "+step +proj=vgridshift +grids=nl_nsgi_nlgeo2018.tif +multiplier=1 "
+ "+step +proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap "
+ "+order=2,1\"],\n"
+ " OPERATIONACCURACY[1.002]]],\n"
+ " USAGE[\n"
+ " SCOPE[\"unknown\"],\n"
+ " AREA[\"Netherlands - onshore, including Waddenzee, Dutch "
+ "Wadden Islands and 12-mile offshore coastal zone.\"],\n"
+ " BBOX[50.75,3.2,53.7,7.22]]]";
+
+ auto obj = WKTParser().createFromWKT(wkt);
+ auto concat = nn_dynamic_pointer_cast<ConcatenatedOperation>(obj);
+ ASSERT_TRUE(concat != nullptr);
+
+ EXPECT_EQ(concat->exportToPROJString(PROJStringFormatter::create().get()),
+ "+proj=pipeline "
+ "+step +inv +proj=sterea +lat_0=52.1561605555556 "
+ "+lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 "
+ "+ellps=bessel "
+ "+step +proj=hgridshift +grids=nl_nsgi_rdtrans2018.tif "
+ "+step +proj=vgridshift +grids=nl_nsgi_nlgeo2018.tif "
+ "+multiplier=1 "
+ "+step +proj=unitconvert +xy_in=rad +xy_out=deg "
+ "+step +proj=axisswap +order=2,1");
+}
+
+// ---------------------------------------------------------------------------
+
TEST(wkt_parse, BOUNDCRS_transformation_from_names) {
auto projcrs = ProjectedCRS::create(
diff --git a/test/unit/test_operationfactory.cpp b/test/unit/test_operationfactory.cpp
index e07f88d1..89c4e7c4 100644
--- a/test/unit/test_operationfactory.cpp
+++ b/test/unit/test_operationfactory.cpp
@@ -3002,6 +3002,69 @@ TEST(operation, nadgrids_with_pm) {
// ---------------------------------------------------------------------------
+TEST(operation, towgs84_pm_3d) {
+ // Test fix for https://github.com/OSGeo/gdal/issues/5408
+
+ auto dbContext = DatabaseContext::create();
+ auto authFactory = AuthorityFactory::create(dbContext, std::string());
+
+ auto objSrc = PROJStringParser().createFromPROJString(
+ "+proj=tmerc +lat_0=0 +lon_0=34 +k=1 +x_0=0 +y_0=-5000000 "
+ "+ellps=bessel +pm=ferro "
+ "+towgs84=1,2,3,4,5,6,7 "
+ "+units=m +no_defs +type=crs");
+ auto src = nn_dynamic_pointer_cast<CRS>(objSrc);
+ ASSERT_TRUE(src != nullptr);
+ auto src3D = src->promoteTo3D(std::string(), dbContext);
+
+ auto objDst = PROJStringParser().createFromPROJString(
+ "+proj=longlat +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +no_defs +type=crs");
+ auto dst = nn_dynamic_pointer_cast<CRS>(objDst);
+ ASSERT_TRUE(dst != nullptr);
+ auto dst3D = dst->promoteTo3D(std::string(), dbContext);
+
+ // Import thing to check is that there's no push/pop v_3
+ const std::string expected_pipeline =
+ "+proj=pipeline "
+ "+step +inv +proj=tmerc +lat_0=0 +lon_0=34 +k=1 +x_0=0 +y_0=-5000000 "
+ "+ellps=bessel +pm=ferro "
+ "+step +proj=cart +ellps=bessel "
+ "+step +proj=helmert +x=1 +y=2 +z=3 +rx=4 "
+ "+ry=5 +rz=6 +s=7 +convention=position_vector "
+ "+step +inv +proj=cart +ellps=GRS80 "
+ "+step +proj=unitconvert +xy_in=rad +z_in=m +xy_out=deg +z_out=m";
+
+ auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0);
+ {
+ auto list = CoordinateOperationFactory::create()->createOperations(
+ src3D, dst3D, ctxt);
+ ASSERT_EQ(list.size(), 1U);
+ EXPECT_EQ(
+ list[0]->exportToPROJString(PROJStringFormatter::create().get()),
+ expected_pipeline);
+ }
+
+ // Retry when creating objects from WKT
+ {
+ auto objSrcFromWkt = WKTParser().createFromWKT(src3D->exportToWKT(
+ WKTFormatter::create(WKTFormatter::Convention::WKT2_2019).get()));
+ auto srcFromWkt = nn_dynamic_pointer_cast<CRS>(objSrcFromWkt);
+ ASSERT_TRUE(srcFromWkt != nullptr);
+ auto objDstFromWkt = WKTParser().createFromWKT(dst3D->exportToWKT(
+ WKTFormatter::create(WKTFormatter::Convention::WKT2_2019).get()));
+ auto dstFromWkt = nn_dynamic_pointer_cast<CRS>(objDstFromWkt);
+ ASSERT_TRUE(dstFromWkt != nullptr);
+ auto list = CoordinateOperationFactory::create()->createOperations(
+ NN_NO_CHECK(srcFromWkt), NN_NO_CHECK(dstFromWkt), ctxt);
+ ASSERT_EQ(list.size(), 1U);
+ EXPECT_EQ(
+ list[0]->exportToPROJString(PROJStringFormatter::create().get()),
+ expected_pipeline);
+ }
+}
+
+// ---------------------------------------------------------------------------
+
TEST(operation, WGS84_G1762_to_compoundCRS_with_bound_vertCRS) {
auto authFactoryEPSG =
AuthorityFactory::create(DatabaseContext::create(), "EPSG");