1 /*
2     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "realtimeequipmentmodel.h"
8 
9 #include <KPublicTransport/Equipment>
10 #include <KPublicTransport/Location>
11 #include <KPublicTransport/LocationQueryModel>
12 
13 #include <QAbstractItemModel>
14 
15 using namespace KOSMIndoorMap;
16 
17 constexpr inline const float EquipmentMatchDistance = 2.0; // meters
18 
RealtimeEquipmentModel(QObject * parent)19 RealtimeEquipmentModel::RealtimeEquipmentModel(QObject *parent)
20     : EquipmentModel(parent)
21 {
22 }
23 
24 RealtimeEquipmentModel::~RealtimeEquipmentModel() = default;
25 
realtimeModel() const26 QObject* RealtimeEquipmentModel::realtimeModel() const
27 {
28     return m_realtimeModel;
29 }
30 
setRealtimeModel(QObject * model)31 void RealtimeEquipmentModel::setRealtimeModel(QObject *model)
32 {
33     if (m_realtimeModel == model) {
34         return;
35     }
36 
37     m_realtimeModel = qobject_cast<QAbstractItemModel*>(model);
38     Q_EMIT realtimeModelChanged();
39 
40     if (m_realtimeModel) {
41         connect(m_realtimeModel, &QAbstractItemModel::modelReset, this, &RealtimeEquipmentModel::updateRealtimeState);
42         connect(m_realtimeModel, &QAbstractItemModel::rowsInserted, this, [this](const auto &parent, auto first, auto last) {
43             if (parent.isValid() || m_pendingRealtimeUpdate) {
44                 return;
45             }
46             for (auto i = first; i <= last; ++i) {
47                 const auto idx = m_realtimeModel->index(i, 0);
48                 const auto loc = idx.data(KPublicTransport::LocationQueryModel::LocationRole).template value<KPublicTransport::Location>();
49                 if (loc.type() == KPublicTransport::Location::Equipment) {
50                     m_pendingRealtimeUpdate = true;
51                     QMetaObject::invokeMethod(this, &RealtimeEquipmentModel::updateRealtimeState, Qt::QueuedConnection);
52                     return;
53                 }
54             }
55         });
56         connect(m_realtimeModel, &QAbstractItemModel::rowsRemoved, this, &RealtimeEquipmentModel::updateRealtimeState);
57         connect(m_realtimeModel, &QAbstractItemModel::dataChanged, this, [this](const auto &fromIdx, const auto &toIdx) {
58             if (m_pendingRealtimeUpdate) {
59                 return;
60             }
61             for (auto i = fromIdx.row(); i <= toIdx.row(); ++i) {
62                 const auto idx = m_realtimeModel->index(i, 0);
63                 const auto loc = idx.data(KPublicTransport::LocationQueryModel::LocationRole).template value<KPublicTransport::Location>();
64                 if (loc.type() == KPublicTransport::Location::Equipment) {
65                     m_pendingRealtimeUpdate = true;
66                     QMetaObject::invokeMethod(this, &RealtimeEquipmentModel::updateRealtimeState, Qt::QueuedConnection);
67                     return;
68                 }
69             }
70         });
71 
72         if (m_realtimeModel->rowCount() > 0) {
73             updateRealtimeState();
74         }
75     }
76 }
77 
isSameEquipmentType(Equipment::Type lhs,KPublicTransport::Equipment::Type rhs)78 static bool isSameEquipmentType(Equipment::Type lhs, KPublicTransport::Equipment::Type rhs)
79 {
80     return (lhs == Equipment::Elevator && rhs == KPublicTransport::Equipment::Elevator)
81         || (lhs == Equipment::Escalator && rhs == KPublicTransport::Equipment::Escalator);
82 }
83 
updateEquipment(Equipment & eq,const KPublicTransport::Equipment & rtEq) const84 void RealtimeEquipmentModel::updateEquipment(Equipment &eq, const KPublicTransport::Equipment &rtEq) const
85 {
86     createSyntheticElement(eq);
87     eq.syntheticElement.setTagValue(m_tagKeys.realtimeStatus, rtEq.disruptionEffect() == KPublicTransport::Disruption::NoService ? "0" : "1");
88 }
89 
matchCount(const std::vector<std::vector<int>> & matches,int idx)90 static int matchCount(const std::vector<std::vector<int>> &matches, int idx)
91 {
92     return std::accumulate(matches.begin(), matches.end(), 0, [idx](int count, const auto &indexes) {
93         return count + std::count(indexes.begin(), indexes.end(), idx);
94     });
95 }
96 
findOtherMatch(const std::vector<std::vector<int>> & matches,int value,std::size_t current)97 static int findOtherMatch(const std::vector<std::vector<int>> &matches, int value, std::size_t current)
98 {
99     for (std::size_t i = 0; i < matches.size(); ++i) {
100         if (i == current) {
101             continue;
102         }
103         if (std::find(matches[i].begin(), matches[i].end(), value) != matches[i].end()) {
104             return i;
105         }
106     }
107     Q_UNREACHABLE();
108     return -1;
109 }
110 
updateRealtimeState()111 void RealtimeEquipmentModel::updateRealtimeState()
112 {
113     m_pendingRealtimeUpdate = false;
114     if (!m_realtimeModel) {
115         return;
116     }
117 
118     // clear previous data
119     for (auto &eq : m_equipment) {
120         if (!eq.syntheticElement) {
121             continue;
122         }
123         eq.syntheticElement.removeTag(m_tagKeys.realtimeStatus);
124     }
125 
126     // find candidates by distance
127     std::vector<std::vector<int>> matches;
128     matches.resize(m_equipment.size());
129     for (auto i = 0; i < m_realtimeModel->rowCount(); ++i) {
130         const auto idx = m_realtimeModel->index(i, 0);
131         const auto loc = idx.data(KPublicTransport::LocationQueryModel::LocationRole).value<KPublicTransport::Location>();
132         if (loc.type() != KPublicTransport::Location::Equipment) {
133             continue;
134         }
135 
136         const auto rtEq = loc.equipment();
137         for (std::size_t j = 0; j < m_equipment.size(); ++j) {
138             const auto &eq = m_equipment[j];
139             if (!isSameEquipmentType(eq.type, rtEq.type())) {
140                 continue;
141             }
142             if (eq.distanceTo(m_data.dataSet(), loc.latitude(), loc.longitude()) < EquipmentMatchDistance) {
143                 matches[j].push_back(i);
144             }
145         }
146     }
147 
148     // apply realtime status
149     // we accept 3 different cases:
150     // - a single 1:1 match
151     // - a 1/2 or a 2/2 match for horizontally adjacent elements if there is a distance difference
152     for (std::size_t i = 0; i < m_equipment.size(); ++i) {
153         if (matches[i].size() == 1) {
154             const auto mcount = matchCount(matches, matches[i][0]);
155             if (mcount == 1) {
156                 const auto idx =  m_realtimeModel->index(matches[i][0], 0);
157                 const auto rtEq = idx.data(KPublicTransport::LocationQueryModel::LocationRole).value<KPublicTransport::Location>().equipment();
158                 updateEquipment(m_equipment[i], rtEq);
159             }
160             else if (mcount == 2) {
161                 const auto other = findOtherMatch(matches, matches[i][0], i);
162                 if (matches[other].size() == 2) {
163                     const auto otherRow = matches[other][0] == matches[i][0] ? matches[other][1] : matches[other][0];
164                     if (matchCount(matches, otherRow) == 1) {
165                         resolveEquipmentPair(i, other, matches[other][0], matches[other][1]);
166                     }
167                 }
168             }
169         }
170 
171         if (matches[i].size() == 2) {
172             if (matchCount(matches, matches[i][0]) == 2 && matchCount(matches, matches[i][1]) == 2) {
173                 const auto it = std::find(std::next(matches.begin() + i), matches.end(), matches[i]);
174                 if (it != matches.end()) {
175                     resolveEquipmentPair(i, std::distance(matches.begin(), it), matches[i][0], matches[i][1]);
176                 }
177             }
178         }
179     }
180 
181     Q_EMIT update();
182 }
resolveEquipmentPair(int eqRow1,int eqRow2,int rtRow1,int rtRow2)183 void RealtimeEquipmentModel::resolveEquipmentPair(int eqRow1, int eqRow2, int rtRow1, int rtRow2)
184 {
185     // check if the equipment pair is horizontally adjacent
186     if (m_equipment[eqRow1].levels != m_equipment[eqRow2].levels) {
187         return;
188     }
189 
190     // match best combination
191     const auto rtIdx1 = m_realtimeModel->index(rtRow1, 0);
192     const auto rtIdx2 = m_realtimeModel->index(rtRow2, 0);
193     const auto rtEq1 = rtIdx1.data(KPublicTransport::LocationQueryModel::LocationRole).value<KPublicTransport::Location>();
194     const auto rtEq2 = rtIdx2.data(KPublicTransport::LocationQueryModel::LocationRole).value<KPublicTransport::Location>();
195 
196     const auto d11 = m_equipment[eqRow1].distanceTo(m_data.dataSet(), rtEq1.latitude(), rtEq1.longitude());
197     const auto d12 = m_equipment[eqRow1].distanceTo(m_data.dataSet(), rtEq2.latitude(), rtEq2.longitude());
198     const auto d21 = m_equipment[eqRow2].distanceTo(m_data.dataSet(), rtEq1.latitude(), rtEq1.longitude());
199     const auto d22 = m_equipment[eqRow2].distanceTo(m_data.dataSet(), rtEq2.latitude(), rtEq2.longitude());
200 
201     const auto swap1 = d11 >= d12;
202     const auto swap2 = d21 < d22;
203 
204     if (swap1 != swap2) {
205         return;
206     }
207 
208     if (swap1) {
209         if (d12 < EquipmentMatchDistance && d21 < EquipmentMatchDistance) {
210             updateEquipment(m_equipment[eqRow1], rtEq2.equipment());
211             updateEquipment(m_equipment[eqRow2], rtEq1.equipment());
212         }
213     } else {
214         if (d11 < EquipmentMatchDistance && d22 < EquipmentMatchDistance) {
215             updateEquipment(m_equipment[eqRow1], rtEq1.equipment());
216             updateEquipment(m_equipment[eqRow2], rtEq2.equipment());
217         }
218     }
219 }
220