From 651397cb040a0724964ffd8d0fd15a8feb5c19f4 Mon Sep 17 00:00:00 2001 From: Sergei Malyshko Date: Tue, 28 Jul 2026 16:06:00 +0200 Subject: [PATCH] Fix Oblique Mercator natural-origin offset to use the central-line azimuth The natural-origin (uc) offset was computed from the rectified bearing (Gamma) via cosrot, but Snyder's Hotine Oblique Mercator defines it with the central-line azimuth (alpha): u_c = (A / B) * atan(sqrt(D^2 - 1) / cos(alpha_c)) When Gamma equals alpha the two agree, which is why this went unnoticed. For definitions where the rectified bearing differs from the azimuth -- notably the ESRI "Local" / mine-grid convention of an explicit +gamma=0 with a non-zero azimuth -- the projection centre was displaced along the central line (e.g. ~2.5 km), so the false easting/northing no longer landed on the centre. Use cos(alpha) for the offset. This is a no-op when Gamma == alpha and matches PROJ output when Gamma != alpha. Adds a regression test (Proj4VariousTest#testObliqueMercatorExplicitGamma) covering +gamma=0 with a non-zero azimuth, with reference coordinates computed by PROJ. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Sergei Malyshko --- CHANGELOG.md | 3 +++ .../proj4j/proj/ObliqueMercatorProjection.java | 6 +++++- .../org/locationtech/proj4j/Proj4VariousTest.java | 13 +++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb254a..1da8c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Oblique Mercator: compute the natural-origin (uc) offset from the central-line azimuth (`+alpha`) instead of the rectified bearing (`+gamma`), so the projection centre maps to the false easting/northing when `+gamma` differs from `+alpha` (e.g. an explicit `+gamma=0` with a non-zero azimuth) + ## [1.4.3] - 2026-06-02 ### Added diff --git a/core/src/main/java/org/locationtech/proj4j/proj/ObliqueMercatorProjection.java b/core/src/main/java/org/locationtech/proj4j/proj/ObliqueMercatorProjection.java index 28e97c7..46f7319 100644 --- a/core/src/main/java/org/locationtech/proj4j/proj/ObliqueMercatorProjection.java +++ b/core/src/main/java/org/locationtech/proj4j/proj/ObliqueMercatorProjection.java @@ -158,8 +158,12 @@ else if (con > Math.PI) sinrot = Math.sin(Gamma); cosrot = Math.cos(Gamma); + // The natural-origin offset uses the azimuth of the central line (alpha), following Snyder's + // Hotine Oblique Mercator: u_c = (A / B) * atan(sqrt(D^2 - 1) / cos(alpha_c)). Using the + // rectified bearing (Gamma) here misplaces the projection centre whenever Gamma != alpha, + // e.g. for definitions with an explicit "+gamma=0" and a non-zero azimuth. u_0 = no_uoff ? 0. : - Math.abs(al * Math.atan(Math.sqrt(d * d - 1.) / cosrot) / bl); + Math.abs(al * Math.atan(Math.sqrt(d * d - 1.) / Math.cos(alpha)) / bl); if (projectionLatitude < 0.) u_0 = - u_0; } diff --git a/core/src/test/java/org/locationtech/proj4j/Proj4VariousTest.java b/core/src/test/java/org/locationtech/proj4j/Proj4VariousTest.java index fd87456..a8eb135 100755 --- a/core/src/test/java/org/locationtech/proj4j/Proj4VariousTest.java +++ b/core/src/test/java/org/locationtech/proj4j/Proj4VariousTest.java @@ -91,6 +91,19 @@ public void testSTS() { 1e-5); } + @Test + public void testObliqueMercatorExplicitGamma() { + // Hotine Oblique Mercator with an explicit "+gamma=0" and a non-zero azimuth. The natural + // origin offset must place the projection centre (lonc, lat_0) exactly at the false + // easting/northing; a regression here shifted the centre by ~2.5 km. Reference coordinates + // computed with PROJ. + String omerc = + "+proj=omerc +lat_0=-20 +lonc=140 +alpha=30 +gamma=0 +k=1 +x_0=200000 +y_0=300000 +ellps=GRS80 +units=m"; + checkTransform("+proj=latlong +ellps=GRS80", p("140 -20"), omerc, p("200000.00 300000.00"), 1e-2); + checkTransform("+proj=latlong +ellps=GRS80", p("140.5 -20.2"), omerc, p("256366.4343 306886.2489"), 1e-2); + checkTransform("+proj=latlong +ellps=GRS80", p("139.7 -19.8"), omerc, p("161721.1705 303433.2577"), 1e-2); + } + // disabled - gamma param not implemented // @Test public void XXX_testRSOBorneo() {