diff --git a/map/traffic_manager.cpp b/map/traffic_manager.cpp index 45fefdabc..965a88b15 100644 --- a/map/traffic_manager.cpp +++ b/map/traffic_manager.cpp @@ -107,6 +107,12 @@ void TrafficManager::Teardown() m_thread.join(); } +std::map TrafficManager::GetMessageCache() +{ + std::lock_guard lock(m_mutex); + return m_messageCache; +} + TrafficManager::TrafficState TrafficManager::GetState() const { return m_state; diff --git a/map/traffic_manager.hpp b/map/traffic_manager.hpp index 2f6536bbc..b3ca6d83d 100644 --- a/map/traffic_manager.hpp +++ b/map/traffic_manager.hpp @@ -111,6 +111,17 @@ public: void Teardown(); + /** + * @brief Returns a copy of the cache of all currently active TraFF messages. + * + * For testing purposes. + * + * Keys are message IDs, values are messages. + * + * This method is safe to call from any thread. + */ + std::map GetMessageCache(); + TrafficState GetState() const; void SetStateListener(TrafficStateChangedFn const & onStateChangedFn); diff --git a/traffxml/traff_assessment_tool/mainwindow.cpp b/traffxml/traff_assessment_tool/mainwindow.cpp index 0313c663f..4476dc345 100644 --- a/traffxml/traff_assessment_tool/mainwindow.cpp +++ b/traffxml/traff_assessment_tool/mainwindow.cpp @@ -445,6 +445,13 @@ void MainWindow::OnSaveTrafficSample() if (fileName.isEmpty()) return; + pugi::xml_document document; + + auto const messageCache = m_framework.GetTrafficManager().GetMessageCache(); + + traffxml::GenerateTraff(messageCache, document); + document.save_file(fileName.toStdString().data(), " " /* indent */); + #ifdef openlr_obsolete if (!m_trafficMode->SaveSampleAs(fileName.toStdString())) { diff --git a/traffxml/traff_model.cpp b/traffxml/traff_model.cpp index bd639e48b..028f720b5 100644 --- a/traffxml/traff_model.cpp +++ b/traffxml/traff_model.cpp @@ -161,6 +161,11 @@ void IsoTime::Shift(IsoTime nowRef) m_tp += offset; } +std::string IsoTime::ToString() const +{ + return std::format("{0:%F}T{0:%T}{0:%Ez}", time_point_cast(m_tp)); +} + bool IsoTime::operator< (IsoTime & rhs) { return m_tp < rhs.m_tp; diff --git a/traffxml/traff_model.hpp b/traffxml/traff_model.hpp index 578fe3733..40bded85a 100644 --- a/traffxml/traff_model.hpp +++ b/traffxml/traff_model.hpp @@ -83,6 +83,12 @@ public: */ void Shift(IsoTime nowRef); + /** + * @brief Returns a string representation of the instance. + * @return The timestamp in ISO 8601 format. + */ + std::string ToString() const; + bool operator< (IsoTime & rhs); bool operator> (IsoTime & rhs); private: diff --git a/traffxml/traff_model_xml.cpp b/traffxml/traff_model_xml.cpp index 3edaa3f0b..4fd1cf910 100644 --- a/traffxml/traff_model_xml.cpp +++ b/traffxml/traff_model_xml.cpp @@ -9,34 +9,43 @@ #include #include +#include + #include using namespace std; namespace traffxml { -const std::map kDirectionalityMap{ +template +boost::bimap +MakeBimap(std::initializer_list::value_type> list) +{ + return boost::bimap(list.begin(), list.end()); +} + +const boost::bimap kDirectionalityMap = MakeBimap({ {"ONE_DIRECTION", Directionality::OneDirection}, {"BOTH_DIRECTIONS", Directionality::BothDirections} -}; +}); -const std::map kRampsMap{ +const boost::bimap kRampsMap = MakeBimap({ {"ALL_RAMPS", Ramps::All}, {"ENTRY_RAMP", Ramps::Entry}, {"EXIT_RAMP", Ramps::Exit}, {"NONE", Ramps::None} -}; +}); -const std::map kRoadClassMap{ +const boost::bimap kRoadClassMap = MakeBimap({ {"MOTORWAY", RoadClass::Motorway}, {"TRUNK", RoadClass::Trunk}, {"PRIMARY", RoadClass::Primary}, {"SECONDARY", RoadClass::Secondary}, {"TERTIARY", RoadClass::Tertiary}, {"OTHER", RoadClass::Other} -}; +}); -const std::map kEventClassMap{ +const boost::bimap kEventClassMap = MakeBimap({ {"INVALID", EventClass::Invalid}, {"ACTIVITY", EventClass::Activity}, {"AUTHORITY", EventClass::Authority}, @@ -52,9 +61,9 @@ const std::map kEventClassMap{ {"SECURITY", EventClass::Security}, {"TRANSPORT", EventClass::Transport}, {"WEATHER", EventClass::Weather} -}; +}); -const std::map kEventTypeMap{ +const boost::bimap kEventTypeMap = MakeBimap({ {"INVALID", EventType::Invalid}, // TODO Activity*, Authority*, Carpool* (not in enum yet) {"CONGESTION_CLEARED", EventType::CongestionCleared}, @@ -105,7 +114,7 @@ const std::map kEventTypeMap{ {"RESTRICTION_SPEED_LIMIT", EventType::RestrictionSpeedLimit}, {"RESTRICTION_SPEED_LIMIT_LIFTED", EventType::RestrictionSpeedLimitLifted}, // TODO Security*, Transport*, Weather* (not in enum yet) -}; +}); /** * @brief Retrieves an integer value from an attribute. @@ -291,13 +300,13 @@ std::optional OptionalBoolFromXml(pugi::xml_attribute attribute) * @return `true` on success, `false` if the attribute is not set or its value is not found in `map`. */ template -bool EnumFromXml(pugi::xml_attribute attribute, Value & value, std::map map) +bool EnumFromXml(pugi::xml_attribute attribute, Value & value, boost::bimap map) { std::string string; if (StringFromXml(attribute, string)) { - auto it = map.find(string); - if (it != map.end()) + auto it = map.left.find(string); + if (it != map.left.end()) { value = it->second; return true; @@ -320,13 +329,13 @@ bool EnumFromXml(pugi::xml_attribute attribute, Value & value, std::map -std::optional OptionalEnumFromXml(pugi::xml_attribute attribute, std::map map) +std::optional OptionalEnumFromXml(pugi::xml_attribute attribute, boost::bimap map) { std::string string; if (StringFromXml(attribute, string)) { - auto it = map.find(string); - if (it != map.end()) + auto it = map.left.find(string); + if (it != map.left.end()) return it->second; else LOG(LWARNING, ("Unknown value for", attribute.name(), ":", string, "(ignoring)")); @@ -334,6 +343,18 @@ std::optional OptionalEnumFromXml(pugi::xml_attribute attribute, std::map return std::nullopt; } +template +void EnumToXml(Value const & value, std::string name, pugi::xml_node & node, boost::bimap const & map) +{ + auto it = map.right.find(value); + if (it != map.right.end()) + node.append_attribute(name).set_value(it->second); + else + { + ASSERT(false, ("Enum value not found in map for", name)); + } +} + /** * @brief Retrieves the IDs of replaced messages from an XML element. * @param node The XML element to retrieve (`merge`). @@ -431,6 +452,19 @@ std::optional OptionalPointFromXml(pugi::xml_node node) return result; } +void PointToXml(Point const & point, std::string name, pugi::xml_node & parentNode) +{ + auto node = parentNode.append_child(name); + if (point.m_distance) + node.append_attribute("distance").set_value(point.m_junctionName.value()); + if (point.m_junctionName) + node.append_attribute("junction_name").set_value(point.m_junctionName.value()); + if (point.m_junctionRef) + node.append_attribute("junction_ref").set_value(point.m_junctionRef.value()); + + node.text() = std::format("{0:+.5f} {1:+.5f}", point.m_coordinates.m_lat, point.m_coordinates.m_lon); +} + /** * @brief Retrieves a `TraffLocation` from an XML element. * @param node The XML element to retrieve (`location`). @@ -480,6 +514,47 @@ bool LocationFromXml(pugi::xml_node node, TraffLocation & location) return true; } +void LocationToXml(TraffLocation const & location, pugi::xml_node & node) +{ + if (location.m_country) + node.append_attribute("country").set_value(location.m_country.value()); + if (location.m_destination) + node.append_attribute("destination").set_value(location.m_destination.value()); + if (location.m_direction) + node.append_attribute("direction").set_value(location.m_direction.value()); + EnumToXml(location.m_directionality, "directionality", node, kDirectionalityMap); + + // TODO fuzziness (not yet implemented in struct) + + if (location.m_origin) + node.append_attribute("origin").set_value(location.m_origin.value()); + EnumToXml(location.m_ramps, "ramps", node, kRampsMap); + if (location.m_roadClass) + EnumToXml(location.m_roadClass.value(), "roadClass", node, kRoadClassMap); + // TODO roadIsUrban (disabled for now) + if (location.m_roadRef) + node.append_attribute("roadRef").set_value(location.m_roadRef.value()); + if (location.m_roadName) + node.append_attribute("roadName").set_value(location.m_roadName.value()); + if (location.m_territory) + node.append_attribute("territory").set_value(location.m_territory.value()); + if (location.m_town) + node.append_attribute("town").set_value(location.m_town.value()); + + if (location.m_from) + PointToXml(location.m_from.value(), "from", node); + if (location.m_at) + PointToXml(location.m_at.value(), "at", node); + if (location.m_via) + PointToXml(location.m_via.value(), "via", node); + if (location.m_notVia) + PointToXml(location.m_notVia.value(), "not_via", node); + if (location.m_to) + PointToXml(location.m_to.value(), "to", node); + + // TODO decoded segments +} + /** * @brief Retrieves a `TraffQuantifier` from an XML element. * @@ -571,6 +646,26 @@ bool EventFromXml(pugi::xml_node node, TraffEvent & event) return true; } +void EventToXml(TraffEvent const & event, pugi::xml_node & node) +{ + EnumToXml(event.m_class, "class", node, kEventClassMap); + EnumToXml(event.m_type, "type", node, kEventTypeMap); + if (event.m_length) + node.append_attribute("length").set_value(event.m_length.value()); + if (event.m_probability) + node.append_attribute("probability").set_value(event.m_probability.value()); + + if (event.m_qDurationMins) + node.append_attribute("q_duration").set_value(std::format("{:%H:%M}", std::chrono::minutes(event.m_qDurationMins.value()))); + + // TODO other quantifiers (not yet implemented in struct) + + if (event.m_speed) + node.append_attribute("speed").set_value(event.m_speed.value()); + + // TODO supplementary information (not yet implemented in struct) +} + /** * @brief Retrieves the TraFF events associsted with a message from an XML element. * @param node The XML element to retrieve (`events`). @@ -664,6 +759,49 @@ bool MessageFromXml(pugi::xml_node node, TraffMessage & message) return true; } +void MessageToXml(TraffMessage const & message, pugi::xml_node node) +{ + node.append_attribute("id").set_value(message.m_id); + node.append_attribute("receive_time").set_value(message.m_receiveTime.ToString()); + node.append_attribute("update_time").set_value(message.m_updateTime.ToString()); + node.append_attribute("expiration_time").set_value(message.m_expirationTime.ToString()); + if (message.m_startTime) + node.append_attribute("start_time").set_value(message.m_startTime.value().ToString()); + if (message.m_endTime) + node.append_attribute("end_time").set_value(message.m_endTime.value().ToString()); + + node.append_attribute("cancellation").set_value(message.m_cancellation); + node.append_attribute("forecast").set_value(message.m_forecast); + + // TODO urgency (not yet implemented in struct) + + if (!message.m_replaces.empty()) + { + auto mergeNode = node.append_child("merge"); + for (auto const & id : message.m_replaces) + { + auto replacesNode = mergeNode.append_child("replaces"); + replacesNode.append_attribute("id").set_value(id); + } + } + + if (message.m_location) + { + auto locationNode = node.append_child("location"); + LocationToXml(message.m_location.value(), locationNode); + } + + if (!message.m_events.empty()) + { + auto eventsNode = node.append_child("events"); + for (auto const event : message.m_events) + { + auto eventNode = eventsNode.append_child("event"); + EventToXml(event, eventNode); + } + } +} + bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed) { bool result = false; @@ -690,6 +828,27 @@ bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed) return result; } +void GenerateTraff(TraffFeed const & feed, pugi::xml_document & document) +{ + auto root = document.append_child("feed"); + for (auto const & message : feed) + { + auto child = root.append_child("message"); + MessageToXml(message, child); + } +} + +void GenerateTraff(std::map const & messages, + pugi::xml_document & document) +{ + auto root = document.append_child("feed"); + for (auto const & [id, message] : messages) + { + auto child = root.append_child("message"); + MessageToXml(message, child); + } +} + std::string FiltersToXml(std::vector & bboxRects) { std::ostringstream os; diff --git a/traffxml/traff_model_xml.hpp b/traffxml/traff_model_xml.hpp index fa2320982..34ad26d2a 100644 --- a/traffxml/traff_model_xml.hpp +++ b/traffxml/traff_model_xml.hpp @@ -23,8 +23,6 @@ namespace traffxml * The name of the root element is not verified, but the `message` elements must be its immediate * children. * - * Custom elements and attributes which are not part of the TraFF specification are ignored. - * * Values which cannot be parsed correctly are skipped. * * Events whose event type does not match their event class are skipped. @@ -36,12 +34,48 @@ 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. + * * @param document The XML document from which to retrieve the messages. * @param feed Receives the TraFF feed. * @return `true` on success, `false` on failure. */ bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed); +/** + * @brief Generates XML from a TraFF feed. + * + * The resulting document largely conforms to the TraFF specification (currently version 0.8), but + * may contain custom elements. + * + * The root element of the generated XML document is `feed`. + * + * @note Currently no custom elements are generated. Future versions may add the location decoded + * into MWM IDs, feature IDs, directions and segments, along with their speed groups. + * + * @param feed The TraFF feed to encode. + * @param document The XML document in which to store the messages. + */ +void GenerateTraff(TraffFeed const & feed, pugi::xml_document & document); + +/** + * @brief Generates XML from a map of TraFF messages. + * + * The resulting document largely conforms to the TraFF specification (currently version 0.8), but + * may contain custom elements. + * + * The root element of the generated XML document is `feed`. + * + * @note Currently no custom elements are generated. Future versions may add the location decoded + * into MWM IDs, feature IDs, directions and segments, along with their speed groups. + * + * @param messages A map whose values contain the TraFF messages to encode. + * @param document The XML document in which to store the messages. + */ +void GenerateTraff(std::map const & messages, + pugi::xml_document & document); + /** * @brief Generates a list of XML `filter` elements from a vector of rects representing bboxes. *