diff --git a/map/traffic_manager.cpp b/map/traffic_manager.cpp index c89065841..15dac614f 100644 --- a/map/traffic_manager.cpp +++ b/map/traffic_manager.cpp @@ -16,6 +16,8 @@ #include "platform/platform.hpp" +#include "traffxml/traff_model_xml.hpp" + using namespace std::chrono; namespace @@ -25,6 +27,10 @@ auto constexpr kOutdatedDataTimeout = minutes(5) + kUpdateInterval; auto constexpr kNetworkErrorTimeout = minutes(20); auto constexpr kMaxRetriesCount = 5; + +// Number of identical data sources to create for the OpenLR decoder, one source per worker thread. +// TODO how to determine the best number of worker threads? +auto constexpr kNumDecoderThreads = 1; } // namespace TrafficManager::CacheEntry::CacheEntry() @@ -53,7 +59,10 @@ TrafficManager::TrafficManager(const CountryParentNameGetterFn &countryParentNam , m_observer(observer) , m_currentDataVersion(0) , m_state(TrafficState::Disabled) +// TODO no longer needed +#ifdef traffic_dead_code , m_maxCacheSizeBytes(maxCacheSizeBytes) +#endif , m_isRunning(true) , m_isPaused(false) , m_thread(&TrafficManager::ThreadRoutine, this) @@ -113,7 +122,10 @@ void TrafficManager::SetEnabled(bool enabled) void TrafficManager::Clear() { +// TODO no longer needed +#ifdef traffic_dead_code m_currentCacheSizeBytes = 0; +#endif m_mwmCache.clear(); m_lastDrapeMwmsByRect.clear(); m_lastRoutingMwmsByRect.clear(); @@ -190,6 +202,7 @@ void TrafficManager::UpdateActiveMwms(m2::RectD const & rect, { std::lock_guard lock(m_mutex); + m_activeMwmsChanged = true; activeMwms.clear(); for (auto const & mwm : mwms) { @@ -232,11 +245,309 @@ void TrafficManager::UpdateViewport(ScreenBase const & screen) UpdateActiveMwms(screen.ClipRect(), m_lastDrapeMwmsByRect, m_activeDrapeMwms); } +// TODO make this work with multiple sources (e.g. Android) +bool TrafficManager::Subscribe(std::set & mwms) +{ + // TODO what if we’re subscribed already? + // TODO + LOG(LINFO, ("Would subscribe to", mwms)); + m_subscriptionId = "placeholder_subscription_id"; + m_isPollNeeded = true; // would be false if we got a feed here + return true; +} + +// TODO make this work with multiple sources (e.g. Android) +bool TrafficManager::ChangeSubscription(std::set & mwms) +{ + // TODO what if we’re not subscribed yet? + // TODO + LOG(LINFO, ("Would change subscription", m_subscriptionId, "to", mwms)); + m_isPollNeeded = true; // would be false if we got a feed here + return true; +} + +bool TrafficManager::SetSubscriptionArea() +{ + std::set activeMwms; + if (!IsSubscribed()) + { + { + std::lock_guard lock(m_mutex); + m_activeMwmsChanged = false; + UniteActiveMwms(activeMwms); + } + if (!Subscribe(activeMwms)) + return false; + } + else if (m_activeMwmsChanged) + { + { + std::lock_guard lock(m_mutex); + m_activeMwmsChanged = false; + UniteActiveMwms(activeMwms); + } + if (!ChangeSubscription(activeMwms)) + return false; + } + return true; +} + +// TODO make this work with multiple sources (e.g. Android) +void TrafficManager::Unsubscribe() +{ + if (!IsSubscribed()) + return; + // TODO + LOG(LINFO, ("Would unsubscribe from", m_subscriptionId)); + m_subscriptionId.clear(); +} + +bool TrafficManager::IsSubscribed() +{ + return !m_subscriptionId.empty(); +} + +// TODO make this work with multiple sources (e.g. Android) +// TODO deal with subscriptions rejected by the server (delete, resubscribe) +bool TrafficManager::Poll() +{ + // TODO + //std::string path("/home/michael/src/organicmaps/data/test_data/traff/PL-A18-Krzyzowa-Lipiany.xml"); + std::string path("/home/michael/src/organicmaps/data/test_data/traff/PL-A18-Krzyzowa-Lipiany-bidir.xml"); + //std::string path("/home/michael/src/organicmaps/data/test_data/traff/LT-A1-Vezaiciai-Endriejavas.xml"); + pugi::xml_document document; + auto const load_result = document.load_file(path.data()); + if (!load_result) + { + LOG(LERROR, ("Can't load file", path, ":", load_result.description())); + return false; + } + + std::setlocale(LC_ALL, "en_US.UTF-8"); + traffxml::TraffFeed feed; + if (traffxml::ParseTraff(document, feed)) + { + { + std::lock_guard lock(m_mutex); + m_feeds.push_back(feed); + } + return true; + } + else + { + LOG(LWARNING, ("An error occurred parsing the TraFF feed")); + return false; + } +} + +void TrafficManager::Push(traffxml::TraffFeed feed) +{ + std::lock_guard lock(m_mutex); + m_feeds.push_back(feed); +} + +void TrafficManager::UpdateMessageCache(std::map & cache) +{ + traffxml::TraffFeed feed; + // Thread-safe iteration over m_feeds, releasing the mutex during the loop + while (true) + { + { + std::lock_guard lock(m_mutex); + if (!m_feeds.empty()) + { + feed = m_feeds.front(); + m_feeds.erase(m_feeds.begin()); + } + else + break; + } + + for (auto message : feed) + { + LOG(LINFO, (" message:", message)); + auto it = cache.find(message.m_id); + bool process = (it == cache.end()); + if (!process) + process = (timegm(&(it->second.m_updateTime)) < timegm(&(message.m_updateTime))); + if (process) + cache.insert_or_assign(message.m_id, message); + } + } +} + +void TrafficManager::InitializeDataSources(std::vector & dataSources) +{ + /* + * TODO can we include all available MWMs in the list (including non-active ones)? + * Then we could initialize the decoder once and for all. + */ + ForEachActiveMwm([this, &dataSources](MwmSet::MwmId const & mwmId) { + ASSERT(mwmId.IsAlive(), ()); + // TODO do we need .SyncWithDisk() for the file? + for (size_t i = 0; i < dataSources.size(); i++) + dataSources[i].RegisterMap(mwmId.GetInfo()->GetLocalFile()); + }); +} + +/* + * 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. + */ +void TrafficManager::DecodeMessage(openlr::OpenLRDecoder & decoder, + traffxml::TraffMessage & message, std::map & trafficCache) +{ + if (message.m_location) + { + // Decode events into consolidated traffic impact + std::optional impact = message.GetTrafficImpact(); + + LOG(LINFO, (" Impact: ", impact)); + + // Skip further processing if there is no impact + if (!impact) + return; + + // Convert the location to a format understood by the OpenLR decoder. + std::vector segments + = message.m_location.value().ToOpenLrSegments(message.m_id); + + for (auto segment : segments) + { + LOG(LINFO, (" Segment:", segment.m_segmentId)); + for (int 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 paths(segments.size()); + decoder.DecodeV3(segments, kNumDecoderThreads, paths); + + for (size_t i = 0; i < paths.size(); i++) + { + LOG(LINFO, (" Path", i)); + LOG(LINFO, (" Partner segment ID:", paths[i].m_segmentId)); + LOG(LINFO, (" Edges:", paths[i].m_path.size())); + for (size_t j = 0; j < paths[i].m_path.size(); j++) + { + LOG(LINFO, (" ", paths[i].m_path[j])); + } + } + + // TODO store maxspeed in edges + // store decoded paths and speed groups in trafficCache + if (impact) + { + /* + * TODO fully process TrafficImpact (unless m_speedGroup is TempBlock, which overrules everything else) + * If no maxspeed or delay is set, just give out speed groups. + * Else, examine segments, length, normal travel time, travel time considering impact, and + * determine the closest matching speed group. + */ + for (size_t i = 0; i < paths.size(); i++) + for (size_t j = 0; j < paths[i].m_path.size(); j++) + { + std::string countryName = paths[i].m_path[j].GetFeatureId().m_mwmId.GetInfo()->GetCountryName(); + 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; + // TODO process all TrafficImpact fields and determine the speed group based on that + trafficCache[countryName][traffic::TrafficInfo::RoadSegmentId(fid, segment, direction)] = impact.value().m_speedGroup; + } + } + } +} + void TrafficManager::ThreadRoutine() { std::vector mwms; while (WaitForRequest(mwms)) { + // TODO clean out expired messages + + // poll is always needed, unless a new subscription or a subscription change returns a feed + m_isPollNeeded = true; + + if (!SetSubscriptionArea()) + { + LOG(LWARNING, ("SetSubscriptionArea failed.")); + if (!IsSubscribed()) + // do not skip out of the loop, we may need to process pushed feeds + LOG(LWARNING, ("No subscription, no traffic data will be retrieved.")); + } + + // fetch traffic data if subscribed, unless this has already happened in the previous step + if (m_isPollNeeded && IsSubscribed()) + { + if (!Poll()) + { + LOG(LWARNING, ("Poll failed.")); + // TODO set failed status somewhere and retry + } + } + LOG(LINFO, (m_feeds.size(), "feed(s) in queue")); + + /* + * TODO call on a temp struct, then unite with m_messageCache, processing only messages with changes + * (adding segments for new messages, removing segments for deleted messages, replacing segments + * for updated messages) and leaving all other segments untouched + */ + UpdateMessageCache(m_messageCache); + LOG(LINFO, (m_messageCache.size(), "message(s) in cache")); + + // initialize the decoder + /* + * Access to `DataSource` is not thread-safe. The main app, which works with + * `EditableDataSource` (as the map can be edited), wraps map operations into a + * `FeaturesLoaderGuard`. The OpenLR decoder expects one `FrozenDataSource` (a read-only + * subclass) per worker thread – which works as long as the map is not modified. + * Edits are not relevant to the OpenLR decoder. However, if the edits modify MWM files (rather + * than being stored separately), this might confuse the `FrozenDataSource`. In this case, we + * would need to rewrite the OpenLR decoder to work with a `FeaturesLoaderGuard` (which is + * probably the more elegant way to do this anyway). + */ + std::vector dataSources(kNumDecoderThreads); + // TODO test with data source from framework + InitializeDataSources(dataSources); + openlr::OpenLRDecoder decoder(dataSources, m_countryParentNameGetterFn); + + /* + * Map between country names and their colorings. + * TODO use MwmId as map keys: + * As long as we don’t/can‘t use the framework’s `DataSource` instance for the OpenLR decoder, + * `MwmId` instances from the decoder will not match those from the framework because of the + * way the identity operator is currently implemented (comparing `MwmInfo` instances rather than + * their contents). The ultimate goal is to do matching based on `MwmId`s, but that requires + * either running the OpenLR decoder off the shared `DataSource` or changing the way `MwmInfo` + * comparison works, eitehr of which may come with regressions and needs to be tested. + */ + std::map allMwmColoring; + for (auto [id, message] : m_messageCache) + { + LOG(LINFO, (" ", id, ":", message)); + DecodeMessage(decoder, message, allMwmColoring); + } + + // set new coloring for MWMs + OnTrafficDataUpdate(allMwmColoring); + +// TODO no longer needed +#ifdef traffic_dead_code for (auto const & mwm : mwms) { if (!mwm.IsAlive()) @@ -265,8 +576,11 @@ void TrafficManager::ThreadRoutine() m_trafficETags[mwm] = tag; } } +#endif mwms.clear(); } + // Calling Unsubscribe() form the worker thread on exit makes thread synchronization easier + Unsubscribe(); } bool TrafficManager::WaitForRequest(std::vector & mwms) @@ -335,6 +649,8 @@ void TrafficManager::RequestTrafficData() UpdateState(); } +// TODO no longer needed +#ifdef traffic_dead_code void TrafficManager::OnTrafficRequestFailed(traffic::TrafficInfo && info) { std::lock_guard lock(m_mutex); @@ -364,7 +680,57 @@ void TrafficManager::OnTrafficRequestFailed(traffic::TrafficInfo && info) UpdateState(); } +#endif +void TrafficManager::OnTrafficDataUpdate(std::map & trafficCache) +{ + /* + * Much of this code is copied and pasted together from old MWM code, with some minor adaptations: + * + * ForEachActiveMwm and the assertion (not the rest of the body) is from RequestTrafficData(). + * trafficCache lookup is original code. + * TrafficInfo construction is taken fron TheadRoutine(), with modifications (different constructor). + * Updating m_mwmCache is from RequestTrafficData(MwmSet::MwmId const &, bool), with modifications. + * The remainder of the loop is from OnTrafficDataResponse(traffic::TrafficInfo &&), with minor modifications + */ + ForEachActiveMwm([this, trafficCache](MwmSet::MwmId const & mwmId) { + ASSERT(mwmId.IsAlive(), ()); + auto tcit = trafficCache.find(mwmId.GetInfo()->GetCountryName()); + if (tcit != trafficCache.end()) + { + std::lock_guard lock(m_mutex); + + traffic::TrafficInfo::Coloring coloring = tcit->second; + LOG(LINFO, ("Setting new coloring for", mwmId, "with", coloring.size(), "entries")); + traffic::TrafficInfo info(mwmId, std::move(coloring)); + + m_mwmCache.try_emplace(mwmId, CacheEntry(steady_clock::now())); + + auto it = m_mwmCache.find(mwmId); + if (it != m_mwmCache.end()) + { + it->second.m_isLoaded = true; + it->second.m_lastResponseTime = steady_clock::now(); + it->second.m_isWaitingForResponse = false; + it->second.m_lastAvailability = info.GetAvailability(); + + UpdateState(); + + if (!info.GetColoring().empty()) + { + m_drapeEngine.SafeCall(&df::DrapeEngine::UpdateTraffic, + static_cast(info)); + + // Update traffic colors for routing. + m_observer.OnTrafficInfoAdded(std::move(info)); + } + } + } + }); +} + +// TODO no longer needed +#ifdef traffic_dead_code void TrafficManager::OnTrafficDataResponse(traffic::TrafficInfo && info) { { @@ -401,6 +767,7 @@ void TrafficManager::OnTrafficDataResponse(traffic::TrafficInfo && info) m_observer.OnTrafficInfoAdded(std::move(info)); } } +#endif void TrafficManager::UniteActiveMwms(std::set & activeMwms) const { @@ -408,6 +775,8 @@ void TrafficManager::UniteActiveMwms(std::set & activeMwms) const activeMwms.insert(m_activeRoutingMwms.cbegin(), m_activeRoutingMwms.cend()); } +// TODO no longer needed +#ifdef traffic_dead_code void TrafficManager::ShrinkCacheToAllowableSize() { // Calculating number of different active mwms. @@ -429,6 +798,7 @@ void TrafficManager::ShrinkCacheToAllowableSize() } } } +#endif void TrafficManager::ClearCache(MwmSet::MwmId const & mwmId) { @@ -438,8 +808,11 @@ void TrafficManager::ClearCache(MwmSet::MwmId const & mwmId) if (it->second.m_isLoaded) { +// TODO no longer needed +#ifdef traffic_dead_code ASSERT_GREATER_OR_EQUAL(m_currentCacheSizeBytes, it->second.m_dataSize, ()); m_currentCacheSizeBytes -= it->second.m_dataSize; +#endif m_drapeEngine.SafeCall(&df::DrapeEngine::ClearTrafficCache, mwmId); diff --git a/map/traffic_manager.hpp b/map/traffic_manager.hpp index 2f7690dde..9f6f9a2e2 100644 --- a/map/traffic_manager.hpp +++ b/map/traffic_manager.hpp @@ -11,6 +11,8 @@ #include "openlr/openlr_decoder.hpp" +#include "traffxml/traff_model.hpp" + #include "geometry/point2d.hpp" #include "geometry/polyline2d.hpp" #include "geometry/screenbase.hpp" @@ -179,6 +181,85 @@ private: traffic::TrafficInfo::Availability m_lastAvailability; }; + /** + * @brief Subscribes to a traffic service. + * + * @param mwms The MWMs for which data is needed. + * @return true on success, false on failure. + */ + bool Subscribe(std::set & mwms); + + /** + * @brief Changes an existing traffic subscription. + * + * @param mwms The new set of MWMs for which data is needed. + * @return true on success, false on failure. + */ + bool ChangeSubscription(std::set & mwms); + + /** + * @brief Ensures we have a subscription covering all currently active MWMs. + * + * This method subscribes to a traffic service if not already subscribed, or changes the existing + * subscription otherwise. + * + * @return true on success, false on failure. + */ + bool SetSubscriptionArea(); + + /** + * @brief Unsubscribes from a traffic service we are subscribed to. + */ + void Unsubscribe(); + + /** + * @brief Whether we are currently subscribed to a traffic service. + * @return + */ + bool IsSubscribed(); + + /** + * @brief Polls the traffic service for updates. + * + * @return true on success, false on failure. + */ + bool Poll(); + + /** + * @brief Processes a traffic feed received through a push operation. + * + * Push operations are not supported on all platforms. + * + * @param feed The traffic feed. + */ + void Push(traffxml::TraffFeed feed); + + /** + * @brief Merges new messages from `m_feeds` into a message cache. + * + * Existing messages in `cache` will be overwritten by newer messages with the same ID in `m_feeds`. + * + * @param cache The message cache. + */ + void UpdateMessageCache(std::map & cache); + + /** + * @brief Initializes the data sources for an OpenLR decoder. + * + * @param dataSources Receives the data sources for the decoder (one per worker thread). + */ + void InitializeDataSources(std::vector &dataSources); + + /** + * @brief Decodes a single message to its segments and their speed groups. + * + * @param decoder The OpenLR decoder instance. + * @param message The message to decode. + * @param trafficCache The cache in which all decoded paths with their speed groups will be stored. + */ + void DecodeMessage(openlr::OpenLRDecoder &decoder, traffxml::TraffMessage & message, + std::map & trafficCache); + /** * @brief Event loop for the traffic worker thread. * @@ -199,8 +280,18 @@ private: * @param mwms Receives a list of MWMs for which to update traffic data. * @return `true` during normal operation, `false` during teardown (signaling the event loop to exit). */ + // TODO mwms argument is no longer needed bool WaitForRequest(std::vector & mwms); + /** + * @brief Processes new traffic data. + * + * @param trafficCache The new per-MWM colorings (preprocessed traffic information). + */ + void OnTrafficDataUpdate(std::map & trafficCache); + +// TODO no longer needed +#ifdef traffic_dead_code void OnTrafficDataResponse(traffic::TrafficInfo && info); /** * @brief Processes a failed traffic request. @@ -218,6 +309,7 @@ private: * @param info */ void OnTrafficRequestFailed(traffic::TrafficInfo && info); +#endif /** * @brief Updates `activeMwms` and requests traffic data. @@ -275,7 +367,10 @@ private: void Clear(); void ClearCache(MwmSet::MwmId const & mwmId); +// TODO no longer needed +#ifdef traffic_dead_code void ShrinkCacheToAllowableSize(); +#endif /** * @brief Updates the state of the traffic manager based on the state of all MWMs used by the renderer. @@ -323,8 +418,11 @@ private: bool m_hasSimplifiedColorScheme = true; +// TODO no longer needed +#ifdef traffic_dead_code size_t m_maxCacheSizeBytes; size_t m_currentCacheSizeBytes = 0; +#endif std::map m_mwmCache; @@ -378,6 +476,41 @@ private: * @brief Worker thread which fetches traffic updates. */ threads::SimpleThread m_thread; + + /** + * @brief Whether active MWMs have changed since the last request. + */ + bool m_activeMwmsChanged = false; + + /** + * @brief The subscription ID received from the traffic server. + * + * An empty subscription ID means no subscription. + */ + std::string m_subscriptionId; + + /** + * @brief Whether a poll operation is needed. + * + * Used in the worker thread. A poll operation is needed unless a subscription (or subscription + * change) operation was performed before and a feed was received a part of it. + */ + bool m_isPollNeeded; + + /** + * @brief Queue of feeds waiting to be processed. + * + * Threads must lock `m_mutex` before accessing `m_feeds`, as some platforms may receive feeds + * on multiple threads. + */ + std::vector m_feeds; + + /** + * @brief Cache of all currently active TraFF messages. + * + * Keys are message IDs, values are messages. + */ + std::map m_messageCache; }; extern std::string DebugPrint(TrafficManager::TrafficState state); diff --git a/traffic/traffic_info.cpp b/traffic/traffic_info.cpp index 29f845fa4..b7b646898 100644 --- a/traffic/traffic_info.cpp +++ b/traffic/traffic_info.cpp @@ -61,6 +61,8 @@ bool ReadRemoteFile(string const & url, vector & contents, int & errorC return true; } +// TODO no longer needed +#ifdef traffic_dead_code string MakeRemoteURL(string const & name, uint64_t version) { if (string(TRAFFIC_DATA_BASE_URL).empty()) @@ -73,6 +75,7 @@ string MakeRemoteURL(string const & name, uint64_t version) ss << url::UrlEncode(name) << TRAFFIC_FILE_EXTENSION; return ss.str(); } +#endif char const kETag[] = "etag"; } // namespace @@ -91,6 +94,8 @@ TrafficInfo::RoadSegmentId::RoadSegmentId(uint32_t fid, uint16_t idx, uint8_t di uint8_t const TrafficInfo::kLatestKeysVersion = 0; uint8_t const TrafficInfo::kLatestValuesVersion = 0; +// TODO no longer needed +#ifdef traffic_dead_code TrafficInfo::TrafficInfo(MwmSet::MwmId const & mwmId, int64_t currentDataVersion) : m_mwmId(mwmId) , m_currentDataVersion(currentDataVersion) @@ -132,6 +137,7 @@ TrafficInfo::TrafficInfo(MwmSet::MwmId const & mwmId, int64_t currentDataVersion LOG(LWARNING, ("Could not initialize traffic keys")); } } +#endif TrafficInfo::TrafficInfo(MwmSet::MwmId const & mwmId, Coloring && coloring) : m_mwmId(mwmId) @@ -154,6 +160,8 @@ void TrafficInfo::SetTrafficKeysForTesting(vector const & keys) m_availability = Availability::IsAvailable; } +// TODO no longer needed +#ifdef traffic_dead_code bool TrafficInfo::ReceiveTrafficData(string & etag) { vector values; @@ -169,6 +177,7 @@ bool TrafficInfo::ReceiveTrafficData(string & etag) } return false; } +#endif SpeedGroup TrafficInfo::GetSpeedGroup(RoadSegmentId const & id) const { @@ -399,6 +408,8 @@ void TrafficInfo::DeserializeTrafficValues(vector const & data, ASSERT_EQUAL(src.Size(), 0, ()); } +// TODO no longer needed +#ifdef traffic_dead_code // todo(@m) This is a temporary method. Do not refactor it. bool TrafficInfo::ReceiveTrafficKeys() { @@ -437,7 +448,10 @@ bool TrafficInfo::ReceiveTrafficKeys() m_keys.swap(keys); return true; } +#endif +// TODO no longer needed +#ifdef traffic_dead_code TrafficInfo::ServerDataStatus TrafficInfo::ReceiveTrafficValues(string & etag, vector & values) { if (!m_mwmId.IsAlive()) @@ -481,6 +495,7 @@ TrafficInfo::ServerDataStatus TrafficInfo::ReceiveTrafficValues(string & etag, v m_availability = Availability::IsAvailable; return ServerDataStatus::New; } +#endif bool TrafficInfo::UpdateTrafficData(vector const & values) { @@ -504,6 +519,8 @@ bool TrafficInfo::UpdateTrafficData(vector const & values) return true; } +// TODO no longer needed +#ifdef traffic_dead_code TrafficInfo::ServerDataStatus TrafficInfo::ProcessFailure(platform::HttpClient const & request, int64_t const mwmVersion) { switch (request.ErrorCode()) @@ -532,6 +549,7 @@ TrafficInfo::ServerDataStatus TrafficInfo::ProcessFailure(platform::HttpClient c return ServerDataStatus::Error; } +#endif string DebugPrint(TrafficInfo::RoadSegmentId const & id) { diff --git a/traffic/traffic_info.hpp b/traffic/traffic_info.hpp index fc71c8340..61a4e3474 100644 --- a/traffic/traffic_info.hpp +++ b/traffic/traffic_info.hpp @@ -105,9 +105,13 @@ public: TrafficInfo() = default; +// TODO no longer needed +#ifdef traffic_dead_code TrafficInfo(MwmSet::MwmId const & mwmId, int64_t currentDataVersion); +#endif TrafficInfo(MwmSet::MwmId const & mwmId, Coloring && coloring); + /** * @brief Returns a `TrafficInfo` instance with pre-populated traffic information. * @param coloring The traffic information (road segments and their speed group) @@ -116,6 +120,8 @@ public: static TrafficInfo BuildForTesting(Coloring && coloring); void SetTrafficKeysForTesting(std::vector const & keys); +// TODO no longer needed +#ifdef traffic_dead_code /** * @brief Fetches the latest traffic data from the server and updates the coloring and ETag. * @@ -131,6 +137,7 @@ public: * @return True on success, false on failure. */ bool ReceiveTrafficData(std::string & etag); +#endif /** * @brief Returns the latest known speed group by a feature segment's ID. @@ -198,19 +205,28 @@ private: friend void UnitTest_TrafficInfo_UpdateTrafficData(); +// TODO no longer needed +#ifdef traffic_dead_code // todo(@m) A temporary method. Remove it once the keys are added // to the generator and the data is regenerated. bool ReceiveTrafficKeys(); +#endif +// TODO no longer needed +#ifdef traffic_dead_code // Tries to read the values of the Coloring map from server into |values|. // Returns result of communicating with server as ServerDataStatus. // Otherwise, returns false and does not change m_coloring. ServerDataStatus ReceiveTrafficValues(std::string & etag, std::vector & values); +#endif // Updates the coloring and changes the availability status if needed. bool UpdateTrafficData(std::vector const & values); +// TODO no longer needed +#ifdef traffic_dead_code ServerDataStatus ProcessFailure(platform::HttpClient const & request, int64_t const mwmVersion); +#endif /** * @brief The mapping from feature segments to speed groups (see speed_groups.hpp). diff --git a/traffxml/traff_model.cpp b/traffxml/traff_model.cpp index cd29d0dd9..14b469473 100644 --- a/traffxml/traff_model.cpp +++ b/traffxml/traff_model.cpp @@ -1,9 +1,246 @@ #include "traffxml/traff_model.hpp" +#include "base/logging.hpp" + +#include "geometry/mercator.hpp" + using namespace std; namespace traffxml { +const std::map kEventSpeedGroupMap{ + // TODO Activity*, Authority*, Carpool* (not in enum yet) + {EventType::CongestionHeavyTraffic, traffic::SpeedGroup::G4}, + {EventType::CongestionLongQueue, traffic::SpeedGroup::G0}, + {EventType::CongestionNone, traffic::SpeedGroup::G5}, + {EventType::CongestionNormalTraffic, traffic::SpeedGroup::G5}, + {EventType::CongestionQueue, traffic::SpeedGroup::G2}, + {EventType::CongestionQueueLikely, traffic::SpeedGroup::G3}, + {EventType::CongestionSlowTraffic, traffic::SpeedGroup::G3}, + {EventType::CongestionStationaryTraffic, traffic::SpeedGroup::G1}, + {EventType::CongestionStationaryTrafficLikely, traffic::SpeedGroup::G2}, + {EventType::CongestionTrafficBuildingUp, traffic::SpeedGroup::G4}, + {EventType::CongestionTrafficCongestion, traffic::SpeedGroup::G3}, // TODO or G2? Unquantified, below normal + {EventType::CongestionTrafficFlowingFreely, traffic::SpeedGroup::G5}, + {EventType::CongestionTrafficHeavierThanNormal, traffic::SpeedGroup::G4}, + {EventType::CongestionTrafficLighterThanNormal, traffic::SpeedGroup::G5}, + {EventType::CongestionTrafficMuchHeavierThanNormal, traffic::SpeedGroup::G3}, + {EventType::CongestionTrafficProblem, traffic::SpeedGroup::G3}, // TODO or G2? Unquantified, below normal + // TODO Construction* (not in enum yet) + /* + * Some delay types have a duration which depends on the route. This is better expressed as a + * speed group, although the mapping may be somewhat arbitrary and may need to be corrected. + */ + {EventType::DelayDelay, traffic::SpeedGroup::G2}, + {EventType::DelayDelayPossible, traffic::SpeedGroup::G3}, + {EventType::DelayLongDelay, traffic::SpeedGroup::G1}, + {EventType::DelayVeryLongDelay, traffic::SpeedGroup::G0}, + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + {EventType::RestrictionBlocked, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionBlockedAhead, traffic::SpeedGroup::TempBlock}, + //{EventType::RestrictionCarriagewayBlocked, traffic::SpeedGroup::TempBlock}, // TODO FIXME other carriageways may still be open + //{EventType::RestrictionCarriagewayClosed, traffic::SpeedGroup::TempBlock}, // TODO FIXME other carriageways may still be open + {EventType::RestrictionClosed, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionClosedAhead, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionEntryBlocked, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionExitBlocked, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionRampBlocked, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionRampClosed, traffic::SpeedGroup::TempBlock}, + {EventType::RestrictionSpeedLimit, traffic::SpeedGroup::G4}, + // TODO Security*, Transport*, Weather* (not in enum yet) +}; + +// none of the currently define events imply an explicit maxspeed +#if 0 +const std::map kEventMaxspeedMap{ + // TODO Activity*, Authority*, Carpool* (not in enum yet) + // TODO Construction* (not in enum yet) + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + // TODO Security*, Transport*, Weather* (not in enum yet) +}; +#endif + +const std::map kEventDelayMap{ + // TODO Activity*, Authority*, Carpool* (not in enum yet) + // TODO Construction* (not in enum yet) + //{EventType::DelayDelay, }, // mapped to speed group + //{EventType::DelayDelayPossible, }, // mapped to speed group + //{EventType::DelayLongDelay, }, // mapped to speed group + {EventType::DelaySeveralHours, 150}, // assumption: 2.5 hours + {EventType::DelayUncertainDuration, 60}, // assumption: 1 hour + //{EventType::DelayVeryLongDelay, }, // mapped to speed group + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + // TODO Security*, Transport*, Weather* (not in enum yet) +}; + +openlr::LocationReferencePoint Point::ToLrp() +{ + openlr::LocationReferencePoint result; + result.m_latLon = ms::LatLon(this->m_coordinates.m_lat, this->m_coordinates.m_lon); + return result; +} + +openlr::LinearLocationReference TraffLocation::ToLinearLocationReference(bool backwards) +{ + openlr::LinearLocationReference locationReference; + locationReference.m_points.clear(); + std::vector points; + if (m_from) + points.push_back(m_from.value()); + if (m_at) + points.push_back(m_at.value()); + else if (m_via) + points.push_back(m_via.value()); + if (m_to) + points.push_back(m_to.value()); + if (backwards) + std::reverse(points.begin(), points.end()); + // m_notVia is ignored as OpenLR does not support this functionality. + // TODO do we ensure a minimum of two reference points (from/to/at) when building the location? + CHECK_GREATER(points.size(), 1, ("At least two reference points must be given")); + for (auto point : points) + { + openlr::LocationReferencePoint lrp = point.ToLrp(); + if (!locationReference.m_points.empty()) + { + locationReference.m_points.back().m_distanceToNextPoint + = GuessDnp(locationReference.m_points.back(), lrp); + locationReference.m_points.back().m_functionalRoadClass = GetFrc(); + } + locationReference.m_points.push_back(lrp); + } + return locationReference; +} + +// TODO make segment ID in OpenLR a string value, and store messageId +std::vector TraffLocation::ToOpenLrSegments(std::string & messageId) +{ + // Convert the location to a format understood by the OpenLR decoder. + std::vector segments; + int dirs = (m_directionality == Directionality::BothDirections) ? 2 : 1; + for (int dir = 0; dir < dirs; dir++) + { + openlr::LinearSegment segment; + // TODO make this a reference to the TraFF message ID + segment.m_segmentId = 42; + /* + * 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 = this->ToLinearLocationReference(dir == 0 ? false : true); + + segments.push_back(segment); + } + return segments; +} + +openlr::FunctionalRoadClass TraffLocation::GetFrc() +{ + if (!m_roadClass) + return openlr::FunctionalRoadClass::NotAValue; + switch (m_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 FRC4–7. + * FRC4 matches secondary/tertiary (zero score) and anything below (full score). + * FRC5–7 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(); +} + +std::optional TraffMessage::GetTrafficImpact() +{ + // no events, no impact + if (m_events.empty()) + return std::nullopt; + + // examine events + std::vector impacts; + for (auto event : m_events) + { + TrafficImpact impact; + + if (auto it = kEventSpeedGroupMap.find(event.m_type); it != kEventSpeedGroupMap.end()) + impact.m_speedGroup = it->second; + + if (event.m_speed) + impact.m_maxspeed = event.m_speed.value(); + // TODO if no explicit speed given, look up in kEventMaxspeedMap (once we have entries) + + // TODO if event is in delay class and has an explicit duration quantifier, use that and skip the map lookup. + if (auto it = kEventDelayMap.find(event.m_type); it != kEventDelayMap.end()) + impact.m_delayMins = it->second; + + // TempBlock overrules everything else, return immediately + if (impact.m_speedGroup == traffic::SpeedGroup::TempBlock) + return impact; + // if there is no actual impact, discard + if ((impact.m_maxspeed < kMaxspeedNone) + || (impact.m_delayMins > 0) + || (impact.m_speedGroup != traffic::SpeedGroup::Unknown)) + impacts.push_back(impact); + } + + if (impacts.empty()) + return std::nullopt; + + TrafficImpact result; + for (auto impact : impacts) + { + ASSERT(impact.m_speedGroup != traffic::SpeedGroup::TempBlock, ("Got SpeedGroup::TempBlock, which should not happen at this stage")); + if (result.m_speedGroup == traffic::SpeedGroup::Unknown) + result.m_speedGroup = impact.m_speedGroup; + // TempBlock cannot occur here, so we can do just a simple comparison + else if ((impact.m_speedGroup != traffic::SpeedGroup::Unknown) && (impact.m_speedGroup < result.m_speedGroup)) + result.m_speedGroup = impact.m_speedGroup; + + if (impact.m_maxspeed < result.m_maxspeed) + result.m_maxspeed = impact.m_maxspeed; + + if (impact.m_delayMins > result.m_delayMins) + result.m_delayMins = impact.m_delayMins; + } + if ((result.m_maxspeed < kMaxspeedNone) + || (result.m_delayMins > 0) + || (result.m_speedGroup != traffic::SpeedGroup::Unknown)) + return result; + else + // should never happen, unless we have a bug somewhere + return std::nullopt; +} + +uint32_t GuessDnp(openlr::LocationReferencePoint & p1, openlr::LocationReferencePoint & p2) +{ + double doe = mercator::DistanceOnEarth(mercator::FromLatLon(p1.m_latLon), + mercator::FromLatLon(p2.m_latLon)); + /* + * The tolerance factor is currently 1/0.6, or ~1.67, so that direct distance is just at the + * lower boundary for `openlr::LinearSegmentSource::FromLocationReferenceTag`. Since we use + * `openlr::LinearSegmentSource::FromCoordinatesTag`, different tolerance values apply, + * so direct distance is well within the lower boundary, where the upper boundary is ~6.67 times + * the direct distance. This should work even in mountain areas, where the shortest route from + * one valley to the next, using long-distance roads, can be up to ~3 times the direct distance. + */ + return doe / 0.6f + 0.5f; +} + /* string DebugPrint(LinearSegmentSource source) { @@ -140,6 +377,17 @@ std::string DebugPrint(EventType eventType) UNREACHABLE(); } +std::string DebugPrint(TrafficImpact impact) +{ + std::ostringstream os; + os << "TrafficImpact { "; + os << "speedGroup: " << DebugPrint(impact.m_speedGroup) << ", "; + os << "maxspeed: " << (impact.m_maxspeed == kMaxspeedNone ? "none" : std::to_string(impact.m_maxspeed)) << ", "; + os << "delayMins: " << impact.m_delayMins; + os << " }"; + return os.str(); +} + std::string DebugPrint(Point point) { std::ostringstream os; diff --git a/traffxml/traff_model.hpp b/traffxml/traff_model.hpp index bb76f74f7..3e8fd539f 100644 --- a/traffxml/traff_model.hpp +++ b/traffxml/traff_model.hpp @@ -5,11 +5,17 @@ #include "geometry/latlon.hpp" #include "geometry/point2d.hpp" +#include "openlr/openlr_model.hpp" + +#include "traffic/speed_groups.hpp" + #include #include namespace traffxml { +constexpr uint8_t kMaxspeedNone = 255; + /** * @brief Date and time decoded from ISO 8601. * @@ -55,6 +61,13 @@ enum class RoadClass Other }; +/* + * When adding a new event class to this enum, be sure to do the following: + * + * * in `traff_model_xml.cpp`, add the corresponding mapping to `kEventClassMap` + * * in `traff_model.cpp`, extend `DebugPrint(EventClass)` to correctly process the new event classes + * * in this file, add event types for this class to `EventType` + */ enum class EventClass { Invalid, @@ -74,6 +87,16 @@ enum class EventClass Weather }; +/* + * When adding a new event type to this enum, be sure to do the following: + * + * * in `traff_model_xml.cpp`, add the corresponding mapping to `kEventTypeMap` + * * in `traff_model.cpp`: + * * add speed group mappings in `kEventSpeedGroupMap`, if any + * * add maxspeed mappings in `kEventMaxspeedMap`, if any (uncomment if needed) + * * add delay mappings in `kEventDelayMap`, if any + * * extend `DebugPrint(TraffEvent)` to correctly process the new events + */ enum class EventType { Invalid, @@ -128,8 +151,57 @@ enum class EventType // TODO Security*, Transport*, Weather* }; +/** + * @brief Represents the impact of one or more traffic events. + * + * Impact can be expressed in three ways: + * + * Traffic may flow at a certain percentage of the posted limit, often divided in bins. This is + * used by some traffic services which report e.g. “slow traffic”, “stationary traffic” or + * “queues”, and maps to speed groups in a straightforward way. + * + * Traffic may flow at, or be restricted to, a given speed. This is common with traffic flow + * measurement data, or with temporary speed limits. Converting this to a speed group requires + * knowledge of the regular speed limit. + * + * There may be a fixed delay, expressed as a duration in time. This may happen at checkpoints, + * at sections where traffic flow is limited or where there is single alternate-lane traffic. + * As the routing data model does not provide for explicit delays, they have to be converted into + * speed groups. Again, this requires knowledge of the regular travel time along the route, as well + * as its length. + * + * Closures can be expressed by setting `m_speedGroup` to `traffic::SpeedGroup::TempBlock`. If that + * is the case, the other struct members are to be ignored. + */ +struct TrafficImpact +{ + /** + * @brief The speed group for the affected segments, or `traffic::SpeedGroup::Unknown` if unknown. + */ + traffic::SpeedGroup m_speedGroup = traffic::SpeedGroup::Unknown; + + /** + * @brief The speed limit, or speed of flowing traffic; `kMaxspeedNone` if none or unknown. + */ + uint8_t m_maxspeed = kMaxspeedNone; + + /** + * @brief The delay in minutes; 0 if none or unknown. + */ + uint16_t m_delayMins = 0; +}; + struct Point { + /** + * @brief Converts the point to an OpenLR location reference point. + * + * Only coordinates are populated. + * + * @return An OpenLR LRP with the coordinates of the point. + */ + openlr::LocationReferencePoint ToLrp(); + // TODO role? ms::LatLon m_coordinates = ms::LatLon::Zero(); // TODO optional float m_distance; @@ -139,6 +211,34 @@ struct Point struct TraffLocation { + /** + * @brief Converts the location to an OpenLR linear location reference. + * + * @param backwards If true, gnerates a linear location reference for the backwards direction, + * with the order of points reversed. + * @return An OpenLR linear location reference which corresponds to the location. + */ + openlr::LinearLocationReference ToLinearLocationReference(bool backwards); + + /** + * @brief Converts the location to a vector of OpenLR segments. + * + * Depending on the directionality, the resulting vector will hold one or two elements: one for + * the forward direction, and for bidirectional locations, a second one for the backward + * direction. + * + * @param messageId The message ID + * @return A vector holding the resulting OpenLR segments. + */ + std::vector ToOpenLrSegments(std::string & messageId); + + /** + * @brief Returns the OpenLR functional road class (FRC) matching `m_roadClass`. + * + * @return The FRC. + */ + openlr::FunctionalRoadClass GetFrc(); + std::optional m_country; std::optional m_destination; std::optional m_direction; @@ -173,6 +273,20 @@ struct TraffEvent struct TraffMessage { + /** + * @brief Retrieves the traffic impact of all events. + * + * If the message has multiple events, the traffic impact is determined separately for each + * event and then aggregated. Aggregation takes the most restrictive value in each category + * (speed group, maxspeed, delay). + * + * If the aggregated traffic impact includes `SpeedGroup::TempBlock`, its other members are to + * be considered invalid. + * + * @return The aggregated traffic impact, or `std::nullopt` if the message has no events with traffic impact. + */ + std::optional GetTrafficImpact(); + std::string m_id; IsoTime m_receiveTime = {}; IsoTime m_updateTime = {}; @@ -189,12 +303,27 @@ struct TraffMessage using TraffFeed = std::vector; +/** + * @brief Guess the distance to the next point. + * + * This is calculated as direct distance, multiplied with a tolerance factor to account for the + * fact that the road is not always a straight line. + * + * The result can be used to provide some semi-valid DNP values. + * + * @param p1 The first point. + * @param p2 The second point. + * @return The approximate distance on the ground, in meters. + */ +uint32_t GuessDnp(openlr::LocationReferencePoint & p1, openlr::LocationReferencePoint & p2); + std::string DebugPrint(IsoTime time); std::string DebugPrint(Directionality directionality); std::string DebugPrint(Ramps ramps); std::string DebugPrint(RoadClass roadClass); std::string DebugPrint(EventClass eventClass); std::string DebugPrint(EventType eventType); +std::string DebugPrint(TrafficImpact impact); std::string DebugPrint(Point point); std::string DebugPrint(TraffLocation location); std::string DebugPrint(TraffEvent event); diff --git a/traffxml/traff_model_xml.cpp b/traffxml/traff_model_xml.cpp index 58532712d..ae7cb4f6d 100644 --- a/traffxml/traff_model_xml.cpp +++ b/traffxml/traff_model_xml.cpp @@ -462,9 +462,14 @@ bool LocationFromXml(pugi::xml_node node, TraffLocation & location) location.m_via = OptionalPointFromXml(node.child("via")); location.m_notVia = OptionalPointFromXml(node.child("not_via")); - if (!location.m_from && !location.m_to && !location.m_at) + int numPoints = 0; + for (std::optional point : {location.m_from, location.m_to, location.m_at}) + if (point) + numPoints++; + // single-point locations are not supported, locations without points are not valid + if (numPoints < 2) { - LOG(LWARNING, ("Neither from, to nor at point is specified, ignoring location")); + LOG(LWARNING, ("Only", numPoints, "points of from/to/at specified, ignoring location")); return false; }