diff --git a/data/test_data/traff/DE-B2R-LerchenauerStrasse-Petueltunnel.xml b/data/test_data/traff/DE-B2R-LerchenauerStrasse-Petueltunnel.xml
new file mode 100644
index 000000000..f8ac9beb7
--- /dev/null
+++ b/data/test_data/traff/DE-B2R-LerchenauerStrasse-Petueltunnel.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ +48.176102 +11.558100
+ +48.178001 +11.572800
+
+
+
+
+
+
+
diff --git a/data/test_data/traff/DE-B2R-SendlingSued-Passauerstrasse.xml b/data/test_data/traff/DE-B2R-SendlingSued-Passauerstrasse.xml
index 72eb3e515..50df83776 100644
--- a/data/test_data/traff/DE-B2R-SendlingSued-Passauerstrasse.xml
+++ b/data/test_data/traff/DE-B2R-SendlingSued-Passauerstrasse.xml
@@ -3,6 +3,7 @@
A combination of reference points on the opposite carriageway, high offroad cost,
allowing any road to be used and a low penalty results in a completely incorrect
location (through a residential area rather than along the opposite carriageway).
+ Test case for turn penalty (eastern end) if the above is resolved.
-->
diff --git a/data/test_data/traff/DE-GM4-UrbanDualCarriagewayEndpoint.xml b/data/test_data/traff/DE-GM4-UrbanDualCarriagewayEndpoint.xml
new file mode 100644
index 000000000..5340bdd5e
--- /dev/null
+++ b/data/test_data/traff/DE-GM4-UrbanDualCarriagewayEndpoint.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ +48.167301 +11.586200
+ +48.164200 +11.586500
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/traffxml/traff_decoder.cpp b/libs/traffxml/traff_decoder.cpp
index 55cf13ad7..97480ec79 100644
--- a/libs/traffxml/traff_decoder.cpp
+++ b/libs/traffxml/traff_decoder.cpp
@@ -91,6 +91,21 @@ auto constexpr kAttributePenalty = 4;
*/
auto constexpr kReducedAttributePenalty = 2;
+/*
+ * Maximum distance in meters from location endpoint at which a turn penalty is applied
+ */
+auto constexpr kTurnPenaltyMaxDist = 100.0;
+
+/*
+ * Minimum angle in degrees at which turn penalty is applied
+ */
+auto constexpr kTurnPenaltyMinAngle = 65.0;
+
+/*
+ * Minimum angle in degrees at which the full turn penalty is applied
+ */
+auto constexpr kTurnPenaltyFullAngle = 90.0;
+
/*
* Invalid feature ID.
* Borrowed from indexer/feature_decl.hpp.
@@ -563,15 +578,67 @@ double RoutingTraffDecoder::TraffEstimator::GetUTurnPenalty(Purpose /* purpose *
return 2 * 60; // seconds
}
-double RoutingTraffDecoder::TraffEstimator::GetTurnPenalty(Purpose /* purpose */, double /* angle */,
- routing::RoadGeometry const & /* from_road */,
- routing::RoadGeometry const & /* to_road */,
- bool /* is_left_hand_traffic */) const
+double RoutingTraffDecoder::TraffEstimator::GetTurnPenalty(Purpose /* purpose */, double angle,
+ routing::RoadGeometry const & from_road,
+ routing::RoadGeometry const & to_road,
+ bool is_left_hand_traffic) const
{
/*
* TODO determine if turn penalties make sense for the traffic decoder, else leave them out.
+ * `angle` seems to be in degrees, right is negative
+ * Turn is at the first or last point of the roads involved, compare points to find out.
*/
- return 0.0;
+ // Flip sign for left-hand traffic, so a positive angle always means a turn across traffic
+ if (is_left_hand_traffic)
+ angle *= -1;
+
+ // We only penalize sharp turns (above kTurnPenaltyMinAngle) across traffic
+ if (angle < kTurnPenaltyMinAngle)
+ return 0.0;
+
+ /*
+ * Identify coordinates of location endpoints and of the turn, and establish distance between the
+ * turn and the nearest endpoint.
+ */
+ ms::LatLon from = m_decoder.m_message.value().m_location.value().m_from
+ ? m_decoder.m_message.value().m_location.value().m_from.value().m_coordinates
+ : m_decoder.m_message.value().m_location.value().m_at.value().m_coordinates;
+ ms::LatLon to = m_decoder.m_message.value().m_location.value().m_to
+ ? m_decoder.m_message.value().m_location.value().m_to.value().m_coordinates
+ : m_decoder.m_message.value().m_location.value().m_at.value().m_coordinates;
+
+ // Upper boundary for distance (approximately earth circumference)
+ double dist = 4.0e+7;
+
+ for (auto & fromPoint : { from_road.GetPoint(0), from_road.GetPoint(from_road.GetPointsCount() - 1) })
+ for (auto & toPoint : { to_road.GetPoint(0), to_road.GetPoint(to_road.GetPointsCount() - 1) })
+ if (fromPoint == toPoint)
+ for (auto & endpoint : { from, to })
+ {
+ auto newdist = ms::DistanceOnEarth(fromPoint, endpoint);
+ if (newdist < dist)
+ dist = newdist;
+ }
+
+ // We only penalize turns close to an endpoint
+ if (dist > kTurnPenaltyMaxDist)
+ return 0.0;
+
+ /*
+ * The penalty depends on the distance between the turn point and the nearest endpoint: the
+ * shorter the distance, the greater the penalty. This is obtained by subtracting the distance
+ * from `kTurnPenaltyMaxDist`.
+ *
+ * Above `kTurnPenaltyFullAngle`, the full turn penalty applies, i.e. the distance-based value is
+ * multiplied with `kAttributePenalty`.
+ *
+ * Between `kTurnPenaltyMinAngle` and `kTurnPenaltyFullAngle`, the penalty proportionally
+ * increases from 0 to the full value.
+ */
+ double result = (kTurnPenaltyMaxDist - dist) * kAttributePenalty;
+ if (angle < kTurnPenaltyFullAngle)
+ result *= (angle - kTurnPenaltyMinAngle) / (kTurnPenaltyFullAngle - kTurnPenaltyMinAngle);
+ return result;
}
double RoutingTraffDecoder::TraffEstimator::GetFerryLandingPenalty(Purpose /* purpose */) const