mirror of
https://codeberg.org/comaps/comaps
synced 2025-12-19 13:03:36 +00:00
578 lines
17 KiB
C++
578 lines
17 KiB
C++
#include "traffic_model.hpp"
|
|
|
|
#ifdef openlr_obsolete
|
|
#include "openlr/openlr_model_xml.hpp"
|
|
#endif
|
|
|
|
#include "indexer/data_source.hpp"
|
|
|
|
#include "base/assert.hpp"
|
|
#include "base/scope_guard.hpp"
|
|
|
|
#include <QItemSelection>
|
|
#include <QMessageBox>
|
|
|
|
namespace traffxml
|
|
{
|
|
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
|
|
|
|
// TrafficModel -------------------------------------------------------------------------------------
|
|
TrafficModel::TrafficModel(std::string const & dataFileName, DataSource const & dataSource,
|
|
std::unique_ptr<TrafficDrawerDelegateBase> drawerDelegate,
|
|
std::unique_ptr<PointsControllerDelegateBase> pointsDelegate,
|
|
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)
|
|
{
|
|
auto const xmlSegment = xpathNode.node();
|
|
|
|
openlr::Path matchedPath;
|
|
openlr::Path fakePath;
|
|
openlr::Path goldenPath;
|
|
|
|
openlr::LinearSegment segment;
|
|
|
|
// 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."));
|
|
}
|
|
|
|
// 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
|
|
{
|
|
#ifdef openlr_obsolete
|
|
return static_cast<int>(m_segments.size());
|
|
#else
|
|
// TODO return a meaningful value
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
int TrafficModel::columnCount(const QModelIndex & parent) const { return 4; }
|
|
|
|
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
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
QVariant TrafficModel::headerData(int section, Qt::Orientation orientation,
|
|
int role /* = Qt::DisplayRole */) const
|
|
{
|
|
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;
|
|
}
|
|
UNREACHABLE();
|
|
}
|
|
|
|
void TrafficModel::OnItemSelected(QItemSelection const & selected, QItemSelection const &)
|
|
{
|
|
#ifdef openlr_obsolete
|
|
ASSERT(!selected.empty(), ());
|
|
ASSERT(!m_segments.empty(), ());
|
|
|
|
auto const row = selected.front().top();
|
|
|
|
CHECK_LESS(static_cast<size_t>(row), m_segments.size(), ());
|
|
m_currentSegment = &m_segments[row];
|
|
|
|
auto const & partnerSegment = m_currentSegment->GetPartnerSegment();
|
|
auto const & partnerSegmentPoints = partnerSegment.GetMercatorPoints();
|
|
auto const & viewportCenter = partnerSegmentPoints.front();
|
|
|
|
m_drawerDelegate->ClearAllPaths();
|
|
// TODO(mgsergio): Use a better way to set viewport and scale.
|
|
m_drawerDelegate->SetViewportCenter(viewportCenter);
|
|
m_drawerDelegate->DrawEncodedSegment(partnerSegmentPoints);
|
|
if (m_currentSegment->HasMatchedPath())
|
|
m_drawerDelegate->DrawDecodedSegments(GetPoints(m_currentSegment->GetMatchedPath()));
|
|
if (m_currentSegment->HasGoldenPath())
|
|
m_drawerDelegate->DrawGoldenPath(GetPoints(m_currentSegment->GetGoldenPath()));
|
|
|
|
emit SegmentSelected(static_cast<int>(partnerSegment.m_segmentId));
|
|
#endif
|
|
}
|
|
|
|
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
|