KPublicTransport

backendmodel.cpp
1/*
2 SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "backendmodel.h"
8
9#include <KPublicTransport/Backend>
10#include <KPublicTransport/Manager>
11
12#include <QDebug>
13
14using namespace KPublicTransport;
15
16namespace KPublicTransport {
17struct BackendInfo
18{
19 Backend backend;
20 QString country;
21 bool isNationWide;
22 CoverageArea::Type coverageType;
23};
24
25class BackendModelPrivate
26{
27public:
28 void repopulateModel(BackendModel *q);
29 void repopulateFlat();
30 void repopulateGrouped();
31 void sortModel();
32
33 Manager *mgr = nullptr;
34 std::vector<BackendInfo> rows;
36};
37}
38
39void BackendModelPrivate::repopulateModel(BackendModel *q)
40{
41 if (!mgr) {
42 return;
43 }
44
45 q->beginResetModel();
46 rows.clear();
47 switch (mode) {
49 repopulateFlat();
50 break;
52 repopulateGrouped();
53 break;
54 }
55
56 sortModel();
57 q->endResetModel();
58}
59
60void BackendModelPrivate::repopulateFlat()
61{
62 rows.reserve(mgr->backends().size());
63 for (const auto &b : mgr->backends()) {
64 if (b.identifier().size() > 3 && b.identifier().at(2) == QLatin1Char('_')) {
65 rows.push_back({ b, b.identifier().left(2).toUpper(), true, CoverageArea::Any });
66 }
67 }
68}
69
70void BackendModelPrivate::repopulateGrouped()
71{
72 for (const auto &b : mgr->backends()) {
73 for (const auto type : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
74 const auto c = b.coverageArea(type);
75 if (c.isEmpty()) {
76 continue;
77 }
78
79 for (const auto &region: c.regions()) {
80 const auto country = region.left(2);
81 if (!rows.empty() && rows.back().backend.identifier() == b.identifier() && rows.back().country == country) {
82 continue; // deduplicate backends covering multiple regions in the same country
83 }
84 rows.push_back({ b, country, region.size() == 2, type });
85 }
86 }
87 }
88}
89
90void BackendModelPrivate::sortModel()
91{
92 // group by country
93 const auto orderByCountry = [](const auto &lhs, const auto &rhs) {
94 return lhs.country < rhs.country;
95 };
96 std::sort(rows.begin(), rows.end(), orderByCountry);
97
98 // process each country individually
99 for (auto next = rows.begin(); next != rows.end();) {
100 const auto [begin, end] = std::equal_range(next, rows.end(), *next, orderByCountry);
101 next = end;
102
103 // find best coverage quality for nation-wide services
104 CoverageArea::Type bestCoverageType = CoverageArea::Any;
105 for (auto it = begin; it != end; ++it) {
106 if ((*it).isNationWide) {
107 bestCoverageType = std::min(bestCoverageType, (*it).coverageType);
108 }
109 }
110
111 // sort within one country: nation wide with best quality first, then regional, then nation-wide with subpar coverage
112 std::sort(begin, end, [bestCoverageType](const auto &lhs, const auto &rhs) {
113 if (lhs.isNationWide && rhs.isNationWide) {
114 if ((lhs.coverageType > bestCoverageType && rhs.coverageType > bestCoverageType)
115 || (lhs.coverageType <= bestCoverageType && rhs.coverageType <= bestCoverageType)) {
116 return lhs.backend.name() < rhs.backend.name();
117 }
118 return lhs.coverageType < rhs.coverageType;
119 }
120 if (lhs.isNationWide && !rhs.isNationWide) {
121 return lhs.coverageType <= bestCoverageType;
122 }
123 if (!lhs.isNationWide && rhs.isNationWide) {
124 return rhs.coverageType > bestCoverageType;
125 }
126 assert(!lhs.isNationWide && !rhs.isNationWide);
127 return lhs.backend.name() < rhs.backend.name();
128 });
129
130 // drop entries for nationwide providers that are only the second best coverage quality for a country
131 // and that have better coverage elsewhere
132 // since removing entries here immediately would mess with the active iterators, we just clear the country
133 // and do the actual deletion below
134 if (mode == BackendModel::GroupByCountry) {
135 for (auto it = begin; it != end; ++it) {
136 if (!(*it).isNationWide || (*it).coverageType <= bestCoverageType) {
137 continue;
138 }
139
140 for (auto type : { CoverageArea::Realtime, CoverageArea::Regular }) {
141 if (type >= (*it).coverageType) {
142 break;
143 }
144 if (!(*it).backend.coverageArea(type).isEmpty()) {
145 (*it).country.clear();
146 break;
147 }
148 }
149 }
150 }
151 }
152
153 // clean up entries marked for deletion above
154 rows.erase(std::remove_if(rows.begin(), rows.end(), [](const auto &r) { return r.country.isEmpty(); }), rows.end());
155}
156
157
158BackendModel::BackendModel(QObject *parent)
159 : QAbstractListModel(parent)
160 , d(new BackendModelPrivate)
161{
162}
163
164BackendModel::~BackendModel() = default;
165
167{
168 return d->mgr;
169}
170
171void BackendModel::setManager(Manager *mgr)
172{
173 if (d->mgr == mgr) {
174 return;
175 }
176 if (d->mgr) {
177 disconnect(d->mgr, nullptr, this, nullptr);
178 }
179
180 d->mgr = mgr;
181 connect(mgr, &Manager::configurationChanged, this, [this]() {
182 Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
183 });
184 connect(mgr, &Manager::backendsChanged, this, [this]() {
185 d->repopulateModel(this);
186 });
187 d->repopulateModel(this);
188 Q_EMIT managerChanged();
189}
190
192{
193 return d->mode;
194}
195
196void BackendModel::setMode(BackendModel::Mode mode)
197{
198 if (d->mode == mode) {
199 return;
200 }
201
202 d->mode = mode;
203 Q_EMIT modeChanged();
204 d->repopulateModel(this);
205}
206
207int BackendModel::rowCount(const QModelIndex &parent) const
208{
209 if (parent.isValid()) {
210 return 0;
211 }
212 return d->rows.size();
213}
214
215QVariant BackendModel::data(const QModelIndex &index, int role) const
216{
217 if (!index.isValid() || !d->mgr) {
218 return {};
219 }
220
221 const auto &row = d->rows[index.row()];
222 switch (role) {
223 case NameRole:
224 return row.backend.name();
225 case DescriptionRole:
226 return row.backend.description();
227 case IdentifierRole:
228 return row.backend.identifier();
229 case SecureRole:
230 return row.backend.isSecure();
231 case ItemEnabledRole:
232 return row.backend.isSecure() || d->mgr->allowInsecureBackends();
233 case BackendEnabledRole:
234 if (!row.backend.isSecure() && !d->mgr->allowInsecureBackends()) {
235 return false;
236 }
237 return d->mgr->isBackendEnabled(row.backend.identifier());
239 if (!row.backend.isSecure() && !d->mgr->allowInsecureBackends()) {
240 return Qt::Unchecked;
241 }
242 return d->mgr->isBackendEnabled(row.backend.identifier()) ? Qt::Checked : Qt::Unchecked;
244 case CountryCodeRole:
245 return row.country;
246 }
247
248 return {};
249}
250
251bool BackendModel::setData(const QModelIndex &index, const QVariant &value, int role)
252{
253 const auto &row = d->rows[index.row()];
254 switch (role) {
255 case BackendModel::BackendEnabledRole:
256 d->mgr->setBackendEnabled(row.backend.identifier(), value.toBool());
257 return true;
259 d->mgr->setBackendEnabled(row.backend.identifier(), value.toInt() == Qt::Checked);
260 return true;
261 }
262 return false;
263}
264
265Qt::ItemFlags BackendModel::flags(const QModelIndex &index) const
266{
268 if (!d->mgr || !index.isValid()) {
269 return f;
270 }
272
273 const auto &row = d->rows[index.row()];
274 if (!d->mgr->allowInsecureBackends() && !row.backend.isSecure()) {
275 return f & ~Qt::ItemIsEnabled;
276 }
277
278 return f;
279}
280
281QHash<int, QByteArray> BackendModel::roleNames() const
282{
283 auto names = QAbstractListModel::roleNames();
284 names.insert(NameRole, "name");
285 names.insert(DescriptionRole, "description");
286 names.insert(IdentifierRole, "identifier");
287 names.insert(SecureRole, "isSecure");
288 names.insert(ItemEnabledRole, "itemEnabled");
289 names.insert(BackendEnabledRole, "backendEnabled");
290 names.insert(PrimaryCountryCodeRole, "primaryCountryCode");
291 names.insert(CountryCodeRole, "countryCode");
292 return names;
293}
Model listing backends and allowing to configure which ones are active.
Mode mode
Configures the grouping mode for the content of this model.
Mode
Content grouping modes.
@ GroupByCountry
A backend might occur multiple times, for each country it is associated with.
@ Flat
Each backend appears exactly once, grouping by country is possible (the data is order by country),...
@ CountryCodeRole
a ISO 3166-1 code usable for grouping content by country
KPublicTransport::Manager * manager
Sets the KPublicTransport::Manager instance.
Information about a backend service queried for location/departure/journey data.
Definition backend.h:22
Type
Coverage quality as defined by the Transport API Repository format.
QVariantList backends
QML-compatible access to backends().
Definition manager.h:57
Query operations and data types for accessing realtime public transport information from online servi...
QAction * next(const QObject *recvr, const char *slot, QObject *parent)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
virtual QHash< int, QByteArray > roleNames() const const
virtual Qt::ItemFlags flags(const QModelIndex &index) const const override
virtual QModelIndex index(int row, int column, const QModelIndex &parent) const const override
bool isValid() const const
int row() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
Unchecked
CheckStateRole
typedef ItemFlags
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:10:40 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.