Files
comaps/tools/traff_assessment_tool/traffic_model.cpp
mvglasow 3c9eeb9a75 [traff_assessment_tool] Introduce status line for traffic panel
Signed-off-by: mvglasow <michael -at- vonglasow.com>
2025-10-21 21:13:26 +03:00

757 lines
23 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "traffic_model.hpp"
#ifdef openlr_obsolete
#include "openlr/openlr_model_xml.hpp"
#endif
#include "drape_frontend/drape_api.hpp"
#include "indexer/data_source.hpp"
#include "map/framework.hpp"
#include "base/assert.hpp"
#include "base/scope_guard.hpp"
#include <QItemSelection>
#include <QMessageBox>
namespace traffxml
{
constexpr static dp::Color kColorFrom(0x309302ff);
constexpr static dp::Color kColorAt(0x1a5ec1ff);
constexpr static dp::Color kColorVia(0xf19721ff);
constexpr static dp::Color kColorNotVia(0x8c5678ff);
constexpr static dp::Color kColorTo(0xe42300ff);
namespace
{
void RemovePointFromPull(m2::PointD const & toBeRemoved, std::vector<m2::PointD> & pool)
{
pool.erase(
remove_if(begin(pool), end(pool),
[&toBeRemoved](m2::PointD const & p) { return p.EqualDxDy(toBeRemoved, 1e-6); }),
end(pool));
}
std::vector<m2::PointD> GetReachablePoints(m2::PointD const & srcPoint,
std::vector<m2::PointD> const path,
PointsControllerDelegateBase const & pointsDelegate,
size_t const lookbackIndex)
{
auto reachablePoints = pointsDelegate.GetReachablePoints(srcPoint);
if (lookbackIndex < path.size())
{
auto const & toBeRemoved = path[path.size() - lookbackIndex - 1];
RemovePointFromPull(toBeRemoved, reachablePoints);
}
return reachablePoints;
}
} // namespace
#ifdef openlr_obsolete
namespace impl
{
// static
size_t const RoadPointCandidate::kInvalidId = std::numeric_limits<size_t>::max();
/// This class denotes a "non-deterministic" feature point.
/// I.e. it is a set of all pairs <FeatureID, point index>
/// located at a specified coordinate.
/// Only one point at a time is considered active.
RoadPointCandidate::RoadPointCandidate(std::vector<FeaturePoint> const & points,
m2::PointD const & coord)
: m_coord(coord)
, m_points(points)
{
LOG(LDEBUG, ("Candidate points:", points));
}
void RoadPointCandidate::ActivateCommonPoint(RoadPointCandidate const & rpc)
{
for (auto const & fp1 : m_points)
{
for (auto const & fp2 : rpc.m_points)
{
if (fp1.first == fp2.first)
{
SetActivePoint(fp1.first);
return;
}
}
}
CHECK(false, ("One common feature id should exist."));
}
FeaturePoint const & RoadPointCandidate::GetPoint() const
{
CHECK_NOT_EQUAL(m_activePointIndex, kInvalidId, ("No point is active."));
return m_points[m_activePointIndex];
}
m2::PointD const & RoadPointCandidate::GetCoordinate() const
{
return m_coord;
}
void RoadPointCandidate::SetActivePoint(FeatureID const & fid)
{
for (size_t i = 0; i < m_points.size(); ++i)
{
if (m_points[i].first == fid)
{
m_activePointIndex = i;
return;
}
}
CHECK(false, ("One point should match."));
}
} // 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<Point> loc1 = message.m_location.value().m_from;
std::optional<Point> 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)
// origindestination 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(Framework & framework,
QObject * parent)
: QAbstractTableModel(parent)
, m_framework(framework)
, m_dataSource(framework.GetDataSource())
, m_drawerDelegate(std::make_unique<TrafficDrawerDelegate>(framework))
, m_pointsDelegate(std::make_unique<PointsControllerDelegate>(framework))
{
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, final]()
{
beginResetModel();
auto const messageCache = framework.GetTrafficManager().GetMessageCache();
m_messages.clear();
m_messages.reserve(messageCache.size());
for (auto & entry : messageCache)
m_messages.push_back(std::move(entry.second));
endResetModel();
// clear markers
auto editSession = m_framework.GetBookmarkManager().GetEditSession();
editSession.ClearGroup(UserMark::Type::COLORED);
editSession.SetIsVisible(UserMark::Type::COLORED, false);
// restore status bar
if (final)
m_trafficPanel->SetStatus(false, m_messages.size());
LOG(LINFO, ("Messages:", m_messages.size()));
});
});
}
// TODO(mgsergio): Check if a path was committed, or commit it.
bool TrafficModel::SaveSampleAs(std::string const & fileName) const
{
CHECK(!fileName.empty(), ("Can't save to an empty file."));
pugi::xml_document result;
result.reset(m_template);
auto root = result.document_element();
#ifdef openlr_obsolete
for (auto const & sc : m_segments)
{
auto segment = root.append_child("Segment");
segment.append_copy(sc.GetPartnerXMLSegment());
if (sc.GetStatus() == SegmentCorrespondence::Status::Ignored)
{
segment.append_child("Ignored").text() = true;
}
if (sc.HasMatchedPath())
{
auto node = segment.append_child("Route");
openlr::PathToXML(sc.GetMatchedPath(), node);
}
if (sc.HasFakePath())
{
auto node = segment.append_child("FakeRoute");
openlr::PathToXML(sc.GetFakePath(), node);
}
if (sc.HasGoldenPath())
{
auto node = segment.append_child("GoldenRoute");
openlr::PathToXML(sc.GetGoldenPath(), node);
}
}
#endif
result.save_file(fileName.data(), " " /* indent */);
return true;
}
int TrafficModel::rowCount(const QModelIndex & parent) const
{
return static_cast<int>(m_messages.size());
}
int TrafficModel::columnCount(const QModelIndex & parent) const { return 2; }
QVariant TrafficModel::data(const QModelIndex & index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() >= rowCount())
return QVariant();
if (role != Qt::DisplayRole && role != Qt::EditRole)
return QVariant();
#ifdef openlr_obsolete
if (index.column() == 0)
return m_segments[index.row()].GetPartnerSegmentId();
if (index.column() == 1)
return static_cast<int>(m_segments[index.row()].GetStatus());
if (index.column() == 2)
return m_segments[index.row()].GetPositiveOffset();
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();
}
UNREACHABLE();
}
QVariant TrafficModel::headerData(int section, Qt::Orientation orientation,
int role /* = Qt::DisplayRole */) const
{
/*
* 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 "Road ref"; break;
case 1: return "Description"; break;
}
return QVariant();
}
void TrafficModel::OnItemSelected(QItemSelection const & selected, QItemSelection const &)
{
ASSERT(!selected.empty(), ());
ASSERT(!m_messages.empty(), ());
auto const row = selected.front().top();
auto editSession = m_framework.GetBookmarkManager().GetEditSession();
editSession.ClearGroup(UserMark::Type::COLORED);
if (static_cast<size_t>(row) >= m_messages.size())
{
editSession.SetIsVisible(UserMark::Type::COLORED, false);
return;
}
auto message = &m_messages[row];
if (!message->m_location)
{
editSession.SetIsVisible(UserMark::Type::COLORED, false);
return;
}
m2::RectD rect;
editSession.SetIsVisible(UserMark::Type::COLORED, true);
for (auto & [coords, color] : {
std::pair{message->m_location.value().m_from, kColorFrom},
std::pair{message->m_location.value().m_at, kColorAt},
std::pair{message->m_location.value().m_via, kColorVia},
std::pair{message->m_location.value().m_notVia, kColorNotVia},
std::pair{message->m_location.value().m_to, kColorTo}
})
if (coords)
{
auto point = mercator::FromLatLon(coords.value().m_coordinates);
rect.Add(point);
auto mark = editSession.CreateUserMark<ColoredMarkPoint>(point);
mark->SetColor(color);
}
if (rect.IsValid())
{
rect.Scale(1.5);
m_framework.ShowRect(rect, 15 /* maxScale */, true /* animation */, true /* useVisibleViewport */);
}
}
Qt::ItemFlags TrafficModel::flags(QModelIndex const & index) const
{
if (!index.isValid())
return Qt::ItemIsEnabled;
return QAbstractItemModel::flags(index);
}
#ifdef openlr_obsolete
void TrafficModel::GoldifyMatchedPath()
{
if (!m_currentSegment->HasMatchedPath())
{
QMessageBox::information(nullptr /* parent */, "Error",
"The selected segment does not have a matched path");
return;
}
if (!StartBuildingPathChecks())
return;
m_currentSegment->SetGoldenPath(m_currentSegment->GetMatchedPath());
m_goldenPath.clear();
m_drawerDelegate->DrawGoldenPath(GetPoints(m_currentSegment->GetGoldenPath()));
}
void TrafficModel::StartBuildingPath()
{
if (!StartBuildingPathChecks())
return;
m_currentSegment->SetGoldenPath({});
m_buildingPath = true;
m_drawerDelegate->ClearGoldenPath();
m_drawerDelegate->VisualizePoints(m_pointsDelegate->GetAllJunctionPointsInViewport());
}
void TrafficModel::PushPoint(m2::PointD const & coord, std::vector<FeaturePoint> const & points)
{
impl::RoadPointCandidate point(points, coord);
if (!m_goldenPath.empty())
m_goldenPath.back().ActivateCommonPoint(point);
m_goldenPath.push_back(point);
}
void TrafficModel::PopPoint()
{
CHECK(!m_goldenPath.empty(), ("Attempt to pop point from an empty path."));
m_goldenPath.pop_back();
}
void TrafficModel::CommitPath()
{
CHECK(m_currentSegment, ("No segments selected"));
if (!m_buildingPath)
MYTHROW(TrafficModelError, ("Path building is not started"));
SCOPE_GUARD(guard, [this] { emit EditingStopped(); });
m_buildingPath = false;
m_drawerDelegate->ClearAllVisualizedPoints();
if (m_goldenPath.size() == 1)
{
LOG(LDEBUG, ("Golden path is empty"));
return;
}
CHECK_GREATER(m_goldenPath.size(), 1, ("Path cannot consist of only one point"));
// Activate last point. Since no more points will be availabe we link it to the same
// feature as the previous one was linked to.
m_goldenPath.back().ActivateCommonPoint(m_goldenPath[GetPointsCount() - 2]);
openlr::Path path;
for (size_t i = 1; i < GetPointsCount(); ++i)
{
auto const prevPoint = m_goldenPath[i - 1];
auto point = m_goldenPath[i];
// The start and the end of the edge should lie on the same feature.
point.ActivateCommonPoint(prevPoint);
auto const & prevFt = prevPoint.GetPoint();
auto const & ft = point.GetPoint();
path.push_back(Edge::MakeReal(
ft.first, prevFt.second < ft.second /* forward */, base::checked_cast<uint32_t>(prevFt.second),
geometry::PointWithAltitude(prevPoint.GetCoordinate(), 0 /* altitude */),
geometry::PointWithAltitude(point.GetCoordinate(), 0 /* altitude */)));
}
m_currentSegment->SetGoldenPath(path);
m_goldenPath.clear();
}
void TrafficModel::RollBackPath()
{
CHECK(m_currentSegment, ("No segments selected"));
CHECK(m_buildingPath, ("No path building is in progress."));
m_buildingPath = false;
// TODO(mgsergio): Add a method for common visual manipulations.
m_drawerDelegate->ClearAllVisualizedPoints();
m_drawerDelegate->ClearGoldenPath();
if (m_currentSegment->HasGoldenPath())
m_drawerDelegate->DrawGoldenPath(GetPoints(m_currentSegment->GetGoldenPath()));
m_goldenPath.clear();
emit EditingStopped();
}
void TrafficModel::IgnorePath()
{
CHECK(m_currentSegment, ("No segments selected"));
if (m_currentSegment->HasGoldenPath())
{
auto const btn =
QMessageBox::question(nullptr /* parent */, "Override warning",
"The selected segment has a golden path. Do you want to discard it?");
if (btn == QMessageBox::No)
return;
}
m_buildingPath = false;
// TODO(mgsergio): Add a method for common visual manipulations.
m_drawerDelegate->ClearAllVisualizedPoints();
m_drawerDelegate->ClearGoldenPath();
m_currentSegment->Ignore();
m_goldenPath.clear();
emit EditingStopped();
}
size_t TrafficModel::GetPointsCount() const
{
return m_goldenPath.size();
}
m2::PointD const & TrafficModel::GetPoint(size_t const index) const
{
return m_goldenPath[index].GetCoordinate();
}
m2::PointD const & TrafficModel::GetLastPoint() const
{
CHECK(!m_goldenPath.empty(), ("Attempt to get point from an empty path."));
return m_goldenPath.back().GetCoordinate();
}
std::vector<m2::PointD> TrafficModel::GetGoldenPathPoints() const
{
std::vector<m2::PointD> coordinates;
for (auto const & roadPoint : m_goldenPath)
coordinates.push_back(roadPoint.GetCoordinate());
return coordinates;
}
// TODO(mgsergio): Draw the first point when the path size is 1.
void TrafficModel::HandlePoint(m2::PointD clickPoint, Qt::MouseButton const button)
{
if (!m_buildingPath)
return;
auto const currentPathLength = GetPointsCount();
auto const lastClickedPoint = currentPathLength != 0
? GetLastPoint()
: m2::PointD::Zero();
auto const & p = m_pointsDelegate->GetCandidatePoints(clickPoint);
auto const & candidatePoints = p.first;
clickPoint = p.second;
if (candidatePoints.empty())
return;
auto reachablePoints = GetReachablePoints(clickPoint, GetGoldenPathPoints(), *m_pointsDelegate,
0 /* lookBackIndex */);
auto const & clickablePoints = currentPathLength != 0
? GetReachablePoints(lastClickedPoint, GetGoldenPathPoints(), *m_pointsDelegate,
1 /* lookbackIndex */)
// TODO(mgsergio): This is not quite correct since view port can change
// since first call to visualize points. But it's ok in general.
: m_pointsDelegate->GetAllJunctionPointsInViewport();
using ClickType = PointsControllerDelegateBase::ClickType;
switch (m_pointsDelegate->CheckClick(clickPoint, lastClickedPoint, clickablePoints))
{
case ClickType::Add:
// TODO(mgsergio): Think of refactoring this with if (accumulator.empty)
// instead of pushing point first ad then removing last selection.
PushPoint(clickPoint, candidatePoints);
if (currentPathLength > 0)
{
// TODO(mgsergio): Should I remove lastClickedPoint from clickablePoints
// as well?
RemovePointFromPull(lastClickedPoint, reachablePoints);
m_drawerDelegate->DrawGoldenPath(GetGoldenPathPoints());
}
m_drawerDelegate->ClearAllVisualizedPoints();
m_drawerDelegate->VisualizePoints(reachablePoints);
m_drawerDelegate->VisualizePoints({clickPoint});
break;
case ClickType::Remove: // TODO(mgsergio): Rename this case.
if (button == Qt::MouseButton::LeftButton) // RemovePoint
{
m_drawerDelegate->ClearAllVisualizedPoints();
m_drawerDelegate->ClearGoldenPath();
PopPoint();
if (m_goldenPath.empty())
{
m_drawerDelegate->VisualizePoints(m_pointsDelegate->GetAllJunctionPointsInViewport());
}
else
{
m_drawerDelegate->VisualizePoints(GetReachablePoints(
GetLastPoint(), GetGoldenPathPoints(), *m_pointsDelegate, 1 /* lookBackIndex */));
}
if (GetPointsCount() > 1)
m_drawerDelegate->DrawGoldenPath(GetGoldenPathPoints());
}
else if (button == Qt::MouseButton::RightButton)
{
CommitPath();
}
break;
case ClickType::Miss:
// TODO(mgsergio): This situation should be handled by checking candidatePoitns.empty() above.
// Not shure though if all cases are handled by that check.
return;
}
}
bool TrafficModel::StartBuildingPathChecks() const
{
CHECK(m_currentSegment, ("A segment should be selected before path building is started."));
if (m_buildingPath)
MYTHROW(TrafficModelError, ("Path building already in progress."));
if (m_currentSegment->HasGoldenPath())
{
auto const btn = QMessageBox::question(
nullptr /* parent */, "Override warning",
"The selected segment already has a golden path. Do you want to override?");
if (btn == QMessageBox::No)
return false;
}
return true;
}
#endif
} // namespace traffxml