-
Notifications
You must be signed in to change notification settings - Fork 882
Description
Summary
When transforming between two vertical CRSs and no direct registered operation exists for the exact pair, PROJ should look for registered operations involving a different vertical CRS that shares the same datum as the source or target. The intermediate CRS may differ in axis direction (height vs depth), units (metres vs feet), or both. Currently PROJ falls back to a ballpark transformation in these cases, discarding available registered operations.
Example
EPSG:5705 (Baltic 1977 height) → EPSG:5706 (Caspian depth)
The EPSG registry contains operation EPSG:5438 which transforms 5705 → 5611 (Caspian height). CRS 5611 and 5706 share the same datum (Caspian Sea) but differ in axis direction (UP vs DOWN).
Expected: PROJ composes 5705 →(EPSG:5438)→ 5611 →(axis/unit conversion)→ 5706
Actual: PROJ returns a ballpark vertical transformation, discarding the registered operation.
Current Behaviour
The registered height→height operation works correctly:
from pyproj.transformer import TransformerGroup # ✅ Works: Baltic 1977 height → Caspian height (EPSG:5438) tg = TransformerGroup("EPSG:5705", "EPSG:5611", always_xy=True) t = tg.transformers[0] print(t.to_proj4()) # +proj=geogoffset +dh=28 print(t.transform(50.0, 40.0, 100)) # (50.0, 40.0, 128.0) ← correctBut height→depth on the same datum falls back to ballpark:
# ❌ Fails: Baltic 1977 height → Caspian depth (same datum, axis down) tg = TransformerGroup("EPSG:5705", "EPSG:5706", always_xy=True) t = tg.transformers[0] print(t.description) # "... (ballpark vertical transformation)" print(t.to_proj4()) # +proj=affine +s33=-1 ← geogoffset lost print(t.transform(50.0, 40.0, 100)) # (50.0, 40.0, -100.0) ← wrong: should be -128.0
Should Be
PROJ chains the registered transformation with the axis conversion:
# ✅ 5705 → 5611 (height → height): uses registered EPSG:5438 print(t.to_proj4()) # +proj=geogoffset +dh=28 print(t.transform(50.0, 40.0, 100)) # (50.0, 40.0, 128.0) # ✅ 5705 → 5706 (height → depth): chains EPSG:5438 + axis conversion print(t.description) # Baltic 1977 height to Caspian height (1) # + Conversion from Caspian height to Caspian depth print(t.to_proj4()) # +proj=pipeline +step +proj=geogoffset +dh=28 # +step +proj=axisswap +order=1,2,-3 print(t.transform(50.0, 40.0, 100)) # (50.0, 40.0, -128.0)
Affected Scenario
Any vertical-to-vertical transformation where:
- No direct registered operation exists between the exact source/target CRS pair, but
- A registered operation exists to/from a variant vertical CRS that shares the same datum as the target (or source), differing in axis direction, units, or both.
Proposed Behaviour
When searching for vertical-to-vertical operations, if no direct result is found, PROJ should:
- Strategy 1 (pivot on target datum): Find candidate vertical CRSs sharing the target's datum. If a registered operation exists from the source to any candidate, compose it with the candidate→target axis/unit conversion.
- Strategy 2 (pivot on source datum): Find candidate vertical CRSs sharing the source's datum. If a registered operation exists from any candidate to the target, compose the source→candidate axis/unit conversion with that operation.
This mirrors the existing logic in createOperationsGeogToVertWithIntermediateVert (which handles geographic→vertical paths) but extends it to vertical→vertical paths.