diff --git a/map/traffic_manager.cpp b/map/traffic_manager.cpp index 965a88b15..df7382df4 100644 --- a/map/traffic_manager.cpp +++ b/map/traffic_manager.cpp @@ -13,7 +13,6 @@ #include "platform/platform.hpp" #include "traffxml/traff_model_xml.hpp" -#include "traffxml/traff_storage.hpp" using namespace std::chrono; @@ -43,6 +42,16 @@ auto constexpr kDrapeUpdateInterval = seconds(10); * Interval at which the traffic observer gets traffic updates while messages are being processed. */ auto constexpr kObserverUpdateInterval = minutes(1); + +/** + * Interval at which the message cache file is updated while messages are being processed. + */ +auto constexpr kStorageUpdateInterval = minutes(1); + +/** + * File name at which traffic data is persisted. + */ +auto constexpr kTrafficXMLFileName = "traffic.xml"; } // namespace TrafficManager::CacheEntry::CacheEntry() @@ -125,14 +134,28 @@ void TrafficManager::SetStateListener(TrafficStateChangedFn const & onStateChang void TrafficManager::SetEnabled(bool enabled) { + /* + * Whether to notify interested parties that traffic data has been updated. + * This is based on the return value of RestoreCache(). + */ + bool notifyUpdate = false; { std::lock_guard lock(m_mutex); if (enabled == IsEnabled()) return; - if (enabled && !m_traffDecoder) - // deferred decoder initialization (requires maps to be loaded) - m_traffDecoder = make_unique(m_dataSource, m_countryInfoGetterFn, - m_countryParentNameGetterFn, m_messageCache); + if (enabled) + { + if (!m_traffDecoder) + // deferred decoder initialization (requires maps to be loaded) + m_traffDecoder = make_unique(m_dataSource, m_countryInfoGetterFn, + m_countryParentNameGetterFn, m_messageCache); + if (!m_storage && !IsTestMode()) + { + m_storage = make_unique(kTrafficXMLFileName); + notifyUpdate = RestoreCache(); + m_lastStorageUpdate = steady_clock::now(); + } + } ChangeState(enabled ? TrafficState::Enabled : TrafficState::Disabled); } @@ -140,7 +163,11 @@ void TrafficManager::SetEnabled(bool enabled) if (enabled) { - Invalidate(); + if (notifyUpdate) + OnTrafficDataUpdate(); + else + // TODO After sorting out invalidation, figure out if we need that here. + Invalidate(); m_canSetMode = false; } else @@ -227,6 +254,28 @@ void TrafficManager::OnRecoverSurface() Resume(); } +/* + * TODO Revisit invalidation logic. + * We currently invalidate when enabling and resuming, and when a new MWM file is downloaded + * (behavior inherited from MapsWithMe). + * Traffic data in MapsWithMe was a set of pre-decoded messages per MWM; the whole set would get + * re-fetched periodically. Invalidation meant discarding and re-fetching all traffic data. + * This logic is different for TraFF: + * - Messages expire individually or get replaced by updates, thus there is hardly ever a reason + * to discard messages. + * - Messages are decoded into segments in the app. Discarding decoded segments may be needed on + * a per-message basis for the following reasons: + * - the message is replaced by a new one and the location or traffic situation has changed + * (this is dealt with as part of the message update process) + * - one of the underlying MWMs has been updated to a new version + * - a new MWM has been added, and a message location that previously could not be decoded + * completely now can + * The sensible equivalent in TraFF would be to discard and re-generate decoded locations, and + * possibly poll for updates. Discarding and re-generating decoded locations could be done + * selectively: + * - compare map versions of decoded segments to current map version + * - figure out when a new map has been added, and which segments are affected by it + */ void TrafficManager::Invalidate() { if (!IsEnabled()) @@ -369,6 +418,56 @@ bool TrafficManager::IsSubscribed() return !m_subscriptionId.empty(); } +bool TrafficManager::RestoreCache() +{ + ASSERT(m_storage, ("m_storage cannot be null")); + pugi::xml_document document; + if (!m_storage->Load(document)) + { + LOG(LWARNING, ("Failed to reload cache from storage")); + return false; + } + + traffxml::TraffFeed feedIn; + traffxml::TraffFeed feedOut; + bool hasDecoded = false; + bool hasUndecoded = false; + if (traffxml::ParseTraff(document, std::nullopt /* dataSource */, feedIn)) + { + while (!feedIn.empty()) + { + traffxml::TraffMessage message; + std::swap(message, feedIn.front()); + feedIn.erase(feedIn.begin()); + + if (!message.IsExpired(traffxml::IsoTime::Now())) + { + if (!message.m_decoded.empty()) + { + hasDecoded = true; + // store message in cache + m_messageCache.insert_or_assign(message.m_id, message); + } + else + { + hasUndecoded = true; + // message needs decoding, prepare to enqueue + feedOut.push_back(message); + } + } + } + if (!feedOut.empty()) + m_feedQueue.insert(m_feedQueue.begin(), feedOut); + // update notification is caller’s responsibility + return hasDecoded && !hasUndecoded; + } + else + { + LOG(LWARNING, ("An error occurred parsing the cache file")); + } + return false; +} + // TODO make this work with multiple sources (e.g. Android) // TODO deal with subscriptions rejected by the server (delete, resubscribe) bool TrafficManager::Poll() @@ -385,7 +484,7 @@ bool TrafficManager::Poll() std::setlocale(LC_ALL, "en_US.UTF-8"); traffxml::TraffFeed feed; - if (traffxml::ParseTraff(document, feed)) + if (traffxml::ParseTraff(document, std::nullopt /* dataSource */, feed)) { { std::lock_guard lock(m_mutex); @@ -784,13 +883,34 @@ void TrafficManager::OnTrafficDataUpdate() // Whether to notify the observer of the update. bool notifyObserver = (feedQueueEmpty); + // Whether to update the cache file. + bool updateStorage = (feedQueueEmpty); + if (!feedQueueEmpty) { auto const currentTime = steady_clock::now(); auto const drapeAge = currentTime - m_lastDrapeUpdate; auto const observerAge = currentTime - m_lastObserverUpdate; + auto const storageAge = currentTime - m_lastStorageUpdate; notifyDrape = (drapeAge >= kDrapeUpdateInterval); notifyObserver = (observerAge >= kObserverUpdateInterval); + updateStorage = (storageAge >= kStorageUpdateInterval); + } + + if (!m_storage || IsTestMode()) + updateStorage = false; + + if (updateStorage) + { + std::lock_guard lock(m_mutex); + + pugi::xml_document document; + + traffxml::GenerateTraff(m_messageCache, document); + if (!m_storage->Save(document)) + LOG(LWARNING, ("Storing message cache to file failed.")); + + m_lastStorageUpdate = steady_clock::now(); } if (!notifyDrape && !notifyObserver) diff --git a/map/traffic_manager.hpp b/map/traffic_manager.hpp index b3ca6d83d..46cb20d4f 100644 --- a/map/traffic_manager.hpp +++ b/map/traffic_manager.hpp @@ -13,6 +13,7 @@ #include "traffxml/traff_decoder.hpp" #include "traffxml/traff_model.hpp" +#include "traffxml/traff_storage.hpp" #include "geometry/point2d.hpp" #include "geometry/polyline2d.hpp" @@ -356,6 +357,22 @@ private: */ bool IsSubscribed(); + /** + * @brief Restores the message cache from file storage. + * + * @note The caller must lock `m_mutex` prior to calling this function, as it makes unprotected + * changes to shared data structures. + * + * @note The return value indicates whether actions related to a traffic update should be taken, + * such as notifying the routing and drape engine. It is true if at least one message with a + * decoded location was read, and no messages without decoded locations. If messages without a + * decoded location were read, the return value is false, as the location decoding will trigger + * updates by itself. If errors occurred and no messages are read, the return value is also false. + * + * @return True if a traffic update needs to be sent, false if not + */ + bool RestoreCache(); + /** * @brief Polls the traffic service for updates. * @@ -667,6 +684,11 @@ private: */ std::chrono::time_point m_lastObserverUpdate; + /** + * @brief When the cache file was last updated. + */ + std::chrono::time_point m_lastStorageUpdate; + /** * @brief Whether active MWMs have changed since the last request. */ @@ -706,6 +728,13 @@ private: */ std::map m_messageCache; + /** + * @brief The storage instance. + * + * Used to persist the TraFF message cache between sessions. + */ + std::unique_ptr m_storage; + /** * @brief The TraFF decoder instance. * diff --git a/traffxml/traff_assessment_tool/mainwindow.cpp b/traffxml/traff_assessment_tool/mainwindow.cpp index 4476dc345..ef2440139 100644 --- a/traffxml/traff_assessment_tool/mainwindow.cpp +++ b/traffxml/traff_assessment_tool/mainwindow.cpp @@ -367,7 +367,7 @@ void MainWindow::OnOpenTrafficSample() std::setlocale(LC_ALL, "en_US.UTF-8"); traffxml::TraffFeed feed; traffxml::TraffFeed shiftedFeed; - if (traffxml::ParseTraff(document, feed)) + if (traffxml::ParseTraff(document, std::nullopt /* dataSource */, feed)) { for (auto message : feed) { diff --git a/traffxml/traff_model_xml.cpp b/traffxml/traff_model_xml.cpp index e6335ad16..2466b5e65 100644 --- a/traffxml/traff_model_xml.cpp +++ b/traffxml/traff_model_xml.cpp @@ -122,6 +122,42 @@ const boost::bimap kEventTypeMap = MakeBimap kSpeedGroupMap = MakeBimap({ + {"G0", traffic::SpeedGroup::G0}, + {"G1", traffic::SpeedGroup::G1}, + {"G2", traffic::SpeedGroup::G2}, + {"G3", traffic::SpeedGroup::G3}, + {"G4", traffic::SpeedGroup::G4}, + {"G5", traffic::SpeedGroup::G5}, + {"TEMP_BLOCK", traffic::SpeedGroup::TempBlock}, + {"UNKNOWN", traffic::SpeedGroup::Unknown} +}); + +/** + * @brief Retrieves an integer value from an attribute. + * + * @param attribute The XML attribute to retrieve. + * @param value The variable which will receive the value, must be of an integer type + * @return `true` on success, `false` if the attribute is not set or does not contain an integer value. + */ +template +bool IntegerFromXml(pugi::xml_attribute const & attribute, Value & value) +{ + if (attribute.empty()) + return false; + try + { + value = static_cast(is_signed::value + ? std::stoll(attribute.as_string()) + : std::stoull(attribute.as_string())); + return true; + } + catch (std::invalid_argument const& ex) + { + return false; + } +} + /** * @brief Retrieves an integer value from an attribute. * @@ -581,8 +617,6 @@ void LocationToXml(TraffLocation const & location, pugi::xml_node & node) PointToXml(location.m_notVia.value(), "not_via", node); if (location.m_to) PointToXml(location.m_to.value(), "to", node); - - // TODO decoded segments } /** @@ -734,13 +768,180 @@ bool EventsFromXml(pugi::xml_node const & node, std::vector & events return result; } +/** + * @brief Retrieves a coloring segment (segment with speed group) from XML + * @param node The `segment` node + * @param coloring The coloring to which the segment will be added. + * @return true if each segment was parsed successfully, false if errors occurred (in this case, + * the decoded coloring for this message should be discarded and regenerated from scratch) + */ +bool SegmentFromXml(pugi::xml_node const & node, + std::map & coloring) +{ + uint32_t fid; + uint16_t idx; + uint8_t dir; + if (IntegerFromXml(node.attribute("fid"), fid) + && IntegerFromXml(node.attribute("idx"), idx) + && IntegerFromXml(node.attribute("dir"), dir)) + { + traffic::TrafficInfo::RoadSegmentId segment(fid, idx, dir); + traffic::SpeedGroup sg = traffic::SpeedGroup::Unknown; + if (EnumFromXml(node.attribute("speed_group"), sg, kSpeedGroupMap)) + coloring[segment] = sg; + else + { + LOG(LWARNING, ("missing or invalid speed group for", segment, "(aborting)")); + return false; + } + } + else + { + LOG(LWARNING, ("segment with incomplete information (fid, idx, dir), aborting")); + return false; + } + return true; +} + +/** + * @brief Retrieves coloring for a single MWM from XML. + * + * This function returns false if errors occurred during decoding (due to invalid data), or if the + * data version to which the segments refer does not coincide with the currently used version of the + * corresponding MWM. In this case, the entire coloring for this message should be discarded and the + * message should be decoded from scratch. + * + * @todo Errors in segments are currently not considered, i.e. this function may return true even if + * one or more segments have errors. + * + * @param node The `coloring` node. + * @param dataSource The data source for coloring. + * @param decoded Receives the decoded global coloring. + * @return whether the decoded segments can be used, see description + */ +bool ColoringFromXml(pugi::xml_node const & node, DataSource const & dataSource, + MultiMwmColoring & decoded) +{ + std::string countryName; + if (!StringFromXml(node.attribute("country_name"), countryName)) + { + LOG(LWARNING, ("coloring element without coutry_name attribute, skipping")); + return false; + } + auto const & mwmId = dataSource.GetMwmIdByCountryFile(platform::CountryFile(countryName)); + if (!mwmId.IsAlive()) + { + LOG(LWARNING, ("Can’t get MWM ID for country", countryName, "(skipping)")); + return false; + } + + uint64_t version = 0; + if (!IntegerFromXml(node.attribute("version"), version)) + { + LOG(LWARNING, ("Can’t get version for country", countryName, "(skipping)")); + return false; + } + else if (version != mwmId.GetInfo()->GetVersion()) + { + LOG(LINFO, ("XML data for country", countryName, "has version", version, "while MWM has", mwmId.GetInfo()->GetVersion(), "(skipping)")); + return false; + } + + auto const segmentNodes = node.select_nodes("./segment"); + + if (segmentNodes.empty()) + return true; + + std::map coloring; + + for (auto const & segmentXpathNode : segmentNodes) + { + auto const & segmentNode = segmentXpathNode.node(); + if (!SegmentFromXml(segmentNode, coloring)) + return false; + } + + if (!coloring.empty()) + decoded[mwmId] = coloring; + + return true; +} + +/** + * @brief Stores coloring for an indidual MWM in an XML node. + * + * The vaues of `mwmId` will be added to `node` as attributes. The segments and their traffic group + * will be added to `node` as child nodes. + * + * @param mwmId + * @param coloring + * @param node The `coloring` node to store the coloring in. + */ +void ColoringToXml(MwmSet::MwmId const & mwmId, + std::map const & coloring, + pugi::xml_node node) +{ + node.append_attribute("country_name").set_value(mwmId.GetInfo()->GetCountryName()); + node.append_attribute("version").set_value(mwmId.GetInfo()->GetVersion()); + for (auto & [segId, sg] : coloring) + { + auto segNode = node.append_child("segment"); + segNode.append_attribute("fid").set_value(segId.GetFid()); + segNode.append_attribute("idx").set_value(segId.GetIdx()); + segNode.append_attribute("dir").set_value(segId.GetDir()); + EnumToXml(sg, "speed_group", segNode, kSpeedGroupMap); + } +} + +/** + * @brief Retrieves global coloring from XML. + * + * If the MWM version does not match for at least one MWM, no coloring is decoded (`decoded` is + * empty after this function returns) and the message needs to be decoded from scratch. + * + * @param node The `mwm_coloring` node. + * @param dataSource The data source for coloring, see `ParseTraff()`. + * @param decoded Receives the decoded global coloring. + */ +void AllMwmColoringFromXml(pugi::xml_node const & node, + std::optional> dataSource, + MultiMwmColoring & decoded) +{ + if (!node) + return; + + if (!dataSource) + { + LOG(LWARNING, ("Message has mwm_coloring but it cannot be parsed as no data source was specified")); + return; + } + + auto const coloringNodes = node.select_nodes("./coloring"); + + if (coloringNodes.empty()) + return; + + for (auto const & coloringXpathNode : coloringNodes) + { + auto const & coloringNode = coloringXpathNode.node(); + if (!ColoringFromXml(coloringNode, dataSource->get(), decoded)) + { + decoded.clear(); + return; + } + } +} + /** * @brief Retrieves a TraFF message from an XML element. * @param node The XML element to retrieve (`message`). + * @param dataSource The data source for coloring, see `ParseTraff()`. * @param message Receives the message. * @return `true` on success, `false` if the node does not exist or does not contain valid message data. */ -bool MessageFromXml(pugi::xml_node const & node, TraffMessage & message) +bool MessageFromXml(pugi::xml_node const & node, + std::optional> dataSource, + TraffMessage & message) { if (!StringFromXml(node.attribute("id"), message.m_id)) { @@ -779,7 +980,9 @@ bool MessageFromXml(pugi::xml_node const & node, TraffMessage & message) if (!message.m_cancellation) { message.m_location.emplace(); - if (!LocationFromXml(node.child("location"), message.m_location.value())) + if (LocationFromXml(node.child("location"), message.m_location.value())) + AllMwmColoringFromXml(node.child("mwm_coloring"), dataSource, message.m_decoded); + else { message.m_location.reset(); LOG(LWARNING, ("Message", message.m_id, "has no location but is not a cancellation message")); @@ -842,9 +1045,21 @@ void MessageToXml(TraffMessage const & message, pugi::xml_node node) EventToXml(event, eventNode); } } + + if (!message.m_decoded.empty()) + { + auto allMwmColoringNode = node.append_child("mwm_coloring"); + for (auto & [mwmId, coloring] : message.m_decoded) + { + auto coloringNode = allMwmColoringNode.append_child("coloring"); + ColoringToXml(mwmId, coloring, coloringNode); + } + } } -bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed) +bool ParseTraff(pugi::xml_document const & document, + std::optional> dataSource, + TraffFeed & feed) { bool result = false; @@ -859,7 +1074,7 @@ bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed) { auto const messageNode = xpathNode.node(); TraffMessage message; - if (MessageFromXml(messageNode, message)) + if (MessageFromXml(messageNode, dataSource, message)) { feed.push_back(message); result = true; diff --git a/traffxml/traff_model_xml.hpp b/traffxml/traff_model_xml.hpp index 34ad26d2a..ad2ef1371 100644 --- a/traffxml/traff_model_xml.hpp +++ b/traffxml/traff_model_xml.hpp @@ -4,6 +4,9 @@ #include "geometry/rect2d.hpp" +#include "indexer/data_source.hpp" + +#include #include #include @@ -34,14 +37,25 @@ namespace traffxml * Parsing the feed will report failure if all its messages fail to parse, but not if it has no * messages. * - * @note Custom elements and attributes which are not part of the TraFF specification are currently - * ignored. Future versions may process certain custom elements. + * In addition to the TraFF specification, we also use a custom extension, `mwm_coloring`, which is + * a child of `message` and holds decoded traffic coloring. In order to parse it, `dataSource` must + * be specified. If `dataSource` is `nullopt`, coloring will be ignored. It is recommended to pass + * `dataSource` if, and only if, parsing an XML stream that is expected to contain traffic coloring. + * + * @note To pass a reference to the framework data source (assuming the `framework` is the framework + * instance), use `std::cref(framework.GetDataSource())`. + * + * @note Custom elements and attributes which are not part of the TraFF specification, other than + * `mwm_coloring`, are ignored. * * @param document The XML document from which to retrieve the messages. + * @param dataSource The data source for coloring, see description. * @param feed Receives the TraFF feed. * @return `true` on success, `false` on failure. */ -bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed); +bool ParseTraff(pugi::xml_document const & document, + std::optional> dataSource, + TraffFeed & feed); /** * @brief Generates XML from a TraFF feed. diff --git a/traffxml/traff_storage.hpp b/traffxml/traff_storage.hpp index 4cbac8127..f59bfb071 100644 --- a/traffxml/traff_storage.hpp +++ b/traffxml/traff_storage.hpp @@ -45,7 +45,7 @@ public: * interpreted relative to the platform-specific path; absolute paths are not supported as some * platforms restrict applications’ access to files outside their designated path. */ - LocalStorage(std::string & fileName) + LocalStorage(std::string const & fileName) : m_fileName(fileName) {}