Files
comaps/traffxml/traff_model.cpp
mvglasow 9fb08bdc56 [traffxml] Store message cache in file
So far only API and tests, and without decoded segments

Signed-off-by: mvglasow <michael -at- vonglasow.com>
2025-07-28 00:33:23 +03:00

581 lines
23 KiB
C++

#include "traffxml/traff_model.hpp"
#include "base/logging.hpp"
#include <regex>
using namespace std;
namespace traffxml
{
const std::map<EventType, traffic::SpeedGroup> kEventSpeedGroupMap{
// TODO Activity*, Authority*, Carpool* (not in enum yet)
{EventType::CongestionHeavyTraffic, traffic::SpeedGroup::G4},
{EventType::CongestionLongQueue, traffic::SpeedGroup::G0},
{EventType::CongestionNone, traffic::SpeedGroup::G5},
{EventType::CongestionNormalTraffic, traffic::SpeedGroup::G5},
{EventType::CongestionQueue, traffic::SpeedGroup::G2},
{EventType::CongestionQueueLikely, traffic::SpeedGroup::G3},
{EventType::CongestionSlowTraffic, traffic::SpeedGroup::G3},
{EventType::CongestionStationaryTraffic, traffic::SpeedGroup::G1},
{EventType::CongestionStationaryTrafficLikely, traffic::SpeedGroup::G2},
{EventType::CongestionTrafficBuildingUp, traffic::SpeedGroup::G4},
{EventType::CongestionTrafficCongestion, traffic::SpeedGroup::G3}, // TODO or G2? Unquantified, below normal
{EventType::CongestionTrafficFlowingFreely, traffic::SpeedGroup::G5},
{EventType::CongestionTrafficHeavierThanNormal, traffic::SpeedGroup::G4},
{EventType::CongestionTrafficLighterThanNormal, traffic::SpeedGroup::G5},
{EventType::CongestionTrafficMuchHeavierThanNormal, traffic::SpeedGroup::G3},
{EventType::CongestionTrafficProblem, traffic::SpeedGroup::G3}, // TODO or G2? Unquantified, below normal
// TODO Construction* (not in enum yet)
/*
* Some delay types have a duration which depends on the route. This is better expressed as a
* speed group, although the mapping may be somewhat arbitrary and may need to be corrected.
*/
{EventType::DelayDelay, traffic::SpeedGroup::G2},
{EventType::DelayDelayPossible, traffic::SpeedGroup::G3},
{EventType::DelayLongDelay, traffic::SpeedGroup::G1},
{EventType::DelayVeryLongDelay, traffic::SpeedGroup::G0},
// TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet)
// TODO complete Restriction* (not in enum yet)
{EventType::RestrictionBlocked, traffic::SpeedGroup::TempBlock},
{EventType::RestrictionBlockedAhead, traffic::SpeedGroup::TempBlock},
//{EventType::RestrictionCarriagewayBlocked, traffic::SpeedGroup::TempBlock}, // TODO FIXME other carriageways may still be open
//{EventType::RestrictionCarriagewayClosed, traffic::SpeedGroup::TempBlock}, // TODO FIXME other carriageways may still be open
{EventType::RestrictionClosed, traffic::SpeedGroup::TempBlock},
{EventType::RestrictionClosedAhead, traffic::SpeedGroup::TempBlock},
{EventType::RestrictionEntryBlocked, traffic::SpeedGroup::TempBlock},
{EventType::RestrictionExitBlocked, traffic::SpeedGroup::TempBlock},
{EventType::RestrictionRampBlocked, traffic::SpeedGroup::TempBlock},
{EventType::RestrictionRampClosed, traffic::SpeedGroup::TempBlock},
{EventType::RestrictionSpeedLimit, traffic::SpeedGroup::G4},
// TODO Security*, Transport*, Weather* (not in enum yet)
};
// none of the currently define events imply an explicit maxspeed
#if 0
const std::map<EventType, uint8_t> kEventMaxspeedMap{
// TODO Activity*, Authority*, Carpool* (not in enum yet)
// TODO Construction* (not in enum yet)
// TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet)
// TODO complete Restriction* (not in enum yet)
// TODO Security*, Transport*, Weather* (not in enum yet)
};
#endif
const std::map<EventType, uint16_t> kEventDelayMap{
// TODO Activity*, Authority*, Carpool* (not in enum yet)
// TODO Construction* (not in enum yet)
//{EventType::DelayDelay, }, // mapped to speed group
//{EventType::DelayDelayPossible, }, // mapped to speed group
//{EventType::DelayLongDelay, }, // mapped to speed group
{EventType::DelaySeveralHours, 150}, // assumption: 2.5 hours
{EventType::DelayUncertainDuration, 60}, // assumption: 1 hour
//{EventType::DelayVeryLongDelay, }, // mapped to speed group
// TODO Environment*, EquipmentStatus*, Hazard*, Incident* (not in enum yet)
// TODO complete Restriction* (not in enum yet)
// TODO Security*, Transport*, Weather* (not in enum yet)
};
std::optional<IsoTime> IsoTime::ParseIsoTime(std::string timeString)
{
/*
* We cannot use `std::chrono::from_stream` because it requires GCC 14+, and as of mid-2025, the
* supported development platform (Ubuntu 24.04) has GCC 13.2. Clang still does not support it.
*
* As a reasonably portable workaround, we first parse the time string into its constituent values
* using a regex, then build a `sys_seconds` instance from the values and use `clock_cast` to
* convert it to a `std::chrono::time_point<std::chrono::utc_clock>` instance, which is the data
* type we use internally.
*
* Once we have proper support for `std::chrono::from_stream` in all toolchains we support, this
* function can be rewritten accordingly. In GCC 14+, using `%FT%T%z` for the format string will
* work with all known UTC offset formats (+01, +0100 and +01:00), just like the regex does.
*/
/*
* 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;
const auto y = static_cast<std::chrono::year>(std::stoi(iso8601Matcher[1]));
const auto mo = static_cast<std::chrono::month>(std::stoi(iso8601Matcher[2]));
const auto d = static_cast<std::chrono::day>(std::stoi(iso8601Matcher[3]));
const std::chrono::hours h{std::stoi(iso8601Matcher[4]) - offset_h};
const std::chrono::minutes min{std::stoi(iso8601Matcher[5]) - offset_m};
const std::chrono::seconds s{static_cast<uint8_t>(std::stof(iso8601Matcher[6]) + 0.5f)};
std::chrono::sys_seconds sys_s = std::chrono::sys_days{y/mo/d};
sys_s = sys_s + h + min + s;
std::chrono::time_point<std::chrono::utc_clock> tp = std::chrono::clock_cast<std::chrono::utc_clock>(sys_s);
IsoTime result(tp);
return result;
}
else
{
LOG(LINFO, ("Not a valid ISO 8601 timestamp:", timeString));
return std::nullopt;
}
}
IsoTime IsoTime::Now()
{
return IsoTime(std::chrono::utc_clock::now());
}
IsoTime::IsoTime(std::chrono::time_point<std::chrono::utc_clock> tp)
: m_tp(tp)
{}
bool IsoTime::IsPast()
{
return m_tp < std::chrono::utc_clock::now();
}\
void IsoTime::Shift(IsoTime nowRef)
{
auto const offset = std::chrono::utc_clock::now() - nowRef.m_tp;
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)
{
return m_tp < rhs.m_tp;
}
bool IsoTime::operator> (IsoTime & rhs)
{
return m_tp > rhs.m_tp;
}
bool operator==(TrafficImpact const & lhs, TrafficImpact const & rhs)
{
if ((lhs.m_speedGroup == traffic::SpeedGroup::TempBlock)
&& (rhs.m_speedGroup == traffic::SpeedGroup::TempBlock))
return true;
return (lhs.m_speedGroup == rhs.m_speedGroup)
&& (lhs.m_maxspeed == rhs.m_maxspeed)
&& (lhs.m_delayMins == rhs.m_delayMins);
}
bool operator==(Point const & lhs, Point const & rhs)
{
return lhs.m_coordinates == rhs.m_coordinates;
}
bool operator==(TraffLocation const & lhs, TraffLocation const & rhs)
{
return (lhs.m_from == rhs.m_from)
&& (lhs.m_at == rhs.m_at)
&& (lhs.m_via == rhs.m_via)
&& (lhs.m_notVia == rhs.m_notVia)
&& (lhs.m_to == rhs.m_to);
}
IsoTime TraffMessage::GetEffectiveExpirationTime()
{
IsoTime result = m_expirationTime;
if (m_startTime && m_startTime.value() > result)
result = m_startTime.value();
if (m_endTime && m_endTime.value() > result)
result = m_endTime.value();
return result;
}
bool TraffMessage::IsExpired(IsoTime now)
{
return GetEffectiveExpirationTime() < now;
}
std::optional<TrafficImpact> TraffMessage::GetTrafficImpact()
{
// no events, no impact
if (m_events.empty())
return std::nullopt;
// examine events
std::vector<TrafficImpact> impacts;
for (auto event : m_events)
{
TrafficImpact impact;
if (auto it = kEventSpeedGroupMap.find(event.m_type); it != kEventSpeedGroupMap.end())
impact.m_speedGroup = it->second;
if (event.m_speed)
impact.m_maxspeed = event.m_speed.value();
// TODO if no explicit speed given, look up in kEventMaxspeedMap (once we have entries)
if (event.m_class == EventClass::Delay
&& event.m_type != EventType::DelayClearance
&& event.m_type != EventType::DelayForecastWithdrawn
&& event.m_type != EventType::DelaySeveralHours
&& event.m_type != EventType::DelayUncertainDuration
&& event.m_qDurationMins)
impact.m_delayMins = event.m_qDurationMins.value();
else if (auto it = kEventDelayMap.find(event.m_type); it != kEventDelayMap.end())
impact.m_delayMins = it->second;
// TempBlock overrules everything else, return immediately
if (impact.m_speedGroup == traffic::SpeedGroup::TempBlock)
return impact;
// if there is no actual impact, discard
if ((impact.m_maxspeed < kMaxspeedNone)
|| (impact.m_delayMins > 0)
|| (impact.m_speedGroup != traffic::SpeedGroup::Unknown))
impacts.push_back(impact);
}
if (impacts.empty())
return std::nullopt;
TrafficImpact result;
for (auto impact : impacts)
{
ASSERT(impact.m_speedGroup != traffic::SpeedGroup::TempBlock, ("Got SpeedGroup::TempBlock, which should not happen at this stage"));
if (result.m_speedGroup == traffic::SpeedGroup::Unknown)
result.m_speedGroup = impact.m_speedGroup;
// TempBlock cannot occur here, so we can do just a simple comparison
else if ((impact.m_speedGroup != traffic::SpeedGroup::Unknown) && (impact.m_speedGroup < result.m_speedGroup))
result.m_speedGroup = impact.m_speedGroup;
if (impact.m_maxspeed < result.m_maxspeed)
result.m_maxspeed = impact.m_maxspeed;
if (impact.m_delayMins > result.m_delayMins)
result.m_delayMins = impact.m_delayMins;
}
if ((result.m_maxspeed < kMaxspeedNone)
|| (result.m_delayMins > 0)
|| (result.m_speedGroup != traffic::SpeedGroup::Unknown))
return result;
else
// should never happen, unless we have a bug somewhere
return std::nullopt;
}
void TraffMessage::ShiftTimestamps()
{
IsoTime nowRef = m_updateTime;
m_receiveTime.Shift(nowRef);
m_updateTime.Shift(nowRef);
m_expirationTime.Shift(nowRef);
if (m_startTime)
m_startTime.value().Shift(nowRef);
if (m_endTime)
m_endTime.value().Shift(nowRef);
}
void MergeMultiMwmColoring(const MultiMwmColoring & delta, MultiMwmColoring & target)
{
// for each mwm in delta
for (auto [mwmId, coloring] : delta)
// if target contains mwm
if (auto target_it = target.find(mwmId); target_it != target.end())
// for each segment in delta[mwm] (coloring)
for (auto [rsid, sg] : coloring)
// if target[mwm] contains segment
if (auto c_it = target_it->second.find(rsid) ; c_it != target_it->second.end())
{
// if delta overrules target (target is Unknown, delta is TempBlock or delta is slower than target)
if ((sg == traffic::SpeedGroup::TempBlock)
|| (c_it->second == traffic::SpeedGroup::Unknown) || (sg < c_it->second))
target_it->second[rsid] = sg;
}
else
// if target[mwm] does not contain segment, add speed group
target_it->second[rsid] = sg;
else
// if target does not contain mwm, add coloring
target[mwmId] = coloring;
}
/*
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.m_tm, "%Y-%m-%d %H:%M:%S %z");
// %FT%T%z
os << std::format("{0:%F} {0:%T} {0:%z}", time.m_tp);
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(TrafficImpact impact)
{
std::ostringstream os;
os << "TrafficImpact { ";
os << "speedGroup: " << DebugPrint(impact.m_speedGroup) << ", ";
os << "maxspeed: " << (impact.m_maxspeed == kMaxspeedNone ? "none" : std::to_string(impact.m_maxspeed)) << ", ";
os << "delayMins: " << impact.m_delayMins;
os << " }";
return os.str();
}
std::string DebugPrint(Point point)
{
std::ostringstream os;
os << "Point { ";
os << "coordinates: " << DebugPrint(point.m_coordinates) << ", ";
os << "distance: " << (point.m_distance ? std::to_string(point.m_distance.value()) : "nullopt") << ", ";
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: " << DebugPrint(location.m_ramps);
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") << ", ";
os << "q_duration: "
<< (event.m_qDurationMins
? std::format("{:1d}:{:02d}", event.m_qDurationMins.value() / 60, event.m_qDurationMins.value() % 60)
: "nullopt")
<< ", ";
// TODO other quantifiers
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<Urgency> 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