Files
comaps/traffxml/traff_model_xml.cpp
mvglasow 247f88254e [traffxml] Fix erroneous parsing of event length
Signed-off-by: mvglasow <michael -at- vonglasow.com>
2025-07-28 00:33:24 +03:00

864 lines
30 KiB
C++

#include "traffxml/traff_model_xml.hpp"
#include "traffxml/traff_model.hpp"
#include "base/logging.hpp"
#include <cstring>
#include <optional>
#include <regex>
#include <type_traits>
#include <utility>
#include <boost/bimap.hpp>
#include <pugixml.hpp>
using namespace std;
namespace traffxml
{
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},
{"BOTH_DIRECTIONS", Directionality::BothDirections}
});
const boost::bimap<std::string, Ramps> kRampsMap = MakeBimap<std::string, Ramps>({
{"ALL_RAMPS", Ramps::All},
{"ENTRY_RAMP", Ramps::Entry},
{"EXIT_RAMP", Ramps::Exit},
{"NONE", Ramps::None}
});
const boost::bimap<std::string, traffxml::RoadClass> kRoadClassMap = MakeBimap<std::string, RoadClass>({
{"MOTORWAY", RoadClass::Motorway},
{"TRUNK", RoadClass::Trunk},
{"PRIMARY", RoadClass::Primary},
{"SECONDARY", RoadClass::Secondary},
{"TERTIARY", RoadClass::Tertiary},
{"OTHER", RoadClass::Other}
});
const boost::bimap<std::string, EventClass> kEventClassMap = MakeBimap<std::string, EventClass>({
{"INVALID", EventClass::Invalid},
{"ACTIVITY", EventClass::Activity},
{"AUTHORITY", EventClass::Authority},
{"CARPOOL", EventClass::Carpool},
{"CONGESTION", EventClass::Congestion},
{"CONSTRUCTION", EventClass::Construction},
{"DELAY", EventClass::Delay},
{"ENVIRONMENT", EventClass::Environment},
{"EQUIPMENT_STATUS", EventClass::EquipmentStatus},
{"HAZARD", EventClass::Hazard},
{"INCIDENT", EventClass::Incident},
{"RESTRICTION", EventClass::Restriction},
{"SECURITY", EventClass::Security},
{"TRANSPORT", EventClass::Transport},
{"WEATHER", EventClass::Weather}
});
const boost::bimap<std::string, EventType> kEventTypeMap = MakeBimap<std::string, EventType>({
{"INVALID", EventType::Invalid},
// TODO Activity*, Authority*, Carpool* (not in enum yet)
{"CONGESTION_CLEARED", EventType::CongestionCleared},
{"CONGESTION_FORECAST_WITHDRAWN", EventType::CongestionForecastWithdrawn},
{"CONGESTION_HEAVY_TRAFFIC", EventType::CongestionHeavyTraffic},
{"CONGESTION_LONG_QUEUE", EventType::CongestionLongQueue},
{"CONGESTION_NONE", EventType::CongestionNone},
{"CONGESTION_NORMAL_TRAFFIC", EventType::CongestionNormalTraffic},
{"CONGESTION_QUEUE", EventType::CongestionQueue},
{"CONGESTION_QUEUE_LIKELY", EventType::CongestionQueueLikely},
{"CONGESTION_SLOW_TRAFFIC", EventType::CongestionSlowTraffic},
{"CONGESTION_STATIONARY_TRAFFIC", EventType::CongestionStationaryTraffic},
{"CONGESTION_STATIONARY_TRAFFIC_LIKELY", EventType::CongestionStationaryTrafficLikely},
{"CONGESTION_TRAFFIC_BUILDING_UP", EventType::CongestionTrafficBuildingUp},
{"CONGESTION_TRAFFIC_CONGESTION", EventType::CongestionTrafficCongestion},
{"CONGESTION_TRAFFIC_EASING", EventType::CongestionTrafficEasing},
{"CONGESTION_TRAFFIC_FLOWING_FREELY", EventType::CongestionTrafficFlowingFreely},
{"CONGESTION_TRAFFIC_HEAVIER_THAN_NORMAL", EventType::CongestionTrafficHeavierThanNormal},
{"CONGESTION_TRAFFIC_LIGHTER_THAN_NORMAL", EventType::CongestionTrafficLighterThanNormal},
{"CONGESTION_TRAFFIC_MUCH_HEAVIER_THAN_NORMAL", EventType::CongestionTrafficMuchHeavierThanNormal},
{"CONGESTION_TRAFFIC_PROBLEM", EventType::CongestionTrafficProblem},
// TODO Construction* (not in enum yet)
{"DELAY_CLEARANCE", EventType::DelayClearance},
{"DELAY_DELAY", EventType::DelayDelay},
{"DELAY_DELAY_POSSIBLE", EventType::DelayDelayPossible},
{"DELAY_FORECAST_WITHDRAWN", EventType::DelayForecastWithdrawn},
{"DELAY_LONG_DELAY", EventType::DelayLongDelay},
{"DELAY_SEVERAL_HOURS", EventType::DelaySeveralHours},
{"DELAY_UNCERTAIN_DURATION", EventType::DelayUncertainDuration},
{"DELAY_VERY_LONG_DELAY", EventType::DelayVeryLongDelay},
// TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet)
// TODO complete Restriction* (not in enum yet)
{"RESTRICTION_BLOCKED", EventType::RestrictionBlocked},
{"RESTRICTION_BLOCKED_AHEAD", EventType::RestrictionBlockedAhead},
{"RESTRICTION_CARRIAGEWAY_BLOCKED", EventType::RestrictionCarriagewayBlocked},
{"RESTRICTION_CARRIAGEWAY_CLOSED", EventType::RestrictionCarriagewayClosed},
{"RESTRICTION_CLOSED", EventType::RestrictionClosed},
{"RESTRICTION_CLOSED_AHEAD", EventType::RestrictionClosedAhead},
{"RESTRICTION_ENTRY_BLOCKED", EventType::RestrictionEntryBlocked},
{"RESTRICTION_ENTRY_REOPENED", EventType::RestrictionEntryReopened},
{"RESTRICTION_EXIT_BLOCKED", EventType::RestrictionExitBlocked},
{"RESTRICTION_EXIT_REOPENED", EventType::RestrictionExitReopened},
{"RESTRICTION_OPEN", EventType::RestrictionOpen},
{"RESTRICTION_RAMP_BLOCKED", EventType::RestrictionRampBlocked},
{"RESTRICTION_RAMP_CLOSED", EventType::RestrictionRampClosed},
{"RESTRICTION_RAMP_REOPENED", EventType::RestrictionRampReopened},
{"RESTRICTION_REOPENED", EventType::RestrictionReopened},
{"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.
*
* @param attribute The XML attribute to retrieve.
* @return `true` on success, `false` if the attribute is not set or does not contain an integer value.
*/
std::optional<int> OptionalIntegerFromXml(pugi::xml_attribute const & attribute)
{
if (attribute.empty())
return std::nullopt;
try
{
int result = std::stoi(attribute.as_string());
return result;
}
catch (std::invalid_argument const& ex)
{
return std::nullopt;
}
}
/**
* @brief Retrieves a float value from an attribute.
*
* @param attribute The XML attribute to retrieve.
* @return `true` on success, `false` if the attribute is not set or does not contain a float value.
*/
std::optional<float> OptionalFloatFromXml(pugi::xml_attribute const & attribute)
{
if (attribute.empty())
return std::nullopt;
try
{
float result = std::stof(attribute.as_string());
return result;
}
catch (std::invalid_argument const& ex)
{
return std::nullopt;
}
}
/**
* @brief Retrieves a string from an attribute.
*
* @param attribute The XML attribute to retrieve.
* @param string Receives the string retrieved.
* @return `true` on success, `false` if the attribute is not set or set to an empty string.
*/
bool StringFromXml(pugi::xml_attribute const & attribute, std::string & string)
{
if (attribute.empty())
return false;
string = attribute.as_string();
return true;
}
/**
* @brief Retrieves a string from an XML element.
*
* @param node The XML element to retrieve.
* @param string Receives the string retrieved.
* @return `true` on success, `false` if the node does not exist.
*/
bool StringFromXml(pugi::xml_node const & node, std::string & string)
{
if (!node)
return false;
string = node.text().as_string();
return true;
}
/**
* @brief Retrieves a string from an attribute.
*
* @param attribute The XML attribute to retrieve.
* @return The string, or `std::nullopt` if the attribute is not set or set to an empty string.
*/
std::optional<std::string> OptionalStringFromXml(pugi::xml_attribute const & attribute)
{
std::string result;
if (!StringFromXml(attribute, result))
return std::nullopt;
return result;
}
/**
* @brief Parses time in ISO 8601 format from a time attribute and stores it in an `IsoTime`.
*
* ISO 8601 timestamps have the format `yyyy-mm-ddThh:mm:ss[.sss]`, optionally followed by a UTC
* offset. For example, `2019-11-01T11:45:42+01:00` refers to 11:45:42 in the UTC+1 timezone, which
* is 10:45:42 UTC.
*
* A UTC offset of `Z` denotes UTC and is equivalent to `+00:00` or `-00:00`. UTC is also assumed
* if no UTC offset is specified.
*
* The UTC offset can be specified as `hh:mm`, `hhmm` or `hh`.
*
* Seconds can be specified as integer or float, but will be rounded to the nearest integer. For
* example, 42.645 seconds will be rounded to 43 seconds.
*
* @param attribute The XML attribute from which to receive time.
* @param tm Receives the parsed time.
* @return `true` on success, `false` if the attribute is not set or does not contain a timestamp.
*/
bool TimeFromXml(pugi::xml_attribute const & attribute, IsoTime & tm)
{
std::string timeString;
if (!StringFromXml(attribute, timeString))
return false;
std::optional<IsoTime> result = IsoTime::ParseIsoTime(timeString);
if (!result)
return false;
tm = result.value();
return true;
}
/**
* @brief Parses time in ISO 8601 format from a time attribute and stores it in an `IsoTime`.
*
* ISO 8601 timestamps have the format `yyyy-mm-ddThh:mm:ss[.sss]`, optionally followed by a UTC
* offset. For example, `2019-11-01T11:45:42+01:00` refers to 11:45:42 in the UTC+1 timezone, which
* is 10:45:42 UTC.
*
* A UTC offset of `Z` denotes UTC and is equivalent to `+00:00` or `-00:00`. UTC is also assumed
* if no UTC offset is specified.
*
* The UTC offset can be specified as `hh:mm`, `hhmm` or `hh`.
*
* Seconds can be specified as integer or float, but will be rounded to the nearest integer. For
* example, 42.645 seconds will be rounded to 43 seconds.
*
* @param attribute The XML attribute from which to receive time.
* @return The parsed time, or `std::nullopt` if the attribute is not set or does not contain a timestamp.
*/
std::optional<IsoTime> OptionalTimeFromXml(pugi::xml_attribute const & attribute)
{
IsoTime result = IsoTime::Now();
if (!TimeFromXml(attribute, result))
return std::nullopt;
return result;
}
/**
* @brief Retrieves a boolean value from an attribute.
* @param attribute The XML attribute to retrieve.
* @param defaultValue The default value to return.
* @return The value of the attribute, or `defaultValue` if the attribute is not set.
*/
bool BoolFromXml(pugi::xml_attribute const & attribute, bool defaultValue)
{
if (attribute.empty())
return defaultValue;
return attribute.as_bool();
}
/**
* @brief Retrieves a boolean value from an attribute.
* @param attribute The XML attribute to retrieve.
* @return The value of the attribute, or `std::nullopt` if the attribute is not set.
*/
std::optional<bool> OptionalBoolFromXml(pugi::xml_attribute const & attribute)
{
if (attribute.empty())
return std::nullopt;
return attribute.as_bool();
}
/**
* @brief Retrieves an enum value from an attribute.
*
* Enum values are retrieved in two steps: first, a string is retrieved, which is then decoded to
* an enum value. The enum type is determined by the type of `value` and the value type of `map`,
* both of which must match. The mapping between strings and their corresponding enum values is
* determined by the entries in `map`.
*
* @param attribute The XML attribute to retrieve.
* @param Value Receives the enum value to retrieve.
* @param map A map from strings to their respective enum values.
* @return `true` on success, `false` if the attribute is not set or its value is not found in `map`.
*/
template <typename Value>
bool EnumFromXml(pugi::xml_attribute const & attribute, Value & value,
boost::bimap<std::string, Value> const & map)
{
std::string string;
if (StringFromXml(attribute, string))
{
auto it = map.left.find(string);
if (it != map.left.end())
{
value = it->second;
return true;
}
else
LOG(LWARNING, ("Unknown value for", attribute.name(), ":", string, "(ignoring)"));
}
return false;
}
/**
* @brief Retrieves an enum value from an attribute.
*
* Enum values are retrieved in two steps: first, a string is retrieved, which is then decoded to
* an enum value. The enum type is determined by the value type of `map`. The mapping between
* strings and their corresponding enum values is determined by the entries in `map`.
*
* @param attribute The XML attribute to retrieve.
* @param map A map from strings to their respective enum values.
* @return The enum value, or `std::nullopt` if the attribute is not set or its value is not found in `map`.
*/
template <typename Value>
std::optional<Value> OptionalEnumFromXml(pugi::xml_attribute const & attribute,
boost::bimap<std::string, Value> const & map)
{
std::string string;
if (StringFromXml(attribute, string))
{
auto it = map.left.find(string);
if (it != map.left.end())
return it->second;
else
LOG(LWARNING, ("Unknown value for", attribute.name(), ":", string, "(ignoring)"));
}
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.
* @param node The XML element to retrieve (`merge`).
* @param replacedIds Receives the replaced IDs.
* @return `true` on success (including if the node contains no replaced IDs), `false` if the node does not exist or does not contain valid data.
*/
bool ReplacedMessageIdsFromXml(pugi::xml_node const & node, std::vector<std::string> & replacedIds)
{
if (!node)
return false;
bool result = false;
auto const replacedIdNodes = node.select_nodes("./replaces");
if (replacedIdNodes.empty())
return true;
for (auto const & xpathNode : replacedIdNodes)
{
auto const replacedIdNode = xpathNode.node();
std::string replacedId;
if (StringFromXml(replacedIdNode.attribute("id"), replacedId))
{
replacedIds.push_back(replacedId);
result = true;
}
else
LOG(LWARNING, ("Could not parse merge element, skipping"));
}
return result;
}
/**
* @brief Retrieves a latitude/longitude pair from an XML element.
*
* Coordinates must be given as latitude, followed by as space, then longitude. Latitude and
* longitude are given as floating-point numbers, optionally with a sign (plus is assumed if no
* sign is given). Coordinates are interpreted as degrees in WGS84 format.
*
* @param node The XML element to retrieve.
* @param latLon Receives the latitude/longitude pair.
* @return `true` on success, `false` if the node does not exist or does not contain valid coordinates.
*/
bool LatLonFromXml(pugi::xml_node const & node, ms::LatLon & latLon)
{
if (!node)
return false;
std::string string;
if (StringFromXml(node, string))
{
std::regex latLonRegex("([+-]?[0-9]*\\.?[0-9]*)\\s+([+-]?[0-9]*\\.?[0-9]*)");
std::smatch latLonMatcher;
if (std::regex_search(string, latLonMatcher, latLonRegex) && latLonMatcher[1].matched && latLonMatcher[2].matched)
{
try
{
latLon.m_lat = std::stod(latLonMatcher[1]);
latLon.m_lon = std::stod(latLonMatcher[2]);
return true;
}
catch (std::invalid_argument const& ex)
{
LOG(LWARNING, ("Not a valid coordinate pair:", string));
}
}
else
LOG(LWARNING, ("Not a valid coordinate pair:", string));
}
return false;
}
/**
* @brief Retrieves a Traff `Point` from an XML element.
* @param node The XML element to retrieve (any child of `location`).
* @return The point, or `std::nullopt` if the node does not exist or does not contain valid point data,
*/
std::optional<Point> OptionalPointFromXml(pugi::xml_node const & node)
{
if (!node)
return std::nullopt;
Point result;
if (!LatLonFromXml(node, result.m_coordinates))
{
LOG(LWARNING, (node.name(), "has no coordinates, ignoring"));
return std::nullopt;
}
result.m_junctionName = OptionalStringFromXml(node.attribute("junction_name"));
result.m_junctionRef = OptionalStringFromXml(node.attribute("junction_ref"));
result.m_distance = OptionalFloatFromXml(node.attribute("distance"));
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`).
* @param location Receives the location.
* @return `true` on success, `false` if the node does not exist or does not contain valid location data.
*/
bool LocationFromXml(pugi::xml_node const & node, TraffLocation & location)
{
if (!node)
return false;
location.m_from = OptionalPointFromXml(node.child("from"));
location.m_to = OptionalPointFromXml(node.child("to"));
location.m_at = OptionalPointFromXml(node.child("at"));
location.m_via = OptionalPointFromXml(node.child("via"));
location.m_notVia = OptionalPointFromXml(node.child("not_via"));
int numPoints = 0;
for (std::optional<Point> 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, ("Only", numPoints, "points of from/to/at specified, ignoring location"));
return false;
}
location.m_country = OptionalStringFromXml(node.attribute("country"));
location.m_destination = OptionalStringFromXml(node.attribute("destination"));
location.m_direction = OptionalStringFromXml(node.attribute("direction"));
EnumFromXml(node.attribute("directionality"), location.m_directionality, kDirectionalityMap);
// TODO fuzziness (not yet implemented in struct)
location.m_origin = OptionalStringFromXml(node.attribute("origin"));
EnumFromXml(node.attribute("ramps"), location.m_ramps, kRampsMap);
location.m_roadClass = OptionalEnumFromXml(node.attribute("road_class"), kRoadClassMap);
// disabled for now
//location.m_roadIsUrban = OptionalBoolFromXml(node.attribute(("road_is_urban")));
location.m_roadRef = OptionalStringFromXml(node.attribute("road_ref"));
location.m_roadName = OptionalStringFromXml(node.attribute("road_name"));
location.m_territory = OptionalStringFromXml(node.attribute("territory"));
location.m_town = OptionalStringFromXml(node.attribute("town"));
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.
*
* The TraFF specification allows only one quantifier per event. The quantifier type depends on the
* event type, and not all events allow quantifiers.
*
* Quantifiers which violate these constraints are not filtered out, i.e. this function may return a
* quantifier for event types that do not allow quantifiers, or of a type illegal for the event type.
* If an event contains multiple quantifiers of different types, any one of these quantifiers may be
* returned, with no preference for legal quantifiers over illegal ones.
*
* @param node The node from which to retrieve the quantifier (`event`).
* @return The quantifier, or `std::nullopt`
*/
std::optional<uint16_t> OptionalDurationFromXml(pugi::xml_attribute const & attribute)
{
std::string durationString;
if (!StringFromXml(attribute, durationString))
return std::nullopt;
/*
* Valid time formats:
* 01:30 (hh:mm)
* 1 h
* 30 min
*/
std::regex durationRegex("(([0-9]+):([0-9]{2}))|(([0-9]+) *h)|(([0-9]+) *min)");
std::smatch durationMatcher;
if (std::regex_search(durationString, durationMatcher, durationRegex))
{
if (!durationMatcher.str(2).empty() && !durationMatcher.str(3).empty())
return std::stoi(durationMatcher[2]) * 60 + std::stoi(durationMatcher[3]);
else if (!durationMatcher.str(5).empty())
return std::stoi(durationMatcher[5]) * 60;
else if (!durationMatcher.str(7).empty())
return std::stoi(durationMatcher[7]);
UNREACHABLE();
return std::nullopt;
}
else
{
LOG(LINFO, ("Not a valid duration:", durationString));
return std::nullopt;
}
}
/**
* @brief Retrieves a `TraffEvent` from an XML element.
* @param node The XML element to retrieve (`event`).
* @param event Receives the event.
* @return `true` on success, `false` if the node does not exist or does not contain valid event data.
*/
bool EventFromXml(pugi::xml_node const & node, TraffEvent & event)
{
std::string eventClass;
if (!StringFromXml(node.attribute("class"), eventClass))
{
LOG(LWARNING, ("No event class specified, ignoring"));
return false;
}
if (!EnumFromXml(node.attribute("class"), event.m_class, kEventClassMap))
return false;
std::string eventType;
if (!StringFromXml(node.attribute("type"), eventType))
{
LOG(LWARNING, ("No event type specified, ignoring"));
return false;
}
if (!eventType.starts_with(eventClass + "_"))
{
LOG(LWARNING, ("Event type", eventType, "does not match event class", eventClass, "(ignoring)"));
return false;
}
if (!EnumFromXml(node.attribute("type"), event.m_type, kEventTypeMap))
return false;
event.m_length = OptionalIntegerFromXml(node.attribute("length"));
event.m_probability = OptionalIntegerFromXml(node.attribute("probability"));
event.m_qDurationMins = OptionalDurationFromXml(node.attribute("q_duration"));
// TODO other quantifiers (not yet implemented in struct)
event.m_speed = OptionalIntegerFromXml(node.attribute("speed"));
// TODO supplementary information (not yet implemented in struct)
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 associated with a message from an XML element.
* @param node The XML element to retrieve (`events`).
* @param events Receives the events.
* @return `true` on success, `false` if the node does not exist or does not contain valid event data (including if the node contains no events).
*/
bool EventsFromXml(pugi::xml_node const & node, std::vector<TraffEvent> & events)
{
if (!node)
return false;
bool result = false;
auto const eventNodes = node.select_nodes("./event");
if (eventNodes.empty())
return false;
for (auto const & xpathNode : eventNodes)
{
auto const eventNode = xpathNode.node();
TraffEvent event;
if (EventFromXml(eventNode, event))
{
events.push_back(event);
result = true;
}
else
LOG(LWARNING, ("Could not parse event, skipping"));
}
return result;
}
/**
* @brief Retrieves a TraFF message from an XML element.
* @param node The XML element to retrieve (`message`).
* @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)
{
if (!StringFromXml(node.attribute("id"), message.m_id))
{
LOG(LWARNING, ("Message has no id"));
return false;
}
if (!TimeFromXml(node.attribute("receive_time"), message.m_receiveTime))
{
LOG(LWARNING, ("Message", message.m_id, "has no receive_time"));
return false;
}
if (!TimeFromXml(node.attribute("update_time"), message.m_updateTime))
{
LOG(LWARNING, ("Message", message.m_id, "has no update_time"));
return false;
}
if (!TimeFromXml(node.attribute("expiration_time"), message.m_expirationTime))
{
LOG(LWARNING, ("Message", message.m_id, "has no expiration_time"));
return false;
}
message.m_startTime = OptionalTimeFromXml(node.attribute("start_time"));
message.m_endTime = OptionalTimeFromXml(node.attribute("end_time"));
message.m_cancellation = BoolFromXml(node.attribute("cancellation"), false);
message.m_forecast = BoolFromXml(node.attribute("forecast"), false);
// TODO urgency (not yet implemented in struct)
ReplacedMessageIdsFromXml(node.child("merge"), message.m_replaces);
if (!message.m_cancellation)
{
message.m_location.emplace();
if (!LocationFromXml(node.child("location"), message.m_location.value()))
{
message.m_location.reset();
LOG(LWARNING, ("Message", message.m_id, "has no location but is not a cancellation message"));
return false;
}
if (!EventsFromXml(node.child("events"), message.m_events))
{
LOG(LWARNING, ("Message", message.m_id, "has no events but is not a cancellation message"));
return false;
}
}
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;
// Select all messages elements that are direct children of the root.
auto const messages = document.document_element().select_nodes("./message");
if (messages.empty())
return true;
// TODO try block?
for (auto const & xpathNode : messages)
{
auto const messageNode = xpathNode.node();
TraffMessage message;
if (MessageFromXml(messageNode, message))
{
feed.push_back(message);
result = true;
}
else
LOG(LWARNING, ("Could not parse message, skipping"));
}
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::ostringstream os;
for (auto rect : bboxRects)
os << std::format("<filter bbox=\"{} {} {} {}\"/>\n",
mercator::YToLat(rect.minY()),
mercator::XToLon(rect.minX()),
mercator::YToLat(rect.maxY()),
mercator::XToLon(rect.maxX()));
return os.str();
}
} // namespace openlr