[traffxml] Store message cache in file

So far only API and tests, and without decoded segments

Signed-off-by: mvglasow <michael -at- vonglasow.com>
This commit is contained in:
mvglasow
2025-06-20 17:23:22 +03:00
parent 371a58f6f9
commit 9fb08bdc56
7 changed files with 246 additions and 18 deletions

View File

@@ -107,6 +107,12 @@ void TrafficManager::Teardown()
m_thread.join(); m_thread.join();
} }
std::map<std::string, traffxml::TraffMessage> TrafficManager::GetMessageCache()
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_messageCache;
}
TrafficManager::TrafficState TrafficManager::GetState() const TrafficManager::TrafficState TrafficManager::GetState() const
{ {
return m_state; return m_state;

View File

@@ -111,6 +111,17 @@ public:
void Teardown(); 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<std::string, traffxml::TraffMessage> GetMessageCache();
TrafficState GetState() const; TrafficState GetState() const;
void SetStateListener(TrafficStateChangedFn const & onStateChangedFn); void SetStateListener(TrafficStateChangedFn const & onStateChangedFn);

View File

@@ -445,6 +445,13 @@ void MainWindow::OnSaveTrafficSample()
if (fileName.isEmpty()) if (fileName.isEmpty())
return; 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 #ifdef openlr_obsolete
if (!m_trafficMode->SaveSampleAs(fileName.toStdString())) if (!m_trafficMode->SaveSampleAs(fileName.toStdString()))
{ {

View File

@@ -161,6 +161,11 @@ void IsoTime::Shift(IsoTime nowRef)
m_tp += offset; m_tp += offset;
} }
std::string IsoTime::ToString() const
{
return std::format("{0:%F}T{0:%T}{0:%Ez}", time_point_cast<std::chrono::seconds>(m_tp));
}
bool IsoTime::operator< (IsoTime & rhs) bool IsoTime::operator< (IsoTime & rhs)
{ {
return m_tp < rhs.m_tp; return m_tp < rhs.m_tp;

View File

@@ -83,6 +83,12 @@ public:
*/ */
void Shift(IsoTime nowRef); 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);
bool operator> (IsoTime & rhs); bool operator> (IsoTime & rhs);
private: private:

View File

@@ -9,34 +9,43 @@
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
#include <boost/bimap.hpp>
#include <pugixml.hpp> #include <pugixml.hpp>
using namespace std; using namespace std;
namespace traffxml namespace traffxml
{ {
const std::map<std::string, Directionality> kDirectionalityMap{ template <typename L, typename R>
boost::bimap<L, R>
MakeBimap(std::initializer_list<typename boost::bimap<L, R>::value_type> list)
{
return boost::bimap<L, R>(list.begin(), list.end());
}
const boost::bimap<std::string, Directionality> kDirectionalityMap = MakeBimap<std::string, Directionality>({
{"ONE_DIRECTION", Directionality::OneDirection}, {"ONE_DIRECTION", Directionality::OneDirection},
{"BOTH_DIRECTIONS", Directionality::BothDirections} {"BOTH_DIRECTIONS", Directionality::BothDirections}
}; });
const std::map<std::string, Ramps> kRampsMap{ const boost::bimap<std::string, Ramps> kRampsMap = MakeBimap<std::string, Ramps>({
{"ALL_RAMPS", Ramps::All}, {"ALL_RAMPS", Ramps::All},
{"ENTRY_RAMP", Ramps::Entry}, {"ENTRY_RAMP", Ramps::Entry},
{"EXIT_RAMP", Ramps::Exit}, {"EXIT_RAMP", Ramps::Exit},
{"NONE", Ramps::None} {"NONE", Ramps::None}
}; });
const std::map<std::string, RoadClass> kRoadClassMap{ const boost::bimap<std::string, traffxml::RoadClass> kRoadClassMap = MakeBimap<std::string, RoadClass>({
{"MOTORWAY", RoadClass::Motorway}, {"MOTORWAY", RoadClass::Motorway},
{"TRUNK", RoadClass::Trunk}, {"TRUNK", RoadClass::Trunk},
{"PRIMARY", RoadClass::Primary}, {"PRIMARY", RoadClass::Primary},
{"SECONDARY", RoadClass::Secondary}, {"SECONDARY", RoadClass::Secondary},
{"TERTIARY", RoadClass::Tertiary}, {"TERTIARY", RoadClass::Tertiary},
{"OTHER", RoadClass::Other} {"OTHER", RoadClass::Other}
}; });
const std::map<std::string, EventClass> kEventClassMap{ const boost::bimap<std::string, EventClass> kEventClassMap = MakeBimap<std::string, EventClass>({
{"INVALID", EventClass::Invalid}, {"INVALID", EventClass::Invalid},
{"ACTIVITY", EventClass::Activity}, {"ACTIVITY", EventClass::Activity},
{"AUTHORITY", EventClass::Authority}, {"AUTHORITY", EventClass::Authority},
@@ -52,9 +61,9 @@ const std::map<std::string, EventClass> kEventClassMap{
{"SECURITY", EventClass::Security}, {"SECURITY", EventClass::Security},
{"TRANSPORT", EventClass::Transport}, {"TRANSPORT", EventClass::Transport},
{"WEATHER", EventClass::Weather} {"WEATHER", EventClass::Weather}
}; });
const std::map<std::string, EventType> kEventTypeMap{ const boost::bimap<std::string, EventType> kEventTypeMap = MakeBimap<std::string, EventType>({
{"INVALID", EventType::Invalid}, {"INVALID", EventType::Invalid},
// TODO Activity*, Authority*, Carpool* (not in enum yet) // TODO Activity*, Authority*, Carpool* (not in enum yet)
{"CONGESTION_CLEARED", EventType::CongestionCleared}, {"CONGESTION_CLEARED", EventType::CongestionCleared},
@@ -105,7 +114,7 @@ const std::map<std::string, EventType> kEventTypeMap{
{"RESTRICTION_SPEED_LIMIT", EventType::RestrictionSpeedLimit}, {"RESTRICTION_SPEED_LIMIT", EventType::RestrictionSpeedLimit},
{"RESTRICTION_SPEED_LIMIT_LIFTED", EventType::RestrictionSpeedLimitLifted}, {"RESTRICTION_SPEED_LIMIT_LIFTED", EventType::RestrictionSpeedLimitLifted},
// TODO Security*, Transport*, Weather* (not in enum yet) // TODO Security*, Transport*, Weather* (not in enum yet)
}; });
/** /**
* @brief Retrieves an integer value from an attribute. * @brief Retrieves an integer value from an attribute.
@@ -291,13 +300,13 @@ std::optional<bool> OptionalBoolFromXml(pugi::xml_attribute attribute)
* @return `true` on success, `false` if the attribute is not set or its value is not found in `map`. * @return `true` on success, `false` if the attribute is not set or its value is not found in `map`.
*/ */
template <typename Value> template <typename Value>
bool EnumFromXml(pugi::xml_attribute attribute, Value & value, std::map<std::string, Value> map) bool EnumFromXml(pugi::xml_attribute attribute, Value & value, boost::bimap<std::string, Value> map)
{ {
std::string string; std::string string;
if (StringFromXml(attribute, string)) if (StringFromXml(attribute, string))
{ {
auto it = map.find(string); auto it = map.left.find(string);
if (it != map.end()) if (it != map.left.end())
{ {
value = it->second; value = it->second;
return true; return true;
@@ -320,13 +329,13 @@ bool EnumFromXml(pugi::xml_attribute attribute, Value & value, std::map<std::str
* @return The enum value, or `std::nullopt` if the attribute is not set or its value is not found in `map`. * @return The enum value, or `std::nullopt` if the attribute is not set or its value is not found in `map`.
*/ */
template <typename Value> template <typename Value>
std::optional<Value> OptionalEnumFromXml(pugi::xml_attribute attribute, std::map<std::string, Value> map) std::optional<Value> OptionalEnumFromXml(pugi::xml_attribute attribute, boost::bimap<std::string, Value> map)
{ {
std::string string; std::string string;
if (StringFromXml(attribute, string)) if (StringFromXml(attribute, string))
{ {
auto it = map.find(string); auto it = map.left.find(string);
if (it != map.end()) if (it != map.left.end())
return it->second; return it->second;
else else
LOG(LWARNING, ("Unknown value for", attribute.name(), ":", string, "(ignoring)")); LOG(LWARNING, ("Unknown value for", attribute.name(), ":", string, "(ignoring)"));
@@ -334,6 +343,18 @@ std::optional<Value> OptionalEnumFromXml(pugi::xml_attribute attribute, std::map
return std::nullopt; return std::nullopt;
} }
template <typename Value>
void EnumToXml(Value const & value, std::string name, pugi::xml_node & node, boost::bimap<std::string, Value> 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. * @brief Retrieves the IDs of replaced messages from an XML element.
* @param node The XML element to retrieve (`merge`). * @param node The XML element to retrieve (`merge`).
@@ -431,6 +452,19 @@ std::optional<Point> OptionalPointFromXml(pugi::xml_node node)
return result; 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. * @brief Retrieves a `TraffLocation` from an XML element.
* @param node The XML element to retrieve (`location`). * @param node The XML element to retrieve (`location`).
@@ -480,6 +514,47 @@ bool LocationFromXml(pugi::xml_node node, TraffLocation & location)
return true; 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. * @brief Retrieves a `TraffQuantifier` from an XML element.
* *
@@ -571,6 +646,26 @@ bool EventFromXml(pugi::xml_node node, TraffEvent & event)
return true; 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. * @brief Retrieves the TraFF events associsted with a message from an XML element.
* @param node The XML element to retrieve (`events`). * @param node The XML element to retrieve (`events`).
@@ -664,6 +759,49 @@ bool MessageFromXml(pugi::xml_node node, TraffMessage & message)
return true; 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 ParseTraff(pugi::xml_document const & document, TraffFeed & feed)
{ {
bool result = false; bool result = false;
@@ -690,6 +828,27 @@ bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed)
return result; 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<std::string, traffxml::TraffMessage> 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<m2::RectD> & bboxRects) std::string FiltersToXml(std::vector<m2::RectD> & bboxRects)
{ {
std::ostringstream os; std::ostringstream os;

View File

@@ -23,8 +23,6 @@ namespace traffxml
* The name of the root element is not verified, but the `message` elements must be its immediate * The name of the root element is not verified, but the `message` elements must be its immediate
* children. * children.
* *
* Custom elements and attributes which are not part of the TraFF specification are ignored.
*
* Values which cannot be parsed correctly are skipped. * Values which cannot be parsed correctly are skipped.
* *
* Events whose event type does not match their event class 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 * Parsing the feed will report failure if all its messages fail to parse, but not if it has no
* messages. * 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 document The XML document from which to retrieve the messages.
* @param feed Receives the TraFF feed. * @param feed Receives the TraFF feed.
* @return `true` on success, `false` on failure. * @return `true` on success, `false` on failure.
*/ */
bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed); 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<std::string, traffxml::TraffMessage> const & messages,
pugi::xml_document & document);
/** /**
* @brief Generates a list of XML `filter` elements from a vector of rects representing bboxes. * @brief Generates a list of XML `filter` elements from a vector of rects representing bboxes.
* *