Skip to content

Commit 0283967

Browse files
committed
feat: Add configurable max_collapse_distance parameter to Lua profiles
Implements configurable max_collapse_distance parameter to address issue #6171. This allows precise control over turn instruction collapsing in guidance, particularly important for pedestrian routing where short crossings need to be preserved for safety analysis. Changes: * Add max_collapse_distance property to ProfileProperties with getter/setter * Expose parameter to Lua scripting environment for profile configuration * Add GetMaxCollapseDistance() method to DataFacade interface * Replace hardcoded MAX_COLLAPSE_DISTANCE constant with thread-local variable * Update guidance collapse functions to use configurable parameter * Add documentation and example profile demonstrating usage The parameter defaults to 30.0 meters (preserving existing behavior) but can now be customized in Lua profiles: ```lua function setup() return { properties = { max_collapse_distance = 10.0, -- Custom value in meters }, } end ``` This enables sepcific usages who requires precise pedestrian routing instructions by preventing short road crossings from being collapsed into longer segments. Fixes #6171 Developed with the help of Claude Sonnet 4 from Anthropic AI.
1 parent 0ae940c commit 0283967

17 files changed

+608
-17
lines changed

docs/profiles.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ continue_straight_at_waypoint | Boolean | Must the route continue straig
103103
max_speed_for_map_matching | Float | Maximum vehicle speed to be assumed in matching (in m/s)
104104
max_turn_weight | Float | Maximum turn penalty weight
105105
force_split_edges | Boolean | True value forces a split of forward and backward edges of extracted ways and guarantees that `process_segment` will be called for all segments (default `false`)
106+
max_collapse_distance | Float | Maximum distance in meters for collapsing turn instructions in guidance (default `30.0`)
106107

107108

108109
The following additional global properties can be set in the hash you return in the `setup` function:

include/engine/api/route_api.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,7 @@ class RouteAPI : public BaseAPI
979979

980980
guidance::trimShortSegments(steps, leg_geometry);
981981
leg.steps = guidance::handleRoundabouts(std::move(steps));
982-
leg.steps = guidance::collapseTurnInstructions(std::move(leg.steps));
982+
leg.steps = guidance::collapseTurnInstructions(BaseAPI::facade, std::move(leg.steps));
983983
leg.steps = guidance::anticipateLaneChange(std::move(leg.steps));
984984
leg.steps = guidance::buildIntersections(std::move(leg.steps));
985985
leg.steps = guidance::suppressShortNameSegments(std::move(leg.steps));

include/engine/datafacade/contiguous_internalmem_datafacade.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,11 @@ class ContiguousInternalMemoryDataFacadeBase : public BaseDataFacade
499499
return m_profile_properties->max_speed_for_map_matching;
500500
}
501501

502+
double GetMaxCollapseDistance() const override final
503+
{
504+
return m_profile_properties->GetMaxCollapseDistance();
505+
}
506+
502507
const char *GetWeightName() const override final { return m_profile_properties->weight_name; }
503508

504509
unsigned GetWeightPrecision() const override final

include/engine/datafacade/datafacade_base.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ class BaseDataFacade
164164

165165
virtual double GetMapMatchingMaxSpeed() const = 0;
166166

167+
virtual double GetMaxCollapseDistance() const = 0;
168+
167169
virtual const char *GetWeightName() const = 0;
168170

169171
virtual unsigned GetWeightPrecision() const = 0;

include/engine/guidance/collapse_turns.hpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
#include <type_traits>
66
#include <vector>
77

