diff --git a/map/traffic_manager.cpp b/map/traffic_manager.cpp index ca85b7437..5b143933b 100644 --- a/map/traffic_manager.cpp +++ b/map/traffic_manager.cpp @@ -170,6 +170,11 @@ void TrafficManager::Clear() OnTrafficDataUpdate(); } +void TrafficManager::SetTrafficUpdateCallbackFn(TrafficUpdateCallbackFn && fn) +{ + m_trafficUpdateCallbackFn = std::move(fn); +} + void TrafficManager::SetDrapeEngine(ref_ptr engine) { m_drapeEngine.Set(engine); @@ -815,6 +820,9 @@ void TrafficManager::OnTrafficDataUpdate() m_lastStorageUpdate = steady_clock::now(); } + if (m_trafficUpdateCallbackFn) + m_trafficUpdateCallbackFn.value()(feedQueueEmpty); + if (!notifyDrape && !notifyObserver) return; diff --git a/map/traffic_manager.hpp b/map/traffic_manager.hpp index 6e14bb4c7..8c24ec2f2 100644 --- a/map/traffic_manager.hpp +++ b/map/traffic_manager.hpp @@ -42,6 +42,7 @@ class TrafficManager final : public traffxml::TraffSourceManager public: using CountryInfoGetterFn = std::function; using CountryParentNameGetterFn = std::function; + using TrafficUpdateCallbackFn = std::function; /** * @brief Global state of traffic information. @@ -303,6 +304,15 @@ public: */ void Clear(); + /** + * @brief Registers a callback function which gets called on traffic updates. + * + * Intended for testing. + * + * @param fn The callback function. + */ + void SetTrafficUpdateCallbackFn(TrafficUpdateCallbackFn && fn); + private: /** @@ -682,6 +692,13 @@ private: * thread). */ std::map m_allMwmColoring; + + /** + * @brief Callback function which gets called on traffic updates. + * + * Intended for testing. + */ + std::optional m_trafficUpdateCallbackFn; }; extern std::string DebugPrint(TrafficManager::TrafficState state); diff --git a/traffxml/traff_assessment_tool/mainwindow.cpp b/traffxml/traff_assessment_tool/mainwindow.cpp index 9741f021c..3bb37d106 100644 --- a/traffxml/traff_assessment_tool/mainwindow.cpp +++ b/traffxml/traff_assessment_tool/mainwindow.cpp @@ -311,35 +311,42 @@ MainWindow::MainWindow(Framework & framework) #endif } -void MainWindow::CreateTrafficPanel(std::string const & dataFilePath) +void MainWindow::CreateTrafficPanel() { - m_trafficModel = new TrafficModel(dataFilePath, - m_framework.GetDataSource(), - std::make_unique(m_framework), - std::make_unique(m_framework)); + if (!m_trafficModel) + { + // TODO simplify the call, everything depends on m_framework + m_trafficModel = new TrafficModel(m_framework, m_framework.GetDataSource(), + std::make_unique(m_framework), + std::make_unique(m_framework)); - connect(m_mapWidget, &MapWidget::TrafficMarkupClick, - m_trafficModel, &TrafficModel::OnClick); - connect(m_trafficModel, &TrafficModel::EditingStopped, - this, &MainWindow::OnPathEditingStop); - connect(m_trafficModel, &TrafficModel::SegmentSelected, - [](int segmentId) { QApplication::clipboard()->setText(QString::number(segmentId)); }); + connect(m_mapWidget, &MapWidget::TrafficMarkupClick, + m_trafficModel, &TrafficModel::OnClick); + connect(m_trafficModel, &TrafficModel::EditingStopped, + this, &MainWindow::OnPathEditingStop); + connect(m_trafficModel, &TrafficModel::SegmentSelected, + [](int segmentId) { QApplication::clipboard()->setText(QString::number(segmentId)); }); + } - m_docWidget = new QDockWidget(tr("Routes"), this); - addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_docWidget); + if (!m_dockWidget) + { + m_dockWidget = new QDockWidget(tr("Messages"), this); + addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_dockWidget); - m_docWidget->setWidget(new TrafficPanel(m_trafficModel, m_docWidget)); + m_dockWidget->setWidget(new TrafficPanel(m_trafficModel, m_dockWidget)); - m_docWidget->adjustSize(); - m_docWidget->setMinimumWidth(400); - m_docWidget->show(); + m_dockWidget->adjustSize(); + m_dockWidget->setMinimumWidth(400); + } + m_dockWidget->show(); } void MainWindow::DestroyTrafficPanel() { - removeDockWidget(m_docWidget); - delete m_docWidget; - m_docWidget = nullptr; + LOG(LINFO, ("enter")); + removeDockWidget(m_dockWidget); + delete m_dockWidget; + m_dockWidget = nullptr; delete m_trafficModel; m_trafficModel = nullptr; @@ -386,19 +393,16 @@ void MainWindow::OnOpenTrafficSample() return; } -// TODO create traffic panel with TraFF messages -#if 0 try { - CreateTrafficPanel(dlg.GetDataFilePath()); + CreateTrafficPanel(); } catch (TrafficModelError const & e) { - QMessageBox::critical(this, "Data loading error", QString("Can't load data file.")); + QMessageBox::critical(this, "Data loading error", QString("Can't create traffic panel.")); LOG(LERROR, (e.Msg())); return; } -#endif #ifdef openlr_obsolete m_goldifyMatchedPathAction->setEnabled(true /* enabled */); diff --git a/traffxml/traff_assessment_tool/mainwindow.hpp b/traffxml/traff_assessment_tool/mainwindow.hpp index e3c460df4..c6b964098 100644 --- a/traffxml/traff_assessment_tool/mainwindow.hpp +++ b/traffxml/traff_assessment_tool/mainwindow.hpp @@ -33,7 +33,7 @@ public: explicit MainWindow(Framework & framework); private: - void CreateTrafficPanel(std::string const & dataFilePath); + void CreateTrafficPanel(); void DestroyTrafficPanel(); /** @@ -57,7 +57,7 @@ private: Framework & m_framework; traffxml::TrafficModel * m_trafficModel = nullptr; - QDockWidget * m_docWidget = nullptr; + QDockWidget * m_dockWidget = nullptr; #ifdef openlr_obsolete QAction * m_goldifyMatchedPathAction = nullptr; diff --git a/traffxml/traff_assessment_tool/traffic_model.cpp b/traffxml/traff_assessment_tool/traffic_model.cpp index 12cb8c9d2..ec1493f8d 100644 --- a/traffxml/traff_assessment_tool/traffic_model.cpp +++ b/traffxml/traff_assessment_tool/traffic_model.cpp @@ -99,94 +99,223 @@ void RoadPointCandidate::SetActivePoint(FeatureID const & fid) } // namespace impl #endif +QVariant GetCountryAndRoadRef(TraffMessage const & message) +{ + std::string result = ""; + if (message.m_location) + { + if (message.m_location.value().m_country) + result += message.m_location.value().m_country.value(); + if (message.m_location.value().m_roadRef) + { + if (!result.empty()) + result += '\n'; + result += message.m_location.value().m_roadRef.value(); + } + } + return QString::fromStdString(result); +} + +/** + * @brief Returns a descriptive text for a point. + * + * The result is the junction name (if any), followed by the junction number or kilometric point (if any). + * + * @param point + * @return + */ +std::string GetPointDetail(Point const & point) +{ + std::string result = point.m_junctionName ? point.m_junctionName.value() : ""; + std::string junctionRefOrKmp = point.m_junctionRef ? point.m_junctionRef.value() : + point.m_distance ? std::format("km {0:.0f}", point.m_distance.value()) : ""; + if (!junctionRefOrKmp.empty()) + { + if (result.empty()) + result = junctionRefOrKmp; + else + result += " (" + junctionRefOrKmp + ")"; + } + return result; +} + +/** + * @brief Returns a descriptive text for the location on the road. + * + * The result is one or two junctions, with an arrow to indicate direction where applicable. + * + * @param location + * @return + */ +std::string GetLocationDetail(TraffLocation const & location) +{ + if (location.m_at) + return GetPointDetail(location.m_at.value()); + + std::string nameFrom = location.m_from ? GetPointDetail(location.m_from.value()) : ""; + std::string nameTo = location.m_from ? GetPointDetail(location.m_to.value()) : ""; + + if (nameFrom == nameTo) + return nameFrom; + else if (!nameFrom.empty()) + { + if (nameTo.empty()) + return nameFrom + ((location.m_directionality == Directionality::OneDirection) ? " →" : " ↔"); + else + return nameFrom + + ((location.m_directionality == Directionality::OneDirection) ? " → " : " ↔ ") + + nameTo; + } + else if (!nameTo.empty()) + return ((location.m_directionality == Directionality::OneDirection) ? "→ " : "↔ ") + nameTo; + else if (location.m_via) + return GetPointDetail(location.m_via.value()); + return ""; +} + +/** + * @brief Returns a description for the events in the message. + * + * @param message + * @return + */ +std::string GetEventText(TraffMessage const & message) +{ + std::string result = ""; + for (auto const & event : message.m_events) + { + if (!result.empty()) + result += ", "; + result += DebugPrint(event.m_type); + // TODO quantifiers (format "q = {}") + // TODO supplementary information (not in struct yet) + if (event.m_length) + result += std::format(" for {0:d} m", event.m_length.value()); + if (event.m_speed) + result += std::format(", speed {0:d} km/h", event.m_speed.value()); + } + return result; +} + +QVariant GetDescription(TraffMessage const & message) +{ + std::string result = ""; + if (message.m_cancellation) + { + result = "Cancellation"; + } + else + { + if (message.m_location) + { + std::string direction = ""; + if (message.m_location.value().m_directionality == Directionality::BothDirections) + direction = "both directions"; + else + { + // TODO determine bearing and convert it to a string (northbound, southeastbound etc.) + /* + std::optional loc1 = message.m_location.value().m_from; + std::optional loc2 = message.m_location.value().m_to; + if (loc2 && !loc1) + loc1 = message.m_location.value().m_at; + else if (loc1 && !loc2) + loc2 = message.m_location.value().m_at; + if (loc1 && loc2) + { + ms::LatLon c1 = loc1.value().m_coordinates; + ms::LatLon c2 = loc2.value().m_coordinates; + // TODO figure out bearing (as string) + } + */ + } + if (message.m_location.value().m_roadName) + { + // roadName, town, direction + if (message.m_location.value().m_town) + result += message.m_location.value().m_town.value() + ", "; + // TODO territory? + result += message.m_location.value().m_roadName.value(); + if (!direction.empty()) + result += ", " + direction; + } + else if (message.m_location.value().m_origin && message.m_location.value().m_destination) + // origin–destination with arrow + result += message.m_location.value().m_origin.value() + + ((message.m_location.value().m_directionality == Directionality::BothDirections) ? " ↔ " : " → ") + + message.m_location.value().m_destination.value(); + else if (!message.m_location.value().m_origin && !message.m_location.value().m_destination) + // direction, if available + result += direction; + else if (message.m_location.value().m_directionality == Directionality::OneDirection) + { + // unidirectional, one endpoint; replacce the other with direction + if (message.m_location.value().m_origin) + result += message.m_location.value().m_origin.value() + " → " + direction; + else + result += direction + " → " + message.m_location.value().m_destination.value(); + } + else + // direction, if available (no meaningful way to use origin or destination) + result += direction; + + auto locationDetail = GetLocationDetail(message.m_location.value()); + if (!locationDetail.empty()) + { + if (!result.empty()) + result += "\n"; + result += locationDetail; + } + } + auto eventText = GetEventText(message); + if (!eventText.empty()) + { + if (!result.empty()) + result += "\n"; + result += eventText; + } + // TODO start/end date + } + if (!result.empty()) + result += "\n"; + result += message.m_id.substr(0, message.m_id.find(':')); + result += "\t" + DebugPrint(message.m_updateTime); + // add an extra line to get bigger rows (default is too small) + result += "\n"; + return QString::fromStdString(result); +} + // TrafficModel ------------------------------------------------------------------------------------- -TrafficModel::TrafficModel(std::string const & dataFileName, DataSource const & dataSource, - std::unique_ptr drawerDelegate, - std::unique_ptr pointsDelegate, +TrafficModel::TrafficModel(Framework & framework, DataSource const & dataSource, + std::unique_ptr drawerDelegate, // TODO do we need that? + std::unique_ptr pointsDelegate, // TODO do we need that? QObject * parent) : QAbstractTableModel(parent) , m_dataSource(dataSource) , m_drawerDelegate(std::move(drawerDelegate)) , m_pointsDelegate(std::move(pointsDelegate)) { - // TODO(mgsergio): Collect stat how many segments of each kind were parsed. - pugi::xml_document doc; - if (!doc.load_file(dataFileName.data())) - MYTHROW(TrafficModelError, ("Can't load file:", strerror(errno))); - - // Save root node without children. - { - auto const root = doc.document_element(); - auto node = m_template.append_child(root.name()); - for (auto const & attr : root.attributes()) - node.append_copy(attr); - } - - // Select all Segment elements that are direct children of the root. - auto const segments = doc.document_element().select_nodes("./Segment"); - -#ifdef openlr_obsolete - try - { - for (auto const & xpathNode : segments) + framework.GetTrafficManager().SetTrafficUpdateCallbackFn([this, &framework](bool final) { + /* + * If final is true, this indicates the queue has been emptied and no further updates are + * imminent. Such updates should always be processed. If final is false, we can optimize by + * selectively skipping updates. + */ + GetPlatform().RunTask(Platform::Thread::Gui, [this, &framework]() { - auto const xmlSegment = xpathNode.node(); + beginResetModel(); + auto const messageCache = framework.GetTrafficManager().GetMessageCache(); + m_messages.clear(); + m_messages.reserve(messageCache.size()); - openlr::Path matchedPath; - openlr::Path fakePath; - openlr::Path goldenPath; + for (auto & entry : messageCache) + m_messages.push_back(std::move(entry.second)); - openlr::LinearSegment segment; + endResetModel(); - // TODO(mgsergio): Unify error handling interface of openlr_xml_mode and decoded_path parsers. - auto const partnerSegmentXML = xmlSegment.child("reportSegments"); - if (!openlr::SegmentFromXML(partnerSegmentXML, segment)) - MYTHROW(TrafficModelError, ("An error occurred while parsing: can't parse segment")); - - if (auto const route = xmlSegment.child("Route")) - openlr::PathFromXML(route, m_dataSource, matchedPath); - if (auto const route = xmlSegment.child("FakeRoute")) - openlr::PathFromXML(route, m_dataSource, fakePath); - if (auto const route = xmlSegment.child("GoldenRoute")) - openlr::PathFromXML(route, m_dataSource, goldenPath); - - uint32_t positiveOffsetM = 0; - uint32_t negativeOffsetM = 0; - if (auto const reportSegmentLRC = partnerSegmentXML.child("ReportSegmentLRC")) - { - if (auto const method = reportSegmentLRC.child("method")) - { - if (auto const locationReference = method.child("olr:locationReference")) - { - if (auto const optionLinearLocationReference = locationReference - .child("olr:optionLinearLocationReference")) - { - if (auto const positiveOffset = optionLinearLocationReference.child("olr:positiveOffset")) - positiveOffsetM = UintValueFromXML(positiveOffset); - - if (auto const negativeOffset = optionLinearLocationReference.child("olr:negativeOffset")) - negativeOffsetM = UintValueFromXML(negativeOffset); - } - } - } - } - - m_segments.emplace_back(segment, positiveOffsetM, negativeOffsetM, matchedPath, fakePath, - goldenPath, partnerSegmentXML); - if (auto const status = xmlSegment.child("Ignored")) - { - if (status.text().as_bool()) - m_segments.back().Ignore(); - } - } - } - catch (openlr::DecodedPathLoadError const & e) - { - MYTHROW(TrafficModelError, ("An exception occurred while parsing", dataFileName, e.Msg())); - } -#endif - - LOG(LINFO, (segments.size(), "segments are loaded.")); + LOG(LINFO, ("Messages:", m_messages.size())); + }); + }); } // TODO(mgsergio): Check if a path was committed, or commit it. @@ -232,15 +361,10 @@ bool TrafficModel::SaveSampleAs(std::string const & fileName) const int TrafficModel::rowCount(const QModelIndex & parent) const { -#ifdef openlr_obsolete - return static_cast(m_segments.size()); -#else - // TODO return a meaningful value - return 0; -#endif + return static_cast(m_messages.size()); } -int TrafficModel::columnCount(const QModelIndex & parent) const { return 4; } +int TrafficModel::columnCount(const QModelIndex & parent) const { return 2; } QVariant TrafficModel::data(const QModelIndex & index, int role) const { @@ -266,24 +390,39 @@ QVariant TrafficModel::data(const QModelIndex & index, int role) const if (index.column() == 3) return m_segments[index.row()].GetNegativeOffset(); #endif + switch (index.column()) + { + case 0: + return GetCountryAndRoadRef(m_messages[index.row()]); + case 1: + return GetDescription(m_messages[index.row()]); + default: + return QVariant(); + } - return QVariant(); + UNREACHABLE(); } QVariant TrafficModel::headerData(int section, Qt::Orientation orientation, int role /* = Qt::DisplayRole */) const { - if (orientation != Qt::Horizontal && role != Qt::DisplayRole) + /* + * Qt seems buggy here. Initially, we seem to get called with Qt::Vertical for a horizontal + * header, i.e. a row of column headers, and Qt::Horizontal for a vertical header (column of row + * headers). Using the intuitively correct value will result in incorrect behavior and a lot of + * head-scratching if you use just one type of header. + * However, this (presumed) bug does not seem to be consistent, as updates call us with + * Qt::Vertical and a row number (which can be beyond the number of columns). + */ + if (orientation == Qt::Horizontal && role != Qt::DisplayRole) return QVariant(); switch (section) { - case 0: return "Segment id"; break; - case 1: return "Status code"; break; - case 2: return "Positive offset (Meters)"; break; - case 3: return "Negative offset (Meters)"; break; + case 0: return "Road ref"; break; + case 1: return "Description"; break; } - UNREACHABLE(); + return QVariant(); } void TrafficModel::OnItemSelected(QItemSelection const & selected, QItemSelection const &) diff --git a/traffxml/traff_assessment_tool/traffic_model.hpp b/traffxml/traff_assessment_tool/traffic_model.hpp index eeccca3e7..7fa37ee75 100644 --- a/traffxml/traff_assessment_tool/traffic_model.hpp +++ b/traffxml/traff_assessment_tool/traffic_model.hpp @@ -9,6 +9,7 @@ #ifdef openlr_obsolete #include "openlr/decoded_path.hpp" #endif +#include "traffxml/traff_model.hpp" #include "indexer/data_source.hpp" @@ -68,7 +69,7 @@ class TrafficModel : public QAbstractTableModel public: // TODO(mgsergio): Check we are on the right mwm. I.e. right mwm version and everything. - TrafficModel(std::string const & dataFileName, DataSource const & dataSource, + TrafficModel(Framework & framework, DataSource const & dataSource, std::unique_ptr drawerDelegate, std::unique_ptr pointsDelegate, QObject * parent = Q_NULLPTR); @@ -126,6 +127,12 @@ private: // Non-owning pointer to an element of m_segments. SegmentCorrespondence * m_currentSegment = nullptr; #endif + std::vector m_messages; + + /** + * Non-owning pointer to an element of m_messages. + */ + TraffMessage * m_message = nullptr; std::unique_ptr m_drawerDelegate; std::unique_ptr m_pointsDelegate; diff --git a/traffxml/traff_assessment_tool/traffic_panel.cpp b/traffxml/traff_assessment_tool/traffic_panel.cpp index 89030d16b..caeb2670d 100644 --- a/traffxml/traff_assessment_tool/traffic_panel.cpp +++ b/traffxml/traff_assessment_tool/traffic_panel.cpp @@ -67,15 +67,23 @@ void TrafficPanel::CreateTable(QAbstractItemModel * trafficModel) m_table->setShowGrid(false); m_table->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows); m_table->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection); - m_table->verticalHeader()->setVisible(false); - m_table->horizontalHeader()->setVisible(true); - m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - m_table->setModel(trafficModel); m_table->setItemDelegate(new ComboBoxDelegate()); + // the model must be set before we can set dimensions and headers + m_table->verticalHeader()->setVisible(false); + m_table->horizontalHeader()->setVisible(true); + //m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_table->setColumnWidth(0, 80); + m_table->setColumnWidth(1, 300); + connect(m_table->selectionModel(), SIGNAL(selectionChanged(QItemSelection const &, QItemSelection const &)), trafficModel, SLOT(OnItemSelected(QItemSelection const &, QItemSelection const &))); + connect(trafficModel, &QAbstractItemModel::modelReset, + m_table, [this]() { + m_table->resizeRowsToContents(); + //m_table->resizeColumnsToContents(); + }); } } // namespace traffxml