Skip to content

Commit 5fda148

Browse files
authored
Floating-point precision fix in meili::Project() fixes map-match issue (valhalla#2946)
Fixed the segfault by introducing some fp-precision and tolerance logic in meili::helpers::Project() and meili::cut_segments().
1 parent 92fd175 commit 5fda148

7 files changed

+251
-69
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
* FIXED: Skip bindings if there's no Python development version [#2893](https://github.com/valhalla/valhalla/pull/2878)
2727
* FIXED: Use CMakes built-in Python variables to configure installation [#2931](https://github.com/valhalla/valhalla/pull/2931)
2828
* FIXED: Sometimes emitting zero-length route geometry when traffic splits edge twice [#2943](https://github.com/valhalla/valhalla/pull/2943)
29+
* FIXED: Fix map-match segfault when gps-points project very near a node [#2946](https://github.com/valhalla/valhalla/pull/2946)
2930
* FIXED: Use kServiceRoad edges while searching for ferry connection [#2933](https://github.com/valhalla/valhalla/pull/2933)
3031

3132
* **Enhancement**

src/meili/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ set(sources
55
topk_search.cc
66
routing.cc
77
candidate_search.cc
8+
geometry_helpers.cc
89
transition_cost_model.cc
910
map_matcher.cc
1011
map_matcher_factory.cc

src/meili/candidate_search.cc

+8-8
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ CandidateCollector::WithinSquaredDistance(const midgard::PointLL& location,
6767

6868
// Projection information
6969
midgard::PointLL point;
70-
float sq_distance = 0.f;
70+
double sq_distance = 0.0;
7171
size_t segment;
72-
float offset;
72+
double offset;
7373

7474
baldr::GraphId snapped_node;
7575
baldr::PathLocation correlated(baldr::Location(location, stop_type));
@@ -81,10 +81,10 @@ CandidateCollector::WithinSquaredDistance(const midgard::PointLL& location,
8181
std::tie(point, sq_distance, segment, offset) = helpers::Project(projector, shape);
8282

8383
if (sq_distance <= sq_search_radius) {
84-
const float dist = edge->forward() ? offset : 1.f - offset;
85-
if (dist == 1.f) {
84+
const double dist = edge->forward() ? offset : 1.0 - offset;
85+
if (dist == 1.0) {
8686
snapped_node = edge->endnode();
87-
} else if (dist == 0.f) {
87+
} else if (dist == 0.0) {
8888
snapped_node = opp_edge->endnode();
8989
}
9090
correlated.edges.emplace_back(edgeid, dist, point, sq_distance);
@@ -100,10 +100,10 @@ CandidateCollector::WithinSquaredDistance(const midgard::PointLL& location,
100100
std::tie(point, sq_distance, segment, offset) = helpers::Project(projector, shape);
101101
}
102102
if (sq_distance <= sq_search_radius) {
103-
const float dist = opp_edge->forward() ? offset : 1.f - offset;
104-
if (dist == 1.f) {
103+
const double dist = opp_edge->forward() ? offset : 1.0 - offset;
104+
if (dist == 1.0) {
105105
snapped_node = opp_edge->endnode();
106-
} else if (dist == 0.f) {
106+
} else if (dist == 0.0) {
107107
snapped_node = edge->endnode();
108108
}
109109
correlated.edges.emplace_back(opp_edgeid, dist, point, sq_distance);

src/meili/geometry_helpers.cc

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#include "meili/geometry_helpers.h"
2+
3+
#include <valhalla/midgard/constants.h>
4+
#include <valhalla/midgard/distanceapproximator.h>
5+
#include <valhalla/midgard/encoded.h>
6+
#include <valhalla/midgard/pointll.h>
7+
#include <valhalla/midgard/util.h>
8+
9+
using namespace valhalla;
10+
using namespace valhalla::meili;
11+
using namespace valhalla::midgard;
12+
13+
namespace valhalla {
14+
namespace meili {
15+
namespace helpers {
16+
17+
// snapped point, squared distance, segment index, offset
18+
std::tuple<PointLL, double, typename std::vector<PointLL>::size_type, double>
19+
Project(const projector_t& p, Shape7Decoder<midgard::PointLL>& shape, double snap_distance) {
20+
PointLL first_point(shape.pop());
21+
auto closest_point = first_point;
22+
auto closest_segment_point = first_point;
23+
double closest_distance = std::numeric_limits<double>::max();
24+
size_t closest_segment = 0;
25+
double closest_partial_length = 0.0;
26+
double total_length = 0.0;
27+
28+
// for each segment
29+
auto u = first_point;
30+
size_t i = 0;
31+
for (; !shape.empty(); ++i) {
32+
// project a onto b where b is the origin vector representing this segment
33+
// and a is the origin vector to the point we are projecting, (a.b/b.b)*b
34+
auto v = shape.pop();
35+
36+
auto projection = p(u, v);
37+
38+
// check if this point is better
39+
const auto distance = p.approx.DistanceSquared(projection);
40+
if (distance < closest_distance) {
41+
closest_point = std::move(projection);
42+
closest_distance = distance;
43+
closest_segment = i;
44+
closest_partial_length = total_length;
45+
closest_segment_point = u;
46+
}
47+
48+
// total edge length
49+
total_length += u.Distance(v);
50+
u = v;
51+
}
52+
53+
// percent_along is a double between 0 and 1 representing the location of
54+
// the closest point on LineString to the given Point, as a fraction
55+
// of total 2d line length.
56+
closest_partial_length += closest_segment_point.Distance(closest_point);
57+
double percent_along =
58+
total_length > 0.0 ? static_cast<double>(closest_partial_length / total_length) : 0.0;
59+
60+
// Not so much "snapping" as recognizing that floating-point has limited precision.
61+
// For example:
62+
// double k = 1e-18; // valid
63+
// double r = 1.0 - k; // sure why not
64+
// Result: r == 1.0 // exactly 1.0?! yep.
65+
//
66+
// Downstream logic looks at percentages along the edge and the opp-edge. We want to be
67+
// sure that values very close to 0.0 snap to 0.0 since downstream math may flip this
68+
// percentage_along to (1.0-percentage_along) if the the edge is not forward, making it
69+
// exactly 1.0. This causes issues because the percentage_along the opp_edge is not
70+
// exactly 0.0, which is not expected.
71+
//
72+
// double has ~16 decimal digits of precision. Hence, we will consider 1e-15 as our
73+
// representative small figure, below which we snap to 0.0 and within that distance
74+
// of 1.0 will snap to 1.0.
75+
constexpr double double_precision_at_one = 1e-15;
76+
constexpr double very_close_to_one = 1.0 - double_precision_at_one;
77+
if (percent_along < double_precision_at_one) {
78+
percent_along = 0.0;
79+
} else if (percent_along > very_close_to_one) {
80+
percent_along = 1.0;
81+
}
82+
83+
// Snap to nearest node using snap_distance
84+
if (total_length * percent_along <= snap_distance) {
85+
closest_point = first_point;
86+
closest_distance = p.approx.DistanceSquared(closest_point);
87+
closest_segment = 0;
88+
percent_along = 0.f;
89+
} else if (total_length * (1.f - percent_along) <= snap_distance) {
90+
closest_point = u;
91+
closest_distance = p.approx.DistanceSquared(closest_point);
92+
closest_segment = i - 1;
93+
percent_along = 1.f;
94+
}
95+
96+
return std::make_tuple(std::move(closest_point), closest_distance, closest_segment, percent_along);
97+
}
98+
99+
} // namespace helpers
100+
} // namespace meili
101+
} // namespace valhalla

src/meili/match_route.cc

+10-3
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,22 @@ void cut_segments(const std::vector<MatchResult>& match_results,
141141
if (!curr_match.is_break_point && curr_idx != last_idx) {
142142
continue;
143143
}
144+
145+
// Allow for some fp-fuzz in this gt comparison
146+
bool prev_gt_curr = prev_match.distance_along > curr_match.distance_along + 1e-3;
147+
144148
// we want to handle to loop by locating the correct target edge by comparing the distance alone
145-
bool loop = prev_match.edgeid == curr_match.edgeid &&
146-
prev_match.distance_along > curr_match.distance_along;
149+
bool loop = (prev_match.edgeid == curr_match.edgeid) && prev_gt_curr;
150+
147151
// if it is a loop, we start the search after the first edge
148152
auto last_segment = std::find_if(first_segment + static_cast<size_t>(loop), segments.end(),
149153
[&curr_match](const EdgeSegment& segment) {
150154
return (segment.edgeid == curr_match.edgeid);
151155
});
152-
assert(last_segment != segments.cend());
156+
157+
if (last_segment == segments.cend()) {
158+
throw std::logic_error("In meili::cutsegments(), unexpectedly unable to locate target edge.");
159+
}
153160

154161
// we need to close the previous edge
155162
size_t old_size = new_segments.size();

test/geometry.cc

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#include "meili/geometry_helpers.h"
2+
#include "midgard/encoded.h"
3+
#include "midgard/polyline2.h"
4+
#include <gtest/gtest.h>
5+
6+
// Foundational geometric unit tests.
7+
8+
using namespace valhalla;
9+
using namespace valhalla::midgard;
10+
11+
namespace {
12+
13+
//===========================================================================
14+
// Take a given real-world road shape. Create a second shape that is the
15+
// reverse shape-point order of the first.
16+
// Take a handful of "gps points" and project them onto both shapes.
17+
// Assert that:
18+
// 0) for percentage_along's that are small, that they're not so small
19+
// that 1.0-percentage_along==1.0. See this line of code in
20+
// candidate_search.cc, WithinSquaredDistance():
21+
// const double dist = edge->forward() ? offset : 1.0 - offset;
22+
// 1) the fwd/rev projections project to the same location
23+
// 2) that the fwd_percentage_along + rev_percentage_along == 1.0 (within tol)
24+
//===========================================================================
25+
TEST(geometry, fwd_rev_projection_tols) {
26+
// Some real world encoded shape data.
27+
const std::string fwd_enc_shape =
28+
{-24, -109, -78, 36, -90, -4, -46, 12, 33, 118, 71, 16, 117, 47, 41, 91,
29+
113, 44, 45, 112, -125, 1, 80, -67, 1, 54, -1, 1, 117, -31, 2, -43,
30+
1, -33, 1, 21, -93, 2, 15, -25, 1, 47, -97, 1, 86, -123, 1, -64,
31+
1, 51, -110, 2, 48, -90, 1, 29, -84, 1, 67, -94, 2, 84, -10, 1,
32+
-86, 1, 82, -106, 2, 70, -32, 1, 64, -96, 3, -20, 1, -126, 2, 112,
33+
-56, 3, 26, -104, 3, 15, -30, 3, 101, -48, 3, -87, 1, -28, 3, -107,
34+
1, -62, 1, 113, 118, -81, 1, 110, 27, 56, -106, 1, 71, -78, 1, -67,
35+
1, -122, 2, -7, 1, -40, 1, -81, 1, -86, 1, 17, 108, 26, -84, 1,
36+
-100, 1, 54, -62, 1, -111, 2, -120, 3, -127, 2, -16, 1, 127, -62, 1,
37+
-43, 1, -32, 1, -67, 2, 38, -49, 1, -100, 1, 64, 14, -74, 1, 105,
38+
-100, 2, -125, 1, -44, 3, -81, 1, -126, 3, -93, 1, -64, 1, -67, 2,
39+
-82, 3, -85, 1, -36, 1, -75, 1, 48, -67, 1, -128, 1, -111, 1, -72,
40+
2, -59, 2, -8, 3, -113, 3, -40, 3, -85, 2, -126, 2, -43, 2, -68,
41+
2, -17, 1, -46, 1, -35, 2, -4, 2, 42, -106, 1, -96, 1, 48, -82,
42+
7, -38, 4, -102, 2, -90, 1, -74, 3, -36, 1, -16, 1, 47, -38, 2,
43+
-105, 2, -70, 1, -37, 1, -34, 2, -85, 2, -44, 1, 117, -106, 2, 91,
44+
-42, 2, -68, 1, -18, 2, -126, 1, -62, 7, -62, 2, -4, 2, -106, 1,
45+
-2, 3, -118, 1, -78, 1, -118, 1};
46+
47+
std::vector<PointLL> fwd_shape_points = decode7<std::vector<PointLL>>(fwd_enc_shape);
48+
std::vector<PointLL> rev_shape_points(fwd_shape_points);
49+
std::reverse(rev_shape_points.begin(), rev_shape_points.end());
50+
std::string rev_enc_shape = midgard::encode7(rev_shape_points);
51+
52+
// This first check ensures that the length of a real-world series of segments
53+
// is equal when measured forwards and backwards. This is reasonable to expect.
54+
// The possible issues that might come into play is if our underlying Distance()
55+
// computation is 1) too approximate, and/or 2) employs some sort of curved
56+
// earth bias based on the first point of each segment.
57+
double fwd_shape_length = midgard::length(fwd_shape_points);
58+
double rev_shape_length = midgard::length(rev_shape_points);
59+
EXPECT_TRUE(std::fabs(fwd_shape_length - rev_shape_length) < 1e-5);
60+
61+
// Project these gps points onto the fwd-shape and the rev-shape.
62+
// See that they project to the same point and that their projection's
63+
// distance-along percentages sum to 1.0.
64+
const PointLL gps_points[] = {{13.262609100000001, 38.159699600000003},
65+
{13.262637200000002, 38.159659599999998},
66+
{13.262741800000005, 38.159575799999997},
67+
{13.262665099999999, 38.159486100000001},
68+
{13.262594400000001, 38.159471100000003},
69+
{13.262595700000003, 38.159450400000005}};
70+
71+
size_t i = 0;
72+
for (const auto& gps_point : gps_points) {
73+
// meili::helpers::Project() consumes the incoming shape so we have to
74+
// create it with each iteration.
75+
midgard::Shape7Decoder<midgard::PointLL> fwd_shape(fwd_enc_shape.c_str(), fwd_enc_shape.size());
76+
77+
double fwd_sq_distance = -1.0;
78+
size_t fwd_segment;
79+
double fwd_percentage_along = -1.0;
80+
PointLL fwd_proj_point;
81+
std::tie(fwd_proj_point, fwd_sq_distance, fwd_segment, fwd_percentage_along) =
82+
valhalla::meili::helpers::Project(gps_point, fwd_shape);
83+
84+
// make sure the fwd_percentage_along is big enough that it doesn't
85+
// disappear when subtracted from 1.0.
86+
if (fwd_percentage_along > 0.0) {
87+
double reverse_pct_along = 1.0 - fwd_percentage_along;
88+
ASSERT_NE(reverse_pct_along, 1.0);
89+
}
90+
91+
// meili::helpers::Project() consumes the incoming shape so we have to
92+
// create it with each iteration.
93+
midgard::Shape7Decoder<midgard::PointLL> rev_shape(rev_enc_shape.c_str(), rev_enc_shape.size());
94+
95+
double rev_sq_distance = -1.0;
96+
size_t rev_segment;
97+
double rev_percentage_along = -1.0;
98+
PointLL rev_proj_point;
99+
std::tie(rev_proj_point, rev_sq_distance, rev_segment, rev_percentage_along) =
100+
valhalla::meili::helpers::Project(gps_point, rev_shape);
101+
102+
// make sure the rev_percentage_along is big enough that it doesn't
103+
// disappear when subtracted from 1.0.
104+
if (rev_percentage_along > 0.0) {
105+
double forward_pct_along = 1.0 - rev_percentage_along;
106+
ASSERT_NE(forward_pct_along, 1.0);
107+
}
108+
109+
// The first gps point is a special case! Without the fix, fwd_percentage_along
110+
// is ~1e-9; the fix is to snap it to 0.f.
111+
if (i++ == 0) {
112+
ASSERT_EQ(fwd_percentage_along, 0.0);
113+
ASSERT_EQ(rev_percentage_along, 1.0);
114+
}
115+
116+
// 6 decimal digits is roughly 0.1 m, a reasonable expectation
117+
constexpr double tenth_of_meter_in_deg = 0.000001;
118+
ASSERT_TRUE(std::fabs(fwd_proj_point.lat() - rev_proj_point.lat()) < tenth_of_meter_in_deg);
119+
ASSERT_TRUE(std::fabs(fwd_proj_point.lng() - rev_proj_point.lng()) < tenth_of_meter_in_deg);
120+
121+
// wherever we project, the fwd+rev percentages must add up to 1.0 (within tol)
122+
double total_percent = fwd_percentage_along + rev_percentage_along;
123+
ASSERT_TRUE(std::fabs(total_percent - 1.0) < 0.0001);
124+
}
125+
}
126+
127+
} // anonymous namespace

valhalla/meili/geometry_helpers.h

+3-58
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
#pragma once
33
#include <cmath>
44

5-
#include <algorithm>
65
#include <vector>
76

87
#include <valhalla/midgard/aabb2.h>
@@ -16,65 +15,11 @@ namespace valhalla {
1615
namespace meili {
1716
namespace helpers {
1817

19-
// snapped point, sqaured distance, segment index, offset
20-
inline std::tuple<midgard::PointLL, float, typename std::vector<midgard::PointLL>::size_type, float>
18+
// snapped point, squared distance, segment index, offset
19+
std::tuple<midgard::PointLL, double, typename std::vector<midgard::PointLL>::size_type, double>
2120
Project(const midgard::projector_t& p,
2221
midgard::Shape7Decoder<midgard::PointLL>& shape,
23-
float snap_distance = 0.f) {
24-
midgard::PointLL first_point(shape.pop());
25-
auto closest_point = first_point;
26-
auto closest_segment_point = first_point;
27-
float closest_distance = p.approx.DistanceSquared(closest_point);
28-
size_t closest_segment = 0;
29-
float closest_partial_length = 0.f;
30-
float total_length = 0.f;
31-
32-
// for each segment
33-
auto u = first_point;
34-
size_t i = 0;
35-
for (; !shape.empty(); ++i) {
36-
// project a onto b where b is the origin vector representing this segment
37-
// and a is the origin vector to the point we are projecting, (a.b/b.b)*b
38-
auto v = shape.pop();
39-
auto point = p(u, v);
40-
41-
// check if this point is better
42-
const auto distance = p.approx.DistanceSquared(point);
43-
if (distance < closest_distance) {
44-
closest_point = std::move(point);
45-
closest_distance = distance;
46-
closest_segment = i;
47-
closest_partial_length = total_length;
48-
closest_segment_point = u;
49-
}
50-
51-
// total edge length
52-
total_length += u.Distance(v);
53-
u = v;
54-
}
55-
56-
// Offset is a float between 0 and 1 representing the location of
57-
// the closest point on LineString to the given Point, as a fraction
58-
// of total 2d line length.
59-
closest_partial_length += closest_segment_point.Distance(closest_point);
60-
float offset = total_length > 0.f ? static_cast<float>(closest_partial_length / total_length) : 0.f;
61-
offset = std::max(0.f, std::min(offset, 1.f));
62-
63-
// Snap to vertices if it's close
64-
if (total_length * offset <= snap_distance) {
65-
closest_point = first_point;
66-
closest_distance = p.approx.DistanceSquared(closest_point);
67-
closest_segment = 0;
68-
offset = 0.f;
69-
} else if (total_length * (1.f - offset) <= snap_distance) {
70-
closest_point = u;
71-
closest_distance = p.approx.DistanceSquared(closest_point);
72-
closest_segment = i - 1;
73-
offset = 1.f;
74-
}
75-
76-
return std::make_tuple(std::move(closest_point), closest_distance, closest_segment, offset);
77-
}
22+
double snap_distance = 0.0);
7823

7924
} // namespace helpers
8025
} // namespace meili

0 commit comments

Comments
 (0)