diff --git a/Tests/src/RoutePostprocessorTest.cpp b/Tests/src/RoutePostprocessorTest.cpp index 4fc33a11e..9e425e0a4 100644 --- a/Tests/src/RoutePostprocessorTest.cpp +++ b/Tests/src/RoutePostprocessorTest.cpp @@ -272,7 +272,7 @@ class MockContext: public osmscout::PostprocessorContext osmscout::Duration GetTime([[maybe_unused]] osmscout::DatabaseId dbId, [[maybe_unused]] const osmscout::Way &way, [[maybe_unused]] const osmscout::Distance &deltaDistance) const override { - assert(false); + // DistanceAndTimePostprocessor using this method, but we don't need valid time for testing return osmscout::Duration(); } @@ -486,6 +486,7 @@ class MockContext: public osmscout::PostprocessorContext void Postprocess(RouteDescription &description, MockContext &context) { + RoutePostprocessor::DistanceAndTimePostprocessor().Process(context, description); RoutePostprocessor::LanesPostprocessor().Process(context, description); RoutePostprocessor::SuggestedLanesPostprocessor().Process(context, description); @@ -1077,10 +1078,9 @@ TEST_CASE("Describe complex city junction: Průmyslová, Černokostecká") auto nodeIt = description.Nodes().begin(); auto suggestedLanes = nodeIt->GetDescription(); REQUIRE(suggestedLanes); - // TODO: improve lane evaluation on similar junctions - // REQUIRE(suggestedLanes->GetFrom() == 0); - // REQUIRE(suggestedLanes->GetTo() == 1); - // REQUIRE(suggestedLanes->GetTurn() == LaneTurn::Left); + REQUIRE(suggestedLanes->GetFrom() == 0); + REQUIRE(suggestedLanes->GetTo() == 1); + REQUIRE(suggestedLanes->GetTurn() == LaneTurn::Left); } } diff --git a/libosmscout/include/osmscout/routing/RoutePostprocessor.h b/libosmscout/include/osmscout/routing/RoutePostprocessor.h index cda18cc8e..f5add6b0c 100644 --- a/libosmscout/include/osmscout/routing/RoutePostprocessor.h +++ b/libosmscout/include/osmscout/routing/RoutePostprocessor.h @@ -459,15 +459,19 @@ namespace osmscout { private: RouteDescription::LaneDescriptionRef GetLaneDescription(const RouteDescription::Node &node) const; - /** Evaluate suggested lanes on nodes from "backBuffer", followed by "node". - * Node itself is junction, where count of lanes (on way from the node) - * is smaller than on the previous node (back of backBuffer). + /** Evaluate suggested lanes on nodes from "backBuffer", followed by junction node(s). + * Junction nodes are crossings where the route transitions from the incoming lane + * configuration (back of backBuffer) to a different one. Multiple consecutive junction + * nodes that are close together and connected by segments with the same lane configuration + * are grouped into a single logical junction — their exits are aggregated. * - * @param node - * @param backBuffer buffer of traveled nodes, recent node at back + * @param junctionNodes one or more consecutive junction nodes forming a logical junction + * @param lastNode the node after the last junction node (provides outgoing way info) + * @param backBuffer buffer of traveled nodes before the junction, recent node at back */ void EvaluateLaneSuggestion(const PostprocessorContext& context, - const RouteDescription::Node &node, + const std::vector &junctionNodes, + const RouteDescription::Node &lastNode, const std::list &backBuffer) const; private: diff --git a/libosmscout/src/osmscout/routing/RoutePostprocessor.cpp b/libosmscout/src/osmscout/routing/RoutePostprocessor.cpp index b8490e083..221e8492e 100644 --- a/libosmscout/src/osmscout/routing/RoutePostprocessor.cpp +++ b/libosmscout/src/osmscout/routing/RoutePostprocessor.cpp @@ -29,8 +29,9 @@ #include #include #include -#include #include +#include +#include namespace osmscout { @@ -1851,75 +1852,135 @@ namespace osmscout { } // end of anonymous namespace void RoutePostprocessor::SuggestedLanesPostprocessor::EvaluateLaneSuggestion(const PostprocessorContext& postprocessor, - const RouteDescription::Node &node, + const std::vector &junctionNodes, + const RouteDescription::Node &lastNode, const std::list &backBuffer) const { - if (node.GetPathObject().GetType()!=refWay) { + assert(!junctionNodes.empty()); + const RouteDescription::Node &firstJunctionNode = *junctionNodes.front(); + const RouteDescription::Node &lastJunctionNode = *junctionNodes.back(); + + if (firstJunctionNode.GetPathObject().GetType()!=refWay) { return; // areas not considered now } assert(!backBuffer.empty()); const RouteDescription::Node &prevNode=*backBuffer.back(); - DatabaseId dbId = node.GetDatabaseId(); + DatabaseId dbId = firstJunctionNode.GetDatabaseId(); if (prevNode.GetDatabaseId() != dbId) { return; // we consider nodes just from the same database for simplicity } - auto lanes = GetLaneDescription(node); + auto lanes = GetLaneDescription(lastNode); + if (!lanes) { + lanes = GetLaneDescription(lastJunctionNode); + } assert(lanes); - auto prevLanes = GetLaneDescription(prevNode); + // When backward grouping pulled approach nodes out of backBuffer, use their lane + // config (from firstJunctionNode) instead of the now-exposed backBuffer.back(). + // The grouped nodes were verified to have the approach config during grouping. + RouteDescription::LaneDescriptionRef prevLanes; + if (junctionNodes.size() > 1) { + prevLanes = GetLaneDescription(firstJunctionNode); + } + if (!prevLanes) { + prevLanes = GetLaneDescription(prevNode); + } assert(prevLanes); - // read lanes on the ALL junction ways, use heuristics what lanes we can use to move forward - Id nodeId = postprocessor.GetNodeId(node); + // Collect node IDs internal to the junction group (to exclude from exits) + std::set internalNodeIds; + for (const auto* jNode : junctionNodes) { + internalNodeIds.insert(postprocessor.GetNodeId(*jNode)); + } WayRef prevWay = postprocessor.GetWay(prevNode.GetDBFileOffset()); Id prevNodeId = prevWay->GetId(prevNode.GetCurrentNodeIndex()); - // bearing from current node to previous + // bearing from first junction node to previous node Bearing prevNodeBearing = GetSphericalBearingInitial(prevWay->nodes[prevNode.GetTargetNodeIndex()].GetCoord(), prevWay->nodes[prevNode.GetCurrentNodeIndex()].GetCoord()); - WayRef way = postprocessor.GetWay(node.GetDBFileOffset()); - Id nextNodeId = way->GetId(node.GetTargetNodeIndex()); - Bearing nextNodeBearing = GetSphericalBearingInitial(way->nodes[node.GetCurrentNodeIndex()].GetCoord(), - way->nodes[node.GetTargetNodeIndex()].GetCoord()); + // Outgoing way: from the last junction node + WayRef outWay; + Id nextNodeId; + Bearing nextNodeBearing; + if (lastJunctionNode.GetPathObject().Valid() && lastJunctionNode.GetPathObject().GetType() == refWay) { + outWay = postprocessor.GetWay(lastJunctionNode.GetDBFileOffset()); + nextNodeId = outWay->GetId(lastJunctionNode.GetTargetNodeIndex()); + nextNodeBearing = GetSphericalBearingInitial(outWay->nodes[lastJunctionNode.GetCurrentNodeIndex()].GetCoord(), + outWay->nodes[lastJunctionNode.GetTargetNodeIndex()].GetCoord()); + } else { + return; // no valid outgoing way + } + + // Collect exits from ALL junction nodes + size_t totalObjects = 0; + for (const auto* jNode : junctionNodes) { + totalObjects += jNode->GetObjects().size(); + } std::vector junctionExits; // excluding incoming and outgoing way std::vector junctionLeftExits; std::vector junctionRightExits; - junctionExits.reserve(node.GetObjects().size()); - junctionLeftExits.reserve(node.GetObjects().size()); - junctionRightExits.reserve(node.GetObjects().size()); - - for (const auto &o: node.GetObjects()){ - if (!o.IsWay()) { - continue; // areas not considered now - } - - way=postprocessor.GetWay(DBFileOffset(node.GetDatabaseId(), o.GetFileOffset())); - for (size_t i = 0; i < way->nodes.size(); i++) { - if (way->nodes[i].GetId() == nodeId) { - if (i < way->nodes.size()-1 && postprocessor.CanUseForward(dbId, nodeId, o)) { - Id junctionNodeId = way->nodes[i+1].GetId(); - if (junctionNodeId!=prevNodeId && junctionNodeId!=nextNodeId) { - auto bearing = GetSphericalBearingInitial(way->nodes[i].GetCoord(), way->nodes[i + 1].GetCoord()); - auto wayLanes = postprocessor.GetLanes(node.GetDatabaseId(), way, true); - junctionExits.push_back({junctionNodeId, bearing, Bearing(), wayLanes}); + junctionExits.reserve(totalObjects); + junctionLeftExits.reserve(totalObjects); + junctionRightExits.reserve(totalObjects); + + // Track exit node IDs we've already added to avoid duplicates from overlapping object lists + std::set addedExitNodeIds; + + for (const auto* jNode : junctionNodes) { + Id jNodeId = postprocessor.GetNodeId(*jNode); + + for (const auto &o: jNode->GetObjects()) { + if (!o.IsWay()) { + continue; // areas not considered now + } + + WayRef way = postprocessor.GetWay(DBFileOffset(dbId, o.GetFileOffset())); + for (size_t i = 0; i < way->nodes.size(); i++) { + if (way->nodes[i].GetId() == jNodeId) { + if (i < way->nodes.size()-1 && postprocessor.CanUseForward(dbId, jNodeId, o)) { + Id junctionNodeId = way->nodes[i+1].GetId(); + if (junctionNodeId!=prevNodeId && junctionNodeId!=nextNodeId + && internalNodeIds.count(junctionNodeId) == 0 + && addedExitNodeIds.count(junctionNodeId) == 0) { + auto bearing = GetSphericalBearingInitial(way->nodes[i].GetCoord(), way->nodes[i + 1].GetCoord()); + auto wayLanes = postprocessor.GetLanes(dbId, way, true); + junctionExits.push_back({junctionNodeId, bearing, Bearing(), wayLanes}); + addedExitNodeIds.insert(junctionNodeId); + } } - } - if (i > 0 && postprocessor.CanUseBackward(dbId, nodeId, o)) { - Id junctionNodeId = way->nodes[i-1].GetId(); - if (junctionNodeId!=prevNodeId && junctionNodeId!=nextNodeId) { - auto bearing = GetSphericalBearingInitial(way->nodes[i].GetCoord(), way->nodes[i - 1].GetCoord()); - auto wayLanes = postprocessor.GetLanes(node.GetDatabaseId(), way, false); - junctionExits.push_back({junctionNodeId, bearing, Bearing(), wayLanes}); + if (i > 0 && postprocessor.CanUseBackward(dbId, jNodeId, o)) { + Id junctionNodeId = way->nodes[i-1].GetId(); + if (junctionNodeId!=prevNodeId && junctionNodeId!=nextNodeId + && internalNodeIds.count(junctionNodeId) == 0 + && addedExitNodeIds.count(junctionNodeId) == 0) { + auto bearing = GetSphericalBearingInitial(way->nodes[i].GetCoord(), way->nodes[i - 1].GetCoord()); + auto wayLanes = postprocessor.GetLanes(dbId, way, false); + junctionExits.push_back({junctionNodeId, bearing, Bearing(), wayLanes}); + addedExitNodeIds.insert(junctionNodeId); + } } + break; } - break; } } } + // Filter exits whose bearing is too close to the incoming direction (opposite carriageway / U-turn). + // An exit within ~30° of prevNodeBearing is going back toward where we came from. + if (junctionNodes.size() > 1) { + junctionExits.erase( + std::remove_if(junctionExits.begin(), junctionExits.end(), + [&prevNodeBearing](const JunctionExit &exit) { + Bearing diff = exit.bearing - prevNodeBearing; + // diff near 0° means exit goes same direction as "junction→prev", i.e., back where we came from + return diff < Bearing::Degrees(30) || diff > Bearing::Degrees(330); + }), + junctionExits.end()); + } + Bearing prevNodeRelativeToNextBearing = prevNodeBearing - nextNodeBearing; for (JunctionExit &exit: junctionExits){ Bearing relativeToNextBearing = exit.bearing - nextNodeBearing; // relative to next @@ -2022,6 +2083,15 @@ namespace osmscout { } nodePtr->AddDescription(suggested); } + // Also write suggestion to grouped junction nodes that have the same lane config + // as the approach. These nodes were removed from backBuffer during grouping but + // still represent approach segments that need the suggestion. + for (auto* jNode : junctionNodes) { + auto nodeLanes = GetLaneDescription(*jNode); + if (nodeLanes && *prevLanes == *nodeLanes) { + jNode->AddDescription(suggested); + } + } } RouteDescription::LaneDescriptionRef RoutePostprocessor::SuggestedLanesPostprocessor::GetLaneDescription(const RouteDescription::Node &node) const @@ -2032,8 +2102,6 @@ namespace osmscout { bool RoutePostprocessor::SuggestedLanesPostprocessor::Process(const PostprocessorContext& postprocessor, RouteDescription& description) { - using namespace std::string_view_literals; - // buffer of traveled nodes, recent node at back std::list backBuffer; for (auto& node : description.Nodes()) { @@ -2055,9 +2123,58 @@ namespace osmscout { if (prevLanes->GetLaneCount() > lanes->GetLaneCount() // lane count was decreased || prevLanes->GetLaneTurns() != lanes->GetLaneTurns()) { // lane turns changed - EvaluateLaneSuggestion(postprocessor, node, backBuffer); + // Group junction nodes: scan backwards through backBuffer for preceding junction nodes + // that are close together and have the same lane config. This handles complex intersections + // modeled as multiple OSM nodes connected by short internal segments. + std::vector junctionNodes; + junctionNodes.push_back(&node); + + size_t backwardCount = 0; + for (auto bufIt = backBuffer.rbegin(); bufIt != backBuffer.rend(); ++bufIt) { + // Keep at least one node in backBuffer as the prevNode for evaluation + if (backwardCount + 1 >= backBuffer.size()) { + break; + } + + RouteDescription::Node* candidate = *bufIt; + + // Must be a real junction (multiple ways crossing) + if (candidate->GetObjects().size() <= 1) { + break; + } + // Must be close to the trigger node (first junction node) + Distance dist = GetSphericalDistance(candidate->GetLocation(), + junctionNodes.front()->GetLocation()); + if (dist > Meters(35)) { + break; + } + // The segment from this node should have the same lane config as the approach + auto candidateLanes = GetLaneDescription(*candidate); + if (!candidateLanes || + candidateLanes->GetLaneCount() != prevLanes->GetLaneCount() || + candidateLanes->GetLaneTurns() != prevLanes->GetLaneTurns()) { + break; + } + + junctionNodes.push_back(candidate); + backwardCount++; + } + + // Reverse so junctionNodes is in route order (earliest first) + std::reverse(junctionNodes.begin(), junctionNodes.end()); + + // Remove backward-grouped nodes from backBuffer + for (size_t i = 0; i < backwardCount && !backBuffer.empty(); ++i) { + backBuffer.pop_back(); + } + + if (!backBuffer.empty()) { + EvaluateLaneSuggestion(postprocessor, junctionNodes, node, backBuffer); + } backBuffer.clear(); + backBuffer.push_back(&node); + continue; } }