Files
comaps/libs/traffxml/traff_decoder.cpp
2025-11-08 18:16:59 +00:00

1547 lines
61 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "traffxml/traff_decoder.hpp"
//#include "traffxml/traff_foo.hpp"
#include "geometry/distance_on_sphere.hpp"
#include "geometry/mercator.hpp"
#include "indexer/feature.hpp"
#include "indexer/road_shields_parser.hpp"
// Only needed for OpenlrTraffDecoder, see below
#if 0
#include "openlr/decoded_path.hpp"
#include "openlr/openlr_decoder.hpp"
#include "openlr/openlr_model.hpp"
#endif
#include "routing/async_router.hpp"
#include "routing/checkpoints.hpp"
#include "routing/edge_estimator.hpp"
#include "routing/maxspeeds.hpp"
#include "routing/route.hpp"
#include "routing/router_delegate.hpp"
#include "routing/routing_helpers.hpp"
#include "routing_common/car_model.hpp"
#include "routing_common/maxspeed_conversion.hpp"
#include "storage/routing_helpers.hpp"
#include "traffic/traffic_cache.hpp"
#include <boost/algorithm/string.hpp>
namespace traffxml
{
enum class RefParserState
{
Whitespace,
Alpha,
Numeric
};
// Only needed for OpenlrTraffDecoder, see below
#if 0
// Number of worker threads for the OpenLR decoder
/*
* TODO how to determine the best number of worker threads?
* One per direction? Does not seem to help with bidirectional locations (two reference points).
* One per segment (fromvia/fromat, viato/atto)? Not yet tested.
* Otherwise there is little to be gained, as we decode messages one at a time.
*/
auto constexpr kNumDecoderThreads = 1;
#endif
// Timeout for the router in seconds, used by RoutingTraffDecoder
// TODO set to a sensible value
auto constexpr kRouterTimeoutSec = 30;
/*
* One meter per second. The TraffEstimator works on distance in meters, not travel time. For code
* which works with speeds and assumes cost to be time-based, a speed of 1 m/s means such
* calculations will effectively return distances in meters.
*/
auto constexpr kOneMpSInKmpH = 3.6;
/*
* Penalty factor for using a fake segment to get to a nearby road.
* Offroad penalty applies to direct distance whereas road penalty applies to roads, which can be up
* to around 3 times the direct distance (theoretically unlimited). Therefore, a factor of 34 times
* the penalty of a well-matched road may be needed to avoid competing with the correct route.
* On the other hand, a very high offroad penalty would give preference to a poorly matched route
* over a well-matched one if it is closer to the reference points.
* Maximum penalty for roads is currently 64 (4 for ramps * 4 for road type * 4 for ref).
* A well-matched road may still have a penalty of around 4 (twice the reduced attribute penalty, or
* once the full attribute penalty).
* A “wrong” road may also just have a penalty of 4 (e.g. road name mismatch, but road class and
* ramp type match).
* A value of 16 has worked well for the DE-B2R-SendlingSued-Passauerstrasse test case. (The
* DE-A10-Werder-GrossKreutz or DE-A115-PotsdamDrewitz-Nuthetal test cases gave incorrect results
* due to lack of fake segments, which was fixed through truncation and now works correctly even
* with an offroad penalty of 128.)
*/
auto constexpr kOffroadPenalty = 16;
/*
* Penalty factor for non-matching attributes
*/
auto constexpr kAttributePenalty = 4;
/*
* Penalty factor for partially matching attributes
*/
auto constexpr kReducedAttributePenalty = 2;
/*
* Lower and upper boundary for radius around endpoint in which to search for junctions, depending
* on the road class. Boundaries are in meters. If the lower boundary exceeds half the distance
* between endpoints, the latter is used instead.
*/
const std::unordered_map<std::optional<RoadClass>, std::array<double, 2>> kJunctionRadiusBoundaries{
{ RoadClass::Motorway, { {300.0, 500.0} } },
{ RoadClass::Trunk, { {300.0, 500.0} } },
{ RoadClass::Primary, { {200.0, 300.0} } },
{ RoadClass::Secondary, { {200.0, 300.0} } },
{ RoadClass::Tertiary, { {200.0, 300.0} } },
{ RoadClass::Other, { {200.0, 300.0} } },
{ std::nullopt, { {300.0, 500.0} } }
};
/*
* 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.
*/
uint32_t constexpr kInvalidFeatureId = std::numeric_limits<uint32_t>::max();
TraffDecoder::TraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter,
const CountryParentNameGetterFn & countryParentNameGetter,
std::map<std::string, TraffMessage> & messageCache)
: m_dataSource(dataSource)
, m_countryInfoGetterFn(countryInfoGetter)
, m_countryParentNameGetterFn(countryParentNameGetter)
, m_messageCache(messageCache)
{}
void TraffDecoder::ApplyTrafficImpact(traffxml::TrafficImpact & impact, traffxml::MultiMwmColoring & decoded)
{
traffic::SpeedGroup fromDelay = traffic::SpeedGroup::Unknown;
/*
* Convert delay into speed group (skip if the location is blocked).
* TODO: mapping delay to speed group is not optimal, as a long delay on a short location can
* increase travel time by several orders of magnitude.
* Assume a stretch of road with a speed limit of 60 km/h, i.e. 1 km/min, and a delay of 30 min.
* The lowest speed group, G0, translates to 8% of the posted limit. Since travel time ratio is
* the inverse of speed ratio, in order to represent 30 seconds of delay, the location would have
* to be long enough to take ~8% of that time to traverse under normal conditions, i.e. 2.4 min.,
* or 2.4 km at 60 km/h. For shorter locations, the speed group will underestimate the delay.
* Higher speeds and longer delays would increase the required length. This is somewhat offset by
* the fact that the router adds a somewhat arbitrary extra penalty to segments for which traffic
* problems are reported.
*/
if ((impact.m_delayMins > 0) && (impact.m_speedGroup != traffic::SpeedGroup::TempBlock))
{
double normalDurationS = 0;
for (auto dit = decoded.begin(); dit != decoded.end(); dit++)
{
/*
* We are initializing two structures for map access here: an MWM handle to get maxspeeds,
* and a FeaturesLoaderGuard to retrieve points. This may not be very elegant, but works.
* Since both involve somewhat complex structures, a rewrite might not be simple, although
* improvements are certainly welcome.
*/
auto const handle = m_dataSource.GetMwmHandleById(dit->first);
auto const speeds = routing::LoadMaxspeeds(handle);
FeaturesLoaderGuard g(m_dataSource, dit->first);
uint32_t lastFid = kInvalidFeatureId;
std::vector<m2::PointD> points;
routing::MaxspeedType speedKmPH = routing::kInvalidSpeed;
for (auto cit = dit->second.begin(); cit != dit->second.end(); cit++)
{
// read points only once per feature ID, not once per segment
if (lastFid != cit->first.GetFid())
{
auto f = g.GetOriginalFeatureByIndex(cit->first.GetFid());
f->ResetGeometry();
assign_range(points, f->GetPoints(FeatureType::BEST_GEOMETRY));
lastFid = cit->first.GetFid();
auto const speed = speeds->GetMaxspeed(cit->first.GetFid());
speedKmPH = speed.GetSpeedKmPH(cit->first.GetDir() == traffic::TrafficInfo::RoadSegmentId::kForwardDirection);
}
auto const idx = cit->first.GetIdx();
/*
* TODO sum of all lengths may differ from route length by up to ~6% in either direction.
* This is partly due to the fact that route length also includes fake endings, which we are
* not counting here. However, this only explains routes being longer than the sum of all
* lengths calculated here, yet in some cases the route is shorter.
*/
auto const length = mercator::DistanceOnEarth(points[idx], points[idx + 1]);
if (speedKmPH != routing::kInvalidSpeed)
normalDurationS += length * kOneMpSInKmpH / speedKmPH;
}
}
auto const delayedDurationS = normalDurationS + impact.m_delayMins * 60;
fromDelay = traffic::GetSpeedGroupByPercentage(normalDurationS * 100.0f / delayedDurationS);
LOG(LINFO, ("Normal duration:", normalDurationS, "delayed duration:", delayedDurationS, "speed group:", DebugPrint(fromDelay)));
}
for (auto dit = decoded.begin(); dit != decoded.end(); dit++)
{
std::unique_ptr<routing::Maxspeeds> speeds = nullptr;
if ((impact.m_speedGroup != traffic::SpeedGroup::TempBlock) && (impact.m_maxspeed != traffxml::kMaxspeedNone))
{
// load maxspeeds once per MWM and only if needed
auto const handle = m_dataSource.GetMwmHandleById(dit->first);
speeds = routing::LoadMaxspeeds(handle);
}
for (auto cit = dit->second.begin(); cit != dit->second.end(); cit++)
{
/*
* Consolidate TrafficImpact into a single SpeedGroup per segment.
* Exception: if TrafficImpact already has SpeedGrup::TempBlock, no need to evaluate
* the rest.
*/
traffic::SpeedGroup sg = impact.m_speedGroup;
if ((sg != traffic::SpeedGroup::TempBlock) && (fromDelay != traffic::SpeedGroup::Unknown))
// process delay
if ((sg == traffic::SpeedGroup::Unknown) || (fromDelay < sg))
sg = fromDelay;
if ((sg != traffic::SpeedGroup::TempBlock) && (impact.m_maxspeed != traffxml::kMaxspeedNone))
{
// process maxspeed
if (speeds)
{
traffic::SpeedGroup fromMaxspeed = traffic::SpeedGroup::Unknown;
auto const speed = speeds->GetMaxspeed(cit->first.GetFid());
auto const speedKmPH = speed.GetSpeedKmPH(cit->first.GetDir() == traffic::TrafficInfo::RoadSegmentId::kForwardDirection);
if (speedKmPH != routing::kInvalidSpeed)
{
fromMaxspeed = traffic::GetSpeedGroupByPercentage(impact.m_maxspeed * 100.0f / speedKmPH);
if ((sg == traffic::SpeedGroup::Unknown) || (fromMaxspeed < sg))
sg = fromMaxspeed;
}
}
}
cit->second = sg;
}
}
}
void TraffDecoder::DecodeMessage(traffxml::TraffMessage & message)
{
if (!message.m_location)
return;
// Decode events into consolidated traffic impact
std::optional<traffxml::TrafficImpact> impact = message.GetTrafficImpact();
LOG(LINFO, (" Impact: ", impact));
// Skip further processing if there is no impact
if (!impact)
return;
traffxml::MultiMwmColoring decoded;
bool isDecoded = false;
std::vector<std::string> ids = message.m_replaces;
ids.insert(ids.begin(), message.m_id);
for (auto & id : ids)
{
auto it = m_messageCache.find(id);
if ((it != m_messageCache.end())
&& !it->second.m_decoded.empty()
&& (it->second.m_location == message.m_location))
{
// cache already has a message with reusable location
LOG(LINFO, (" Location for message", message.m_id, "can be reused from cache"));
std::optional<traffxml::TrafficImpact> cachedImpact = it->second.GetTrafficImpact();
if (cachedImpact.has_value() && cachedImpact.value() == impact.value())
{
LOG(LINFO, (" Impact for message", message.m_id, "unchanged, reusing cached coloring"));
// same impact, m_decoded can be reused altogether
message.m_decoded = it->second.m_decoded;
return;
}
else if (!isDecoded)
{
/*
* populate only on first occurrence but continue searching, we might find a matching
* location with matching impact
*/
decoded = it->second.m_decoded;
isDecoded = true;
}
}
}
if (!isDecoded)
DecodeLocation(message, decoded);
if (impact)
{
ApplyTrafficImpact(impact.value(), decoded);
std::swap(message.m_decoded, decoded);
}
}
// Disabled for now, as the OpenLR-based decoder is slow, buggy and not well suited to the task.
#if 0
OpenLrV3TraffDecoder::OpenLrV3TraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter,
const CountryParentNameGetterFn & countryParentNameGetter,
std::map<std::string, TraffMessage> & messageCache)
: TraffDecoder(dataSource, countryInfoGetter, countryParentNameGetter, messageCache)
, m_openLrDecoder(dataSource, countryParentNameGetter)
{}
openlr::FunctionalRoadClass OpenLrV3TraffDecoder::GetRoadClassFrc(std::optional<RoadClass> & roadClass)
{
if (!roadClass)
return openlr::FunctionalRoadClass::NotAValue;
switch (roadClass.value())
{
case RoadClass::Motorway: return openlr::FunctionalRoadClass::FRC0;
case RoadClass::Trunk: return openlr::FunctionalRoadClass::FRC0;
case RoadClass::Primary: return openlr::FunctionalRoadClass::FRC1;
case RoadClass::Secondary: return openlr::FunctionalRoadClass::FRC2;
case RoadClass::Tertiary: return openlr::FunctionalRoadClass::FRC3;
/*
* TODO Revisit FRC for Other.
* Other corresponds to FRC47.
* FRC4 matches secondary/tertiary (zero score) and anything below (full score).
* FRC57 match anything below tertiary (full score); secondary/tertiary never match.
* Primary and above never matches any of these FRCs.
*/
case RoadClass::Other: return openlr::FunctionalRoadClass::FRC4;
}
UNREACHABLE();
}
// TODO tweak formula based on FRC, FOW and direct distance (lower FRC roads may have more and sharper turns)
uint32_t OpenLrV3TraffDecoder::GuessDnp(Point & p1, Point & p2)
{
// direct distance
double doe = mercator::DistanceOnEarth(mercator::FromLatLon(p1.m_coordinates),
mercator::FromLatLon(p2.m_coordinates));
/*
* Acceptance boundaries for candidate paths are currently:
*
* for `openlr::LinearSegmentSource::FromLocationReferenceTag`, 0.6 to ~1.67 (i.e. 1/0.6) times
* the direct distance,
*
* for `openlr::LinearSegmentSource::FromCoordinatesTag`, 0.25 to 4 times the direct distance.
*
* A tolerance factor of 1/0.6 is the maximum for which direct distance would be accepted in all
* cases, with an upper boundary of at least ~2.78 times the direct distance. However, this may
* cause the actual distance to be overestimated and an incorrect route chosen as a result, as
* path candidates are scored based on the match between DNP and their length.
* Also, since we use `openlr::LinearSegmentSource::FromCoordinatesTag`, acceptance limits are
* much wider than that.
* In practice, the shortest route from one valley to the next in a mountain area is seldom more
* than 3 times the direct distance, based on a brief examination. This would be even within the
* limits of direct distance, hence we do not need a large correction factor for this scenario.
*
* Candidate values:
* 1.66 (1/0.6) upper boundary for direct distance to be just within the most stringent limits
* 1.41 (2^0.5) ratio between two sides of a square and its diagonal
* 1.3 close to the square root of 1.66 (halfway between 1 and 1.66)
* 1.19 close to the square root of 1.41
* 1 direct distance unmodified
*/
double dist = doe * 1.19f;
// if we have kilometric points, calculate nominal distance as the difference between them
if (p1.m_distance && p2.m_distance)
{
LOG(LINFO, ("Both points have distance, calculating nominal difference"));
float nominalDist = (p1.m_distance.value() - p2.m_distance.value()) * 1000.0;
if (nominalDist < 0)
nominalDist *= -1;
/*
* Plausibility check for nominal distance, as kilometric points along the route may not be
* continuous: discard if shorter than direct distance (geometrically impossible) or if longer
* than 4 times direct distance (somewhat arbitrary, based on the OpenLR acceptance limit for
* `openlr::LinearSegmentSource::FromCoordinatesTag`, as well as real-world observations of
* distances between two adjacent mountain valleys, which are up to roughly 3 times the direct
* distance). If nominal distance is outside these boundaries, discard it and use `dist` (direct
* distance with a tolerance factor).
*/
if ((nominalDist >= doe) && (nominalDist <= doe * 4))
dist = nominalDist;
else
LOG(LINFO, ("Nominal distance:", nominalDist, "direct distance:", doe, " discarding"));
}
return dist + 0.5f;
}
openlr::LocationReferencePoint OpenLrV3TraffDecoder::PointToLrp(Point & point)
{
openlr::LocationReferencePoint result;
result.m_latLon = ms::LatLon(point.m_coordinates.m_lat, point.m_coordinates.m_lon);
return result;
}
openlr::LinearLocationReference OpenLrV3TraffDecoder::TraffLocationToLinearLocationReference(TraffLocation & location, bool backwards)
{
openlr::LinearLocationReference locationReference;
locationReference.m_points.clear();
std::vector<Point> points;
if (location.m_from)
points.push_back(location.m_from.value());
if (location.m_at)
points.push_back(location.m_at.value());
else if (location.m_via)
points.push_back(location.m_via.value());
if (location.m_to)
points.push_back(location.m_to.value());
if (backwards)
std::reverse(points.begin(), points.end());
// m_notVia is ignored as OpenLR does not support this functionality.
CHECK_GREATER(points.size(), 1, ("At least two reference points must be given"));
for (size_t i = 0; i < points.size(); i++)
{
openlr::LocationReferencePoint lrp = PointToLrp(points[i]);
lrp.m_functionalRoadClass = GetRoadClassFrc(location.m_roadClass);
if (location.m_ramps != traffxml::Ramps::None)
lrp.m_formOfWay = openlr::FormOfWay::Sliproad;
if (i < points.size() - 1)
{
lrp.m_distanceToNextPoint
= GuessDnp(points[i], points[i + 1]);
/*
* Somewhat hackish. LFRCNP is evaluated by the same function as FRC and the candidate is
* used or discarded based on whether a score was returned or not (the score itself is not
* used for LFRCNP). However, this means we can use FRC as LFRCNP.
*/
lrp.m_lfrcnp = GetRoadClassFrc(location.m_roadClass);
}
locationReference.m_points.push_back(lrp);
}
return locationReference;
}
// TODO make segment ID in OpenLR a string value, and store messageId
std::vector<openlr::LinearSegment> OpenLrV3TraffDecoder::TraffLocationToOpenLrSegments(TraffLocation & location, std::string & messageId)
{
// Convert the location to a format understood by the OpenLR decoder.
std::vector<openlr::LinearSegment> segments;
int dirs = (location.m_directionality == Directionality::BothDirections) ? 2 : 1;
for (int dir = 0; dir < dirs; dir++)
{
openlr::LinearSegment segment;
/*
* Segment IDs are used internally by the decoder but nowhere else.
* Since we decode TraFF locations one at a time, there are at most two segments in a single
* decoder instance (one segment per direction). Therefore, a segment ID derived from the
* direction is unique within the decoder instance.
*/
segment.m_segmentId = dir;
segment.m_messageId = messageId;
/*
* Segments generated from coordinates can have any number of points. Each point, except for
* the last point, must indicate the distance to the next point. Line properties (functional
* road class (FRC), form of way, bearing) or path properties other than distance to next point
* (lowest FRC to next point, againstDrivingDirection) are ignored.
* Segment length is never evaluated.
* TODO update OpenLR decoder to make all line and path properties optional.
*/
segment.m_source = openlr::LinearSegmentSource::FromCoordinatesTag;
segment.m_locationReference = TraffLocationToLinearLocationReference(location, dir == 0 ? false : true);
segments.push_back(segment);
}
return segments;
}
/*
* TODO the OpenLR decoder is designed to handle multiple segments (i.e. locations).
* Decoding message by message kind of defeats the purpose.
* But after decoding the location, we need to examine the map features we got in order to
* determine the speed groups, thus we may need to decode one by one (TBD).
* If we batch-decode segments, we need to fix the [partner] segment IDs in the segment and path
* structures to accept a TraFF message ID (string) rather than an integer, or derive
* [partner] segment IDs from TraFF message IDs.
*/
void OpenLrV3TraffDecoder::DecodeLocation(traffxml::TraffMessage & message, traffxml::MultiMwmColoring & decoded)
{
ASSERT(message.m_location, ("Message has no location"));
decoded.clear();
// Convert the location to a format understood by the OpenLR decoder.
std::vector<openlr::LinearSegment> segments
= TraffLocationToOpenLrSegments(message.m_location.value(), message.m_id);
for (auto segment : segments)
{
LOG(LINFO, (" Segment:", segment.m_segmentId));
for (size_t i = 0; i < segment.m_locationReference.m_points.size(); i++)
{
LOG(LINFO, (" ", i, ":", segment.m_locationReference.m_points[i].m_latLon));
if (i < segment.m_locationReference.m_points.size() - 1)
{
LOG(LINFO, (" FRC:", segment.m_locationReference.m_points[i].m_functionalRoadClass));
LOG(LINFO, (" DNP:", segment.m_locationReference.m_points[i].m_distanceToNextPoint));
}
}
}
// Decode the location into a path on the map.
// One path per segment
std::vector<openlr::DecodedPath> paths(segments.size());
m_openLrDecoder.DecodeV3(segments, kNumDecoderThreads, paths);
for (size_t i = 0; i < paths.size(); i++)
for (size_t j = 0; j < paths[i].m_path.size(); j++)
{
auto fid = paths[i].m_path[j].GetFeatureId().m_index;
auto segment = paths[i].m_path[j].GetSegId();
uint8_t direction = paths[i].m_path[j].IsForward() ?
traffic::TrafficInfo::RoadSegmentId::kForwardDirection :
traffic::TrafficInfo::RoadSegmentId::kReverseDirection;
decoded[paths[i].m_path[j].GetFeatureId().m_mwmId][traffic::TrafficInfo::RoadSegmentId(fid, segment, direction)] = traffic::SpeedGroup::Unknown;
}
}
#endif
double RoutingTraffDecoder::TraffEstimator::GetUTurnPenalty(Purpose /* purpose */) const
{
// Adds 2 minutes penalty for U-turn. The value is quite arbitrary
// and needs to be properly selected after a number of real-world
// experiments.
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
{
// 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
{
return 20 * 60; // seconds
}
double RoutingTraffDecoder::TraffEstimator::CalcOffroad(ms::LatLon const & from, ms::LatLon const & to,
Purpose /* purpose */) const
{
/*
* Usage of this method is not quite clear. For some locations this method never gets called.
* There is also no clear pattern in which of the two arguments is the reference point and which
* is part of a segment. Either reference point can appear as either argument for either direction,
* nothing to infer from a particular reference point appearing in a particular argument.
*/
double defaultWeight = ms::DistanceOnEarth(from, to) * kOffroadPenalty;
/*
* Retrieves offroad weight from the junctions map supplied, if found, or default.
*
* Bugs: Due to back-and-forth conversion of `roadPoint` from Mercator to WGS84 and back, it may
* no longer match its counterpart in `junctions` (near-miss).
*
* Tests showed very few actual matches. Extending this logic to return near-matches did return
* some more, but still relatively few. This may be due to the way fake segments are chosen.
*
* refPoint: point from TraFF location
* roadPoint: point on segment
* junctions: known junctions for `refPoint`
*
* Returns: reduced offroad weight from table, or default offroad weight if not found
*/
auto const getOffroadFromJunction = [defaultWeight](ms::LatLon const & refPoint, ms::LatLon const & roadPoint,
std::map<m2::PointF, double> const & junctions)
{
m2::PointF m2RoadPoint = mercator::FromLatLon(roadPoint);
auto it = junctions.find(m2RoadPoint);
if (it != junctions.end())
return it->second;
return defaultWeight;
};
/*
* If one of from/to is a reference point and the other is in the corresponding junction map,
* return the weight from the map
*/
if (m_decoder.m_message.value().m_location.value().m_from)
{
if (m_decoder.m_message.value().m_location.value().m_from.value().m_coordinates == from)
return getOffroadFromJunction(from, to, m_decoder.m_startJunctions);
else if (m_decoder.m_message.value().m_location.value().m_from.value().m_coordinates == to)
return getOffroadFromJunction(to, from, m_decoder.m_startJunctions);
}
if (m_decoder.m_message.value().m_location.value().m_to)
{
if (m_decoder.m_message.value().m_location.value().m_to.value().m_coordinates == from)
return getOffroadFromJunction(from, to, m_decoder.m_endJunctions);
else if (m_decoder.m_message.value().m_location.value().m_to.value().m_coordinates == to)
return getOffroadFromJunction(to, from, m_decoder.m_endJunctions);
}
return defaultWeight;
}
/*
* Currently, the attribute penalty (kAttributePenalty or kReducedAttributePenalty) can be applied
* up to 3 times:
* - ramp attribute mismatch
* - road class mismatch
* - road ref mismatch
*/
double RoutingTraffDecoder::TraffEstimator::CalcSegmentWeight(routing::Segment const & segment, routing::RoadGeometry const & road, Purpose /* purpose */) const
{
double result = road.GetDistance(segment.GetSegmentIdx());
if (!m_decoder.m_message || !m_decoder.m_message.value().m_location.value().m_roadClass)
return result;
result *= GetHighwayTypePenalty(road.GetHighwayType(),
m_decoder.m_message.value().m_location.value().m_roadClass,
m_decoder.m_message.value().m_location.value().m_ramps);
if (!m_decoder.m_roadRef.empty())
{
auto const countryFile = m_decoder.m_numMwmIds->GetFile(segment.GetMwmId());
auto const mwmId = m_decoder.m_dataSource.GetMwmIdByCountryFile(countryFile);
FeaturesLoaderGuard g(m_decoder.m_dataSource, mwmId);
auto f = g.GetOriginalFeatureByIndex(segment.GetFeatureId());
auto refs = ftypes::GetRoadShieldsNames(*f);
result *= m_decoder.GetRoadRefPenalty(refs);
}
return result;
}
RoutingTraffDecoder::DecoderRouter::DecoderRouter(CountryParentNameGetterFn const & countryParentNameGetterFn,
routing::TCountryFileFn const & countryFileFn,
routing::CountryRectFn const & countryRectFn,
std::shared_ptr<routing::NumMwmIds> numMwmIds,
std::unique_ptr<m4::Tree<routing::NumMwmId>> numMwmTree,
DataSource & dataSource, RoutingTraffDecoder & decoder)
: routing::IndexRouter(routing::VehicleType::Car /* VehicleType vehicleType */,
false /* bool loadAltitudes */,
countryParentNameGetterFn,
countryFileFn,
countryRectFn,
numMwmIds,
std::move(numMwmTree),
//std::nullopt /* std::optional<traffic::TrafficCache> const & trafficCache */,
std::make_shared<TraffEstimator>(&dataSource, numMwmIds, kOneMpSInKmpH /* maxWeighSpeedKMpH */,
routing::SpeedKMpH(kOneMpSInKmpH / kOffroadPenalty /* weight */,
routing::kNotUsed /* eta */) /* offroadSpeedKMpH */,
decoder),
dataSource)
//, m_directionsEngine(CreateDirectionsEngine(m_vehicleType, m_numMwmIds, m_dataSource)) // TODO we dont need directions, can we disable that?
{}
routing::RoutingOptions RoutingTraffDecoder::DecoderRouter::GetRoutingOptions()
{
return routing::RoutingOptions();
}
RoutingTraffDecoder::RoutingTraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter,
const CountryParentNameGetterFn & countryParentNameGetter,
std::map<std::string, TraffMessage> & messageCache)
: TraffDecoder(dataSource, countryInfoGetter, countryParentNameGetter, messageCache)
{
m_dataSource.AddObserver(*this);
InitRouter();
}
double RoutingTraffDecoder::GetHighwayTypePenalty(std::optional<routing::HighwayType> highwayType,
std::optional<RoadClass> roadClass,
Ramps ramps)
{
double result = 1.0;
if (highwayType)
{
if (IsRamp(highwayType.value()) != (ramps != Ramps::None))
// if one is a ramp and the other is not, treat it as a mismatch
result *= kAttributePenalty;
if (roadClass)
// if the message specifies a road class, penalize mismatches
result *= GetRoadClassPenalty(roadClass.value(), GetRoadClass(highwayType.value()));
}
else // road has no highway class
{
// we cant determine if it is a ramp, penalize for mismatch
result *= kAttributePenalty;
if (roadClass)
// we cant determine if the road matches the required road class, treat it as mismatch
result *= kAttributePenalty;
}
return result;
}
double RoutingTraffDecoder::GetRoadRefPenalty(std::vector<std::string> & refs) const
{
double result = kAttributePenalty;
for (auto & ref : refs)
{
auto newResult = GetRoadRefPenalty(ref);
if (newResult < result)
result = newResult;
if (result == 1)
break;
}
return result;
}
double RoutingTraffDecoder::GetRoadRefPenalty(std::string const & ref) const
{
// skip parsing if ref is empty
if (ref.empty())
{
if (m_roadRef.empty())
return 1;
else if (!m_roadRef.empty())
return kAttributePenalty;
}
// TODO does caching results per ref improve performance?
std::vector<std::string> r = ParseRef(ref);
size_t matches = 0;
if (m_roadRef.empty() && r.empty())
return 1;
else if (m_roadRef.empty() || r.empty())
return kAttributePenalty;
// work on a copy of `m_decoder.m_roadRef`
std::vector<std::string> l = m_roadRef;
if ((l.size() > 1) && (r.size() > 1) && (l.front() == r.front()))
{
/*
* Discard generic prefixes, which are often used to denote the road class.
* This will turn `A1` and `A2` into `1` and `2`, causing them to be treated as a mismatch,
* not a partial match.
*/
l.erase(l.begin());
r.erase(r.begin());
}
// for both sides, count items matched by the other side
for (auto & litem : l)
for (auto ritem : r)
if (litem == ritem)
{
matches++;
break;
}
for (auto ritem : r)
for (auto & litem : l)
if (litem == ritem)
{
matches++;
break;
}
if (matches == 0)
return kAttributePenalty;
else if (matches == (l.size() + r.size()))
return 1;
else
return kReducedAttributePenalty;
}
void RoutingTraffDecoder::OnMapRegistered(platform::LocalCountryFile const & localFile)
{
std::lock_guard<std::mutex> lock(m_mutex);
// register with our router instance, unless it is World or WorldCoasts.
if (!localFile.GetCountryName().starts_with(WORLD_FILE_NAME))
m_numMwmIds->RegisterFile(localFile.GetCountryFile());
}
bool RoutingTraffDecoder::InitRouter()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_router)
return true;
/*
* Code based on RoutingManager::SetRouterImpl(RouterType), which calls
* m_delegate.RegisterCountryFilesOnRoute(numMwmIds); m_delegate being the framework instance.
* RegisterCountryFilesOnRoute() is protected and uses a private `Storage` instance.
* We therefore have to resort to populating `m_numMwmIds` from `m_dataSource`. Unlike the
* “original”, this will only include MWMs loaded on startup, not those added later.
* For these, we register ourselves as an MwmSet::Observer and add maps to `m_numMwms` as they
* are registered.
* World and WorldCoasts must be excluded (as in the original routine), as they would cause the
* router to return bogus routes. Just like the original, we use a string comparison for this.
*/
std::vector<std::shared_ptr<MwmInfo>> mwmsInfo;
m_dataSource.GetMwmsInfo(mwmsInfo);
for (auto mwmInfo : mwmsInfo)
if (!mwmInfo->GetCountryName().starts_with(WORLD_FILE_NAME))
m_numMwmIds->RegisterFile(mwmInfo->GetLocalFile().GetCountryFile());
if (m_numMwmIds->IsEmpty())
return false;
auto const countryFileGetter = [this](m2::PointD const & p) -> std::string {
// TODO (@gorshenin): fix CountryInfoGetter to return CountryFile
// instances instead of plain strings.
return m_countryInfoGetterFn().GetRegionCountryId(p);
};
auto const getMwmRectByName = [this](std::string const & countryId) -> m2::RectD {
return m_countryInfoGetterFn().GetLimitRectForLeaf(countryId);
};
m_router =
make_unique<RoutingTraffDecoder::DecoderRouter>(m_countryParentNameGetterFn,
countryFileGetter, getMwmRectByName, m_numMwmIds,
routing::MakeNumMwmTree(*m_numMwmIds, m_countryInfoGetterFn()),
m_dataSource, *this);
return true;
}
// Copied from AsyncRouter
// static
void RoutingTraffDecoder::LogCode(routing::RouterResultCode code, double const elapsedSec)
{
switch (code)
{
case routing::RouterResultCode::StartPointNotFound:
LOG(LWARNING, ("Can't find start or end node"));
break;
case routing::RouterResultCode::EndPointNotFound:
LOG(LWARNING, ("Can't find end point node"));
break;
case routing::RouterResultCode::PointsInDifferentMWM:
LOG(LWARNING, ("Points are in different MWMs"));
break;
case routing::RouterResultCode::RouteNotFound:
LOG(LWARNING, ("Route not found"));
break;
case routing::RouterResultCode::RouteFileNotExist:
LOG(LWARNING, ("There is no routing file"));
break;
case routing::RouterResultCode::NeedMoreMaps:
LOG(LINFO,
("Routing can find a better way with additional maps, elapsed seconds:", elapsedSec));
break;
case routing::RouterResultCode::Cancelled:
LOG(LINFO, ("Route calculation cancelled, elapsed seconds:", elapsedSec));
break;
case routing::RouterResultCode::NoError:
LOG(LINFO, ("Route found, elapsed seconds:", elapsedSec));
break;
case routing::RouterResultCode::NoCurrentPosition:
LOG(LINFO, ("No current position"));
break;
case routing::RouterResultCode::InconsistentMWMandRoute:
LOG(LINFO, ("Inconsistent mwm and route"));
break;
case routing::RouterResultCode::InternalError:
LOG(LINFO, ("Internal error"));
break;
case routing::RouterResultCode::FileTooOld:
LOG(LINFO, ("File too old"));
break;
case routing::RouterResultCode::IntermediatePointNotFound:
LOG(LWARNING, ("Can't find intermediate point node"));
break;
case routing::RouterResultCode::TransitRouteNotFoundNoNetwork:
LOG(LWARNING, ("No transit route is found because there's no transit network in the mwm of "
"the route point"));
break;
case routing::RouterResultCode::TransitRouteNotFoundTooLongPedestrian:
LOG(LWARNING, ("No transit route is found because pedestrian way is too long"));
break;
case routing::RouterResultCode::RouteNotFoundRedressRouteError:
LOG(LWARNING, ("Route not found because of a redress route error"));
break;
case routing::RouterResultCode::HasWarnings:
LOG(LINFO, ("Route has warnings, elapsed seconds:", elapsedSec));
break;
}
}
void RoutingTraffDecoder::AddDecodedSegment(traffxml::MultiMwmColoring & decoded, routing::Segment & segment)
{
auto const countryFile = m_numMwmIds->GetFile(segment.GetMwmId());
MwmSet::MwmId mwmId = m_dataSource.GetMwmIdByCountryFile(countryFile);
auto const fid = segment.GetFeatureId();
auto const sid = segment.GetSegmentIdx();
uint8_t direction = segment.IsForward() ?
traffic::TrafficInfo::RoadSegmentId::kForwardDirection :
traffic::TrafficInfo::RoadSegmentId::kReverseDirection;
decoded[mwmId][traffic::TrafficInfo::RoadSegmentId(fid, sid, direction)] = traffic::SpeedGroup::Unknown;
}
void RoutingTraffDecoder::TruncateRoute(std::vector<routing::RouteSegment> & rsegments,
routing::Checkpoints const & checkpoints, bool backwards)
{
double const endWeight = rsegments.back().GetTimeFromBeginningSec();
// erase leading and trailing fake segments
while(!rsegments.empty() && rsegments.front().GetSegment().GetMwmId() == routing::kFakeNumMwmId)
rsegments.erase(rsegments.begin());
while(!rsegments.empty() && rsegments.back().GetSegment().GetMwmId() == routing::kFakeNumMwmId)
rsegments.pop_back();
if (rsegments.size() < 2)
return;
// Index of first segment to keep, or number of segments to truncate at start.
size_t start = 0;
// Cost saved by omitting all segments prior to `start`.
double startSaving = 0;
// Index of last segment to keep.
size_t end = rsegments.size() - 1;
// Cost saved by omitting the last `end` segments.
double endSaving = 0;
TruncateStart(rsegments, checkpoints, start, startSaving,
backwards ? m_endJunctions : m_startJunctions);
TruncateEnd(rsegments, checkpoints, end, endSaving, endWeight,
backwards ? m_startJunctions : m_endJunctions);
/*
* If start <= end, we can truncate both ends at the same time.
* Else, the segments to truncate overlap. In this case, first truncate where the saving is bigger,
* then recalculate the other end and truncate it as well.
*/
if (start <= end)
{
rsegments.erase(rsegments.begin() + end + 1, rsegments.end());
rsegments.erase(rsegments.begin(), rsegments.begin() + start);
}
else if (startSaving > endSaving)
{
// truncate start, then recalculate and truncate end
rsegments.erase(rsegments.begin(), rsegments.begin() + start);
end = rsegments.size() - 1;
endSaving = 0;
TruncateEnd(rsegments, checkpoints, end, endSaving, endWeight,
backwards ? m_startJunctions : m_endJunctions);
rsegments.erase(rsegments.begin() + end + 1, rsegments.end());
}
else
{
// truncate end, then recalculate and truncate start
rsegments.erase(rsegments.begin() + end + 1, rsegments.end());
start = 0;
startSaving = 0;
TruncateStart(rsegments, checkpoints, start, startSaving,
backwards ? m_endJunctions : m_startJunctions);
rsegments.erase(rsegments.begin(), rsegments.begin() + start);
}
}
void RoutingTraffDecoder::DecodeLocationDirection(traffxml::TraffMessage & message,
traffxml::MultiMwmColoring & decoded, bool backwards)
{
bool adjustToPrevRoute = false; // calculate a fresh route, no adjustments to previous one
uint64_t routeId = 0; // used in callbacks to identify the route, we might not need it at all
std::vector<m2::PointD> points;
if (message.m_location.value().m_from)
points.push_back(mercator::FromLatLon(message.m_location.value().m_from.value().m_coordinates));
if (message.m_location.value().m_at)
points.push_back(mercator::FromLatLon(message.m_location.value().m_at.value().m_coordinates));
else if (message.m_location.value().m_via)
points.push_back(mercator::FromLatLon(message.m_location.value().m_via.value().m_coordinates));
if (message.m_location.value().m_to)
points.push_back(mercator::FromLatLon(message.m_location.value().m_to.value().m_coordinates));
if (backwards)
std::reverse(points.begin(), points.end());
// m_notVia is ignored as OpenLR does not support this functionality.
CHECK_GREATER(points.size(), 1, ("At least two reference points must be given"));
/*
* startDirection is the direction of travel at start. Can be m2::PointD::Zero() to ignore
* direction, or PositionAccumulator::GetDirection(), which basically returns the difference
* between the last position and an earlier one (offset between two points from which the
* direction of travel can be inferred).
*
* For our purposes, points[1] - points[0] would be as close as we could get to a start direction.
* This would be accurate on very straight roads, less accurate on not-so-straight ones. However,
* even on a near-straight road, the standard router (with the default EdgeEstimator) seemed quite
* unimpressed by the direction and insisted on starting off on the carriageway closest to the
* start point and sending us on a huge detour, instead of taking the direct route on the opposite
* carriageway.
*/
m2::PointD startDirection = m2::PointD::Zero();
routing::Checkpoints checkpoints(std::move(points));
/*
* This code is mostly lifted from AsyncRouter::CalculateRoute(), both with and without arguments.
*/
/*
* AsyncRouter::CalculateRoute() has a `DelegateProxy`, which is private. We just need the return
* value of GetDelegate(), which is a `routing::RouterDelegate`, so use that instead. We dont
* need any of the callbacks, therefore we dont set them.
*/
routing::RouterDelegate delegate;
delegate.SetTimeout(kRouterTimeoutSec);
routing::RouterResultCode code;
std::shared_ptr<routing::Route> route;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_router && !InitRouter())
return;
/*
* TODO is that for following a track? If so, can we use that with just 23 reference points?
* Doesnt look like it, m_guides only seems to get used in test functions
*/
//router->SetGuides(std::move(m_guides));
//m_guides.clear();
route = std::make_shared<routing::Route>(m_router->GetName(), routeId);
base::Timer timer;
double elapsedSec = 0.0;
try
{
LOG(LINFO, ("Calculating the route of direct length", checkpoints.GetSummaryLengthBetweenPointsMeters(),
"m. checkpoints:", checkpoints, "startDirection:", startDirection, "router name:", m_router->GetName()));
// Run basic request.
code = m_router->CalculateRoute(checkpoints, startDirection, adjustToPrevRoute,
delegate, *route);
m_router->SetGuides({});
elapsedSec = timer.ElapsedSeconds(); // routing time
LogCode(code, elapsedSec);
LOG(LINFO, ("ETA:", route->GetTotalTimeSec(), "sec."));
}
catch (RootException const & e)
{
code = routing::RouterResultCode::InternalError;
LOG(LERROR, ("Exception happened while calculating route:", e.Msg()));
return;
}
}
if (code == routing::RouterResultCode::NoError)
{
std::vector<routing::RouteSegment> rsegments(route->GetRouteSegments());
TruncateRoute(rsegments, checkpoints, backwards);
/*
* `m_onRoundabout` is set only for the first segment after the junction. In order to identify
* all roundabout segments, cache the last segment with `m_onRoundabout` set. Any subsequent
* segment with the same MWM and feature ID is also a roundabout segment.
*/
routing::Segment lastRoundaboutSegment;
/*
* We usually discard roundabouts, unless the location is a point (`at` point set) or the entire
* decoded location is a roundabout.
*/
bool keepRoundabouts = true;
if (!message.m_location.value().m_at)
for (auto & rsegment : rsegments)
{
if (rsegment.GetRoadNameInfo().m_onRoundabout)
lastRoundaboutSegment = rsegment.GetSegment();
else if ((rsegment.GetSegment().GetMwmId() != lastRoundaboutSegment.GetMwmId())
|| (rsegment.GetSegment().GetFeatureId() != lastRoundaboutSegment.GetFeatureId()))
{
keepRoundabouts = false;
break;
}
}
if (!backwards && message.m_location.value().m_at && !message.m_location.value().m_to)
// fromat in forward direction, add last segment
AddDecodedSegment(decoded, rsegments.back().GetSegment());
else if (!backwards && message.m_location.value().m_at && !message.m_location.value().m_from)
// atto in forward direction, add last segment
AddDecodedSegment(decoded, rsegments.front().GetSegment());
else if (backwards && message.m_location.value().m_at && !message.m_location.value().m_to)
// fromat in backward direction, add first segment
AddDecodedSegment(decoded, rsegments.front().GetSegment());
else if (backwards && message.m_location.value().m_at && !message.m_location.value().m_from)
// atto in backward direction, add first segment
AddDecodedSegment(decoded, rsegments.back().GetSegment());
else if (message.m_location.value().m_at)
{
// fromatto, find closest segment
ms::LatLon at = message.m_location.value().m_at.value().m_coordinates;
routing::RouteSegment & closestRSegment = rsegments.front();
double closestDist = ms::DistanceOnEarth(at, mercator::ToLatLon(closestRSegment.GetJunction().GetPoint()));
for (auto & rsegment : rsegments)
{
// If we have more than two checkpoints, fake segments can occur in the middle, skip them.
if (rsegment.GetSegment().GetMwmId() == routing::kFakeNumMwmId)
continue;
double dist = ms::DistanceOnEarth(at, mercator::ToLatLon(rsegment.GetJunction().GetPoint()));
if (dist < closestDist)
{
closestRSegment = rsegment;
closestDist = dist;
}
}
AddDecodedSegment(decoded, closestRSegment.GetSegment());
}
else
// from[via]to, add all real segments
for (auto & rsegment : rsegments)
{
routing::Segment & segment = rsegment.GetSegment();
// Skip roundabouts to avoid side effects on crossing roads
if (!keepRoundabouts)
{
if (rsegment.GetRoadNameInfo().m_onRoundabout)
{
lastRoundaboutSegment = segment;
continue;
}
else if ((segment.GetMwmId() == lastRoundaboutSegment.GetMwmId())
&& (segment.GetFeatureId() == lastRoundaboutSegment.GetFeatureId()))
continue;
}
// If we have more than two checkpoints, fake segments can occur in the middle, skip them.
if (segment.GetMwmId() == routing::kFakeNumMwmId)
continue;
AddDecodedSegment(decoded, segment);
}
}
}
void RoutingTraffDecoder::DecodeLocation(traffxml::TraffMessage & message, traffxml::MultiMwmColoring & decoded)
{
ASSERT(message.m_location, ("Message has no location"));
decoded.clear();
m_message = message;
if (m_message.value().m_location.value().m_roadRef)
m_roadRef = ParseRef(m_message.value().m_location.value().m_roadRef.value());
else
m_roadRef.clear();
GetJunctionPointCandidates();
int dirs = (message.m_location.value().m_directionality == Directionality::BothDirections) ? 2 : 1;
for (int dir = 0; dir < dirs; dir++)
DecodeLocationDirection(message, decoded, dir == 0 ? false : true /* backwards */);
m_message = std::nullopt;
m_roadRef.clear();
}
void RoutingTraffDecoder::GetJunctionPointCandidates()
{
m_startJunctions.clear();
m_endJunctions.clear();
if (m_message.value().m_location.value().m_fuzziness
&& (m_message.value().m_location.value().m_fuzziness.value() == traffxml::Fuzziness::LowRes))
{
/*
* Identify coordinates of location endpoints and of the turn, and determine distance.
*/
ms::LatLon from = m_message.value().m_location.value().m_from
? m_message.value().m_location.value().m_from.value().m_coordinates
: m_message.value().m_location.value().m_at.value().m_coordinates;
ms::LatLon to = m_message.value().m_location.value().m_to
? m_message.value().m_location.value().m_to.value().m_coordinates
: m_message.value().m_location.value().m_at.value().m_coordinates;
auto dist = ms::DistanceOnEarth(from, to);
auto bounds = kJunctionRadiusBoundaries.at(m_message.value().m_location.value().m_roadClass);
m_junctionRadius = dist / 3.0f;
if (m_junctionRadius > bounds[1])
m_junctionRadius = bounds[1];
else if (m_junctionRadius < bounds[0])
{
m_junctionRadius = dist / 2.0f;
if (m_junctionRadius > bounds[0])
m_junctionRadius = bounds[0];
}
if (m_message.value().m_location.value().m_from)
GetJunctionPointCandidates(m_message.value().m_location.value().m_from.value(), m_startJunctions);
if (m_message.value().m_location.value().m_to)
GetJunctionPointCandidates(m_message.value().m_location.value().m_to.value(), m_endJunctions);
}
}
void RoutingTraffDecoder::GetJunctionPointCandidates(Point const & point, std::map<m2::PointF, double> & junctions)
{
m2::PointD const m2Point = mercator::FromLatLon(point.m_coordinates);
std::map<m2::PointD, JunctionCandidateInfo> pointCandidates;
auto const selectCandidates = [&m2Point, &pointCandidates, this](FeatureType & ft)
{
ft.ParseGeometry(FeatureType::BEST_GEOMETRY);
if (ft.GetGeomType() != feature::GeomType::Line || !routing::IsRoad(feature::TypesHolder(ft)))
return;
for (auto i : {size_t(0), ft.GetPointsCount() - 1})
{
double weight = mercator::DistanceOnEarth(m2Point, ft.GetPoint(i));
if (weight > m_junctionRadius)
continue;
weight *= GetHighwayTypePenalty(routing::CarModel::AllLimitsInstance().GetHighwayType(feature::TypesHolder(ft)),
m_message.value().m_location.value().m_roadClass,
m_message.value().m_location.value().m_ramps);
auto refs = ftypes::GetRoadShieldsNames(ft);
weight *= GetRoadRefPenalty(refs);
/*
* Store candidate point and weight (unless we already have a lower weight).
* These are points read directly from the map, so we should be able to work with true matches
* (according to tests, near-matches are rare and the one we examined was close to the
* tolerance limit, so it could have been accidental).
*/
auto it = pointCandidates.find(ft.GetPoint(i));
if (it == pointCandidates.end())
it = pointCandidates.insert(std::make_pair(ft.GetPoint(i), JunctionCandidateInfo(weight))).first;
else if (weight < it->second.m_weight)
it->second.m_weight = weight;
// check oneway attribute and increase appropriate segment count
if (!ftypes::IsOneWayChecker::Instance()(ft))
it->second.m_twoWaySegments++;
else if (i == 0)
it->second.m_segmentsOut++;
else
it->second.m_segmentsIn++;
}
};
m_dataSource.ForEachInRect(selectCandidates, mercator::RectByCenterXYAndSizeInMeters(m2Point, m_junctionRadius),
scales::GetUpperScale());
/*
* Cycle through point candidates and see if they are really junctions. A point is a junction if
* it can be left through more than one segment, other than the one through which it was reached,
* or reached through more than one segment, other than the one through which it will be left.
* Junctions are added to `junctions`, other points are skipped.
* Bug: may fail to catch duplicate ways at MWM boundaries
*/
for (auto & [candidatePoint, candidateInfo] : pointCandidates)
{
if (candidateInfo.m_segmentsIn > 0)
candidateInfo.m_segmentsIn--;
else if (candidateInfo.m_twoWaySegments > 0)
candidateInfo.m_twoWaySegments--;
if (candidateInfo.m_segmentsOut > 0)
candidateInfo.m_segmentsOut--;
else if (candidateInfo.m_twoWaySegments > 0)
candidateInfo.m_twoWaySegments--;
if ((candidateInfo.m_segmentsIn > 0)
|| (candidateInfo.m_segmentsOut > 0)
|| (candidateInfo.m_twoWaySegments > 0))
junctions.insert(std::make_pair(candidatePoint, candidateInfo.m_weight));
}
}
traffxml::RoadClass GetRoadClass(routing::HighwayType highwayType)
{
switch(highwayType)
{
/*
* Parallel carriageways may be tagged as link, hence consider them equivalent to the underlying
* highway type.
*/
case routing::HighwayType::HighwayMotorway:
case routing::HighwayType::HighwayMotorwayLink:
return traffxml::RoadClass::Motorway;
case routing::HighwayType::HighwayTrunk:
case routing::HighwayType::HighwayTrunkLink:
return traffxml::RoadClass::Trunk;
case routing::HighwayType::HighwayPrimary:
case routing::HighwayType::HighwayPrimaryLink:
return traffxml::RoadClass::Primary;
case routing::HighwayType::HighwaySecondary:
case routing::HighwayType::HighwaySecondaryLink:
return traffxml::RoadClass::Secondary;
case routing::HighwayType::HighwayTertiary:
case routing::HighwayType::HighwayTertiaryLink:
return traffxml::RoadClass::Tertiary;
default:
return traffxml::RoadClass::Other;
}
}
/**
* @brief Returns the penalty factor for road class match/mismatch.
*
* If `lhs` and `rhs` are identical, the penalty factor is 1 (no penalty). If they are adjacent road
* classes (e.g. trunk and primary), the penalty factor is `kReducedAttributePenalty`, in all other
* cases it is `kAttributePenalty`.
*
* @param lhs
* @param rhs
* @return The penalty factor, see above.
*/
double GetRoadClassPenalty(traffxml::RoadClass lhs, traffxml::RoadClass rhs)
{
if (lhs == rhs)
return 1;
switch (lhs)
{
case traffxml::RoadClass::Motorway:
if (rhs == traffxml::RoadClass::Trunk)
return kReducedAttributePenalty;
else
return kAttributePenalty;
case traffxml::RoadClass::Trunk:
if (rhs == traffxml::RoadClass::Motorway || rhs == traffxml::RoadClass::Primary)
return kReducedAttributePenalty;
else
return kAttributePenalty;
case traffxml::RoadClass::Primary:
if (rhs == traffxml::RoadClass::Trunk || rhs == traffxml::RoadClass::Secondary)
return kReducedAttributePenalty;
else
return kAttributePenalty;
case traffxml::RoadClass::Secondary:
if (rhs == traffxml::RoadClass::Primary || rhs == traffxml::RoadClass::Tertiary)
return kReducedAttributePenalty;
else
return kAttributePenalty;
case traffxml::RoadClass::Tertiary:
if (rhs == traffxml::RoadClass::Secondary || rhs == traffxml::RoadClass::Other)
return kReducedAttributePenalty;
else
return kAttributePenalty;
case traffxml::RoadClass::Other:
if (rhs == traffxml::RoadClass::Tertiary)
return kReducedAttributePenalty;
else
return kAttributePenalty;
default:
UNREACHABLE();
}
}
bool IsRamp(routing::HighwayType highwayType)
{
switch(highwayType)
{
case routing::HighwayType::HighwayMotorwayLink:
case routing::HighwayType::HighwayTrunkLink:
case routing::HighwayType::HighwayPrimaryLink:
case routing::HighwayType::HighwaySecondaryLink:
case routing::HighwayType::HighwayTertiaryLink:
return true;
default:
return false;
}
}
std::vector<std::string> ParseRef(std::string const & ref)
{
std::vector<std::string> res;
std::string curr = "";
RefParserState state = RefParserState::Whitespace;
for (size_t i = 0; i < ref.size(); i++)
{
// TODO this list of delimiters might not be exhaustive
if ((ref[i] <= 0x20) || (ref[i] == ',') || (ref[i] == '-') || (ref[i] == '.') || (ref[i] == '/'))
{
// whitespace
if (state != RefParserState::Whitespace)
{
if (state == RefParserState::Alpha)
boost::to_lower(curr);
res.push_back(curr);
curr = "";
}
state = RefParserState::Whitespace;
}
/*
* TODO adapt this to other number systems as well.
* Roman numerals (or any use of letters as numbers) are a stupid idea. If they are at least
* properly delimited, they are treated as a letter group, which sort of works for comparison.
* However, `IVbis` will be treated as one group, whereas `IV bis` will be treated as two.
*/
else if ((ref[i] >= '0') && (ref[i] <= '9'))
{
// numeric
if (state == RefParserState::Alpha)
{
boost::to_lower(curr);
res.push_back(curr);
curr = "";
}
curr += ref[i];
state = RefParserState::Numeric;
}
// anything that is not a delimiter or a digit (as per the above rules) is considered a letter
else
{
// alpha
if (state == RefParserState::Numeric)
{
res.push_back(curr);
curr = "";
}
curr += ref[i];
state = RefParserState::Alpha;
}
}
if (!curr.empty())
res.push_back(curr);
return res;
}
void TruncateStart(std::vector<routing::RouteSegment> & rsegments, routing::Checkpoints const & checkpoints,
size_t & start, double & startSaving, std::map<m2::PointF, double> const & junctions)
{
if (rsegments.empty())
return;
for (size_t i = 0; i < rsegments.size(); i++)
{
double newStartSaving = 0;
/*
* Examine end point of segment: for a junction, take weight from table; else calculate it as
* direct distance multiplied with offroad penalty; calculate saving based on that.
*/
m2::PointF point = rsegments[i].GetJunction().GetPoint();
auto it = junctions.find(point);
if (it != junctions.end())
newStartSaving = rsegments[i].GetTimeFromBeginningSec() - it->second;
else
newStartSaving = rsegments[i].GetTimeFromBeginningSec() -
(mercator::DistanceOnEarth(checkpoints.GetStart(), point) * kOffroadPenalty);
if (newStartSaving > startSaving)
{
start = i + 1; // add 1 because we are ditching this segment and keeping the next one
startSaving = newStartSaving;
}
}
}
void TruncateEnd(std::vector<routing::RouteSegment> & rsegments, routing::Checkpoints const & checkpoints, size_t & end,
double & endSaving, double const endWeight, std::map<m2::PointF, double> const & junctions)
{
for (size_t i = 0; i < rsegments.size(); i++)
{
double newEndSaving = 0;
/*
* Examine end point of segment: for a junction, take weight from table; else calculate it as
* direct distance multiplied with offroad penalty; calculate saving based on that.
*/
m2::PointF point = rsegments[i].GetJunction().GetPoint();
auto it = junctions.find(point);
if (it != junctions.end())
newEndSaving = endWeight - rsegments[i].GetTimeFromBeginningSec() - it->second;
else
newEndSaving = endWeight - rsegments[i].GetTimeFromBeginningSec() -
(mercator::DistanceOnEarth(point, checkpoints.GetFinish()) * kOffroadPenalty);
if (newEndSaving > endSaving)
{
end = i;
endSaving = newEndSaving;
}
}
}
} // namespace traffxml