Allocation-free VertexLinker edge distance on packed geometry#7688
Merged
vpaturet merged 6 commits intoJun 18, 2026
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev-2.x #7688 +/- ##
=============================================
+ Coverage 74.04% 74.06% +0.02%
- Complexity 21830 21879 +49
=============================================
Files 2423 2426 +3
Lines 85772 85930 +158
Branches 8536 8563 +27
=============================================
+ Hits 63507 63644 +137
- Misses 19235 19249 +14
- Partials 3030 3037 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…eometry VertexLinker.distance() materialized a full JTS LineString from the packed edge geometry, cloned every coordinate into an equirectangular projection, and ran JTS DistanceOp - allocating Coordinate[], CoordinateXY, PackedCoordinateSequence, Envelope and GeometryLocation for every candidate edge on the linking hot path (and the GBFS-apply path at startup). Add CompactLineStringUtils.distanceToPointEquirectangular(), which streams the segments straight from the Dlugosz-packed deltas via the existing Decoder and runs the same per-segment point-to-segment math JTS uses, with zero allocation and no LineString materialization. StreetEdge exposes it via distanceToPointEquirectangular(); VertexLinker.distance() becomes a one-liner. findSplitVertex() (once per winning edge) is intentionally left on JTS. CompactLineStringDistanceTest checks parity against the old JTS path (equirectangularProject(uncompactLineString(...)).distance) to <1e-12 across straight, single/multi-intermediate, reversed, degenerate and on-vertex cases plus 1000 random polylines.
…andidate sqrt The vertex linker only orders edges and applies thresholds, both monotonic in distance, so the per-candidate sqrt in the edge-distance computation is unnecessary. Return the squared distance instead (squaredDistanceToPointEquirectangular) and square the search-radius threshold once per call. The one consumer that needs real distance - the per-mode duplicate-way dedup, which uses an additive epsilon (closest + DUPLICATE_WAY_EPSILON_DEGREES) that squaring does not preserve - takes a single sqrt per mode group and compares against the squared band: dSq <= (closest + eps)^2 is exactly d <= closest + eps. This moves sqrt from O(candidate edges) to O(modes) with identical edge selection (linking integration tests unchanged).
86dbf03 to
be97d09
Compare
be97d09 to
aa170a8
Compare
…LineString Reorganize the packed line-string geometry API into two cohesive types instead of CompactLineString + CompactLineStringUtils: - CompactLineString: the self-contained value-object contract (endpoints baked into the packed bytes; used by CompactLineStringSequence). - EndpointContextLineString (new, stateless): the endpoint-context contract, where endpoints are supplied externally by StreetEdge and omitted from the packed bytes -- compact() / uncompact() / squaredDistanceToPointEquirectangular(). The shared delta engine (packIntermediateDeltas / decodeInto / the fixed-point conversions) stays package-private in CompactLineString and is reused by both, so there is no logic duplication. StreetEdge keeps its raw byte[] geometry field and delegates to EndpointContextLineString -- no per-edge wrapper object and no serialization change. CompactLineStringUtils is removed. Tests: CompactLineStringTest (value object); EndpointContextLineStringTest and EndpointContextLineStringDistanceTest (endpoint-context round-trip + JTS parity).
aa170a8 to
0fa5445
Compare
optionsome
reviewed
Jun 10, 2026
leonardehrenfried
left a comment
Member
There was a problem hiding this comment.
Claude and myself could not find anything wrong with this but I agree with the small issues that @optionsome has raised.
…distance # Conflicts: # street/src/main/java/org/opentripplanner/street/model/edge/StreetEdge.java
- Rename squaredDistanceToPointEquirectangular -> squaredEquirectangularDistanceToPoint - Remove the seeded-Random fuzz test, keep the deterministic parity cases - Inline the single-use walk helper, rename jtsOracle -> jtsReferenceDistance
leonardehrenfried
approved these changes
Jun 17, 2026
optionsome
approved these changes
Jun 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
VertexLinker.distance()materializes a full JTSLineStringper candidate edge, projects it (cloning everyCoordinate), and runs JTSDistanceOp— producingCoordinateXY,PackedCoordinateSequence,Envelope,GeometryLocationfor every candidate edge on the linking hot path (and the GBFS rental-linking at startup).This PR provides an allocation-free alternative implementation of the same algorithm.
What changed
Allocation-free edge distance. New
EndpointContextLineString.squaredDistanceToPointEquirectangular(...)walks the packed delta geometry directly via the existingDlugoszVarLenIntPacker.Decoder, computing the same per-segment point-to-segment distance JTSDistanceOpproduces (mathematically equivalent up to floating-point rounding) — with zero allocation and noLineStringmaterialization.StreetEdgeexposes it;VertexLinker.distance()becomes a one-liner.findSplitVertex()(once per winning edge, not per candidate) is intentionally left on JTS.Squared distance. The linker only orders edges and applies thresholds (both monotonic), so the per-candidate
sqrtis dropped — the method returns the squared distance and the search radius is squared once. The one consumer needing real distance (the per-mode duplicate-way dedup, an additive epsilon) takes a singlesqrtper mode group:dSq <= (closest + eps)², exactly equivalent tod <= closest + eps. This movessqrtfrom O(candidate edges) to O(modes).Two cohesive packed-geometry types. The packed
byte[]format backs two contracts, now expressed as two single-responsibility types that share one private delta engine:CompactLineString— the self-contained value object (endpoints baked into the packed bytes; used byCompactLineStringSequence).EndpointContextLineString(new, stateless) — the endpoint-context contract, where the endpoints are supplied externally byStreetEdgeand omitted from the bytes (compact/uncompact/squaredDistanceToPointEquirectangular).The shared encode/decode engine (
packIntermediateDeltas/decodeInto+ the fixed-point conversions) stays package-private onCompactLineStringand is reused, so there is no logic duplication.StreetEdgekeeps its rawbyte[]field and delegates toEndpointContextLineString— no per-edge wrapper object, no extra indirection, no serialization change. (This replaces the oldCompactLineStringUtils.)Performance (local A/B: Norway graph, 68 GBFS rental networks, 9,995 real production trip queries; JFR + async-profiler)
Allocation sample counts on the linking path (JFR; startup/GBFS-apply window → warm-load window), confirmed independently by async-profiler:
VertexLinker.distanceDistanceOpequirectangularProjectCoordinateXYThe new walk appears in CPU samples but never in allocation samples (the
Decoderscalar-replaces). Warm-loaddistanceCPU dropped 1,619 → 54 samples.