diff --git a/libs/traffxml/traff_decoder.cpp b/libs/traffxml/traff_decoder.cpp index f67130775..0865d09e1 100644 --- a/libs/traffxml/traff_decoder.cpp +++ b/libs/traffxml/traff_decoder.cpp @@ -21,7 +21,9 @@ #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" @@ -91,6 +93,11 @@ auto constexpr kAttributePenalty = 4; */ auto constexpr kReducedAttributePenalty = 2; +/* + * Radius around reference point in which to search for junctions + */ +auto constexpr kJunctionPointRadius = 500.0; + /* * Maximum distance in meters from location endpoint at which a turn penalty is applied */ @@ -583,11 +590,65 @@ double RoutingTraffDecoder::TraffEstimator::GetFerryLandingPenalty(Purpose /* pu double RoutingTraffDecoder::TraffEstimator::CalcOffroad(ms::LatLon const & from, ms::LatLon const & to, Purpose /* purpose */) const { - double result = ms::DistanceOnEarth(from, to); + /* + * 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. + */ - result *= kOffroadPenalty; + double defaultWeight = ms::DistanceOnEarth(from, to) * kOffroadPenalty; - return result; + /* + * 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 const & junctions) + { + m2::PointD m2RoadPoint = mercator::FromLatLon(roadPoint); + auto it = junctions.find(m2RoadPoint); + if (it != junctions.end()) + return it->second; + // TODO this is likely an inefficient way to return near-matches + for (auto & [point, weight] : junctions) + if (m2RoadPoint.EqualDxDy(point, kMwmPointAccuracy)) + return weight; + 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; } /* @@ -1145,11 +1206,106 @@ void RoutingTraffDecoder::DecodeLocation(traffxml::TraffMessage & message, traff 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)) + { + 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 & junctions) +{ + m2::PointD const m2Point = mercator::FromLatLon(point.m_coordinates); + std::map 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)); + + // TODO make junction point radius dependent on distance between reference points + if (weight > kJunctionPointRadius) + 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, kJunctionPointRadius), + 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) diff --git a/libs/traffxml/traff_decoder.hpp b/libs/traffxml/traff_decoder.hpp index 1a3ec0dbd..58515a02c 100644 --- a/libs/traffxml/traff_decoder.hpp +++ b/libs/traffxml/traff_decoder.hpp @@ -325,6 +325,18 @@ public: RoutingTraffDecoder & m_decoder; }; + struct JunctionCandidateInfo + { + JunctionCandidateInfo(double weight) + : m_weight(weight) + {} + + double m_weight; + size_t m_segmentsIn = 0; + size_t m_segmentsOut = 0; + size_t m_twoWaySegments = 0; + }; + RoutingTraffDecoder(DataSource & dataSource, CountryInfoGetterFn countryInfoGetter, const CountryParentNameGetterFn & countryParentNameGetter, std::map & messageCache); @@ -467,6 +479,36 @@ protected: private: static void LogCode(routing::RouterResultCode code, double const elapsedSec); + /** + * @brief Populates the list of candidates for junction points. + * + * If the location has a fuzziness of `LowRes`, the map is searched for candidates around the + * `from` and `to` points, which are taken from the `m_location` member of `m_message`. The weight + * for each candidate is calculated based on its distance from the reference point and the match + * between the attributes of the segment and the location. Since junction points are part of + * multiple segments, the best match wins. Candidates and their weight are stored in + * `m_startJunctions` and `m_endJunctions`. + * + * If the location’s fuzziness attribute is empty or does not equal `LowRes`, `m_startJunctions` + * and `m_endJunctions` are cleared. + */ + void GetJunctionPointCandidates(); + + /** + * @brief Populates a list of candidates for junction points. + * + * Implementation for `GetJunctionPointCandidates()`. The map is searched for candidates around + * `point`. The weight for each candidate is calculated based on its distance from `point` and + * the match between the attributes of the segment and the location of `m_message`. Since junction + * points are part of multiple segments, the best match wins. Candidates and their weight are + * stored in `junctions`. + * + * @param point The reference point + * @param junctions Receives a list of junction candidates with their weight + */ + void GetJunctionPointCandidates(Point const & point, + std::map & junctions); + /** * @brief Mutex for access to shared members. * @@ -482,6 +524,22 @@ private: std::unique_ptr m_router; std::optional m_message = std::nullopt; + /** + * @brief Junction points near start of location, with their associated offroad weight. + * + * If the list is empty, no junction alignment at the `from` point will be done and decoding + * relies solely on point coordinates. + */ + std::map m_startJunctions; + + /** + * @brief Junction points near end of location, with their associated offroad weight. + * + * If the list is empty, no junction alignment at the `to` point will be done and decoding + * relies solely on point coordinates. + */ + std::map m_endJunctions; + /** * @brief The road ref of `m_message`, parsed with `ParseRef()` */