diff --git a/docs/profiles.md b/docs/profiles.md index 54c4b13348e..5c500ef07c1 100644 --- a/docs/profiles.md +++ b/docs/profiles.md @@ -103,6 +103,7 @@ continue_straight_at_waypoint | Boolean | Must the route continue straig max_speed_for_map_matching | Float | Maximum vehicle speed to be assumed in matching (in m/s) max_turn_weight | Float | Maximum turn penalty weight 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`) +max_collapse_distance | Float | Maximum distance in meters for collapsing turn instructions in guidance (default `30.0`) The following additional global properties can be set in the hash you return in the `setup` function: diff --git a/include/engine/api/route_api.hpp b/include/engine/api/route_api.hpp index 08431a2cf5d..d0f6f5b7c68 100644 --- a/include/engine/api/route_api.hpp +++ b/include/engine/api/route_api.hpp @@ -979,7 +979,7 @@ class RouteAPI : public BaseAPI guidance::trimShortSegments(steps, leg_geometry); leg.steps = guidance::handleRoundabouts(std::move(steps)); - leg.steps = guidance::collapseTurnInstructions(std::move(leg.steps)); + leg.steps = guidance::collapseTurnInstructions(BaseAPI::facade, std::move(leg.steps)); leg.steps = guidance::anticipateLaneChange(std::move(leg.steps)); leg.steps = guidance::buildIntersections(std::move(leg.steps)); leg.steps = guidance::suppressShortNameSegments(std::move(leg.steps)); diff --git a/include/engine/datafacade/contiguous_internalmem_datafacade.hpp b/include/engine/datafacade/contiguous_internalmem_datafacade.hpp index ca380ecefea..8346c56d47e 100644 --- a/include/engine/datafacade/contiguous_internalmem_datafacade.hpp +++ b/include/engine/datafacade/contiguous_internalmem_datafacade.hpp @@ -499,6 +499,11 @@ class ContiguousInternalMemoryDataFacadeBase : public BaseDataFacade return m_profile_properties->max_speed_for_map_matching; } + double GetMaxCollapseDistance() const override final + { + return m_profile_properties->GetMaxCollapseDistance(); + } + const char *GetWeightName() const override final { return m_profile_properties->weight_name; } unsigned GetWeightPrecision() const override final diff --git a/include/engine/datafacade/datafacade_base.hpp b/include/engine/datafacade/datafacade_base.hpp index b42584b3b4f..4fa2b74eafc 100644 --- a/include/engine/datafacade/datafacade_base.hpp +++ b/include/engine/datafacade/datafacade_base.hpp @@ -164,6 +164,8 @@ class BaseDataFacade virtual double GetMapMatchingMaxSpeed() const = 0; + virtual double GetMaxCollapseDistance() const = 0; + virtual const char *GetWeightName() const = 0; virtual unsigned GetWeightPrecision() const = 0; diff --git a/include/engine/guidance/collapse_turns.hpp b/include/engine/guidance/collapse_turns.hpp index bfd1140f20e..978e01e1c8d 100644 --- a/include/engine/guidance/collapse_turns.hpp +++ b/include/engine/guidance/collapse_turns.hpp @@ -5,13 +5,18 @@ #include #include +namespace osrm::engine::datafacade +{ +class BaseDataFacade; +} + namespace osrm::engine::guidance { // Multiple possible reasons can result in unnecessary/confusing instructions // Collapsing such turns into a single turn instruction, we give a clearer // set of instructions that is not cluttered by unnecessary turns/name changes. -[[nodiscard]] std::vector collapseTurnInstructions(std::vector steps); +[[nodiscard]] std::vector collapseTurnInstructions(const datafacade::BaseDataFacade &facade, std::vector steps); // Multiple possible reasons can result in unnecessary/confusing instructions // A prime example would be a segregated intersection. Turning around at this diff --git a/include/engine/guidance/collapsing_utility.hpp b/include/engine/guidance/collapsing_utility.hpp index 983a2eaebfb..8edb3534325 100644 --- a/include/engine/guidance/collapsing_utility.hpp +++ b/include/engine/guidance/collapsing_utility.hpp @@ -15,7 +15,11 @@ namespace osrm::engine::guidance using RouteSteps = std::vector; using RouteStepIterator = typename RouteSteps::iterator; const constexpr std::size_t MIN_END_OF_ROAD_INTERSECTIONS = std::size_t{2}; -const constexpr double MAX_COLLAPSE_DISTANCE = 30.0; +// Default value for max collapse distance +const constexpr double DEFAULT_MAX_COLLAPSE_DISTANCE = 30.0; + +// Thread-local storage for configurable max collapse distance +extern thread_local double current_max_collapse_distance; // a bit larger than 100 to avoid oscillation in tests const constexpr double NAME_SEGMENT_CUTOFF_LENGTH = 105.0; const double constexpr STRAIGHT_ANGLE = 180.; diff --git a/include/extractor/profile_properties.hpp b/include/extractor/profile_properties.hpp index 371155b17fc..d3e1cb5614c 100644 --- a/include/extractor/profile_properties.hpp +++ b/include/extractor/profile_properties.hpp @@ -28,7 +28,7 @@ struct ProfileProperties max_speed_for_map_matching(DEFAULT_MAX_SPEED), continue_straight_at_waypoint(true), use_turn_restrictions(false), left_hand_driving(false), fallback_to_duration(true), weight_name{"duration"}, class_names{{}}, excludable_classes{{}}, - call_tagless_node_function(true) + call_tagless_node_function(true), max_collapse_distance(30.0) { std::fill(excludable_classes.begin(), excludable_classes.end(), INAVLID_CLASS_DATA); BOOST_ASSERT(weight_name[MAX_WEIGHT_NAME_LENGTH] == '\0'); @@ -55,6 +55,13 @@ struct ProfileProperties max_speed_for_map_matching = max_speed_for_map_matching_; } + double GetMaxCollapseDistance() const { return max_collapse_distance; } + + void SetMaxCollapseDistance(const double max_collapse_distance_) + { + max_collapse_distance = max_collapse_distance_; + } + void SetWeightName(const std::string &name) { auto count = std::min(name.length(), MAX_WEIGHT_NAME_LENGTH) + 1; @@ -135,6 +142,8 @@ struct ProfileProperties unsigned weight_precision = 1; bool force_split_edges = false; bool call_tagless_node_function = true; + //! maximum distance for collapsing turns in guidance (in meters) + double max_collapse_distance; }; } // namespace osrm::extractor diff --git a/profiles/foot_test.lua b/profiles/foot_test.lua new file mode 100644 index 00000000000..8ffafdc5b11 --- /dev/null +++ b/profiles/foot_test.lua @@ -0,0 +1,272 @@ +-- Test Foot profile with configurable max_collapse_distance + +api_version = 2 + +Set = require('lib/set') +Sequence = require('lib/sequence') +Handlers = require("lib/way_handlers") +find_access_tag = require("lib/access").find_access_tag + +function setup() + local walking_speed = 5 + return { + properties = { + weight_name = 'duration', + max_speed_for_map_matching = 40/3.6, -- kmph -> m/s + call_tagless_node_function = false, + traffic_signal_penalty = 2, + u_turn_penalty = 2, + continue_straight_at_waypoint = false, + use_turn_restrictions = false, + -- Test with a smaller max_collapse_distance for precise pedestrian routing + max_collapse_distance = 10.0, -- 10 meters instead of default 30 + }, + + default_mode = mode.walking, + default_speed = walking_speed, + oneway_handling = 'specific', -- respect 'oneway:foot' but not 'oneway' + + barrier_blacklist = Set { + 'yes', + 'wall', + 'fence' + }, + + access_tag_whitelist = Set { + 'yes', + 'foot', + 'permissive', + 'designated' + }, + + access_tag_blacklist = Set { + 'no', + 'agricultural', + 'forestry', + 'private', + 'delivery', + }, + + restricted_access_tag_list = Set { }, + + restricted_highway_whitelist = Set { }, + + construction_whitelist = Set {}, + + access_tags_hierarchy = Sequence { + 'foot', + 'access' + }, + + -- tags disallow access to in combination with highway=service + service_access_tag_blacklist = Set { }, + + restrictions = Sequence { + 'foot' + }, + + -- list of suffixes to suppress in name change instructions + suffix_list = Set { + 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'North', 'South', 'West', 'East' + }, + + avoid = Set { + 'impassable', + 'proposed' + }, + + speeds = Sequence { + highway = { + primary = walking_speed, + primary_link = walking_speed, + secondary = walking_speed, + secondary_link = walking_speed, + tertiary = walking_speed, + tertiary_link = walking_speed, + unclassified = walking_speed, + residential = walking_speed, + road = walking_speed, + living_street = walking_speed, + service = walking_speed, + track = walking_speed, + path = walking_speed, + steps = walking_speed, + pedestrian = walking_speed, + platform = walking_speed, + footway = walking_speed, + pier = walking_speed, + }, + + railway = { + platform = walking_speed + }, + + amenity = { + parking = walking_speed, + parking_entrance= walking_speed + }, + + man_made = { + pier = walking_speed + }, + + leisure = { + track = walking_speed + } + }, + + route_speeds = { + ferry = 5 + }, + + bridge_speeds = { + }, + + surface_speeds = { + fine_gravel = walking_speed*0.75, + gravel = walking_speed*0.75, + pebblestone = walking_speed*0.75, + mud = walking_speed*0.5, + sand = walking_speed*0.5 + }, + + tracktype_speeds = { + }, + + smoothness_speeds = { + } + } +end + +function process_node(profile, node, result) + -- parse access and barrier tags + local access = find_access_tag(node, profile.access_tags_hierarchy) + if access then + if profile.access_tag_blacklist[access] then + result.barrier = true + end + else + local barrier = node:get_value_by_key("barrier") + if barrier then + -- make an exception for rising bollard barriers + local bollard = node:get_value_by_key("bollard") + local rising_bollard = bollard and "rising" == bollard + + if profile.barrier_blacklist[barrier] and not rising_bollard then + result.barrier = true + end + end + end + + -- check if node is a traffic light + local tag = node:get_value_by_key("highway") + if "traffic_signals" == tag then + -- Direction should only apply to vehicles + result.traffic_lights = true + end +end + +-- main entry point for processsing a way +function process_way(profile, way, result) + -- the intial filtering of ways based on presence of tags + -- affects processing times significantly, because all ways + -- have to be checked. + -- to increase performance, prefetching and intial tag check + -- is done in directly instead of via a handler. + + -- in general we should try to abort as soon as + -- possible if the way is not routable, to avoid doing + -- unnecessary work. this implies we should check things that + -- commonly forbids access early, and handle edge cases later. + + -- data table for storing intermediate values during processing + local data = { + -- prefetch tags + highway = way:get_value_by_key('highway'), + bridge = way:get_value_by_key('bridge'), + route = way:get_value_by_key('route'), + leisure = way:get_value_by_key('leisure'), + man_made = way:get_value_by_key('man_made'), + railway = way:get_value_by_key('railway'), + platform = way:get_value_by_key('platform'), + amenity = way:get_value_by_key('amenity'), + public_transport = way:get_value_by_key('public_transport') + } + + -- perform an quick initial check and abort if the way is + -- obviously not routable. here we require at least one + -- of the prefetched tags to be present, ie. the data table + -- cannot be empty + if next(data) == nil then -- is the data table empty? + return + end + + local handlers = Sequence { + -- set the default mode for this profile. if can be changed later + -- in case it turns we're e.g. on a ferry + WayHandlers.default_mode, + + -- check various tags that could indicate that the way is not + -- routable. this includes things like status=impassable, + -- toll=yes and oneway=reversible + WayHandlers.blocked_ways, + + -- determine access status by checking our hierarchy of + -- access tags, e.g: motorcar, motor_vehicle, vehicle + WayHandlers.access, + + -- check whether forward/backward directons are routable + WayHandlers.oneway, + + -- check whether forward/backward directons are routable + WayHandlers.destinations, + + -- check whether we're using a special transport mode + WayHandlers.ferries, + WayHandlers.movables, + + -- compute speed taking into account way type, maxspeed tags, etc. + WayHandlers.speed, + WayHandlers.surface, + + -- handle turn lanes and road classification, used for guidance + WayHandlers.classification, + + -- handle various other flags + WayHandlers.roundabouts, + WayHandlers.startpoint, + + -- set name, ref and pronunciation + WayHandlers.names, + + -- set weight properties of the way + WayHandlers.weights + } + + WayHandlers.run(profile, way, result, data, handlers) +end + +function process_turn (profile, turn) + turn.duration = 0. + + if turn.direction_modifier == direction_modifier.u_turn then + turn.duration = turn.duration + profile.properties.u_turn_penalty + end + + if turn.has_traffic_light then + turn.duration = profile.properties.traffic_signal_penalty + end + if profile.properties.weight_name == 'routability' then + -- penalize turns from non-local access only segments onto local access only tags + if not turn.source_restricted and turn.target_restricted then + turn.weight = turn.weight + 3000 + end + end +end + +return { + setup = setup, + process_way = process_way, + process_node = process_node, + process_turn = process_turn +} diff --git a/src/engine/guidance/collapse_scenario_detection.cpp b/src/engine/guidance/collapse_scenario_detection.cpp index e9c00c94035..b9ca3325aed 100644 --- a/src/engine/guidance/collapse_scenario_detection.cpp +++ b/src/engine/guidance/collapse_scenario_detection.cpp @@ -1,4 +1,5 @@ #include "engine/guidance/collapse_scenario_detection.hpp" +#include "engine/guidance/collapsing_utility.hpp" #include "guidance/constants.hpp" #include "util/bearing.hpp" @@ -27,7 +28,7 @@ bool isLinkRoad(const RouteStep &pre_link_step, const RouteStep &link_step, const RouteStep &post_link_step) { - const constexpr double MAX_LINK_ROAD_LENGTH = 2 * MAX_COLLAPSE_DISTANCE; + const auto MAX_LINK_ROAD_LENGTH = 2 * current_max_collapse_distance; const auto is_short = link_step.distance <= MAX_LINK_ROAD_LENGTH; const auto unnamed = link_step.name.empty(); const auto between_named = !pre_link_step.name.empty() && !post_link_step.name.empty(); @@ -38,7 +39,7 @@ bool isLinkRoad(const RouteStep &pre_link_step, // Just like a link step, but shorter and no name restrictions. bool isShortAndUndisturbed(const RouteStep &step) { - const auto is_short = step.distance <= MAX_COLLAPSE_DISTANCE; + const auto is_short = step.distance <= current_max_collapse_distance; return is_short && noIntermediaryIntersections(step); } @@ -269,8 +270,8 @@ bool suppressedStraightBetweenTurns(const RouteStepIterator step_entering_inters return false; const auto both_short_enough = - step_entering_intersection->distance < 0.8 * MAX_COLLAPSE_DISTANCE && - step_at_center_of_intersection->distance < 0.8 * MAX_COLLAPSE_DISTANCE; + step_entering_intersection->distance < 0.8 * current_max_collapse_distance && + step_at_center_of_intersection->distance < 0.8 * current_max_collapse_distance; const auto similar_length = (step_entering_intersection->distance < 5 && @@ -317,13 +318,13 @@ bool maneuverSucceededByNameChange(const RouteStepIterator step_entering_interse const auto is_strong_name_change = hasTurnType(*step_leaving_intersection, TurnType::NewName) && hasModifier(*step_leaving_intersection, DirectionModifier::Straight) && - step_entering_intersection->distance <= 1.5 * MAX_COLLAPSE_DISTANCE; + step_entering_intersection->distance <= 1.5 * current_max_collapse_distance; // also allow a bit more, if the new name is without choice const auto is_choiceless_name_change = hasTurnType(*step_leaving_intersection, TurnType::NewName) && numberOfAllowedTurns(*step_leaving_intersection) == 1 && - step_entering_intersection->distance <= 1.5 * MAX_COLLAPSE_DISTANCE; + step_entering_intersection->distance <= 1.5 * current_max_collapse_distance; return (short_and_undisturbed || is_strong_name_change || is_choiceless_name_change) && followed_by_name_change && is_maneuver; @@ -357,7 +358,7 @@ bool nameChangeImmediatelyAfterSuppressed(const RouteStepIterator step_entering_ if (!basicCollapsePreconditions(step_entering_intersection, step_leaving_intersection)) return false; - const auto very_short = step_entering_intersection->distance < 0.25 * MAX_COLLAPSE_DISTANCE; + const auto very_short = step_entering_intersection->distance < 0.25 * current_max_collapse_distance; const auto correct_types = hasTurnType(*step_entering_intersection, TurnType::Suppressed) && hasTurnType(*step_leaving_intersection, TurnType::NewName); @@ -393,7 +394,7 @@ bool doubleChoiceless(const RouteStepIterator step_entering_intersection, step_entering_intersection->intersections.back().entry.end(), true) == 1); - const auto short_enough = step_entering_intersection->distance < 1.5 * MAX_COLLAPSE_DISTANCE; + const auto short_enough = step_entering_intersection->distance < 1.5 * current_max_collapse_distance; return double_choiceless && short_enough; } @@ -404,7 +405,7 @@ bool straightTurnFollowedByChoiceless(const RouteStepIterator step_entering_inte if (!basicCollapsePreconditions(step_entering_intersection, step_leaving_intersection)) return false; - const auto is_short = step_entering_intersection->distance <= 2 * MAX_COLLAPSE_DISTANCE; + const auto is_short = step_entering_intersection->distance <= 2 * current_max_collapse_distance; const auto has_correct_type = hasTurnType(*step_entering_intersection, TurnType::Continue) || hasTurnType(*step_entering_intersection, TurnType::Turn); diff --git a/src/engine/guidance/collapse_turns.cpp b/src/engine/guidance/collapse_turns.cpp index 535a7539ba3..88783db7681 100644 --- a/src/engine/guidance/collapse_turns.cpp +++ b/src/engine/guidance/collapse_turns.cpp @@ -3,6 +3,7 @@ #include "guidance/turn_instruction.hpp" #include "engine/guidance/collapse_scenario_detection.hpp" #include "engine/guidance/collapsing_utility.hpp" +#include "engine/datafacade/datafacade_base.hpp" #include "util/bearing.hpp" #include "util/guidance/name_announcements.hpp" @@ -15,9 +16,11 @@ namespace osrm::engine::guidance using osrm::util::angularDeviation; using namespace osrm::guidance; +// Thread-local storage for configurable max collapse distance +thread_local double current_max_collapse_distance = DEFAULT_MAX_COLLAPSE_DISTANCE; + namespace { -const constexpr double MAX_COLLAPSE_DISTANCE = 30; // find the combined turn angle for two turns. Not in all scenarios we can easily add both angles // (e.g 90 degree left followed by 90 degree right would be no turn at all). @@ -66,7 +69,7 @@ double findTotalTurnAngle(const RouteStep &entry_step, const RouteStep &exit_ste return false; // entry step is short and the exit and the exit step does not have intersections?? - if (entry_step.distance < MAX_COLLAPSE_DISTANCE) + if (entry_step.distance < current_max_collapse_distance) return true; // both go roughly in the same direction @@ -433,12 +436,15 @@ void suppressStep(RouteStep &step_at_turn_location, RouteStep &step_after_turn_l } // OTHER IMPLEMENTATIONS -[[nodiscard]] RouteSteps collapseTurnInstructions(RouteSteps steps) +[[nodiscard]] RouteSteps collapseTurnInstructions(const datafacade::BaseDataFacade &facade, RouteSteps steps) { // make sure we can safely iterate over all steps (has depart/arrive with TurnType::NoTurn) BOOST_ASSERT(!hasTurnType(steps.front()) && !hasTurnType(steps.back())); BOOST_ASSERT(hasWaypointType(steps.front()) && hasWaypointType(steps.back())); + // Set the thread-local max collapse distance from the profile + current_max_collapse_distance = facade.GetMaxCollapseDistance(); + if (steps.size() <= 2) return steps; diff --git a/src/extractor/scripting_environment_lua.cpp b/src/extractor/scripting_environment_lua.cpp index ca565751c80..d084deecd12 100644 --- a/src/extractor/scripting_environment_lua.cpp +++ b/src/extractor/scripting_environment_lua.cpp @@ -234,7 +234,10 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) "force_split_edges", &ProfileProperties::force_split_edges, "call_tagless_node_function", - &ProfileProperties::call_tagless_node_function); + &ProfileProperties::call_tagless_node_function, + "max_collapse_distance", + sol::property(&ProfileProperties::GetMaxCollapseDistance, + &ProfileProperties::SetMaxCollapseDistance)); context.state.new_usertype>( "vector", @@ -728,6 +731,10 @@ void Sol2ScriptingEnvironment::InitContext(LuaScriptingContext &context) sol::optional force_split_edges = properties["force_split_edges"]; if (force_split_edges != sol::nullopt) context.properties.force_split_edges = force_split_edges.value(); + + sol::optional max_collapse_distance = properties["max_collapse_distance"]; + if (max_collapse_distance != sol::nullopt) + context.properties.SetMaxCollapseDistance(max_collapse_distance.value()); } }; diff --git a/test_max_collapse_distance.cpp b/test_max_collapse_distance.cpp new file mode 100644 index 00000000000..0519ecba6ea --- /dev/null +++ b/test_max_collapse_distance.cpp @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/unit_tests/engine/guidance_collapse_distance.cpp b/unit_tests/engine/guidance_collapse_distance.cpp new file mode 100644 index 00000000000..c4648eaf088 --- /dev/null +++ b/unit_tests/engine/guidance_collapse_distance.cpp @@ -0,0 +1,57 @@ +#include "engine/guidance/collapsing_utility.hpp" + +#include + +BOOST_AUTO_TEST_SUITE(guidance_collapse_distance) + +using namespace osrm::engine::guidance; + +BOOST_AUTO_TEST_CASE(default_max_collapse_distance_constant) +{ + // Test that the default constant is properly defined + BOOST_CHECK_EQUAL(DEFAULT_MAX_COLLAPSE_DISTANCE, 30.0); +} + +BOOST_AUTO_TEST_CASE(thread_local_max_collapse_distance_default) +{ + // Test that thread-local variable defaults to the constant + BOOST_CHECK_EQUAL(current_max_collapse_distance, DEFAULT_MAX_COLLAPSE_DISTANCE); +} + +BOOST_AUTO_TEST_CASE(thread_local_max_collapse_distance_modification) +{ + // Store original value to restore later + const double original_value = current_max_collapse_distance; + + // Test setting custom values + current_max_collapse_distance = 15.0; + BOOST_CHECK_EQUAL(current_max_collapse_distance, 15.0); + + current_max_collapse_distance = 0.5; + BOOST_CHECK_EQUAL(current_max_collapse_distance, 0.5); + + current_max_collapse_distance = 100.0; + BOOST_CHECK_EQUAL(current_max_collapse_distance, 100.0); + + // Restore original value for other tests + current_max_collapse_distance = original_value; + BOOST_CHECK_EQUAL(current_max_collapse_distance, original_value); +} + +BOOST_AUTO_TEST_CASE(thread_local_variable_precision) +{ + // Store original value to restore later + const double original_value = current_max_collapse_distance; + + // Test that precise values are maintained + current_max_collapse_distance = 12.345; + BOOST_CHECK_EQUAL(current_max_collapse_distance, 12.345); + + current_max_collapse_distance = 0.001; + BOOST_CHECK_EQUAL(current_max_collapse_distance, 0.001); + + // Restore original value + current_max_collapse_distance = original_value; +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/unit_tests/engine/guidance_collapse_integration.cpp b/unit_tests/engine/guidance_collapse_integration.cpp new file mode 100644 index 00000000000..480327efe9a --- /dev/null +++ b/unit_tests/engine/guidance_collapse_integration.cpp @@ -0,0 +1,130 @@ +#include "engine/guidance/collapse_turns.hpp" +#include "engine/guidance/collapsing_utility.hpp" +#include "../mocks/mock_datafacade.hpp" + +#include + +BOOST_AUTO_TEST_SUITE(guidance_collapse_integration) + +using namespace osrm::engine::guidance; +using namespace osrm::guidance; +using namespace osrm::extractor; + +// Custom mock datafacade that allows setting max_collapse_distance +class ConfigurableMaxCollapseDistanceFacade : public osrm::test::MockBaseDataFacade +{ +private: + double max_collapse_distance_; + +public: + ConfigurableMaxCollapseDistanceFacade(double max_collapse_distance = 30.0) + : max_collapse_distance_(max_collapse_distance) {} + + double GetMaxCollapseDistance() const override { return max_collapse_distance_; } + + void SetMaxCollapseDistance(double value) { max_collapse_distance_ = value; } +}; + +BOOST_AUTO_TEST_CASE(facade_integration_default_value) +{ + ConfigurableMaxCollapseDistanceFacade facade; + + // Test that facade returns default value + BOOST_CHECK_EQUAL(facade.GetMaxCollapseDistance(), 30.0); +} + +BOOST_AUTO_TEST_CASE(facade_integration_custom_value) +{ + ConfigurableMaxCollapseDistanceFacade facade(15.5); + + // Test that facade returns custom value + BOOST_CHECK_EQUAL(facade.GetMaxCollapseDistance(), 15.5); + + // Test that value can be changed + facade.SetMaxCollapseDistance(42.0); + BOOST_CHECK_EQUAL(facade.GetMaxCollapseDistance(), 42.0); +} + +BOOST_AUTO_TEST_CASE(collapse_turns_sets_thread_local_variable) +{ + // Store original value to restore later + const double original_value = current_max_collapse_distance; + + ConfigurableMaxCollapseDistanceFacade facade(25.5); + + // Create minimal route steps for testing + std::vector steps; + + // Create depart step (required by collapseTurnInstructions) + RouteStep depart_step; + depart_step.maneuver.waypoint_type = WaypointType::Depart; + depart_step.maneuver.instruction = {TurnType::NoTurn, DirectionModifier::UTurn}; + steps.push_back(depart_step); + + // Create arrive step (required by collapseTurnInstructions) + RouteStep arrive_step; + arrive_step.maneuver.waypoint_type = WaypointType::Arrive; + arrive_step.maneuver.instruction = {TurnType::NoTurn, DirectionModifier::UTurn}; + steps.push_back(arrive_step); + + // Call collapseTurnInstructions which should set the thread-local variable + auto result_steps = collapseTurnInstructions(facade, std::move(steps)); + + // Verify that the thread-local variable was set correctly + BOOST_CHECK_EQUAL(current_max_collapse_distance, 25.5); + + // Test with different value + facade.SetMaxCollapseDistance(12.3); + + // Create new steps for second test + std::vector steps2; + steps2.push_back(depart_step); + steps2.push_back(arrive_step); + + auto result_steps2 = collapseTurnInstructions(facade, std::move(steps2)); + + // Verify the thread-local variable was updated + BOOST_CHECK_EQUAL(current_max_collapse_distance, 12.3); + + // Restore original value + current_max_collapse_distance = original_value; +} + +BOOST_AUTO_TEST_CASE(multiple_facades_independence) +{ + // Store original value to restore later + const double original_value = current_max_collapse_distance; + + ConfigurableMaxCollapseDistanceFacade facade1(10.0); + ConfigurableMaxCollapseDistanceFacade facade2(20.0); + + // Test that facades are independent + BOOST_CHECK_EQUAL(facade1.GetMaxCollapseDistance(), 10.0); + BOOST_CHECK_EQUAL(facade2.GetMaxCollapseDistance(), 20.0); + + // Create minimal route steps + std::vector steps1, steps2; + RouteStep depart_step, arrive_step; + depart_step.maneuver.waypoint_type = WaypointType::Depart; + depart_step.maneuver.instruction = {TurnType::NoTurn, DirectionModifier::UTurn}; + arrive_step.maneuver.waypoint_type = WaypointType::Arrive; + arrive_step.maneuver.instruction = {TurnType::NoTurn, DirectionModifier::UTurn}; + + steps1.push_back(depart_step); + steps1.push_back(arrive_step); + steps2.push_back(depart_step); + steps2.push_back(arrive_step); + + // Use facade1 + auto result1 = collapseTurnInstructions(facade1, std::move(steps1)); + BOOST_CHECK_EQUAL(current_max_collapse_distance, 10.0); + + // Use facade2 + auto result2 = collapseTurnInstructions(facade2, std::move(steps2)); + BOOST_CHECK_EQUAL(current_max_collapse_distance, 20.0); + + // Restore original value + current_max_collapse_distance = original_value; +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/unit_tests/engine/offline_facade.cpp b/unit_tests/engine/offline_facade.cpp index 6917e209832..41af0ad3217 100644 --- a/unit_tests/engine/offline_facade.cpp +++ b/unit_tests/engine/offline_facade.cpp @@ -280,6 +280,7 @@ class ContiguousInternalMemoryDataFacade bool GetContinueStraightDefault() const override { return false; } std::string GetTimestamp() const override { return ""; } double GetMapMatchingMaxSpeed() const override { return 0; } + double GetMaxCollapseDistance() const override { return 30.0; } const char *GetWeightName() const override { return ""; } unsigned GetWeightPrecision() const override { return 0; } double GetWeightMultiplier() const override { return 1; } diff --git a/unit_tests/extractor/profile_properties.cpp b/unit_tests/extractor/profile_properties.cpp new file mode 100644 index 00000000000..fef5020ed41 --- /dev/null +++ b/unit_tests/extractor/profile_properties.cpp @@ -0,0 +1,89 @@ +#include "extractor/profile_properties.hpp" + +#include + +BOOST_AUTO_TEST_SUITE(profile_properties) + +using namespace osrm::extractor; + +BOOST_AUTO_TEST_CASE(default_max_collapse_distance) +{ + ProfileProperties properties; + + // Test that default max_collapse_distance is 30.0 meters + BOOST_CHECK_EQUAL(properties.GetMaxCollapseDistance(), 30.0); +} + +BOOST_AUTO_TEST_CASE(set_get_max_collapse_distance) +{ + ProfileProperties properties; + + // Test setting and getting custom values + properties.SetMaxCollapseDistance(15.5); + BOOST_CHECK_EQUAL(properties.GetMaxCollapseDistance(), 15.5); + + // Test setting to zero (edge case) + properties.SetMaxCollapseDistance(0.0); + BOOST_CHECK_EQUAL(properties.GetMaxCollapseDistance(), 0.0); + + // Test setting to large value + properties.SetMaxCollapseDistance(1000.0); + BOOST_CHECK_EQUAL(properties.GetMaxCollapseDistance(), 1000.0); + + // Test setting to small positive value + properties.SetMaxCollapseDistance(0.1); + BOOST_CHECK_EQUAL(properties.GetMaxCollapseDistance(), 0.1); +} + +BOOST_AUTO_TEST_CASE(max_collapse_distance_independence) +{ + ProfileProperties properties1; + ProfileProperties properties2; + + // Test that different instances are independent + properties1.SetMaxCollapseDistance(10.0); + properties2.SetMaxCollapseDistance(25.0); + + BOOST_CHECK_EQUAL(properties1.GetMaxCollapseDistance(), 10.0); + BOOST_CHECK_EQUAL(properties2.GetMaxCollapseDistance(), 25.0); + + // Modify one and check the other is unchanged + properties1.SetMaxCollapseDistance(50.0); + BOOST_CHECK_EQUAL(properties1.GetMaxCollapseDistance(), 50.0); + BOOST_CHECK_EQUAL(properties2.GetMaxCollapseDistance(), 25.0); +} + +BOOST_AUTO_TEST_CASE(copy_constructor_preserves_max_collapse_distance) +{ + ProfileProperties original; + original.SetMaxCollapseDistance(42.5); + + // Test copy constructor + ProfileProperties copy = original; + BOOST_CHECK_EQUAL(copy.GetMaxCollapseDistance(), 42.5); + + // Test that modifying copy doesn't affect original + copy.SetMaxCollapseDistance(100.0); + BOOST_CHECK_EQUAL(original.GetMaxCollapseDistance(), 42.5); + BOOST_CHECK_EQUAL(copy.GetMaxCollapseDistance(), 100.0); +} + +BOOST_AUTO_TEST_CASE(assignment_operator_preserves_max_collapse_distance) +{ + ProfileProperties original; + ProfileProperties assigned; + + original.SetMaxCollapseDistance(33.3); + assigned.SetMaxCollapseDistance(66.6); + + // Test assignment operator + assigned = original; + BOOST_CHECK_EQUAL(assigned.GetMaxCollapseDistance(), 33.3); + + // Test that modifying assigned doesn't affect original + assigned.SetMaxCollapseDistance(99.9); + BOOST_CHECK_EQUAL(original.GetMaxCollapseDistance(), 33.3); + BOOST_CHECK_EQUAL(assigned.GetMaxCollapseDistance(), 99.9); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/unit_tests/mocks/mock_datafacade.hpp b/unit_tests/mocks/mock_datafacade.hpp index 2ab1826aba4..2083e724215 100644 --- a/unit_tests/mocks/mock_datafacade.hpp +++ b/unit_tests/mocks/mock_datafacade.hpp @@ -158,6 +158,7 @@ class MockBaseDataFacade : public engine::datafacade::BaseDataFacade bool GetContinueStraightDefault() const override { return true; } double GetMapMatchingMaxSpeed() const override { return 180 / 3.6; } + double GetMaxCollapseDistance() const override { return 30.0; } const char *GetWeightName() const override final { return "duration"; } unsigned GetWeightPrecision() const override final { return 1; } double GetWeightMultiplier() const override final { return 10.; }