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 &region: 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 }