Skip to content

Commit 78595a1

Browse files
committed
Add support for chaining vertical CRS transformations with intermediate vertical CRSs sharing a datum with the
source or target
1 parent c5c3a86 commit 78595a1

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

src/iso19111/operation/coordinateoperationfactory.cpp

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ struct CoordinateOperationFactory::Private {
560560
bool inCreateOperationsWithDatumPivotAntiRecursion = false;
561561
bool inCreateOperationsGeogToVertWithAlternativeGeog = false;
562562
bool inCreateOperationsGeogToVertWithIntermediateVert = false;
563+
bool inCreateOperationsVertToVertWithIntermediateVert = false;
563564
bool skipHorizontalTransformation = false;
564565
int nRecLevelCreateOperations = 0;
565566
std::map<std::pair<io::AuthorityFactory::ObjectType, std::string>,
@@ -649,6 +650,15 @@ struct CoordinateOperationFactory::Private {
649650
const crs::CRSNNPtr &sourceCRS, const crs::CRSNNPtr &targetCRS,
650651
Context &context);
651652

653+
static std::vector<CoordinateOperationNNPtr>
654+
createOperationsVertToVertWithIntermediateVert(
655+
const crs::CRSNNPtr &sourceCRS,
656+
const util::optional<common::DataEpoch> &sourceEpoch,
657+
const crs::CRSNNPtr &targetCRS,
658+
const util::optional<common::DataEpoch> &targetEpoch,
659+
const crs::VerticalCRS *vertSrc, const crs::VerticalCRS *vertDst,
660+
Context &context);
661+
652662
static void createOperationsFromDatabaseWithVertCRS(
653663
const crs::CRSNNPtr &sourceCRS,
654664
const util::optional<common::DataEpoch> &sourceEpoch,
@@ -4530,6 +4540,143 @@ std::vector<CoordinateOperationNNPtr> CoordinateOperationFactory::Private::
45304540

45314541
// ---------------------------------------------------------------------------
45324542

4543+
// When transforming between two vertical CRS with different datums, check
4544+
// if there are registered operations whose target (or source) is a different
4545+
// vertical CRS sharing the same datum as our target (or source). If so,
4546+
// chain: source → intermediate + intermediate → target (the latter being a
4547+
// simple unit/axis conversion handled by createOperationsVertToVert).
4548+
//
4549+
// Typical example: Baltic 1977 height (EPSG:5705) → Caspian depth (EPSG:5706).
4550+
// EPSG has an operation 5705 → 5611 (Caspian height). 5611 and 5706 share the
4551+
// same datum but differ only in axis direction. We compose:
4552+
// 5705 →(EPSG:5438)→ 5611 →(height-to-depth)→ 5706
4553+
std::vector<CoordinateOperationNNPtr> CoordinateOperationFactory::Private::
4554+
createOperationsVertToVertWithIntermediateVert(
4555+
const crs::CRSNNPtr &sourceCRS,
4556+
const util::optional<common::DataEpoch> &sourceEpoch,
4557+
const crs::CRSNNPtr &targetCRS,
4558+
const util::optional<common::DataEpoch> &targetEpoch,
4559+
const crs::VerticalCRS *vertSrc, const crs::VerticalCRS *vertDst,
4560+
Private::Context &context) {
4561+
4562+
ENTER_FUNCTION();
4563+
4564+
std::vector<CoordinateOperationNNPtr> res;
4565+
4566+
struct AntiRecursionGuard {
4567+
Context &context;
4568+
4569+
explicit AntiRecursionGuard(Context &contextIn) : context(contextIn) {
4570+
assert(!context.inCreateOperationsVertToVertWithIntermediateVert);
4571+
context.inCreateOperationsVertToVertWithIntermediateVert = true;
4572+
}
4573+
4574+
~AntiRecursionGuard() {
4575+
context.inCreateOperationsVertToVertWithIntermediateVert = false;
4576+
}
4577+
};
4578+
AntiRecursionGuard guard(context);
4579+
4580+
const auto &authFactory = context.context->getAuthorityFactory();
4581+
if (!authFactory)
4582+
return res;
4583+
const auto dbContext = authFactory->databaseContext().as_nullable();
4584+
4585+
// Strategy 1: pivot through candidates sharing the TARGET's datum.
4586+
// Find vertical CRSs on the same datum as the target, then look for
4587+
// operations from the source to each candidate (using the full
4588+
// createOperations machinery so multi-step chains are also found).
4589+
auto candidatesDst = findCandidateVertCRSForDatum(
4590+
authFactory, vertDst->datumNonNull(dbContext).get());
4591+
for (const auto &candidateVert : candidatesDst) {
4592+
if (candidateVert->_isEquivalentTo(
4593+
targetCRS.get(),
4594+
util::IComparable::Criterion::EQUIVALENT)) {
4595+
continue; // Skip the target itself — already tried by the caller.
4596+
}
4597+
auto opsFirst = createOperations(sourceCRS, sourceEpoch, candidateVert,
4598+
sourceEpoch, context);
4599+
if (!opsFirst.empty()) {
4600+
const auto opsSecond = createOperations(
4601+
candidateVert, sourceEpoch, targetCRS, targetEpoch, context);
4602+
if (!opsSecond.empty()) {
4603+
// The transformation from candidateVert to targetCRS should
4604+
// be just a unit/axis change typically, so take only the
4605+
// first one, which is likely/hopefully the only one.
4606+
for (const auto &opFirst : opsFirst) {
4607+
if (hasIdentifiers(opFirst)) {
4608+
if (candidateVert->_isEquivalentTo(
4609+
targetCRS.get(),
4610+
util::IComparable::Criterion::EQUIVALENT)) {
4611+
res.emplace_back(opFirst);
4612+
} else {
4613+
try {
4614+
res.emplace_back(
4615+
ConcatenatedOperation::
4616+
createComputeMetadata(
4617+
{opFirst, opsSecond.front()},
4618+
context
4619+
.disallowEmptyIntersection()));
4620+
} catch (
4621+
const InvalidOperationEmptyIntersection &) {
4622+
}
4623+
}
4624+
}
4625+
}
4626+
if (!res.empty())
4627+
return res;
4628+
}
4629+
}
4630+
}
4631+
4632+
// Strategy 2: pivot through candidates sharing the SOURCE's datum.
4633+
// Find vertical CRSs on the same datum as the source, then look for
4634+
// operations from each candidate to the target.
4635+
auto candidatesSrc = findCandidateVertCRSForDatum(
4636+
authFactory, vertSrc->datumNonNull(dbContext).get());
4637+
for (const auto &candidateVert : candidatesSrc) {
4638+
if (candidateVert->_isEquivalentTo(
4639+
sourceCRS.get(),
4640+
util::IComparable::Criterion::EQUIVALENT)) {
4641+
continue;
4642+
}
4643+
auto opsSecond = createOperations(candidateVert, sourceEpoch, targetCRS,
4644+
targetEpoch, context);
4645+
if (!opsSecond.empty()) {
4646+
const auto opsFirst = createOperations(
4647+
sourceCRS, sourceEpoch, candidateVert, sourceEpoch, context);
4648+
if (!opsFirst.empty()) {
4649+
for (const auto &opSecond : opsSecond) {
4650+
if (hasIdentifiers(opSecond)) {
4651+
if (candidateVert->_isEquivalentTo(
4652+
sourceCRS.get(),
4653+
util::IComparable::Criterion::EQUIVALENT)) {
4654+
res.emplace_back(opSecond);
4655+
} else {
4656+
try {
4657+
res.emplace_back(
4658+
ConcatenatedOperation::
4659+
createComputeMetadata(
4660+
{opsFirst.front(), opSecond},
4661+
context
4662+
.disallowEmptyIntersection()));
4663+
} catch (
4664+
const InvalidOperationEmptyIntersection &) {
4665+
}
4666+
}
4667+
}
4668+
}
4669+
if (!res.empty())
4670+
return res;
4671+
}
4672+
}
4673+
}
4674+
4675+
return res;
4676+
}
4677+
4678+
// ---------------------------------------------------------------------------
4679+
45334680
std::vector<CoordinateOperationNNPtr> CoordinateOperationFactory::Private::
45344681
createOperationsGeogToVertWithAlternativeGeog(
45354682
const crs::CRSNNPtr &sourceCRS, // geographic CRS
@@ -4663,6 +4810,19 @@ void CoordinateOperationFactory::Private::
46634810
res = applyInverse(res);
46644811
}
46654812

4813+
// Typically to transform between two vertical CRS with different datums
4814+
// (e.g. "Baltic 1977 height" to "Caspian depth") by pivoting through
4815+
// an intermediate vertical CRS sharing the target's (or source's) datum.
4816+
// This handles the case where a registered CT targets a height CRS but
4817+
// the user requests the corresponding depth CRS (or vice versa).
4818+
if (res.empty() &&
4819+
!context.inCreateOperationsVertToVertWithIntermediateVert && vertSrc &&
4820+
vertDst) {
4821+
res = createOperationsVertToVertWithIntermediateVert(
4822+
sourceCRS, sourceEpoch, targetCRS, targetEpoch, vertSrc, vertDst,
4823+
context);
4824+
}
4825+
46664826
// There's no direct transformation from NAVD88 height to WGS84,
46674827
// so try to research all transformations from NAVD88 to another
46684828
// intermediate GeographicCRS.

test/unit/test_operationfactory.cpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7275,6 +7275,34 @@ TEST(operation, vertCRS_to_vertCRS_New_Zealand_context) {
72757275

72767276
// ---------------------------------------------------------------------------
72777277

7278+
TEST(operation, vertCRS_to_vertCRS_height_depth_pivot_context) {
7279+
// Test that PROJ can chain a registered vertical CT with a
7280+
// height↔depth axis conversion when the target CRS differs from
7281+
// the CT's registered target only by axis direction.
7282+
//
7283+
// EPSG:5705 (Baltic 1977 height) → EPSG:5706 (Caspian depth)
7284+
// should compose: EPSG:5438 (5705→5611, geogoffset +dh=28)
7285+
// + height-to-depth (axisswap order=1,2,-3)
7286+
auto authFactory =
7287+
AuthorityFactory::create(DatabaseContext::create(), "EPSG");
7288+
auto ctxt = CoordinateOperationContext::create(authFactory, nullptr, 0.0);
7289+
auto list = CoordinateOperationFactory::create()->createOperations(
7290+
// Baltic 1977 height
7291+
authFactory->createCoordinateReferenceSystem("5705"),
7292+
// Caspian depth (same datum as Caspian height 5611, but axis: down)
7293+
authFactory->createCoordinateReferenceSystem("5706"), ctxt);
7294+
ASSERT_GE(list.size(), 1U);
7295+
// Must NOT be a ballpark transformation
7296+
EXPECT_FALSE(list[0]->hasBallparkTransformation());
7297+
// The pipeline should contain geogoffset +dh=28 and axisswap
7298+
auto projStr =
7299+
list[0]->exportToPROJString(PROJStringFormatter::create().get());
7300+
EXPECT_TRUE(projStr.find("geogoffset") != std::string::npos) << projStr;
7301+
EXPECT_TRUE(projStr.find("axisswap") != std::string::npos) << projStr;
7302+
}
7303+
7304+
// ---------------------------------------------------------------------------
7305+
72787306
TEST(operation, projCRS_3D_to_geogCRS_3D) {
72797307

72807308
auto compoundcrs_ft_obj = PROJStringParser().createFromPROJString(

0 commit comments

Comments
 (0)