Skip to content

fix: Draw tracks and routes crossing antimeridian without jumping across the globe#364

Open
daniel-freiermuth wants to merge 10 commits into
SignalK:masterfrom
daniel-freiermuth:fix-tracks-crossing-antimeridian
Open

fix: Draw tracks and routes crossing antimeridian without jumping across the globe#364
daniel-freiermuth wants to merge 10 commits into
SignalK:masterfrom
daniel-freiermuth:fix-tracks-crossing-antimeridian

Conversation

@daniel-freiermuth

Copy link
Copy Markdown
Contributor

mapifyCoords is responsible for (un-)wrapping coordinates of line segments around the antimeridian. I.e. a line from [0, 179] → [0, -179] would be translated to [0, 179] → [0, 181].

It had two bugs:

  • Segments with a large longitudal range were not properly handled
  • Segments were not be handled incrementally, but each would be compared to the first point.

This lead to tracks and routes "jumping" around the world when they would cross the antimeridian if the first segment was further away or the crossing segment was a large one.

Additionally, there was an issue with how openlayers draws objects outside [-180,180]. A track that spans the globe, let's say from 0E to 0E, actually becomes a track from 0E to 360E with the above fix. The expected view would show the track seamlessly spanning the world. This requires the track actually to be drawn twice: from 360W to 0E and from 0E to 360E.

Openlayers seemingly has a logic that objects within [-180,180] are drawn multiple times, but objects outside this range are only drawn once. This lead to a problem where a track going beyond the antimeridian that spans the world would jump depending on where the view center is and mostly seem to miss some parts.

This PR thus proposes a fix by keeping tracks withing [-180, 180]. The function mapifyCoords is replaced by splitAtAntimeridian which splits line segments [179,-179] crossing the antimeridian and replaces it by a first segment to the antimeridian [179,180] and a second segment from the antimeridian onwards [-180,-179]. Since the track stays within [-180,180], openlayers duplicates both segments and the track still looks continuous.

Broken
Screenshot From 2026-04-16 22-32-09
Fixed
Screenshot From 2026-04-17 08-10-03
Broken:
Screenshot From 2026-04-16 22-33-11

Fixed
Screenshot From 2026-04-17 08-07-22

Route broken:
Screenshot From 2026-04-17 18-42-19
Route fixed:
Screenshot From 2026-04-17 18-45-56

@daniel-freiermuth daniel-freiermuth changed the title Fix tracks and routes crossing antimeridian fix: Draw tracks and routes crossing antimeridian without jumping across the globe Apr 17, 2026
@daniel-freiermuth daniel-freiermuth force-pushed the fix-tracks-crossing-antimeridian branch from 2775992 to 207bc61 Compare April 19, 2026 10:00
@panaaj

panaaj commented Apr 30, 2026

Copy link
Copy Markdown
Member

What happens when you edit (move points) of a route that crosses the anti-meridian?
Splitting to render in this fashion adds points to the feature which may result in additional (unwanted points) in the route.
This was the initial reasoning for the current approach.

@daniel-freiermuth daniel-freiermuth force-pushed the fix-tracks-crossing-antimeridian branch from 207bc61 to 31e4a74 Compare May 1, 2026 15:45
@daniel-freiermuth

Copy link
Copy Markdown
Contributor Author

Good catch. I think I found a solution, but need to review it once again to make sure I didn't miss anything.

Splits a lon/lat LineString at every ±180° crossing into segments that
each stay within [-180, 180]. Interpolated endpoints are inserted at
exactly ±180° so OpenLayers' wrapX mechanism can clone each segment into
adjacent world copies correctly.
Replace the mapifyCoords + LineString approach with splitAtAntimeridian +
MultiLineString for both the local (self) trail and the server trail.

Each trail segment is split at ±180° so every piece stays within
[-180, 180]. OL's wrapX then clones each piece into all visible world
copies, eliminating the screen-crossing jump.
…unwrapping

The old implementation used an inDLCrossingZone heuristic that only kicked
in when a point was within a configurable zone near ±180°. This broke for
routes that crossed the antimeridian at non-extreme longitudes, and for
routes that crossed it multiple times.

The new algorithm:
1. Normalises the first point into [-180, 180].
2. For each subsequent point, adjusts by ±360 until it is within 180° of
   its predecessor — producing a continuous, unwrapped coordinate sequence.

inDLCrossingZone is removed; it had no callers outside mapifyCoords.
toLonLat() normalises longitude back into [-180, 180], discarding which
world copy the user clicked in. Replace with transform() so that the raw
longitude is preserved (e.g. 190° for world +1, -190° for world -1).

