Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Tests/src/RoutePostprocessorTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -1077,10 +1078,9 @@ TEST_CASE("Describe complex city junction: Průmyslová, Černokostecká")
auto nodeIt = description.Nodes().begin();
auto suggestedLanes = nodeIt->GetDescription<RouteDescription::SuggestedLaneDescription>();
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);
}
}

Expand Down
16 changes: 10 additions & 6 deletions libosmscout/include/osmscout/routing/RoutePostprocessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouteDescription::Node*> &junctionNodes,
const RouteDescription::Node &lastNode,
const std::list<RouteDescription::Node*> &backBuffer) const;

private:
Expand Down
201 changes: 159 additions & 42 deletions libosmscout/src/osmscout/routing/RoutePostprocessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
#include <algorithm>
#include <cassert>
#include <iostream>
#include <string_view>
#include <numeric>
#include <set>
#include <string_view>

namespace osmscout {

Expand Down Expand Up @@ -1851,75 +1852,135 @@ namespace osmscout {
} // end of anonymous namespace

void RoutePostprocessor::SuggestedLanesPostprocessor::EvaluateLaneSuggestion(const PostprocessorContext& postprocessor,
const RouteDescription::Node &node,
const std::vector<RouteDescription::Node*> &junctionNodes,
const RouteDescription::Node &lastNode,
const std::list<RouteDescription::Node*> &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<Id> 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<JunctionExit> junctionExits; // excluding incoming and outgoing way
std::vector<JunctionExit> junctionLeftExits;
std::vector<JunctionExit> 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<Id> 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
Expand Down Expand Up @@ -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
Expand All @@ -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<RouteDescription::Node*> backBuffer;
for (auto& node : description.Nodes()) {
Expand All @@ -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<RouteDescription::Node*> 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;
}
}

Expand Down
Loading