[PostGIS] #6133: Geography line interpolation: NaN heading along a meridian, wrong meridian past a pole, uninitialised Z and M

PostGIS trac at osgeo.org
Mon Sep 14 05:34:37 PDT 2026


#6133: Geography line interpolation: NaN heading along a meridian, wrong meridian
past a pole, uninitialised Z and M
-------------------------------------------------+-------------------------
 Reporter:  ezimanyi                             |      Owner:  pramsey
     Type:  defect                               |     Status:  new
 Priority:  medium                               |  Milestone:  PostGIS
                                                 |  3.6.5
Component:  postgis                              |    Version:  3.6.x
 Keywords:  geography ST_LineInterpolatePoint    |
  ST_LineInterpolatePoints sphere_direction      |
  sphere_project                                 |
-------------------------------------------------+-------------------------
 The interpolation of geography lines, which came with #5460, goes
 through `interpolate_point4d_spheroid()` in
 `liblwgeom/lwgeodetic_measures.c` and, on the sphere, through
 `sphere_direction()` and `sphere_project()` in `liblwgeom/lwgeodetic.c`.
 Each of the three has a defect of its own. All three reproduce on
 PostGIS 3.6.3, and master 2bb71185a8 carries the same code.

 == 1. sphere_direction() returns NaN for two points on one meridian ==

 `sphere_direction()` computes the initial heading from its cosine,

 {{{
 f = (sin(lat2) - sin(lat1) * cos(d)) / (sin(d) * cos(lat1))
 heading = acos(f)
 }}}

 For two points on one meridian the exact value of `f` is 1 or -1, and
 rounding leaves it outside [-1, 1] by more than `FP_TOLERANCE` (5e-14
 for the geodetic code), so `acos()` returns NaN; the `fabs(f) > 1.0`
 branch logs the value and still calls `acos()`. Over 43800 segments
 along the prime meridian between 80S and 80N, up to 0.02 degrees long,
 these expressions give NaN for 17791 of them. `sphere_project()` then
 fails, and (see 3) the result is the caller's uninitialised point:

 {{{#!sql
 SELECT ST_AsText(ST_LineInterpolatePoint(
   'LINESTRING(0 -79.269, 0 -79.239)'::geography, 0.5, false));
 -- POINT(6.95314402316196e-310 6.9229486544248e-310)

 SELECT ST_AsText(ST_LineInterpolatePoint(
   'LINESTRING(0 -79.269, 0 -79.239)'::geography, 0.5, true));
 -- POINT(0 -79.25400000727056)
 }}}

 The circular tree already guards against the NaN:
 `circ_center_spherical()` in `lwgeodetic_tree.c` tests the heading
 with `isnan()` ("Catch sphere_direction when it barfs") and falls back
 to a Cartesian centre.

 == 2. sphere_project() keeps the start meridian past a pole ==

 For an azimuth of 0 or pi, `sphere_project()` keeps the start
 longitude, which holds up to the pole and not beyond it; the source
 marks it with "TODO: this isn't quite true, what if we're going over
 the pole?". Past the pole the path runs down the opposite meridian:

 {{{#!sql
 SELECT ST_AsText(ST_LineInterpolatePoint(
   'LINESTRING(0 88, 180 88)'::geography, 0.75, false));
 -- POINT(0 89.0000000000001)

 SELECT ST_AsText(ST_LineInterpolatePoint(
   'LINESTRING(0 88, 180 88)'::geography, 0.75, true));
 -- POINT(180 89.00000307849999)
 }}}

 == 3. interpolate_point4d_spheroid() leaves Z, M, and a failed x and y
 uninitialised ==

 The function sets only x and y, and only when the projection succeeds:

 {{{
 /* If success, use newly computed lat and lon,
 * otherwise return precomputed cartesian result */
 if (success == LW_SUCCESS)
 {
       p->x = rad2deg(longitude_radians_normalize(g.lon));
       p->y = rad2deg(latitude_radians_normalize(g.lat));
 }
 }}}

 Nothing precomputes the Cartesian result the comment names, and both
 callers, `geography_substring()` and `geography_interpolate_points()`,
 pass an uninitialised `POINT4D pt`. So every interpolated Z and M is
 whatever the stack held, on the sphere and on the spheroid alike:

 {{{#!sql
 SELECT ST_AsText(ST_LineInterpolatePoint(
   'LINESTRING Z(0 0 0, 10 0 10)'::geography, 0.5, true));
 -- POINT Z (5.000000000201743 0 6.95291892163796e-310)

 SELECT ST_AsText(ST_LineInterpolatePoint(
   'LINESTRING Z(0 0 0, 10 0 10)'::geography, 0.5, false));
 -- POINT Z (5.000000000202194 3.057732552914371e-16 5.0170810976182e-310)
 }}}

 The function comes from MobilityDB (#5460), whose version, now in
 `meos/src/geo/tpoint_spatialfuncs.c`, calls `interpolate_point4d(p1,
 p2, p, f)` before the success test; that call is absent from the
 PostGIS version.

 == Fix ==

  1. `sphere_direction()` reads the heading with `atan2()` from both
     components of the initial bearing,
     {{{
 heading = atan2(sin(dlon) * cos(lat2),
                 cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon))
     }}}
     The sine and cosine of the heading are these two components
     divided by sin(d) > 0, so this is the heading wherever the two
     points are distinct, with no cosine to leave its range and no
     clamp. It answers exactly 0 or pi along a meridian and keeps the
     sign convention. The distance argument is no longer read and stays
     in the signature.
  2. `sphere_project()` keeps its shortcut and answers the opposite
     meridian past the pole,
     {{{
 lon2 = ( cos(d) - sin(lat1) * sin(lat2) < 0.0 ) ? lon1 + M_PI : lon1;
     }}}
     `cos(d) - sin(lat1) * sin(lat2)` is the second component of the
     general formula below the shortcut and equals `cos(lat1) *
     cos(lat2) * cos(dlon)`, so along a meridian its sign is that of
     `cos(dlon)`: positive on the start meridian, negative on the
     opposite one. The TODO goes.
  3. `interpolate_point4d_spheroid()` computes the Cartesian
     interpolation first, which also interpolates z and m, and a
     successful projection overwrites x and y.

 == Tests ==

 The patch adds to `liblwgeom/cunit/cu_geodetic.c` the meridian pair
 (0 -79.269) and (0 -79.239) in both directions to
 `test_sphere_direction()`, expecting 0 and pi, and a projection due
 north from 88N over 3 degrees to `test_sphere_project()`, expecting 89N
 on the opposite meridian. It adds to `regress/core/geography.sql` the
 cases `lrs_lip_pole_sphere`, `lrs_lip_meridian_sphere`, `lrs_lip_z` and
 `lrs_lip_zm`.

 Measured on master 2bb71185a8 with PostgreSQL 18.3 and CUnit 2.1-3:
  * with the patch, the cunit geodetic suite passes 25 of 25 tests and
    536 of 536 assertions, and every suite 375 tests and 5901
    assertions, none failing;
  * with the new tests but without the fixes in `lwgeodetic.c` and
    `lwgeodetic_measures.c`, the geodetic suite fails 2 of 25 tests and
    3 of 536 assertions, exactly the new ones: `cu_geodetic.c:96` and
    `:98` (the heading along the meridian, NaN) and `:185` (the
    longitude past the pole, 0 instead of pi);
  * with the patch, `regress/core/geography` passes under
    `run_test.pl --extension`, the four new cases answering
    `POINT(180 89)`, `POINT(0 -79.254)`, `POINT Z (5 0 5)` and
    `MULTIPOINT ZM ((5 0 5 10),(10 0 10 20))`.

 These were found while fixing the same functions in the copy of
 liblwgeom that MobilityDB vendors:
 [https://github.com/MobilityDB/MobilityDB/pull/2722 MobilityDB#2722]
 (the heading) and
 [https://github.com/MobilityDB/MobilityDB/pull/2720 MobilityDB#2720]
 (the pole).
-- 
Ticket URL: <https://trac.osgeo.org/postgis/ticket/6133>
PostGIS <http://trac.osgeo.org/postgis/>
The PostGIS Trac is used for bug, enhancement & task tracking, a user and developer wiki, and a view into the subversion code repository of PostGIS project.


More information about the postgis-tickets mailing list