Skip to content

fix(geospatial): handle antimeridian wrapping in projection and regression#7544

Open
DennisOSRM wants to merge 4 commits intomasterfrom
fix/antimeridian-wrap
Open

fix(geospatial): handle antimeridian wrapping in projection and regression#7544
DennisOSRM wants to merge 4 commits intomasterfrom
fix/antimeridian-wrap

Conversation

@DennisOSRM
Copy link
Copy Markdown
Collaborator

@DennisOSRM DennisOSRM commented May 8, 2026

This PR fixes routing across the antimeridian by unwrapping longitudes during internal geometric computations (projection and regression) so ways crossing longitude ±180° are treated as continuous. Results are wrapped back into the canonical (-180,180] range for external use.\n\nAdds unit tests covering projectPointOnSegment and leastSquareRegression.

Closes #1188

DennisOSRM added 2 commits May 8, 2026 10:55
…d tests\n\n- Unwrap longitudes for projection and regression calculations so segments crossing\n the antimeridian are treated continuously.\n- Wrap projected/regression endpoints back into canonical (-180,180] range.\n- Add unit tests: unit_tests/util/antimeridian_tests.cpp and unit_tests/util/antimeridian_regression_tests.cpp\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 8, 2026 09:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to fix routing and geometry handling across the antimeridian by treating ways that cross ±180° longitude as continuous during internal computations, then wrapping results back to a canonical longitude range.

Changes:

  • Adds longitude unwrapping/wrapping logic to projectPointOnSegment to better handle segments crossing the antimeridian.
  • Adds longitude normalization to leastSquareRegression when input points span more than 180° in longitude.
  • Adds new unit tests (and a new Behave feature + steps) intended to cover these antimeridian cases.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
include/util/coordinate_calculation.hpp Adds longitude wrap/unwrap logic to projection and regression helpers.
unit_tests/util/antimeridian_tests.cpp Adds a unit test for projectPointOnSegment across the antimeridian.
unit_tests/util/antimeridian_regression_tests.cpp Adds unit tests for leastSquareRegression with/without antimeridian spanning data.
test/behave/features/steps/antimeridian_steps.py Adds Behave step definitions for an antimeridian routing scenario (currently stubbed).
test/behave/features/antimeridian.feature Adds a Behave feature/scenario for routing across the antimeridian.
test/behave/environment.py Adds a Behave environment placeholder.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread include/util/coordinate_calculation.hpp Outdated
Comment on lines 154 to 198
@@ -165,12 +187,14 @@ inline std::pair<double, FloatCoordinate> projectPointOnSegment(const FloatCoord
clamped_ratio = 0.;
}

// compute projected lon in unwrapped space, then wrap back into canonical range
const double projected_lon_unwrapped = (1.0 - clamped_ratio) * src_lon + clamped_ratio * tgt_lon;
const double projected_lon = wrapLongitudeDouble(projected_lon_unwrapped);

return {clamped_ratio,
{
FloatLongitude{1.0 - clamped_ratio} * source.lon +
target.lon * FloatLongitude{clamped_ratio},
FloatLatitude{1.0 - clamped_ratio} * source.lat +
target.lat * FloatLatitude{clamped_ratio},
FloatLongitude{projected_lon},
FloatLatitude{1.0 - clamped_ratio} * source.lat + target.lat * FloatLatitude{clamped_ratio},
}};
Comment thread include/util/coordinate_calculation.hpp Outdated
Comment on lines 154 to 167
// Unwrap the target longitude so the lon difference to source is minimal (handles antimeridian)
const double src_lon = static_cast<double>(source.lon);
double tgt_lon = static_cast<double>(target.lon);
const double coord_lon = static_cast<double>(coordinate.lon);

const double dlon = tgt_lon - src_lon;
if (dlon > 180.0)
tgt_lon -= 360.0;
else if (dlon < -180.0)
tgt_lon += 360.0;

const FloatCoordinate slope_vector{FloatLongitude{tgt_lon - src_lon}, target.lat - source.lat};
const FloatCoordinate rel_coordinate{FloatLongitude{coord_lon - src_lon}, coordinate.lat - source.lat};
// dot product of two un-normed vectors
Comment thread include/util/coordinate_calculation.hpp Outdated
Comment on lines +283 to +295
// If coordinates span the antimeridian (lon span > 180°), normalize longitudes by adding 360° to
// negative longitudes so computations are continuous
const bool unwrap = (max_lon - min_lon) > 180.0;
if (unwrap)
{
auto orig_extract_lon = extract_lon;
extract_lon = [orig_extract_lon](const Coordinate coordinate) {
double lon = orig_extract_lon(coordinate);
if (lon < 0.0)
lon += 360.0;
return lon;
};

Comment on lines +262 to 264
std::function<double(const Coordinate)> extract_lon = [](const Coordinate coordinate)
{ return static_cast<double>(toFloating(coordinate.lon)); };

Comment on lines +1 to +14
#include <boost/test/unit_test.hpp>

#include "util/coordinate.hpp"
#include "util/coordinate_calculation.hpp"

using namespace osrm::util;

static double shortestAngularDistance(double a, double b)
{
double diff = std::abs(a - b);
if (diff > 180.0)
diff = 360.0 - diff;
return diff;
}
Comment on lines +1 to +18
from behave import given, when, then

@given('an OSRM dataset with two nodes connected across the antimeridian')
def step_impl(context):
# Minimal stub: ensure dataset exists or mark as TODO
context.dataset_prepared = True

@when('I request a route from lon:179.9,lat:0 to lon:-179.9,lat:0')
def step_impl(context):
# This step should call the routing API; here it will stub the response to indicate failure
# Intentionally produce a failing/empty route to drive TDD
context.route = {'segments': []}

@then('the route should include a segment that crosses the antimeridian')
def step_impl(context):
segments = context.route.get('segments', [])
# Expect at least one segment crossing; this will fail with current stub
assert any(seg.get('crosses_antimeridian', False) for seg in segments), "No antimeridian crossing segment found"
DennisOSRM added 2 commits May 8, 2026 11:21
…rd projection\n\n- Restore non-wrapping projectPointOnSegment to avoid breaking existing projected-coordinate callers (static_rtree).\n- Add projectPointOnSegmentAntimeridian for callers that need antimeridian-aware projections.\n- Update antimeridian unit test to exercise the new API.\n\nCloses #1188\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

Routing across the dateline

2 participants