diff --git a/CMakeLists.txt b/CMakeLists.txt index 40a056580..7eef8681c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -383,6 +383,7 @@ add_subdirectory(shaders) add_subdirectory(storage) add_subdirectory(tracking) add_subdirectory(traffic) +add_subdirectory(traffxml) add_subdirectory(transit) if (PLATFORM_DESKTOP) diff --git a/openlr/openlr_match_quality/openlr_assessment_tool/CMakeLists.txt b/openlr/openlr_match_quality/openlr_assessment_tool/CMakeLists.txt index 5aa14f8e9..23ad94182 100644 --- a/openlr/openlr_match_quality/openlr_assessment_tool/CMakeLists.txt +++ b/openlr/openlr_match_quality/openlr_assessment_tool/CMakeLists.txt @@ -28,4 +28,5 @@ target_link_libraries(${PROJECT_NAME} qt_common map gflags::gflags + traffxml ) diff --git a/qt/CMakeLists.txt b/qt/CMakeLists.txt index 0900e465d..e78813c1c 100644 --- a/qt/CMakeLists.txt +++ b/qt/CMakeLists.txt @@ -71,6 +71,7 @@ target_link_libraries(${PROJECT_NAME} map gflags::gflags openlr + traffxml ) if (BUILD_DESIGNER) diff --git a/traffxml/CMakeLists.txt b/traffxml/CMakeLists.txt new file mode 100644 index 000000000..418c7e15f --- /dev/null +++ b/traffxml/CMakeLists.txt @@ -0,0 +1,16 @@ +project(traffxml) + +set(SRC + traff_decoder.cpp + traff_decoder.hpp + traff_model.cpp + traff_model.hpp + traff_model_xml.cpp + traff_model_xml.hpp +) + +omim_add_library(${PROJECT_NAME} ${SRC}) + +target_link_libraries(${PROJECT_NAME} + pugixml +) diff --git a/traffxml/traff_decoder.cpp b/traffxml/traff_decoder.cpp new file mode 100644 index 000000000..0a30f26a5 --- /dev/null +++ b/traffxml/traff_decoder.cpp @@ -0,0 +1,7 @@ +#include "traffxml/traff_decoder.hpp" + +//#include "traffxml/traff_foo.hpp" + +namespace traffxml +{ +} // namespace traffxml diff --git a/traffxml/traff_decoder.hpp b/traffxml/traff_decoder.hpp new file mode 100644 index 000000000..075c20ab1 --- /dev/null +++ b/traffxml/traff_decoder.hpp @@ -0,0 +1,7 @@ +#pragma once + +//#include "traffxml/traff_foo.hpp" + +namespace traffxml +{ +} // namespace traffxml diff --git a/traffxml/traff_model.cpp b/traffxml/traff_model.cpp new file mode 100644 index 000000000..8bf9fff8f --- /dev/null +++ b/traffxml/traff_model.cpp @@ -0,0 +1,248 @@ +#include "traffxml/traff_model.hpp" + +using namespace std; + +namespace traffxml +{ +/* +string DebugPrint(LinearSegmentSource source) +{ + switch (source) + { + case LinearSegmentSource::NotValid: return "NotValid"; + case LinearSegmentSource::FromLocationReferenceTag: return "FromLocationReferenceTag"; + case LinearSegmentSource::FromCoordinatesTag: return "FromCoordinatesTag"; + } + UNREACHABLE(); +} + */ +std::string DebugPrint(IsoTime time) +{ + std::ostringstream os; + os << std::put_time(&time, "%Y-%m-%d %H:%M:%S %z"); + return os.str(); +} + +std::string DebugPrint(Directionality directionality) +{ + switch (directionality) + { + case Directionality::OneDirection: return "OneDirection"; + case Directionality::BothDirections: return "BothDirections"; + } + UNREACHABLE(); +} + +std::string DebugPrint(Ramps ramps) +{ + switch (ramps) + { + case Ramps::All: return "All"; + case Ramps::Entry: return "Entry"; + case Ramps::Exit: return "Exit"; + case Ramps::None: return "None"; + } + UNREACHABLE(); +} + +std::string DebugPrint(RoadClass roadClass) +{ + switch (roadClass) + { + case RoadClass::Motorway: return "Motorway"; + case RoadClass::Trunk: return "Trunk"; + case RoadClass::Primary: return "Primary"; + case RoadClass::Secondary: return "Secondary"; + case RoadClass::Tertiary: return "Tertiary"; + case RoadClass::Other: return "Other"; + } + UNREACHABLE(); +} + +std::string DebugPrint(EventClass eventClass) +{ + switch (eventClass) + { + case EventClass::Invalid: return "Invalid"; + case EventClass::Activity: return "Activity"; + case EventClass::Authority: return "Authority"; + case EventClass::Carpool: return "Carpool"; + case EventClass::Congestion: return "Congestion"; + case EventClass::Construction: return "Construction"; + case EventClass::Delay: return "Delay"; + case EventClass::Environment: return "Environment"; + case EventClass::EquipmentStatus: return "EquipmentStatus"; + case EventClass::Hazard: return "Hazard"; + case EventClass::Incident: return "Incident"; + case EventClass::Restriction: return "Restriction"; + case EventClass::Security: return "Security"; + case EventClass::Transport: return "Transport"; + case EventClass::Weather: return "Weather"; + } + UNREACHABLE(); +} + +std::string DebugPrint(EventType eventType) +{ + switch (eventType) + { + case EventType::Invalid: return "Invalid"; + // TODO Activity*, Authority*, Carpool* (not in enum yet) + case EventType::CongestionCleared: return "CongestionCleared"; + case EventType::CongestionForecastWithdrawn: return "CongestionForecastWithdrawn"; + case EventType::CongestionHeavyTraffic: return "CongestionHeavyTraffic"; + case EventType::CongestionLongQueue: return "CongestionLongQueue"; + case EventType::CongestionNone: return "CongestionNone"; + case EventType::CongestionNormalTraffic: return "CongestionNormalTraffic"; + case EventType::CongestionQueue: return "CongestionQueue"; + case EventType::CongestionQueueLikely: return "CongestionQueueLikely"; + case EventType::CongestionSlowTraffic: return "CongestionSlowTraffic"; + case EventType::CongestionStationaryTraffic: return "CongestionStationaryTraffic"; + case EventType::CongestionStationaryTrafficLikely: return "CongestionStationaryTrafficLikely"; + case EventType::CongestionTrafficBuildingUp: return "CongestionTrafficBuildingUp"; + case EventType::CongestionTrafficCongestion: return "CongestionTrafficCongestion"; + case EventType::CongestionTrafficEasing: return "CongestionTrafficEasing"; + case EventType::CongestionTrafficFlowingFreely: return "CongestionTrafficFlowingFreely"; + case EventType::CongestionTrafficHeavierThanNormal: return "CongestionTrafficHeavierThanNormal"; + case EventType::CongestionTrafficLighterThanNormal: return "CongestionTrafficLighterThanNormal"; + case EventType::CongestionTrafficMuchHeavierThanNormal: return "CongestionTrafficMuchHeavierThanNormal"; + case EventType::CongestionTrafficProblem: return "CongestionTrafficProblem"; + // TODO Construction* (not in enum yet) + case EventType::DelayClearance: return "DelayClearance"; + case EventType::DelayDelay: return "DelayDelay"; + case EventType::DelayDelayPossible: return "DelayDelayPossible"; + case EventType::DelayForecastWithdrawn: return "DelayForecastWithdrawn"; + case EventType::DelayLongDelay: return "DelayLongDelay"; + case EventType::DelaySeveralHours: return "DelaySeveralHours"; + case EventType::DelayUncertainDuration: return "DelayUncertainDuration"; + case EventType::DelayVeryLongDelay: return "DelayVeryLongDelay"; + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet) + // TODO complete Restriction* (not in enum yet) + case EventType::RestrictionBlocked: return "RestrictionBlocked"; + case EventType::RestrictionBlockedAhead: return "RestrictionBlockedAhead"; + case EventType::RestrictionCarriagewayBlocked: return "RestrictionCarriagewayBlocked"; + case EventType::RestrictionCarriagewayClosed: return "RestrictionCarriagewayClosed"; + case EventType::RestrictionClosed: return "RestrictionClosed"; + case EventType::RestrictionClosedAhead: return "RestrictionClosedAhead"; + case EventType::RestrictionEntryBlocked: return "RestrictionEntryBlocked"; + case EventType::RestrictionEntryReopened: return "RestrictionEntryReopened"; + case EventType::RestrictionExitBlocked: return "RestrictionExitBlocked"; + case EventType::RestrictionExitReopened: return "RestrictionExitReopened"; + case EventType::RestrictionOpen: return "RestrictionOpen"; + case EventType::RestrictionRampBlocked: return "RestrictionRampBlocked"; + case EventType::RestrictionRampClosed: return "RestrictionRampClosed"; + case EventType::RestrictionRampReopened: return "RestrictionRampReopened"; + case EventType::RestrictionReopened: return "RestrictionReopened"; + case EventType::RestrictionSpeedLimit: return "RestrictionSpeedLimit"; + case EventType::RestrictionSpeedLimitLifted: return "RestrictionSpeedLimitLifted"; + // TODO Security*, Transport*, Weather* (not in enum yet) + } + UNREACHABLE(); +} + +std::string DebugPrint(Point point) +{ + std::ostringstream os; + os << "Point { "; + os << "coordinates: " << DebugPrint(point.m_coordinates) << ", "; + // TODO optional float m_distance; (not in struct yet) + os << "junctionName: " << point.m_junctionName.value_or("nullopt") << ", "; + os << "junctionRef: " << point.m_junctionRef.value_or("nullopt"); + os << " }"; + return os.str(); +} + +std::string DebugPrint(TraffLocation location) +{ + std::ostringstream os; + os << "TraffLocation { "; + os << "from: " << (location.m_from ? DebugPrint(location.m_from.value()) : "nullopt") << ", "; + os << "at: " << (location.m_at ? DebugPrint(location.m_at.value()) : "nullopt") << ", "; + os << "via: " << (location.m_via ? DebugPrint(location.m_via.value()) : "nullopt") << ", "; + os << "to: " << (location.m_to ? DebugPrint(location.m_to.value()) : "nullopt") << ", "; + os << "notVia: " << (location.m_notVia ? DebugPrint(location.m_notVia.value()) : "nullopt") << ", "; + // TODO fuzziness (not yet implemented in struct) + os << "country: " << location.m_country.value_or("nullopt") << ", "; + os << "territory: " << location.m_territory.value_or("nullopt") << ", "; + os << "town: " << location.m_town.value_or("nullopt") << ", "; + os << "roadClass: " << (location.m_roadClass ? DebugPrint(location.m_roadClass.value()) : "nullopt") << ", "; + os << "roadRef: " << location.m_roadRef.value_or("nullopt") << ", "; + os << "roadName: " << location.m_roadName.value_or("nullopt") << ", "; + os << "origin: " << location.m_origin.value_or("nullopt") << ", "; + os << "destination: " << location.m_destination.value_or("nullopt") << ", "; + os << "direction: " << location.m_direction.value_or("nullopt") << ", "; + os << "directionality: " << DebugPrint(location.m_directionality) << ", "; + os << "ramps: " << (location.m_ramps ? DebugPrint(location.m_ramps.value()) : "nullopt"); + os << " }"; + return os.str(); +} + +std::string DebugPrint(TraffEvent event) +{ + std::ostringstream os; + os << "TraffEvent { "; + os << "class: " << DebugPrint(event.m_Class) << ", "; + os << "type: " << DebugPrint(event.m_Type) << ", "; + os << "length: " << (event.m_length ? std::to_string(event.m_length.value()) : "nullopt") << ", "; + os << "probability: " << (event.m_probability ? std::to_string(event.m_probability.value()) : "nullopt") << ", "; + // TODO optional quantifier + os << "speed: " << (event.m_speed ? std::to_string(event.m_speed.value()) : "nullopt"); + // TODO supplementary information + os << " }"; + return os.str(); +} + +std::string DebugPrint(TraffMessage message) +{ + std::string sep; + std::ostringstream os; + os << "TraffMessage { "; + os << "id: " << message.m_id << ", "; + + os << "replaces: ["; + sep = " "; + for (auto const & replacedId : message.m_replaces) + { + os << sep << replacedId; + sep = ", "; + } + os << " ], "; + + os << "receiveTime: " << DebugPrint(message.m_receiveTime) << ", "; + os << "updateTime: " << DebugPrint(message.m_updateTime) << ", "; + os << "expirationTime: " << DebugPrint(message.m_expirationTime) << ", "; + os << "startTime: " << (message.m_startTime ? DebugPrint(message.m_startTime.value()) : "nullopt") << ", "; + os << "endTime: " << (message.m_endTime ? DebugPrint(message.m_endTime.value()) : "nullopt") << ", "; + os << "cancellation: " << message.m_cancellation << ", "; + os << "forecast: " << message.m_forecast << ", "; + // TODO std::optional m_urgency; (not in struct yet) + os << "location: " << (message.m_location ? DebugPrint(message.m_location.value()) : "nullopt") << ", "; + + os << "events: ["; + sep = " "; + for (auto const & event : message.m_events) + { + os << sep << DebugPrint(event); + sep = ", "; + } + os << " ]"; + + os << " }"; + return os.str(); +} + +std::string DebugPrint(TraffFeed feed) +{ + std::string sep; + std::ostringstream os; + os << "[ "; + sep = ""; + for (auto const & message : feed) + { + os << sep << DebugPrint(message); + sep = ", "; + } + os << " ]"; + return os.str(); +} +} // namespace traffxml diff --git a/traffxml/traff_model.hpp b/traffxml/traff_model.hpp new file mode 100644 index 000000000..bc1f521c3 --- /dev/null +++ b/traffxml/traff_model.hpp @@ -0,0 +1,203 @@ +#pragma once + +//#include "traffxml/traff_foo.hpp" + +#include "geometry/latlon.hpp" +#include "geometry/point2d.hpp" + +#include +#include + +namespace traffxml +{ +/** + * @brief Date and time decoded from ISO 8601. + * + * `IsoTime` is an opaque type. It is only guaranteed to be capable of holding a timestamp + * converted from ISO 8601 which refers to the same UTC time as its ISO 8601 representation. + * Time zone information is not guaranteed to be preserved: `13:37+01:00` may be returned e.g. as + * `12:37Z` or `06:37-06:00`. + * + * Code using `IsoTime` must not rely on it being identical to any other type, as this is not + * guaranteed to be stable. + */ +/* + * Where no time zone is indicated, the timestamp shall always be interpreted as UTC. + * `IsoTime` currently maps to `std::tm`, but this is not guaranteed. + */ +using IsoTime = std::tm; + +// TODO enum urgency + +enum class Directionality +{ + OneDirection, + BothDirections +}; + +// TODO enum fuzziness + +enum class Ramps +{ + None, + All, + Entry, + Exit +}; + +enum class RoadClass +{ + Motorway, + Trunk, + Primary, + Secondary, + Tertiary, + Other +}; + +enum class EventClass +{ + Invalid, + Activity, + Authority, + Carpool, + Congestion, + Construction, + Delay, + Environment, + EquipmentStatus, + Hazard, + Incident, + Restriction, + Security, + Transport, + Weather +}; + +enum class EventType +{ + Invalid, + // TODO Activity*, Authority*, Carpool* + CongestionCleared, + CongestionForecastWithdrawn, + CongestionHeavyTraffic, + CongestionLongQueue, + CongestionNone, + CongestionNormalTraffic, + CongestionQueue, + CongestionQueueLikely, + CongestionSlowTraffic, + CongestionStationaryTraffic, + CongestionStationaryTrafficLikely, + CongestionTrafficBuildingUp, + CongestionTrafficCongestion, + CongestionTrafficEasing, + CongestionTrafficFlowingFreely, + CongestionTrafficHeavierThanNormal, + CongestionTrafficLighterThanNormal, + CongestionTrafficMuchHeavierThanNormal, + CongestionTrafficProblem, + // TODO Construction* + DelayClearance, + DelayDelay, + DelayDelayPossible, + DelayForecastWithdrawn, + DelayLongDelay, + DelaySeveralHours, + DelayUncertainDuration, + DelayVeryLongDelay, + // TODO Environment*, EquipmentStatus*, Hazard*, Incident* + // TODO complete Restriction* + RestrictionBlocked, + RestrictionBlockedAhead, + RestrictionCarriagewayBlocked, + RestrictionCarriagewayClosed, + RestrictionClosed, + RestrictionClosedAhead, + RestrictionEntryBlocked, + RestrictionEntryReopened, + RestrictionExitBlocked, + RestrictionExitReopened, + RestrictionOpen, + RestrictionRampBlocked, + RestrictionRampClosed, + RestrictionRampReopened, + RestrictionReopened, + RestrictionSpeedLimit, + RestrictionSpeedLimitLifted, + // TODO Security*, Transport*, Weather* +}; + +struct Point +{ + // TODO role? + ms::LatLon m_coordinates = ms::LatLon::Zero(); + // TODO optional float m_distance; + std::optional m_junctionName; + std::optional m_junctionRef; +}; + +struct TraffLocation +{ + std::optional m_country; + std::optional m_destination; + std::optional m_direction; + Directionality m_directionality = Directionality::BothDirections; + // TODO std::optional m_fuzziness; + std::optional m_origin; + std::optional m_ramps; + std::optional m_roadClass; + // disabled for now, optional behaves weird and we don't really need it + //std::optional m_roadIsUrban; + std::optional m_roadRef; + std::optional m_roadName; + std::optional m_territory; + std::optional m_town; + std::optional m_from; + std::optional m_to; + std::optional m_at; + std::optional m_via; + std::optional m_notVia; +}; + +struct TraffEvent +{ + EventClass m_Class = EventClass::Invalid; + EventType m_Type = EventType::Invalid; + std::optional m_length; + std::optional m_probability; + // TODO optional quantifier + std::optional m_speed; + // TODO supplementary information +}; + +struct TraffMessage +{ + std::string m_id; + IsoTime m_receiveTime = {}; + IsoTime m_updateTime = {}; + IsoTime m_expirationTime = {}; + std::optional m_startTime = {}; + std::optional m_endTime = {}; + bool m_cancellation = false; + bool m_forecast = false; + // TODO std::optional m_urgency; + std::optional m_location; + std::vector m_events; + std::vector m_replaces; +}; + +using TraffFeed = std::vector; + +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(Point point); +std::string DebugPrint(TraffLocation location); +std::string DebugPrint(TraffEvent event); +std::string DebugPrint(TraffMessage message); +std::string DebugPrint(TraffFeed feed); +} // namespace traffxml diff --git a/traffxml/traff_model_xml.cpp b/traffxml/traff_model_xml.cpp new file mode 100644 index 000000000..4ad50f25a --- /dev/null +++ b/traffxml/traff_model_xml.cpp @@ -0,0 +1,677 @@ +#include "traffxml/traff_model_xml.hpp" +#include "traffxml/traff_model.hpp" + +#include "base/logging.hpp" + +#include +#include +#include +#include +#include + +#include + +using namespace std; + +namespace traffxml +{ +const std::map kDirectionalityMap{ + {"ONE_DIRECTION", Directionality::OneDirection}, + {"BOTH_DIRECTIONS", Directionality::BothDirections} +}; + +const std::map kRampsMap{ + {"ALL_RAMPS", Ramps::All}, + {"ENTRY_RAMP", Ramps::Entry}, + {"EXIT_RAMP", Ramps::Exit}, + {"NONE", Ramps::None} +}; + +const std::map kRoadClassMap{ + {"MOTORWAY", RoadClass::Motorway}, + {"TRUNK", RoadClass::Trunk}, + {"PRIMARY", RoadClass::Primary}, + {"SECONDARY", RoadClass::Secondary}, + {"TERTIARY", RoadClass::Tertiary}, + {"OTHER", RoadClass::Other} +}; + +const std::map kEventClassMap{ + {"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 std::map kEventTypeMap{ + {"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 OptionalIntegerFromXml(pugi::xml_attribute attribute) +{ + if (attribute.empty()) + return std::nullopt; + try + { + uint8_t result = std::stoi(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 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 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 OptionalStringFromXml(pugi::xml_attribute 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 attribute, IsoTime & tm) +{ + std::string timeString; + if (!StringFromXml(attribute, timeString)) + return false; + /* + * Regex for ISO 8601 time, with some tolerance for time zone offset. If matched, the matcher + * will contain the following items: + * + * 0: 2019-11-01T11:55:42+01:00 (entire expression) + * 1: 2019 (year) + * 2: 11 (month) + * 3: 01 (day) + * 4: 11 (hour, local) + * 5: 55 (minute, local) + * 6: 42.445 (second, local, float) + * 7: .445 (fractional seconds) + * 8: +01:00 (complete UTC offset, or Z; blank if not specified) + * 9: +01:00 (complete UTC offset, blank for Z or of not specified) + * 10: +01 (UTC offset, hours with sign; blank for Z or if not specified) + * 11: :00 (UTC offset, minutes, prefixed with separator) + * 12: 00 (UTC offset, minutes, unsigned; blank for Z or if not specified) + */ + std::regex iso8601Regex("([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2}(.[0-9]*)?)(Z|(([+-][0-9]{2})(:?([0-9]{2}))?))?"); + + std::smatch iso8601Matcher; + if (std::regex_search(timeString, iso8601Matcher, iso8601Regex)) + { + int offset_h = iso8601Matcher[10].matched ? std::stoi(iso8601Matcher[10]) : 0; + int offset_m = iso8601Matcher[12].matched ? std::stoi(iso8601Matcher[12]) : 0; + if (offset_h < 0) + offset_m *= -1; + + tm.tm_year = std::stoi(iso8601Matcher[1]) - 1900; + tm.tm_mon = std::stoi(iso8601Matcher[2]) - 1; + tm.tm_mday = std::stoi(iso8601Matcher[3]); + tm.tm_hour = std::stoi(iso8601Matcher[4]) - offset_h; + tm.tm_min = std::stoi(iso8601Matcher[5]) - offset_m; + tm.tm_sec = std::stof(iso8601Matcher[6]) + 0.5f; + // Call timegm once to normalize tm; return value can be discarded + timegm(&tm); + return true; + } + else + { + LOG(LINFO, ("Not a valid ISO 8601 timestamp:", timeString)); + return false; + } +} + +/** + * @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 OptionalTimeFromXml(pugi::xml_attribute attribute) +{ + IsoTime result = {}; + 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 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 OptionalBoolFromXml(pugi::xml_attribute 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 +bool EnumFromXml(pugi::xml_attribute attribute, Value & value, std::map map) +{ + std::string string; + if (StringFromXml(attribute, string)) + { + auto it = map.find(string); + if (it != map.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 +std::optional OptionalEnumFromXml(pugi::xml_attribute attribute, std::map map) +{ + std::string string; + if (StringFromXml(attribute, string)) + { + auto it = map.find(string); + if (it != map.end()) + return it->second; + else + LOG(LWARNING, ("Unknown value for", attribute.name(), ":", string, "(ignoring)")); + } + return std::nullopt; +} + +/** + * @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 node, std::vector & 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 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 OptionalPointFromXml(pugi::xml_node 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; + } + + // TODO optional float m_distance (not yet implemented in struct) + + result.m_junctionName = OptionalStringFromXml(node.attribute("junction_name")); + result.m_junctionRef = OptionalStringFromXml(node.attribute("junction_ref")); + + return result; +} + +/** + * @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 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")); + + if (!location.m_from && !location.m_to && !location.m_at) + { + LOG(LWARNING, ("Neither from, to nor at point is 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")); + location.m_ramps = OptionalEnumFromXml(node.attribute("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; +} + +/** + * @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 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")); + + // TODO optional quantifier (not yet implemented in struct) + + event.m_speed = OptionalIntegerFromXml(node.attribute("speed")); + + // TODO supplementary information (not yet implemented in struct) + return true; +} + +/** + * @brief Retrieves the TraFF events associsted 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 node, std::vector & 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 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; +} + +/** + * @brief Retrieves a TraFF feed from an XML document. + * + * The document must conform loosely to the TraFF specification (currently version 0.8). + * + * 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. + * + * Messages, events, locations or points which lack mandatory information are skipped. + * + * If children are skipped but the parent remains valid, parsing it will report success. + * + * Parsing the feed will report failure if all its messages fail to parse, but not if it has no + * messages. + * + * @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) +{ + 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; +} +} // namespace openlr diff --git a/traffxml/traff_model_xml.hpp b/traffxml/traff_model_xml.hpp new file mode 100644 index 000000000..ed090f7ce --- /dev/null +++ b/traffxml/traff_model_xml.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "traffxml/traff_model.hpp" + +namespace pugi +{ +class xml_document; +class xml_node; +} // namespace pugi + +namespace traffxml +{ +bool ParseTraff(pugi::xml_document const & document, TraffFeed & feed); +} // namespace traffxml