Skip to content

[GH-3519] Interpolate ST_InterpolatePoint measures within the closest segment - #3520

Merged
jiayuasu merged 2 commits into
apache:masterfrom
PDGGK:fix-interpolatepoint-segment-measure
Nov 6, 2026
Merged

jiayuasu merged 2 commits into
apache:masterfrom
PDGGK:fix-interpolatepoint-segment-measure

Conversation

@PDGGK

@PDGGK PDGGK commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Did you read the Contributor Guide?

Is this PR related to a ticket?

What changes were proposed in this PR?

ST_InterpolatePoint took the closest point's fraction of the total line length and interpolated between the line's first and last M values. The measures of the interior vertices were never read, so the result was only right when M grows in proportion to length. Tracks with time as M and routes with uneven mileposts got wrong values:

line point before after
LINESTRING M (0 0 0, 2 0 2, 4 0 4) (docs example) POINT (1 1) 1.0 1.0
LINESTRING M (0 0 0, 2 0 2, 4 0 20) POINT (1 1) 5.0 1.0
LINESTRING M (0 0 0, 10 0 10, 20 0 1000) POINT (5 1) 250.0 5.0
LINESTRING ZM (0 0 5 100, 10 0 5 90, 10 10 5 0) POINT (10 5) 25.0 45.0

With the old value, ST_LocateAlong(line, ST_InterpolatePoint(line, 'POINT (5 1)')) on the third line returned MULTIPOINT M((12.424242424242424 0 250)). With the fix it returns MULTIPOINT M((5 0 5)), the closest point.

The fix walks the segments of the line and takes the one closest to the point, keeping the first one along the line on a tie, as the previous DistanceOp lookup and PostGIS do. A zero-length segment (a stationary track) therefore keeps the measure of its first vertex. The fix then interpolates between the M values of the two vertices of that segment. For lines whose M is proportional to length the results are the same as before, so the existing test values are unchanged.

ST_LocateAlong and ST_AddMeasure already work per segment, so they need no change. Spark, Flink and the Python API all call Functions.interpolatePoint, so this one change covers them.

How was this patch tested?

  • Added FunctionsTest#interpolatePointMeasuresNotProportionalToLength. It covers a point inside a segment, the closest point at an interior vertex and at each endpoint, a track with uneven timing, an XYZM line with decreasing measures, and a stationary track (a zero-length segment) where POINT (0 1) and POINT (-1 0) must keep the first vertex's measure. Without the change to Functions.java it fails with expected:<1.0> but was:<5.0>. With the change it passes.
  • mvn -pl common test -Dtest='FunctionsTest#interpolatePoint*': 2 tests, 0 failures. With the previous revision of this PR, the stationary-track case fails with expected:<0.0> but was:<10.0>. The rest of FunctionsTest is left to CI.
  • Checked the result on lines read through GeometrySerializer and from WKB, as well as from WKT.
  • mvn -pl common spotless:check passes.
  • I did not run the Spark and Flink ST_InterpolatePoint tests locally. Their cases use M equal to cumulative length, which this change leaves at the same values. CI covers them.

Did this PR include necessary documentation updates?

  • No, this PR does not affect any public API so no need to change the documentation.

ST_InterpolatePoint took the closest point's fraction of the total line
length and interpolated between the line's first and last M values. That
is only right when the measures grow in proportion to length. For a track
with time as M, or a route with uneven mileposts, the result was wrong:
LINESTRING M (0 0 0, 2 0 2, 4 0 20) and POINT (1 1) gave 5.0 instead
of 1.0.

Locate the closest point with LocationIndexedLine and interpolate between
the M values of the two vertices of the segment that contains it.
@PDGGK
PDGGK requested a review from jiayuasu as a code owner November 3, 2026 10:05
// Find the closest point on the line to the input point
Coordinate closestPoint = DistanceOp.nearestPoints(line, point)[0];
// Locate the closest point on the line (the first one along the line if there is a tie)
LinearLocation location = new LocationIndexedLine(line).project(point.getCoordinate());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For LINESTRING M (0 0 0, 0 0 10, 10 0 20) and POINT (0 1), this now returns 10 instead of 0. I checked the parent commit and PostGIS 3.3.2; both return 0. POINT (-1 0) also regresses. JTS advances to the next vertex on the zero-length segment, so a stationary track gets the later timestamp. Please preserve the first closest vertex in this case and add regression tests for these two points.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. I no longer use LocationIndexedLine: the fix now walks the segments and keeps the first closest one on a tie, so the zero-length segment keeps the first vertex's measure. LINESTRING M (0 0 0, 0 0 10, 10 0 20) returns 0 again for both POINT (0 1) and POINT (-1 0), matching the parent commit and PostGIS, and both are now regression cases in interpolatePointMeasuresNotProportionalToLength. With the previous revision that test fails with expected:<0.0> but was:<10.0>. I also updated the PR description, which wrongly said the old lookup and LocationIndexedLine agree on ties.

@jiayuasu jiayuasu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@jiayuasu jiayuasu added this to the sedona-2.0.0 milestone Nov 6, 2026
@jiayuasu
jiayuasu merged commit 20efe7d into apache:master Nov 6, 2026
43 checks passed
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.

ST_InterpolatePoint returns the wrong measure when M is not proportional to length

2 participants