8+
namespace osrm::engine::datafacade
9+
{
10+
class BaseDataFacade;
11+
}
12+
813
namespace osrm::engine::guidance
914
{
1015

1116
// Multiple possible reasons can result in unnecessary/confusing instructions
1217
// Collapsing such turns into a single turn instruction, we give a clearer
1318
// set of instructions that is not cluttered by unnecessary turns/name changes.
14-
[[nodiscard]] std::vector<RouteStep> collapseTurnInstructions(std::vector<RouteStep> steps);
19+
[[nodiscard]] std::vector<RouteStep> collapseTurnInstructions(const datafacade::BaseDataFacade &facade, std::vector<RouteStep> steps);
1520

1621
// Multiple possible reasons can result in unnecessary/confusing instructions
1722
// A prime example would be a segregated intersection. Turning around at this

include/engine/guidance/collapsing_utility.hpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ namespace osrm::engine::guidance
1515
using RouteSteps = std::vector<RouteStep>;
1616
using RouteStepIterator = typename RouteSteps::iterator;
1717
const constexpr std::size_t MIN_END_OF_ROAD_INTERSECTIONS = std::size_t{2};
18-
const constexpr double MAX_COLLAPSE_DISTANCE = 30.0;
18+
// Default value for max collapse distance
19+
const constexpr double DEFAULT_MAX_COLLAPSE_DISTANCE = 30.0;
20+
21+
// Thread-local storage for configurable max collapse distance
22+
extern thread_local double current_max_collapse_distance;
1923
// a bit larger than 100 to avoid oscillation in tests
2024
const constexpr double NAME_SEGMENT_CUTOFF_LENGTH = 105.0;
2125
const double constexpr STRAIGHT_ANGLE = 180.;

include/extractor/profile_properties.hpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct ProfileProperties
2828
max_speed_for_map_matching(DEFAULT_MAX_SPEED), continue_straight_at_waypoint(true),
2929
use_turn_restrictions(false), left_hand_driving(false), fallback_to_duration(true),
3030
weight_name{"duration"}, class_names{{}}, excludable_classes{{}},
31-
call_tagless_node_function(true)
31+
call_tagless_node_function(true), max_collapse_distance(30.0)
3232
{
3333
std::fill(excludable_classes.begin(), excludable_classes.end(), INAVLID_CLASS_DATA);
3434
BOOST_ASSERT(weight_name[MAX_WEIGHT_NAME_LENGTH] == '\0');
@@ -55,6 +55,13 @@ struct ProfileProperties
5555
max_speed_for_map_matching = max_speed_for_map_matching_;
5656
}
5757

58+
double GetMaxCollapseDistance() const { return max_collapse_distance; }
59+
60+
void SetMaxCollapseDistance(const double max_collapse_distance_)
61+
{
62+
max_collapse_distance = max_collapse_distance_;
63+
}
64+
5865
void SetWeightName(const std::string &name)
5966
{
6067
auto count = std::min<std::size_t>(name.length(), MAX_WEIGHT_NAME_LENGTH) + 1;
@@ -135,6 +142,8 @@ struct ProfileProperties
135142
unsigned weight_precision = 1;
136143
bool force_split_edges = false;
137144
bool call_tagless_node_function = true;
145+
//! maximum distance for collapsing turns in guidance (in meters)
146+
double max_collapse_distance;
138147
};
139148
} // namespace osrm::extractor
140149