Also switch the right-click handler from getEventCoordinate() to
getEventCoordinateInternal() for the same reason: the internal method
returns the raw EPSG:3857 coordinate without clamping.

Downstream code that calls fromLonLat() on the resulting lonlat will then
produce a Mercator X in the correct world copy, which is a prerequisite for
placing popups and editing handles in the right location.
OL's Modify interaction attaches vertex handles to the primary-world
Feature (longitude in [-180, 180]). When the user clicks a route on a
non-primary world copy, the handle would snap to the wrong location —
potentially 360° away.

Fix: when entering modify mode for a route, build a scratch LineString
from mapifyCoords()-unwrapped coordinates and shift its Mercator x by the
number of world widths needed to place the nearest vertex at the click
position. OL Modify then operates on this shifted scratch geometry, so
the drag handle appears where the user clicked.

The scratch geometry is invisible (Modify attaches no VectorLayer to
draw.features). Waypoint positions are extracted from it on each
modifyEnd via transformCoordsArray().
Routes crossing the antimeridian previously rendered as a straight Mercator
line jumping across the screen, because OL drew a single LineString between
the last point in one world and the first point in the next.
@daniel-freiermuth daniel-freiermuth force-pushed the fix-tracks-crossing-antimeridian branch from 31e4a74 to 6c21135 Compare May 26, 2026 19:44
@daniel-freiermuth

Copy link
Copy Markdown
Contributor Author

What happens when you edit (move points) of a route that crosses the anti-meridian?
Splitting to render in this fashion adds points to the feature which may result in additional (unwanted points) in the route.

This PR proses now to build editing routes around moving waypoints. For making this happen, this PR customizes the editing interaction. The inserted support points on the anti-meridian will not appear as draggable waypoints. After each moved waypoint, the resulting route is recalculated. Moving a waypoint might lead to a segment "switching direction" and going east instead of west and vice versa if the direction of the shortest path changed. I couldn't make this recalculation happen while dragging, but only when the drag finished. This meant that the intermediate dragged geometry as drawn by openlayers would be misleading (and also expose the support points on the antimeridian if the user changes the segment crossing it). For this reason, I propose not to show the misleading line while dragging, but just update the drawn route whenever a waypoint was updated. Not ideal, but it works. As a bonus, it paves the way to drawing routes segments as great-circle connections (separate PR comes soon).

@daniel-freiermuth

Copy link
Copy Markdown
Contributor Author

The commits:

  • 7c9ebfc feat: Add splitAtAntimeridian utility function
  • ba94be2 fix: Properly draw vessel trails crossing the antimeridian
  • 77ad8b8 fix: Split line using rhumb line instead of linear interpolation
  • 4b902b7 fix: Properly draw resource tracks crossing the antimeridian
  • cf1b098 fix: Properly draw AIS tracks crossing the antimeridian
  • e438426 refactor: Replace zone-heuristic mapifyCoords with longitude unwrapping. Used while route editing.
  • 66455fa fix: Preserve world-copy longitude offset in click events. Make route popups appear in the right world copy.
  • 5d4a497 fix: Place route editing handle in the world copy the user clicked in.
  • 75fcb88 feat: Render routes as MultiLineString split at the antimeridian
  • 6c21135 fix: Show vessel popover in the world-copy the user clicked in

@daniel-freiermuth

daniel-freiermuth commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Apologies for the wall of commits and PRs. Please let me know whether you're actually interested in the proposed fixed and features and if you'd prefer them in a different format.

@joelkoz

joelkoz commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Thanks for working through this. The screenshots make the underlying problem really clear, and I agree this is a real correctness issue for dateline / Pacific / offshore users rather than just a visual nicety.

Before this can land, I'd like to tighten up a couple of things:

  1. The branch currently has merge conflicts with master, so it will need a rebase/update first.

  2. Could you add focused regression coverage for the antimeridian behavior? The most useful seam looks like the coordinate utility logic: eastbound and westbound crossings, large longitude spans, multiple crossings, and ensuring inserted ±180 support points do not become saved route waypoints.

  3. The PR has grown beyond rendering tracks/routes: it now also changes route editing, click world-copy handling, and popup placement. Those may all be necessary for the antimeridian fix, but please either split the interaction/world-copy pieces into a separate PR or update the description to make the dependency between them explicit.

  4. Per the current contribution convention, please retitle this as something like:
    fix(map): draw tracks and routes crossing antimeridian without jumps

I'm interested in getting this fix in; I just want the route-editing invariants and scope to be clear before merge.

@joelkoz joelkoz self-assigned this Jun 26, 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.

3 participants