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