diff --git a/features/guidance/perception.feature b/features/guidance/perception.feature index 0d009840b..370de68a1 100644 --- a/features/guidance/perception.feature +++ b/features/guidance/perception.feature @@ -119,31 +119,28 @@ Feature: Simple Turns | bcegb | place | yes | When I route I should get - | waypoints | turns | route | - | a,d | depart,continue right,turn right,arrive | place,place,bottom,bottom | - | a,f | depart,continue right,continue left,continue right,arrive | place,place,place,place,place | - | d,f | depart,turn right,continue right,arrive | bottom,place,place,place | - | d,h | depart,turn right,continue left,turn right,arrive | bottom,place,place,top,top | + | waypoints | turns | route | + | a,d | depart,turn right,arrive | place,bottom,bottom | + | a,f | depart,continue left,continue right,arrive | place,place,place,place | + | d,f | depart,turn right,continue right,arrive | bottom,place,place,place | + | d,h | depart,turn right,continue left,turn right,arrive | bottom,place,place,top,top | Scenario: Don't Collapse Places: Given the node map + | | | | | | | h | | | | | | | | | | | | | | g | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | - | | | | | | | | | | | | e | f | - | | | | | | | | | | | | | | - | | | | | | | | | | | | | | - | | | | | | | | | | | | | | - | | | | | | | | | | | | | | - | | | | | | | c | | | | | | | + | | b | | | | | | | | | | e | f | And the ways | nodes | name | oneway | - | ef | place | no | - | ceg | place | yes | + | fe | place | yes | + | gh | top | yes | + | egb | place | yes | When I route I should get - | waypoints | turns | route | - | c,f | depart,turn right,continue right,arrive | bottom,place,place,place | + | waypoints | turns | route | + | f,h | depart,turn right,arrive | place,top,top | diff --git a/features/guidance/ramp.feature b/features/guidance/ramp.feature index d7713b396..0d7acb97b 100644 --- a/features/guidance/ramp.feature +++ b/features/guidance/ramp.feature @@ -88,9 +88,9 @@ Feature: Ramp Guidance | | | | d | | And the ways - | nodes | highway | - | abc | tertiary | - | bd | motorway_link | + | nodes | highway | oneway | + | abc | tertiary | yes | + | bd | motorway_link | yes | When I route I should get | waypoints | route | turns | @@ -108,9 +108,9 @@ Feature: Ramp Guidance | bd | motorway_link | When I route I should get - | waypoints | route | turns | - | a,d | abc,bd,bd | depart,on ramp straight,arrive | - | a,c | abc,abc,abc | depart,continue left,arrive | + | waypoints | route | turns | + | a,d | abc,bd,bd | depart,on ramp straight,arrive | + | a,c | abc,abc | depart,arrive | Scenario: Fork Ramp Off Turning Though Street Given the node map @@ -124,9 +124,9 @@ Feature: Ramp Guidance | bd | motorway_link | When I route I should get - | waypoints | route | turns | - | a,d | abc,bd,bd | depart,on ramp right,arrive | - | a,c | abc,abc,abc | depart,continue left,arrive | + | waypoints | route | turns | + | a,d | abc,bd,bd | depart,on ramp right,arrive | + | a,c | abc,abc | depart,arrive | Scenario: Fork Ramp Given the node map @@ -174,9 +174,9 @@ Feature: Ramp Guidance | bd | motorway_link | When I route I should get - | waypoints | route | turns | - | a,d | abc,bd,bd | depart,on ramp slight right,arrive | - | a,c | abc,abc,abc | depart,continue slight left,arrive | + | waypoints | route | turns | + | a,d | abc,bd,bd | depart,on ramp slight right,arrive | + | a,c | abc,abc | depart,arrive | Scenario: Fork Slight Ramp on Obvious Through Street Given the node map diff --git a/include/extractor/guidance/intersection_generator.hpp b/include/extractor/guidance/intersection_generator.hpp index f92f4bf4f..14828cf77 100644 --- a/include/extractor/guidance/intersection_generator.hpp +++ b/include/extractor/guidance/intersection_generator.hpp @@ -35,6 +35,17 @@ class IntersectionGenerator Intersection operator()(const NodeID nid, const EdgeID via_eid) const; + // Graph Compression cannot compress every setting. For example any barrier/traffic light cannot + // be compressed. As a result, a simple road of the form `a ----- b` might end up as having an + // intermediate intersection, if there is a traffic light in between. If we want to look farther + // down a road, finding the next actual decision requires the look at multiple intersections. + // Here we follow the road until we either reach a dead end or find the next intersection with + // more than a single next road. + Intersection GetActualNextIntersection(const NodeID starting_node, + const EdgeID via_edge, + NodeID *resulting_from_node, + EdgeID *resulting_via_edge) const; + private: const util::NodeBasedDynamicGraph &node_based_graph; const RestrictionMap &restriction_map; @@ -86,17 +97,6 @@ class IntersectionGenerator OSRM_ATTR_WARN_UNUSED Intersection AdjustForJoiningRoads(const NodeID node_at_intersection, Intersection intersection) const; - - // Graph Compression cannot compress every setting. For example any barrier/traffic light cannot - // be compressed. As a result, a simple road of the form `a ----- b` might end up as having an - // intermediate intersection, if there is a traffic light in between. If we want to look farther - // down a road, finding the next actual decision requires the look at multiple intersections. - // Here we follow the road until we either reach a dead end or find the next intersection with - // more than a single next road. - inline Intersection GetActualNextIntersection(const NodeID starting_node, - const EdgeID via_edge, - NodeID *resulting_from_node, - EdgeID *resulting_via_edge) const; }; } // namespace guidance diff --git a/include/extractor/guidance/intersection_handler.hpp b/include/extractor/guidance/intersection_handler.hpp index 9759fabe1..23aa7fa87 100644 --- a/include/extractor/guidance/intersection_handler.hpp +++ b/include/extractor/guidance/intersection_handler.hpp @@ -2,6 +2,7 @@ #define OSRM_EXTRACTOR_GUIDANCE_INTERSECTION_HANDLER_HPP_ #include "extractor/guidance/intersection.hpp" +#include "extractor/guidance/intersection_generator.hpp" #include "extractor/query_node.hpp" #include "extractor/suffix_table.hpp" @@ -28,7 +29,8 @@ class IntersectionHandler IntersectionHandler(const util::NodeBasedDynamicGraph &node_based_graph, const std::vector &node_info_list, const util::NameTable &name_table, - const SuffixTable &street_name_suffix_table); + const SuffixTable &street_name_suffix_table, + const IntersectionGenerator &intersection_generator); virtual ~IntersectionHandler() = default; @@ -45,6 +47,7 @@ class IntersectionHandler const std::vector &node_info_list; const util::NameTable &name_table; const SuffixTable &street_name_suffix_table; + const IntersectionGenerator &intersection_generator; // counts the number on allowed entry roads std::size_t countValid(const Intersection &intersection) const; diff --git a/include/extractor/guidance/motorway_handler.hpp b/include/extractor/guidance/motorway_handler.hpp index 11dc1f1f0..2034f5306 100644 --- a/include/extractor/guidance/motorway_handler.hpp +++ b/include/extractor/guidance/motorway_handler.hpp @@ -3,6 +3,7 @@ #include "extractor/guidance/intersection.hpp" #include "extractor/guidance/intersection_handler.hpp" +#include "extractor/guidance/intersection_generator.hpp" #include "extractor/query_node.hpp" #include "util/attributes.hpp" @@ -26,7 +27,8 @@ class MotorwayHandler : public IntersectionHandler MotorwayHandler(const util::NodeBasedDynamicGraph &node_based_graph, const std::vector &node_info_list, const util::NameTable &name_table, - const SuffixTable &street_name_suffix_table); + const SuffixTable &street_name_suffix_table, + const IntersectionGenerator &intersection_generator); ~MotorwayHandler() override final = default; diff --git a/include/extractor/guidance/roundabout_handler.hpp b/include/extractor/guidance/roundabout_handler.hpp index fffbf7197..c352a027f 100644 --- a/include/extractor/guidance/roundabout_handler.hpp +++ b/include/extractor/guidance/roundabout_handler.hpp @@ -4,6 +4,7 @@ #include "extractor/compressed_edge_container.hpp" #include "extractor/guidance/intersection.hpp" #include "extractor/guidance/intersection_handler.hpp" +#include "extractor/guidance/intersection_generator.hpp" #include "extractor/guidance/roundabout_type.hpp" #include "extractor/profile_properties.hpp" #include "extractor/query_node.hpp" @@ -44,7 +45,8 @@ class RoundaboutHandler : public IntersectionHandler const CompressedEdgeContainer &compressed_edge_container, const util::NameTable &name_table, const SuffixTable &street_name_suffix_table, - const ProfileProperties &profile_properties); + const ProfileProperties &profile_properties, + const IntersectionGenerator &intersection_generator); ~RoundaboutHandler() override final = default; diff --git a/include/extractor/guidance/sliproad_handler.hpp b/include/extractor/guidance/sliproad_handler.hpp index cb7034706..462b35dad 100644 --- a/include/extractor/guidance/sliproad_handler.hpp +++ b/include/extractor/guidance/sliproad_handler.hpp @@ -42,9 +42,6 @@ class SliproadHandler : public IntersectionHandler Intersection operator()(const NodeID nid, const EdgeID via_eid, Intersection intersection) const override final; - - private: - const IntersectionGenerator &intersection_generator; }; } // namespace guidance diff --git a/include/extractor/guidance/turn_handler.hpp b/include/extractor/guidance/turn_handler.hpp index 793cf7544..116778c84 100644 --- a/include/extractor/guidance/turn_handler.hpp +++ b/include/extractor/guidance/turn_handler.hpp @@ -3,6 +3,7 @@ #include "extractor/guidance/intersection.hpp" #include "extractor/guidance/intersection_handler.hpp" +#include "extractor/guidance/intersection_generator.hpp" #include "extractor/query_node.hpp" #include "util/attributes.hpp" @@ -28,7 +29,8 @@ class TurnHandler : public IntersectionHandler TurnHandler(const util::NodeBasedDynamicGraph &node_based_graph, const std::vector &node_info_list, const util::NameTable &name_table, - const SuffixTable &street_name_suffix_table); + const SuffixTable &street_name_suffix_table, + const IntersectionGenerator &intersection_generator); ~TurnHandler() override final = default; diff --git a/src/engine/guidance/post_processing.cpp b/src/engine/guidance/post_processing.cpp index cafdf3126..bce40f2dc 100644 --- a/src/engine/guidance/post_processing.cpp +++ b/src/engine/guidance/post_processing.cpp @@ -1,5 +1,3 @@ -#include "util/debug.hpp" - #include "extractor/guidance/turn_instruction.hpp" #include "engine/guidance/post_processing.hpp" @@ -586,7 +584,6 @@ std::vector removeNoTurnInstructions(std::vector steps) // that we come across. std::vector postProcess(std::vector steps) { - util::guidance::print(steps); // the steps should always include the first/last step in form of a location BOOST_ASSERT(steps.size() >= 2); if (steps.size() == 2) diff --git a/src/extractor/guidance/intersection_generator.cpp b/src/extractor/guidance/intersection_generator.cpp index 15b5e6394..0d15fb206 100644 --- a/src/extractor/guidance/intersection_generator.cpp +++ b/src/extractor/guidance/intersection_generator.cpp @@ -218,8 +218,7 @@ bool IntersectionGenerator::CanMerge(const NodeID node_at_intersection, coordinate_at_intersection, node_at_intersection](const std::size_t index, const std::size_t other_index) { - const auto GetActualTarget = [&](const std::size_t index) - { + const auto GetActualTarget = [&](const std::size_t index) { EdgeID last_in_edge_id; GetActualNextIntersection( node_at_intersection, intersection[index].turn.eid, nullptr, &last_in_edge_id); @@ -239,6 +238,10 @@ bool IntersectionGenerator::CanMerge(const NodeID node_at_intersection, const double distance_to_target = util::coordinate_calculation::haversineDistance( coordinate_at_intersection, coordinate_at_target); + const constexpr double MAX_COLLAPSE_DISTANCE = 30; + if (distance_to_target < MAX_COLLAPSE_DISTANCE) + return false; + const bool becomes_narrower = angularDeviation(turn_angle, other_turn_angle) < NARROW_TURN_ANGLE && angularDeviation(turn_angle, other_turn_angle) < @@ -474,14 +477,14 @@ Intersection IntersectionGenerator::MergeSegregatedRoads(const NodeID intersecti Intersection IntersectionGenerator::AdjustForJoiningRoads(const NodeID node_at_intersection, Intersection intersection) const { - // FIXME remove - return intersection; // nothing to do for dead ends if (intersection.size() <= 1) return intersection; - for (auto &road : intersection) + // We can't adjust the very first angle, because the u-turn should always be 0 + for (std::size_t road_index = 1; road_index < intersection.size(); ++road_index) { + auto &road = intersection[road_index]; // to find out about the above situation, we need to look at the next intersection (at d in // the example). If the initial road can be merged to the left/right, we are about to adjust // the angle. @@ -525,7 +528,7 @@ Intersection IntersectionGenerator::AdjustForJoiningRoads(const NodeID node_at_i return intersection; } -inline Intersection +Intersection IntersectionGenerator::GetActualNextIntersection(const NodeID starting_node, const EdgeID via_edge, NodeID *resulting_from_node = nullptr, diff --git a/src/extractor/guidance/intersection_handler.cpp b/src/extractor/guidance/intersection_handler.cpp index a8714cae1..31aadcdb5 100644 --- a/src/extractor/guidance/intersection_handler.cpp +++ b/src/extractor/guidance/intersection_handler.cpp @@ -7,6 +7,7 @@ #include "util/simple_logger.hpp" #include +#include using EdgeData = osrm::util::NodeBasedDynamicGraph::EdgeData; using osrm::util::guidance::getTurnDirection; @@ -29,9 +30,11 @@ inline bool requiresAnnouncement(const EdgeData &from, const EdgeData &to) IntersectionHandler::IntersectionHandler(const util::NodeBasedDynamicGraph &node_based_graph, const std::vector &node_info_list, const util::NameTable &name_table, - const SuffixTable &street_name_suffix_table) + const SuffixTable &street_name_suffix_table, + const IntersectionGenerator &intersection_generator) : node_based_graph(node_based_graph), node_info_list(node_info_list), name_table(name_table), - street_name_suffix_table(street_name_suffix_table) + street_name_suffix_table(street_name_suffix_table), + intersection_generator(intersection_generator) { } @@ -469,10 +472,10 @@ std::size_t IntersectionHandler::findObviousTurn(const EdgeID via_edge, } // has no obvious continued road + const auto &best_data = node_based_graph.GetEdgeData(intersection[best].turn.eid); if (best_continue == 0 || num_continue_names > 2 || - (node_based_graph.GetEdgeData(intersection[best_continue].turn.eid).road_classification == - node_based_graph.GetEdgeData(intersection[best].turn.eid).road_classification && - std::abs(best_continue_deviation) > 1 && best_deviation / best_continue_deviation < 0.75)) + (num_continue_names > 2 && best_continue_deviation >= 2 * NARROW_TURN_ANGLE) || + (best_deviation < FUZZY_ANGLE_DIFFERENCE && !best_data.road_classification.IsRampClass())) { // Find left/right deviation const double left_deviation = angularDeviation( @@ -484,7 +487,6 @@ std::size_t IntersectionHandler::findObviousTurn(const EdgeID via_edge, std::min(left_deviation, right_deviation) > FUZZY_ANGLE_DIFFERENCE) return best; - const auto &best_data = node_based_graph.GetEdgeData(intersection[best].turn.eid); const auto left_index = (best + 1) % intersection.size(); const auto right_index = best - 1; const auto &left_data = node_based_graph.GetEdgeData(intersection[left_index].turn.eid); @@ -538,14 +540,87 @@ std::size_t IntersectionHandler::findObviousTurn(const EdgeID via_edge, if (i == best_continue || !intersection[i].entry_allowed) continue; - if (angularDeviation(intersection[i].turn.angle, STRAIGHT_ANGLE) / deviation < 1.1 && - !obvious_by_road_class( - in_data.road_classification, - continue_data.road_classification, - node_based_graph.GetEdgeData(intersection[i].turn.eid).road_classification)) + const auto &turn_data = node_based_graph.GetEdgeData(intersection[i].turn.eid); + const bool is_obvious_by_road_class = + obvious_by_road_class(in_data.road_classification, + continue_data.road_classification, + turn_data.road_classification); + + // if the main road is obvious by class, we ignore the current road as a potential + // prevention of obviousness + if (is_obvious_by_road_class) + continue; + + // continuation could be grouped with a straight turn and the turning road is a ramp + if (turn_data.road_classification.IsRampClass() && deviation < GROUP_ANGLE) + continue; + + // perfectly straight turns prevent obviousness + const auto turn_deviation = + angularDeviation(intersection[i].turn.angle, STRAIGHT_ANGLE); + if (turn_deviation < FUZZY_ANGLE_DIFFERENCE) + return 0; + + const auto deviation_ratio = turn_deviation / deviation; + + // in comparison to normal devitions, a continue road can offer a smaller distinction + // ratio. Other roads close to the turn angle are not as obvious, if one road continues. + if (deviation_ratio < DISTINCTION_RATIO / 1.5) + return 0; + + // in comparison to another continuing road, we need a better distinction. This prevents + // situations where the turn is probably less obvious. An example are places that have a + // road with the same name entering/exiting: + // + // d + // / + // / + // a -- b + // \ + // \ + // c + + if (turn_data.name_id == continue_data.name_id && + deviation_ratio < 1.5 * DISTINCTION_RATIO) return 0; } - return best_continue; // no obvious turn + + // Segregated intersections can result in us finding an obvious turn, even though its only + // obvious due to a very short segment in between. So if the segment coming in is very + // short, we check the previous intersection for other continues in the opposite bearing. + const auto node_at_intersection = node_based_graph.GetTarget(via_edge); + const util::Coordinate coordinate_at_intersection = node_info_list[node_at_intersection]; + + const auto node_at_u_turn = node_based_graph.GetTarget(intersection[0].turn.eid); + const util::Coordinate coordinate_at_u_turn = node_info_list[node_at_u_turn]; + + const double constexpr MAX_COLLAPSE_DISTANCE = 30; + if (util::coordinate_calculation::haversineDistance( + coordinate_at_intersection, coordinate_at_u_turn) < MAX_COLLAPSE_DISTANCE) + { + // this request here actually goes against the direction of the ingoing edgeid. This can + // even reverse the direction. Since we don't want to compute actual turns but simply + // try to find whether there is a turn going to the opposite direction of our obvious + // turn, this should be alright. + const auto previous_intersection = intersection_generator.GetActualNextIntersection( + node_at_intersection, intersection[0].turn.eid, nullptr, nullptr); + + const auto continue_road = intersection[best_continue]; + for (const auto &comparison_road : previous_intersection) + { + // since we look at the intersection in the wrong direction, a similar angle + // actually represents a near 180 degree different in bearings between the two + // roads. + if (angularDeviation(comparison_road.turn.angle, STRAIGHT_ANGLE) > GROUP_ANGLE && + angularDeviation(comparison_road.turn.angle, continue_road.turn.angle) < + FUZZY_ANGLE_DIFFERENCE && + continue_data.IsCompatibleTo( + node_based_graph.GetEdgeData(comparison_road.turn.eid))) + return 0; + } + } + + return best_continue; } return 0; diff --git a/src/extractor/guidance/motorway_handler.cpp b/src/extractor/guidance/motorway_handler.cpp index 253a1c2ad..01b0748fe 100644 --- a/src/extractor/guidance/motorway_handler.cpp +++ b/src/extractor/guidance/motorway_handler.cpp @@ -1,5 +1,5 @@ -#include "extractor/guidance/motorway_handler.hpp" #include "extractor/guidance/constants.hpp" +#include "extractor/guidance/motorway_handler.hpp" #include "extractor/guidance/road_classification.hpp" #include "extractor/guidance/toolkit.hpp" @@ -43,8 +43,13 @@ inline bool isRampClass(EdgeID eid, const util::NodeBasedDynamicGraph &node_base MotorwayHandler::MotorwayHandler(const util::NodeBasedDynamicGraph &node_based_graph, const std::vector &node_info_list, const util::NameTable &name_table, - const SuffixTable &street_name_suffix_table) - : IntersectionHandler(node_based_graph, node_info_list, name_table, street_name_suffix_table) + const SuffixTable &street_name_suffix_table, + const IntersectionGenerator &intersection_generator) + : IntersectionHandler(node_based_graph, + node_info_list, + name_table, + street_name_suffix_table, + intersection_generator) { } diff --git a/src/extractor/guidance/roundabout_handler.cpp b/src/extractor/guidance/roundabout_handler.cpp index 779602f3c..6eea5657c 100644 --- a/src/extractor/guidance/roundabout_handler.cpp +++ b/src/extractor/guidance/roundabout_handler.cpp @@ -1,5 +1,5 @@ -#include "extractor/guidance/roundabout_handler.hpp" #include "extractor/guidance/constants.hpp" +#include "extractor/guidance/roundabout_handler.hpp" #include "extractor/guidance/toolkit.hpp" #include "util/coordinate_calculation.hpp" @@ -26,8 +26,13 @@ RoundaboutHandler::RoundaboutHandler(const util::NodeBasedDynamicGraph &node_bas const CompressedEdgeContainer &compressed_edge_container, const util::NameTable &name_table, const SuffixTable &street_name_suffix_table, - const ProfileProperties &profile_properties) - : IntersectionHandler(node_based_graph, node_info_list, name_table, street_name_suffix_table), + const ProfileProperties &profile_properties, + const IntersectionGenerator &intersection_generator) + : IntersectionHandler(node_based_graph, + node_info_list, + name_table, + street_name_suffix_table, + intersection_generator), compressed_edge_container(compressed_edge_container), profile_properties(profile_properties) { } diff --git a/src/extractor/guidance/sliproad_handler.cpp b/src/extractor/guidance/sliproad_handler.cpp index 9b09fda71..b071a4fe1 100644 --- a/src/extractor/guidance/sliproad_handler.cpp +++ b/src/extractor/guidance/sliproad_handler.cpp @@ -26,8 +26,11 @@ SliproadHandler::SliproadHandler(const IntersectionGenerator &intersection_gener const std::vector &node_info_list, const util::NameTable &name_table, const SuffixTable &street_name_suffix_table) - : IntersectionHandler(node_based_graph, node_info_list, name_table, street_name_suffix_table), - intersection_generator(intersection_generator) + : IntersectionHandler(node_based_graph, + node_info_list, + name_table, + street_name_suffix_table, + intersection_generator) { } diff --git a/src/extractor/guidance/turn_analysis.cpp b/src/extractor/guidance/turn_analysis.cpp index d9ac72df8..d2d2c9020 100644 --- a/src/extractor/guidance/turn_analysis.cpp +++ b/src/extractor/guidance/turn_analysis.cpp @@ -1,6 +1,6 @@ -#include "extractor/guidance/turn_analysis.hpp" #include "extractor/guidance/constants.hpp" #include "extractor/guidance/road_classification.hpp" +#include "extractor/guidance/turn_analysis.hpp" #include "util/coordinate.hpp" #include "util/coordinate_calculation.hpp" @@ -49,9 +49,18 @@ TurnAnalysis::TurnAnalysis(const util::NodeBasedDynamicGraph &node_based_graph, compressed_edge_container, name_table, street_name_suffix_table, - profile_properties), - motorway_handler(node_based_graph, node_info_list, name_table, street_name_suffix_table), - turn_handler(node_based_graph, node_info_list, name_table, street_name_suffix_table), + profile_properties, + intersection_generator), + motorway_handler(node_based_graph, + node_info_list, + name_table, + street_name_suffix_table, + intersection_generator), + turn_handler(node_based_graph, + node_info_list, + name_table, + street_name_suffix_table, + intersection_generator), sliproad_handler(intersection_generator, node_based_graph, node_info_list, diff --git a/src/extractor/guidance/turn_handler.cpp b/src/extractor/guidance/turn_handler.cpp index cd305f468..321c1a054 100644 --- a/src/extractor/guidance/turn_handler.cpp +++ b/src/extractor/guidance/turn_handler.cpp @@ -24,8 +24,13 @@ namespace guidance TurnHandler::TurnHandler(const util::NodeBasedDynamicGraph &node_based_graph, const std::vector &node_info_list, const util::NameTable &name_table, - const SuffixTable &street_name_suffix_table) - : IntersectionHandler(node_based_graph, node_info_list, name_table, street_name_suffix_table) + const SuffixTable &street_name_suffix_table, + const IntersectionGenerator &intersection_generator) + : IntersectionHandler(node_based_graph, + node_info_list, + name_table, + street_name_suffix_table, + intersection_generator) { }