Skip to content

fix residual axisswap for Geographic3D <-> VerticalCRS pipelines - #4850

Open
phaarnes wants to merge 2 commits into
OSGeo:masterfrom
phaarnes:fix/axisswap_issues_mixed_dim_pipelines
Open

phaarnes wants to merge 2 commits into
OSGeo:masterfrom
phaarnes:fix/axisswap_issues_mixed_dim_pipelines

Conversation

@phaarnes

@phaarnes phaarnes commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

What the fix does

Fixes a bug in CoordinateOperation::normalizeForVisualization() (the
function behind pyproj's always_xy=True and proj_normalize_for_visualization())
where a Geographic3D <-> VerticalCRS pipeline keeps a leftover
+proj=axisswap +order=2,1 step. In practice this means the horizontal
coordinates come out lat/lon instead of lon/lat even though always_xy=True
was requested, with no warning or error.

Example: EPSG:4937 (ETRS89, geographic 3D) -> EPSG:5776 (NN54 height).

Why it happens

A VerticalCRS has no horizontal axes, so
CRS::mustAxisOrderBeSwitchedForVisualization() always returns false for
it, which is correct on its own - there's no axis order to reverse on a 1D
CRS.

The problem is that the actual operation between a geographic and a vertical
CRS still carries a horizontal pass-through internally (built from the
geographic side on both ends of the pipeline), and that pass-through is
symmetric by construction. normalizeForVisualization() decides whether to
swap each end independently, so it ends up swapping only the geographic end
and leaving the vertical end alone - which breaks that symmetry and leaves
one of the two swaps stranded in the final pipeline.

The fix

Added CRS::hasNoHorizontalAxes(), and in normalizeForVisualization(),
when exactly one end of the operation has no horizontal axes, that end now
just inherits the other end's swap decision instead of being evaluated on
its own.

I deliberately kept this at the operation level rather than touching
mustAxisOrderBeSwitchedForVisualization() itself, since that function is
also used to transpose area-of-use extents, and making a VerticalCRS report
true there would mess up bounding boxes for unrelated code paths.

Testing

  • Tests added: two new cases in the existing TEST(operation, normalizeForVisualization) (test/unit/test_operation.cpp) covering EPSG:4937 -> EPSG:5776 and its reverse, asserting no residual axisswap remains in the normalized pipeline

Scope

This only covers Geographic3D <-> VerticalCRS. EngineeringCRS pairs with
axis directions like north/west (e.g. EPSG:5800) have a related problem, but
fixing that needs an axis direction flip that
Conversion::createAxisOrderReversal can't express today, so it's a
separate, bigger change and out of scope here.

@kbevers kbevers added this to the 9.9.1 milestone Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

always_xy=True leaves a residual axisswap for Geographic3D <-> VerticalCRS pipelines (normalizeForVisualization)

2 participants