profiles/foot_test.lua

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
-- Test Foot profile with configurable max_collapse_distance
2+
3+
api_version = 2
4+
5+
Set = require('lib/set')
6+
Sequence = require('lib/sequence')
7+
Handlers = require("lib/way_handlers")
8+
find_access_tag = require("lib/access").find_access_tag
9+
10+
function setup()
11+
local walking_speed = 5
12+
return {
13+
properties = {
14+
weight_name = 'duration',
15+
max_speed_for_map_matching = 40/3.6, -- kmph -> m/s
16+
call_tagless_node_function = false,
17+
traffic_signal_penalty = 2,
18+
u_turn_penalty = 2,
19+
continue_straight_at_waypoint = false,
20+
use_turn_restrictions = false,
21+
-- Test with a smaller max_collapse_distance for precise pedestrian routing
22+
max_collapse_distance = 10.0, -- 10 meters instead of default 30
23+
},
24+
25+
default_mode = mode.walking,
26+
default_speed = walking_speed,
27+
oneway_handling = 'specific', -- respect 'oneway:foot' but not 'oneway'
28+
29+
barrier_blacklist = Set {
30+
'yes',
31+
'wall',
32+
'fence'
33+
},
34+
35+
access_tag_whitelist = Set {
36+
'yes',
37+
'foot',
38+
'permissive',
39+
'designated'
40+
},
41+
42+
access_tag_blacklist = Set {
43+
'no',
44+
'agricultural',
45+
'forestry',
46+
'private',
47+
'delivery',
48+
},
49+
50+
restricted_access_tag_list = Set { },
51+
52+
restricted_highway_whitelist = Set { },
53+
54+
construction_whitelist = Set {},
55+
56+
access_tags_hierarchy = Sequence {
57+
'foot',
58+
'access'
59+
},
60+
61+
-- tags disallow access to in combination with highway=service
62+
service_access_tag_blacklist = Set { },
63+
64+
restrictions = Sequence {
65+
'foot'
66+
},
67+
68+
-- list of suffixes to suppress in name change instructions
69+
suffix_list = Set {
70+
'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'North', 'South', 'West', 'East'
71+
},
72+
73+
avoid = Set {
74+
'impassable',
75+
'proposed'
76+
},
77+
78+
speeds = Sequence {
79+
highway = {
80+
primary = walking_speed,
81+
primary_link = walking_speed,
82+
secondary = walking_speed,
83+
secondary_link = walking_speed,
84+
tertiary = walking_speed,
85+
tertiary_link = walking_speed,
86+
unclassified = walking_speed,
87+
residential = walking_speed,
88+
road = walking_speed,
89+
living_street = walking_speed,
90+
service = walking_speed,
91+
track = walking_speed,
92+
path = walking_speed,
93+
steps = walking_speed,
94+
pedestrian = walking_speed,
95+
platform = walking_speed,
96+
footway = walking_speed,
97+
pier = walking_speed,
98+
},
99+
100+
railway = {
101+
platform = walking_speed
102+
},
103+
104+
amenity = {
105+
parking = walking_speed,
106+
parking_entrance= walking_speed
107+
},
108+
109+
man_made = {
110+
pier = walking_speed
111+
},
112+
113+
leisure = {
114+
track = walking_speed
115+
}
116+
},
117+
118+
route_speeds = {
119+
ferry = 5
120+
},
121+
122+
bridge_speeds = {
123+
},
124+
125+
surface_speeds = {
126+
fine_gravel = walking_speed*0.75,
127+
gravel = walking_speed*0.75,
128+
pebblestone = walking_speed*0.75,
129+
mud = walking_speed*0.5,
130+
sand = walking_speed*0.5
131+
},
132+
133+
tracktype_speeds = {
134+
},
135+
136+
smoothness_speeds = {
137+
}
138+
}
139+
end
140+
141+
function process_node(profile, node, result)
142+
-- parse access and barrier tags
143+
local access = find_access_tag(node, profile.access_tags_hierarchy)
144+
if access then
145+
if profile.access_tag_blacklist[access] then
146+
result.barrier = true
147+
end
148+
else
149+
local barrier = node:get_value_by_key("barrier")
150+
if barrier then
151+
-- make an exception for rising bollard barriers
152+
local bollard = node:get_value_by_key("bollard")
153+
local rising_bollard = bollard and "rising" == bollard
154+
155+
if profile.barrier_blacklist[barrier] and not rising_bollard then
156+
result.barrier = true
157+
end
158+
end
159+
end
160+
161+
-- check if node is a traffic light
162+
local tag = node:get_value_by_key("highway")
163+
if "traffic_signals" == tag then
164+
-- Direction should only apply to vehicles
165+
result.traffic_lights = true
166+
end
167+
end
168+
169+
-- main entry point for processsing a way
170+
function process_way(profile, way, result)
171+
-- the intial filtering of ways based on presence of tags
172+
-- affects processing times significantly, because all ways
173+
-- have to be checked.
174+
-- to increase performance, prefetching and intial tag check
175+
-- is done in directly instead of via a handler.
176+
177+
-- in general we should try to abort as soon as
178+
-- possible if the way is not routable, to avoid doing
179+
-- unnecessary work. this implies we should check things that
180+
-- commonly forbids access early, and handle edge cases later.
181+
182+
-- data table for storing intermediate values during processing
183+
local data = {
184+
-- prefetch tags
185+
highway = way:get_value_by_key('highway'),
186+
bridge = way:get_value_by_key('bridge'),
187+
route = way:get_value_by_key('route'),
188+
leisure = way:get_value_by_key('leisure'),
189+
man_made = way:get_value_by_key('man_made'),
190+
railway = way:get_value_by_key('railway'),
191+
platform = way:get_value_by_key('platform'),
192+
amenity = way:get_value_by_key('amenity'),
193+
public_transport = way:get_value_by_key('public_transport')
194+
}
195+
196+
-- perform an quick initial check and abort if the way is
197+
-- obviously not routable. here we require at least one
198+
-- of the prefetched tags to be present, ie. the data table
199+
-- cannot be empty
200+
if next(data) == nil then -- is the data table empty?
201+
return
202+
end
203+
204+
local handlers = Sequence {
205+
-- set the default mode for this profile. if can be changed later
206+
-- in case it turns we're e.g. on a ferry
207+
WayHandlers.default_mode,
208+
209+
-- check various tags that could indicate that the way is not
210+
-- routable. this includes things like status=impassable,
211+
-- toll=yes and oneway=reversible
212+
WayHandlers.blocked_ways,
213+
214+
-- determine access status by checking our hierarchy of
215+
-- access tags, e.g: motorcar, motor_vehicle, vehicle
216+
WayHandlers.access,
217+
218+
-- check whether forward/backward directons are routable
219+
WayHandlers.oneway,
220+
221+
-- check whether forward/backward directons are routable
222+
WayHandlers.destinations,
223+
224+
-- check whether we're using a special transport mode
225+
WayHandlers.ferries,
226+
WayHandlers.movables,
227+
228+
-- compute speed taking into account way type, maxspeed tags, etc.
229+
WayHandlers.speed,
230+
WayHandlers.surface,
231+
232+
-- handle turn lanes and road classification, used for guidance
233+
WayHandlers.classification,
234+
235+
-- handle various other flags
236+
WayHandlers.roundabouts,
237+
WayHandlers.startpoint,
238+
239+
-- set name, ref and pronunciation
240+
WayHandlers.names,
241+
242+
-- set weight properties of the way
243+
WayHandlers.weights
244+
}
245+
246+
WayHandlers.run(profile, way, result, data, handlers)
247+
end
248+
249+
function process_turn (profile, turn)
250+
turn.duration = 0.
251+
252+
if turn.direction_modifier == direction_modifier.u_turn then
253+
turn.duration = turn.duration + profile.properties.u_turn_penalty
254+
end
255+
256+
if turn.has_traffic_light then
257+
turn.duration = profile.properties.traffic_signal_penalty
258+
end
259+
if profile.properties.weight_name == 'routability' then
260+
-- penalize turns from non-local access only segments onto local access only tags
261+
if not turn.source_restricted and turn.target_restricted then
262+
turn.weight = turn.weight + 3000
263+
end
264+
end
265+
end
266+
267+
return {
268+
setup = setup,
269+
process_way = process_way,
270+
process_node = process_node,
271+
process_turn = process_turn
272+
}

0 commit comments

Comments
 (0)