#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 #include namespace traffxml { namespace { void RemovePointFromPull(m2::PointD const & toBeRemoved, std::vector & pool) { pool.erase( remove_if(begin(pool), end(pool), [&toBeRemoved](m2::PointD const & p) { return p.EqualDxDy(toBeRemoved, 1e-6); }), end(pool)); } std::vector GetReachablePoints(m2::PointD const & srcPoint, std::vector 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::max(); /// This class denotes a "non-deterministic" feature point. /// I.e. it is a set of all pairs /// located at a specified coordinate. /// Only one point at a time is considered active. RoadPointCandidate::RoadPointCandidate(std::vector 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 drawerDelegate, std::unique_ptr 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(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(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(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(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 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(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 TrafficModel::GetGoldenPathPoints() const { std::vector 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