File indexing completed on 2024-05-12 04:42:51
0001 /* 0002 SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "backendmodel.h" 0008 0009 #include <KPublicTransport/Backend> 0010 #include <KPublicTransport/Manager> 0011 0012 #include <QDebug> 0013 0014 using namespace KPublicTransport; 0015 0016 namespace KPublicTransport { 0017 struct BackendInfo 0018 { 0019 Backend backend; 0020 QString country; 0021 bool isNationWide; 0022 CoverageArea::Type coverageType; 0023 }; 0024 0025 class BackendModelPrivate 0026 { 0027 public: 0028 void repopulateModel(BackendModel *q); 0029 void repopulateFlat(); 0030 void repopulateGrouped(); 0031 void sortModel(); 0032 0033 Manager *mgr = nullptr; 0034 std::vector<BackendInfo> rows; 0035 BackendModel::Mode mode = BackendModel::Flat; 0036 }; 0037 } 0038 0039 void BackendModelPrivate::repopulateModel(BackendModel *q) 0040 { 0041 if (!mgr) { 0042 return; 0043 } 0044 0045 q->beginResetModel(); 0046 rows.clear(); 0047 switch (mode) { 0048 case BackendModel::Flat: 0049 repopulateFlat(); 0050 break; 0051 case BackendModel::GroupByCountry: 0052 repopulateGrouped(); 0053 break; 0054 } 0055 0056 sortModel(); 0057 q->endResetModel(); 0058 } 0059 0060 void BackendModelPrivate::repopulateFlat() 0061 { 0062 rows.reserve(mgr->backends().size()); 0063 for (const auto &b : mgr->backends()) { 0064 if (b.identifier().size() > 3 && b.identifier().at(2) == QLatin1Char('_')) { 0065 rows.push_back({ b, b.identifier().left(2).toUpper(), true, CoverageArea::Any }); 0066 } 0067 } 0068 } 0069 0070 void BackendModelPrivate::repopulateGrouped() 0071 { 0072 for (const auto &b : mgr->backends()) { 0073 for (const auto type : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) { 0074 const auto c = b.coverageArea(type); 0075 if (c.isEmpty()) { 0076 continue; 0077 } 0078 0079 for (const auto ®ion: c.regions()) { 0080 const auto country = region.left(2); 0081 if (!rows.empty() && rows.back().backend.identifier() == b.identifier() && rows.back().country == country) { 0082 continue; // deduplicate backends covering multiple regions in the same country 0083 } 0084 rows.push_back({ b, country, region.size() == 2, type }); 0085 } 0086 } 0087 } 0088 } 0089 0090 void BackendModelPrivate::sortModel() 0091 { 0092 // group by country 0093 const auto orderByCountry = [](const auto &lhs, const auto &rhs) { 0094 return lhs.country < rhs.country; 0095 }; 0096 std::sort(rows.begin(), rows.end(), orderByCountry); 0097 0098 // process each country individually 0099 for (auto next = rows.begin(); next != rows.end();) { 0100 const auto [begin, end] = std::equal_range(next, rows.end(), *next, orderByCountry); 0101 next = end; 0102 0103 // find best coverage quality for nation-wide services 0104 CoverageArea::Type bestCoverageType = CoverageArea::Any; 0105 for (auto it = begin; it != end; ++it) { 0106 if ((*it).isNationWide) { 0107 bestCoverageType = std::min(bestCoverageType, (*it).coverageType); 0108 } 0109 } 0110 0111 // sort within one country: nation wide with best quality first, then regional, then nation-wide with subpar coverage 0112 std::sort(begin, end, [bestCoverageType](const auto &lhs, const auto &rhs) { 0113 if (lhs.isNationWide && rhs.isNationWide) { 0114 if ((lhs.coverageType > bestCoverageType && rhs.coverageType > bestCoverageType) 0115 || (lhs.coverageType <= bestCoverageType && rhs.coverageType <= bestCoverageType)) { 0116 return lhs.backend.name() < rhs.backend.name(); 0117 } 0118 return lhs.coverageType < rhs.coverageType; 0119 } 0120 if (lhs.isNationWide && !rhs.isNationWide) { 0121 return lhs.coverageType <= bestCoverageType; 0122 } 0123 if (!lhs.isNationWide && rhs.isNationWide) { 0124 return rhs.coverageType > bestCoverageType; 0125 } 0126 assert(!lhs.isNationWide && !rhs.isNationWide); 0127 return lhs.backend.name() < rhs.backend.name(); 0128 }); 0129 0130 // drop entries for nationwide providers that are only the second best coverage quality for a country 0131 // and that have better coverage elsewhere 0132 // since removing entries here immediately would mess with the active iterators, we just clear the country 0133 // and do the actual deletion below 0134 if (mode == BackendModel::GroupByCountry) { 0135 for (auto it = begin; it != end; ++it) { 0136 if (!(*it).isNationWide || (*it).coverageType <= bestCoverageType) { 0137 continue; 0138 } 0139 0140 for (auto type : { CoverageArea::Realtime, CoverageArea::Regular }) { 0141 if (type >= (*it).coverageType) { 0142 break; 0143 } 0144 if (!(*it).backend.coverageArea(type).isEmpty()) { 0145 (*it).country.clear(); 0146 break; 0147 } 0148 } 0149 } 0150 } 0151 } 0152 0153 // clean up entries marked for deletion above 0154 rows.erase(std::remove_if(rows.begin(), rows.end(), [](const auto &r) { return r.country.isEmpty(); }), rows.end()); 0155 } 0156 0157 0158 BackendModel::BackendModel(QObject *parent) 0159 : QAbstractListModel(parent) 0160 , d(new BackendModelPrivate) 0161 { 0162 } 0163 0164 BackendModel::~BackendModel() = default; 0165 0166 Manager* BackendModel::manager() const 0167 { 0168 return d->mgr; 0169 } 0170 0171 void BackendModel::setManager(Manager *mgr) 0172 { 0173 if (d->mgr == mgr) { 0174 return; 0175 } 0176 0177 d->mgr = mgr; 0178 connect(mgr, &Manager::configurationChanged, this, [this]() { 0179 Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); 0180 }); 0181 d->repopulateModel(this); 0182 Q_EMIT managerChanged(); 0183 } 0184 0185 BackendModel::Mode BackendModel::mode() const 0186 { 0187 return d->mode; 0188 } 0189 0190 void BackendModel::setMode(BackendModel::Mode mode) 0191 { 0192 if (d->mode == mode) { 0193 return; 0194 } 0195 0196 d->mode = mode; 0197 Q_EMIT modeChanged(); 0198 d->repopulateModel(this); 0199 } 0200 0201 int BackendModel::rowCount(const QModelIndex &parent) const 0202 { 0203 if (parent.isValid()) { 0204 return 0; 0205 } 0206 return d->rows.size(); 0207 } 0208 0209 QVariant BackendModel::data(const QModelIndex &index, int role) const 0210 { 0211 if (!index.isValid() || !d->mgr) { 0212 return {}; 0213 } 0214 0215 const auto &row = d->rows[index.row()]; 0216 switch (role) { 0217 case NameRole: 0218 return row.backend.name(); 0219 case DescriptionRole: 0220 return row.backend.description(); 0221 case IdentifierRole: 0222 return row.backend.identifier(); 0223 case SecureRole: 0224 return row.backend.isSecure(); 0225 case ItemEnabledRole: 0226 return row.backend.isSecure() || d->mgr->allowInsecureBackends(); 0227 case BackendEnabledRole: 0228 if (!row.backend.isSecure() && !d->mgr->allowInsecureBackends()) { 0229 return false; 0230 } 0231 return d->mgr->isBackendEnabled(row.backend.identifier()); 0232 case Qt::CheckStateRole: 0233 if (!row.backend.isSecure() && !d->mgr->allowInsecureBackends()) { 0234 return Qt::Unchecked; 0235 } 0236 return d->mgr->isBackendEnabled(row.backend.identifier()) ? Qt::Checked : Qt::Unchecked; 0237 case PrimaryCountryCodeRole: 0238 case CountryCodeRole: 0239 return row.country; 0240 } 0241 0242 return {}; 0243 } 0244 0245 bool BackendModel::setData(const QModelIndex &index, const QVariant &value, int role) 0246 { 0247 const auto &row = d->rows[index.row()]; 0248 switch (role) { 0249 case BackendModel::BackendEnabledRole: 0250 d->mgr->setBackendEnabled(row.backend.identifier(), value.toBool()); 0251 return true; 0252 case Qt::CheckStateRole: 0253 d->mgr->setBackendEnabled(row.backend.identifier(), value.toInt() == Qt::Checked); 0254 return true; 0255 } 0256 return false; 0257 } 0258 0259 Qt::ItemFlags BackendModel::flags(const QModelIndex &index) const 0260 { 0261 auto f = QAbstractListModel::flags(index); 0262 if (!d->mgr || !index.isValid()) { 0263 return f; 0264 } 0265 f |= Qt::ItemIsUserCheckable; 0266 0267 const auto &row = d->rows[index.row()]; 0268 if (!d->mgr->allowInsecureBackends() && !row.backend.isSecure()) { 0269 return f & ~Qt::ItemIsEnabled; 0270 } 0271 0272 return f; 0273 } 0274 0275 QHash<int, QByteArray> BackendModel::roleNames() const 0276 { 0277 auto names = QAbstractListModel::roleNames(); 0278 names.insert(NameRole, "name"); 0279 names.insert(DescriptionRole, "description"); 0280 names.insert(IdentifierRole, "identifier"); 0281 names.insert(SecureRole, "isSecure"); 0282 names.insert(ItemEnabledRole, "itemEnabled"); 0283 names.insert(BackendEnabledRole, "backendEnabled"); 0284 names.insert(PrimaryCountryCodeRole, "primaryCountryCode"); 0285 names.insert(CountryCodeRole, "countryCode"); 0286 return names; 0287 }