File indexing completed on 2024-04-28 04:41:40

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "manager.h"
0008 #include "assetrepository_p.h"
0009 #include "backends/srbijavozbackend.h"
0010 #include "backends/zpcgbackend.h"
0011 #include "journeyreply.h"
0012 #include "journeyrequest.h"
0013 #include "requestcontext_p.h"
0014 #include "locationreply.h"
0015 #include "locationrequest.h"
0016 #include "logging.h"
0017 #include "stopoverreply.h"
0018 #include "stopoverrequest.h"
0019 #include "vehiclelayoutrequest.h"
0020 #include "vehiclelayoutreply.h"
0021 #include "datatypes/attributionutil_p.h"
0022 #include "datatypes/backend.h"
0023 #include "datatypes/backend_p.h"
0024 #include "datatypes/disruption.h"
0025 #include "datatypes/json_p.h"
0026 #include "datatypes/platform.h"
0027 #include "datatypes/vehicle.h"
0028 #include "geo/geojson_p.h"
0029 
0030 #include <KPublicTransport/Journey>
0031 #include <KPublicTransport/Location>
0032 #include <KPublicTransport/Stopover>
0033 
0034 #include "backends/accessibilitycloudbackend.h"
0035 #include "backends/cache.h"
0036 #include "backends/deutschebahnbackend.h"
0037 #include "backends/efabackend.h"
0038 #include "backends/hafasmgatebackend.h"
0039 #include "backends/hafasquerybackend.h"
0040 #include "backends/ivvassbackend.h"
0041 #include "backends/motisbackend.h"
0042 #include "backends/navitiabackend.h"
0043 #include "backends/oebbbackend.h"
0044 #include "backends/openjourneyplannerbackend.h"
0045 #include "backends/opentripplannergraphqlbackend.h"
0046 #include "backends/opentripplannerrestbackend.h"
0047 #include "backends/pasazieruvilciensbackend.h"
0048 #include "backends/ltglinkbackend.h"
0049 #include "gbfs/gbfsbackend.h"
0050 
0051 #include <QDirIterator>
0052 #include <QJsonArray>
0053 #include <QJsonDocument>
0054 #include <QJsonObject>
0055 #include <QMetaProperty>
0056 #include <QNetworkAccessManager>
0057 #include <QStandardPaths>
0058 #include <QTimer>
0059 #include <QTimeZone>
0060 
0061 #include <functional>
0062 
0063 using namespace Qt::Literals::StringLiterals;
0064 using namespace KPublicTransport;
0065 
0066 static inline void initResources() {
0067     Q_INIT_RESOURCE(asset_attributions);
0068     Q_INIT_RESOURCE(gbfs);
0069     Q_INIT_RESOURCE(geometry);
0070     Q_INIT_RESOURCE(networks);
0071     Q_INIT_RESOURCE(network_certs);
0072     Q_INIT_RESOURCE(otp);
0073     Q_INIT_RESOURCE(stations);
0074 }
0075 
0076 namespace KPublicTransport {
0077 class ManagerPrivate {
0078 public:
0079     QNetworkAccessManager* nam();
0080     void loadNetworks();
0081     std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &obj);
0082     template <typename Backend, typename Backend2, typename ...Backends>
0083     static std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &backendType, const QJsonObject &obj);
0084     template <typename Backend> std::unique_ptr<AbstractBackend>
0085     static loadNetwork(const QJsonObject &backendType, const QJsonObject &obj);
0086     template <typename T>
0087     static std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &obj);
0088 
0089     template <typename RequestT> bool shouldSkipBackend(const Backend &backend, const RequestT &req) const;
0090 
0091     void resolveLocation(LocationRequest &&locReq, const AbstractBackend *backend, const std::function<void(const Location &loc)> &callback);
0092     bool queryJourney(const AbstractBackend *backend, const JourneyRequest &req, JourneyReply *reply);
0093     bool queryStopover(const AbstractBackend *backend, const StopoverRequest &req, StopoverReply *reply);
0094 
0095     template <typename RepT, typename ReqT> RepT* makeReply(const ReqT &request);
0096 
0097     void readCachedAttributions();
0098 
0099     int queryLocationOnBackend(const LocationRequest &req, LocationReply *reply, const Backend &backend);
0100 
0101     Manager *q = nullptr;
0102     QNetworkAccessManager *m_nam = nullptr;
0103     std::vector<Backend> m_backends;
0104     std::vector<Attribution> m_attributions;
0105 
0106     // we store both explicitly to have a third state, backends with the enabled state being the "default" (whatever that might eventually be)
0107     QStringList m_enabledBackends;
0108     QStringList m_disabledBackends;
0109 
0110     bool m_allowInsecure = false;
0111     bool m_hasReadCachedAttributions = false;
0112     bool m_backendsEnabledByDefault = true;
0113 
0114 private:
0115     bool shouldSkipBackend(const Backend &backend) const;
0116 };
0117 }
0118 
0119 QNetworkAccessManager* ManagerPrivate::nam()
0120 {
0121     if (!m_nam) {
0122         m_nam = new QNetworkAccessManager(q);
0123         m_nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
0124         m_nam->setStrictTransportSecurityEnabled(true);
0125         m_nam->enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/org.kde.kpublictransport/hsts/"));
0126     }
0127     return m_nam;
0128 }
0129 
0130 
0131 void ManagerPrivate::loadNetworks()
0132 {
0133     if (!m_backends.empty()) {
0134         return;
0135     }
0136 
0137     QStringList searchDirs;
0138 #ifndef Q_OS_ANDROID
0139     searchDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
0140 #endif
0141     searchDirs.push_back(u":/"_s);
0142 
0143     for (const auto &searchDir : searchDirs) {
0144         QDirIterator it(searchDir + "/org.kde.kpublictransport/networks"_L1, {u"*.json"_s}, QDir::Files);
0145         while (it.hasNext()) {
0146             it.next();
0147             const auto id = it.fileInfo().baseName();
0148             if (std::any_of(m_backends.begin(), m_backends.end(), [&id](const auto &backend) { return backend.identifier() == id; })) {
0149                 // already seen in another location
0150                 continue;
0151             }
0152 
0153             QFile f(it.filePath());
0154             if (!f.open(QFile::ReadOnly)) {
0155                 qCWarning(Log) << "Failed to open public transport network configuration:" << f.errorString();
0156                 continue;
0157             }
0158 
0159             QJsonParseError error;
0160             const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
0161             if (error.error != QJsonParseError::NoError) {
0162                 qCWarning(Log) << "Failed to parse public transport network configuration:" << error.errorString() << it.fileName();
0163                 continue;
0164             }
0165 
0166             auto net = loadNetwork(doc.object());
0167             if (net) {
0168                 net->setBackendId(id);
0169                 net->init();
0170                 if (!net->attribution().isEmpty()) {
0171                     m_attributions.push_back(net->attribution());
0172                 }
0173 
0174                 auto b = BackendPrivate::fromJson(doc.object());
0175                 BackendPrivate::setImpl(b, std::move(net));
0176                 m_backends.push_back(std::move(b));
0177             } else {
0178                 qCWarning(Log) << "Failed to load public transport network configuration config:" << it.fileName();
0179             }
0180         }
0181     }
0182 
0183     std::stable_sort(m_backends.begin(), m_backends.end(), [](const auto &lhs, const auto &rhs) {
0184         return lhs.identifier() < rhs.identifier();
0185     });
0186 
0187     AttributionUtil::sort(m_attributions);
0188     qCDebug(Log) << m_backends.size() << "public transport network configurations loaded";
0189 }
0190 
0191 std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &obj)
0192 {
0193     const auto type = obj.value(QLatin1String("type")).toObject();
0194     // backends need to be topologically sorted according to their preference/priority here
0195     return loadNetwork<
0196         NavitiaBackend,
0197         OpenTripPlannerGraphQLBackend,
0198         OpenTripPlannerRestBackend,
0199         DeutscheBahnBackend,
0200         OebbBackend,
0201         HafasMgateBackend,
0202         HafasQueryBackend,
0203         EfaBackend,
0204         IvvAssBackend,
0205         OpenJourneyPlannerBackend,
0206         MotisBackend,
0207         GBFSBackend,
0208         AccessibilityCloudBackend,
0209         PasazieruVilciensBackend,
0210         LTGLinkBackend,
0211         ZPCGBackend,
0212         SrbijavozBackend
0213     >(type, obj);
0214 }
0215 
0216 template <typename Backend, typename Backend2, typename ...Backends>
0217 std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &backendType, const QJsonObject &obj)
0218 {
0219     if (backendType.value(QLatin1String(Backend::type())).toBool()) {
0220         return loadNetwork<Backend>(obj);
0221     }
0222     return loadNetwork<Backend2, Backends...>(backendType, obj);
0223 }
0224 
0225 template <typename Backend>
0226 std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &backendType, const QJsonObject &obj)
0227 {
0228     if (backendType.value(QLatin1String(Backend::type())).toBool()) {
0229         return ManagerPrivate::loadNetwork<Backend>(obj);
0230     }
0231     qCWarning(Log) << "Unknown backend type:" << backendType;
0232     return {};
0233 }
0234 
0235 static void applyBackendOptions(AbstractBackend *backend, const QMetaObject *mo, const QJsonObject &obj)
0236 {
0237     const auto opts = obj.value(QLatin1String("options")).toObject();
0238     for (auto it = opts.begin(); it != opts.end(); ++it) {
0239         const auto idx = mo->indexOfProperty(it.key().toUtf8().constData());
0240         if (idx < 0) {
0241             qCWarning(Log) << "Unknown backend setting:" << it.key();
0242             continue;
0243         }
0244         const auto mp = mo->property(idx);
0245         if (it.value().isObject()) {
0246             mp.writeOnGadget(backend, it.value().toObject());
0247         } else if (it.value().isArray()) {
0248             const auto a = it.value().toArray();
0249             if (mp.userType() == QMetaType::QStringList) {
0250                 QStringList l;
0251                 l.reserve(a.size());
0252                 std::transform(a.begin(), a.end(), std::back_inserter(l), [](const auto &v) { return v.toString(); });
0253                 mp.writeOnGadget(backend, l);
0254             } else {
0255                 mp.writeOnGadget(backend, it.value().toArray());
0256             }
0257         } else {
0258             mp.writeOnGadget(backend, it.value().toVariant());
0259         }
0260     }
0261 
0262     const auto attrObj = obj.value(QLatin1String("attribution")).toObject();
0263     const auto attr = Attribution::fromJson(attrObj);
0264     backend->setAttribution(attr);
0265 
0266     const auto tzId = obj.value(QLatin1String("timezone")).toString();
0267     if (!tzId.isEmpty()) {
0268         QTimeZone tz(tzId.toUtf8());
0269         if (tz.isValid()) {
0270             backend->setTimeZone(tz);
0271         } else {
0272             qCWarning(Log) << "Invalid timezone:" << tzId;
0273         }
0274     }
0275 
0276     const auto langArray = obj.value(QLatin1String("supportedLanguages")).toArray();
0277     QStringList langs;
0278     langs.reserve(langArray.size());
0279     std::transform(langArray.begin(), langArray.end(), std::back_inserter(langs), [](const auto &v) { return v.toString(); });
0280     backend->setSupportedLanguages(langs);
0281 }
0282 
0283 template<typename T> std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &obj)
0284 {
0285     std::unique_ptr<AbstractBackend> backend(new T);
0286     applyBackendOptions(backend.get(), &T::staticMetaObject, obj);
0287     return backend;
0288 }
0289 
0290 bool ManagerPrivate::shouldSkipBackend(const Backend &backend) const
0291 {
0292     if (!backend.isSecure() && !m_allowInsecure) {
0293         qCDebug(Log) << "Skipping insecure backend:" << backend.identifier();
0294         return true;
0295     }
0296     return !q->isBackendEnabled(backend.identifier());
0297 }
0298 
0299 template <typename RequestT>
0300 bool ManagerPrivate::shouldSkipBackend(const Backend &backend, const RequestT &req) const
0301 {
0302     if (!req.backendIds().isEmpty() && !req.backendIds().contains(backend.identifier())) {
0303         //qCDebug(Log) << "Skipping backend" << backend.identifier() << "due to explicit request";
0304         return true;
0305     }
0306     return shouldSkipBackend(backend);
0307 }
0308 
0309 // IMPORTANT callback must not be called directly, but only via queued invocation,
0310 // our callers rely on that to not mess up sync/async response handling
0311 void ManagerPrivate::resolveLocation(LocationRequest &&locReq, const AbstractBackend *backend, const std::function<void(const Location&)> &callback)
0312 {
0313     // check if this location query is cached already
0314     const auto cacheEntry = Cache::lookupLocation(backend->backendId(), locReq.cacheKey());
0315     switch (cacheEntry.type) {
0316         case CacheHitType::Negative:
0317             QTimer::singleShot(0, q, [callback]() { callback({}); });
0318             return;
0319         case CacheHitType::Positive:
0320             if (!cacheEntry.data.empty()) {
0321                 const auto loc = cacheEntry.data[0];
0322                 QTimer::singleShot(0, q, [callback, loc]() { callback(loc); });
0323                 return;
0324             }
0325             break;
0326         case CacheHitType::Miss:
0327             break;
0328     }
0329 
0330     // actually do the location query
0331     locReq.setMaximumResults(1);
0332     auto locReply = new LocationReply(locReq, q);
0333     if (backend->queryLocation(locReq, locReply, nam())) {
0334         locReply->setPendingOps(1);
0335     } else {
0336         locReply->setPendingOps(0);
0337     }
0338     QObject::connect(locReply, &Reply::finished, q, [callback, locReply]() {
0339         locReply->deleteLater();
0340         if (locReply->result().empty()) {
0341             callback({});
0342         } else {
0343             callback(locReply->result()[0]);
0344         }
0345     });
0346 }
0347 
0348 static Location::Types locationTypesForJourneyRequest(const JourneyRequest &req)
0349 {
0350     Location::Types t = Location::Place;
0351     if (req.modes() & JourneySection::PublicTransport) {
0352         t |= Location::Stop;
0353     }
0354     if (req.modes() & JourneySection::RentedVehicle) {
0355         t |= Location::RentedVehicleStation;
0356     }
0357     return t;
0358 }
0359 
0360 bool ManagerPrivate::queryJourney(const AbstractBackend* backend, const JourneyRequest &req, JourneyReply *reply)
0361 {
0362     auto cache = Cache::lookupJourney(backend->backendId(), req.cacheKey());
0363     switch (cache.type) {
0364         case CacheHitType::Negative:
0365             qCDebug(Log) << "Negative cache hit for backend" << backend->backendId();
0366             return false;
0367         case CacheHitType::Positive:
0368             qCDebug(Log) << "Positive cache hit for backend" << backend->backendId();
0369             reply->addAttributions(std::move(cache.attributions));
0370             reply->addResult(backend, std::move(cache.data));
0371             return false;
0372         case CacheHitType::Miss:
0373             qCDebug(Log) << "Cache miss for backend" << backend->backendId();
0374             break;
0375     }
0376 
0377     // resolve locations if needed
0378     if (backend->needsLocationQuery(req.from(), AbstractBackend::QueryType::Journey)) {
0379         LocationRequest fromReq(req.from());
0380         fromReq.setTypes(locationTypesForJourneyRequest(req));
0381         resolveLocation(std::move(fromReq), backend, [reply, backend, req, this](const Location &loc) {
0382             auto jnyRequest = req;
0383             const auto fromLoc = Location::merge(jnyRequest.from(), loc);
0384             jnyRequest.setFrom(fromLoc);
0385 
0386             if (backend->needsLocationQuery(jnyRequest.to(), AbstractBackend::QueryType::Journey)) {
0387                 LocationRequest toReq(jnyRequest.to());
0388                 toReq.setTypes(locationTypesForJourneyRequest(req));
0389                 resolveLocation(std::move(toReq), backend, [jnyRequest, reply, backend, this](const Location &loc) {
0390                     auto jnyReq = jnyRequest;
0391                     const auto toLoc = Location::merge(jnyRequest.to(), loc);
0392                     jnyReq.setTo(toLoc);
0393                     if (!backend->queryJourney(jnyReq, reply, nam())) {
0394                         reply->addError(Reply::NotFoundError, {});
0395                     }
0396                 });
0397 
0398                 return;
0399             }
0400 
0401             if (!backend->queryJourney(jnyRequest, reply, nam())) {
0402                 reply->addError(Reply::NotFoundError, {});
0403             }
0404         });
0405 
0406         return true;
0407     }
0408 
0409     if (backend->needsLocationQuery(req.to(), AbstractBackend::QueryType::Journey)) {
0410         LocationRequest toReq(req.to());
0411         toReq.setTypes(locationTypesForJourneyRequest(req));
0412         resolveLocation(std::move(toReq), backend, [req, toReq, reply, backend, this](const Location &loc) {
0413             const auto toLoc = Location::merge(req.to(), loc);
0414             auto jnyRequest = req;
0415             jnyRequest.setTo(toLoc);
0416             if (!backend->queryJourney(jnyRequest, reply, nam())) {
0417                 reply->addError(Reply::NotFoundError, {});
0418             }
0419         });
0420         return true;
0421     }
0422 
0423     return backend->queryJourney(req, reply, nam());
0424 }
0425 
0426 bool ManagerPrivate::queryStopover(const AbstractBackend *backend, const StopoverRequest &req, StopoverReply *reply)
0427 {
0428     auto cache = Cache::lookupDeparture(backend->backendId(), req.cacheKey());
0429     switch (cache.type) {
0430         case CacheHitType::Negative:
0431             qCDebug(Log) << "Negative cache hit for backend" << backend->backendId();
0432             return false;
0433         case CacheHitType::Positive:
0434             qCDebug(Log) << "Positive cache hit for backend" << backend->backendId();
0435             reply->addAttributions(std::move(cache.attributions));
0436             reply->addResult(backend, std::move(cache.data));
0437             return false;
0438         case CacheHitType::Miss:
0439             qCDebug(Log) << "Cache miss for backend" << backend->backendId();
0440             break;
0441     }
0442 
0443     // check if we first need to resolve the location first
0444     if (backend->needsLocationQuery(req.stop(), AbstractBackend::QueryType::Departure)) {
0445         qCDebug(Log) << "Backend needs location query first:" << backend->backendId();
0446         LocationRequest locReq(req.stop());
0447         locReq.setTypes(Location::Stop); // Stopover can never refer to other location types
0448         resolveLocation(std::move(locReq), backend, [reply, req, backend, this](const Location &loc) {
0449             const auto depLoc = Location::merge(req.stop(), loc);
0450             auto depRequest = req;
0451             depRequest.setStop(depLoc);
0452             if (!backend->queryStopover(depRequest, reply, nam())) {
0453                 reply->addError(Reply::NotFoundError, {});
0454             }
0455         });
0456         return true;
0457     }
0458 
0459     return backend->queryStopover(req, reply, nam());
0460 }
0461 
0462 void ManagerPrivate::readCachedAttributions()
0463 {
0464     if (m_hasReadCachedAttributions) {
0465         return;
0466     }
0467 
0468     Cache::allCachedAttributions(m_attributions);
0469     m_hasReadCachedAttributions = true;
0470 }
0471 
0472 template<typename RepT, typename ReqT>
0473 RepT* ManagerPrivate::makeReply(const ReqT &request)
0474 {
0475     auto reply = new RepT(request, q);
0476     QObject::connect(reply, &Reply::finished, q, [this, reply]() {
0477         AttributionUtil::merge(m_attributions, reply->attributions());
0478     });
0479     return reply;
0480 }
0481 
0482 
0483 
0484 Manager::Manager(QObject *parent)
0485     : QObject(parent)
0486     , d(new ManagerPrivate)
0487 {
0488     initResources();
0489     qRegisterMetaType<Disruption::Effect>();
0490     d->q = this;
0491 
0492     if (!AssetRepository::instance()) {
0493         auto assetRepo = new AssetRepository(this);
0494         assetRepo->setNetworkAccessManagerProvider(std::bind(&ManagerPrivate::nam, d.get()));
0495     }
0496 
0497     Cache::expire();
0498 }
0499 
0500 Manager::~Manager() = default;
0501 
0502 void Manager::setNetworkAccessManager(QNetworkAccessManager *nam)
0503 {
0504     if (d->m_nam == nam) {
0505         return;
0506     }
0507 
0508     if (d->m_nam && d->m_nam->parent() == this) {
0509         delete d->m_nam;
0510     }
0511 
0512     d->m_nam = nam;
0513 }
0514 
0515 bool Manager::allowInsecureBackends() const
0516 {
0517     return d->m_allowInsecure;
0518 }
0519 
0520 void Manager::setAllowInsecureBackends(bool insecure)
0521 {
0522     if (d->m_allowInsecure == insecure) {
0523         return;
0524     }
0525     d->m_allowInsecure = insecure;
0526     Q_EMIT configurationChanged();
0527 }
0528 
0529 JourneyReply* Manager::queryJourney(const JourneyRequest &req) const
0530 {
0531     auto reply = d->makeReply<JourneyReply>(req);
0532     int pendingOps = 0;
0533 
0534     // validate input
0535     req.validate();
0536     if (!req.isValid()) {
0537         reply->addError(Reply::InvalidRequest, {});
0538         reply->setPendingOps(pendingOps);
0539         return reply;
0540     }
0541 
0542     d->loadNetworks();
0543 
0544     // first time/direct query
0545     if (req.contexts().empty()) {
0546         QSet<QString> triedBackends;
0547         bool foundNonGlobalCoverage = false;
0548         for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
0549             const auto checkBackend = [&](const Backend &backend, bool bothLocationMatch) {
0550                 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
0551                     return;
0552                 }
0553                 const auto coverage = backend.coverageArea(coverageType);
0554                 if (coverage.isEmpty()) {
0555                     return;
0556                 }
0557 
0558                 if (bothLocationMatch) {
0559                     if (!coverage.coversLocation(req.from()) || !coverage.coversLocation(req.to())) {
0560                         return;
0561                     }
0562                 } else {
0563                     if (!coverage.coversLocation(req.from()) && !coverage.coversLocation(req.to())) {
0564                         return;
0565                     }
0566                 }
0567 
0568                 triedBackends.insert(backend.identifier());
0569                 foundNonGlobalCoverage |= !coverage.isGlobal();
0570 
0571                 if (d->queryJourney(BackendPrivate::impl(backend), req, reply)) {
0572                     ++pendingOps;
0573                 }
0574             };
0575 
0576             // look for coverage areas which contain both locations first
0577             for (const auto &backend: d->m_backends) {
0578                 checkBackend(backend, true);
0579             }
0580             if (pendingOps && foundNonGlobalCoverage) {
0581                 break;
0582             }
0583 
0584             // if we didn't find one, try with just a single one
0585             for (const auto &backend: d->m_backends) {
0586                 checkBackend(backend, false);
0587             }
0588             if (pendingOps && foundNonGlobalCoverage) {
0589                 break;
0590             }
0591         }
0592 
0593     // subsequent earlier/later query
0594     } else {
0595         for (const auto &context : req.contexts()) {
0596             // backend supports this itself
0597             if ((context.type == RequestContext::Next && context.backend->hasCapability(AbstractBackend::CanQueryNextJourney))
0598               ||(context.type == RequestContext::Previous && context.backend->hasCapability(AbstractBackend::CanQueryPreviousJourney)))
0599             {
0600                 if (d->queryJourney(context.backend, req, reply)) {
0601                     ++pendingOps;
0602                     continue;
0603                 }
0604             }
0605 
0606             // backend doesn't support this, let's try to emulate
0607             if (context.type == RequestContext::Next && req.dateTimeMode() == JourneyRequest::Departure) {
0608                 auto r = req;
0609                 r.setDepartureTime(context.dateTime);
0610                 if (d->queryJourney(context.backend, r, reply)) {
0611                     ++pendingOps;
0612                     continue;
0613                 }
0614             } else if (context.type == RequestContext::Previous && req.dateTimeMode() == JourneyRequest::Departure) {
0615                 auto r = req;
0616                 r.setArrivalTime(context.dateTime);
0617                 if (d->queryJourney(context.backend, r, reply)) {
0618                     ++pendingOps;
0619                     continue;
0620                 }
0621             }
0622         }
0623     }
0624 
0625     if (req.downloadAssets()) {
0626         reply->addAttributions(AssetRepository::instance()->attributions());
0627     }
0628     reply->setPendingOps(pendingOps);
0629     return reply;
0630 }
0631 
0632 StopoverReply* Manager::queryStopover(const StopoverRequest &req) const
0633 {
0634     auto reply = d->makeReply<StopoverReply>(req);
0635     int pendingOps = 0;
0636 
0637     // validate input
0638     if (!req.isValid()) {
0639         reply->addError(Reply::InvalidRequest, {});
0640         reply->setPendingOps(pendingOps);
0641         return reply;
0642     }
0643 
0644     d->loadNetworks();
0645 
0646     // first time/direct query
0647     if (req.contexts().empty()) {
0648         QSet<QString> triedBackends;
0649         bool foundNonGlobalCoverage = false;
0650         for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
0651             for (const auto &backend: d->m_backends) {
0652                 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
0653                     continue;
0654                 }
0655                 if (req.mode() == StopoverRequest::QueryArrival && (BackendPrivate::impl(backend)->capabilities() & AbstractBackend::CanQueryArrivals) == 0) {
0656                     qCDebug(Log) << "Skipping backend due to not supporting arrival queries:" << backend.identifier();
0657                     continue;
0658                 }
0659                 const auto coverage = backend.coverageArea(coverageType);
0660                 if (coverage.isEmpty() || !coverage.coversLocation(req.stop())) {
0661                     continue;
0662                 }
0663                 triedBackends.insert(backend.identifier());
0664                 foundNonGlobalCoverage |= !coverage.isGlobal();
0665 
0666                 if (d->queryStopover(BackendPrivate::impl(backend), req, reply)) {
0667                     ++pendingOps;
0668                 }
0669             }
0670 
0671             if (pendingOps && foundNonGlobalCoverage) {
0672                 break;
0673             }
0674         }
0675 
0676     // subsequent earlier/later query
0677     } else {
0678         for (const auto &context : req.contexts()) {
0679             // backend supports this itself
0680             if ((context.type == RequestContext::Next && context.backend->hasCapability(AbstractBackend::CanQueryNextDeparture))
0681               ||(context.type == RequestContext::Previous && context.backend->hasCapability(AbstractBackend::CanQueryPreviousDeparture)))
0682             {
0683                 if (d->queryStopover(context.backend, req, reply)) {
0684                     ++pendingOps;
0685                     continue;
0686                 }
0687             }
0688 
0689             // backend doesn't support this, let's try to emulate
0690             if (context.type == RequestContext::Next && req.mode() == StopoverRequest::QueryDeparture) {
0691                 auto r = req;
0692                 r.setDateTime(context.dateTime);
0693                 if (d->queryStopover(context.backend, r, reply)) {
0694                     ++pendingOps;
0695                     continue;
0696                 }
0697             }
0698         }
0699     }
0700 
0701     if (req.downloadAssets()) {
0702         reply->addAttributions(AssetRepository::instance()->attributions());
0703     }
0704     reply->setPendingOps(pendingOps);
0705     return reply;
0706 }
0707 
0708 int ManagerPrivate::queryLocationOnBackend(const LocationRequest &req, LocationReply *reply, const Backend &backend)
0709 {
0710     auto cache = Cache::lookupLocation(backend.identifier(), req.cacheKey());
0711     switch (cache.type) {
0712         case CacheHitType::Negative:
0713             qCDebug(Log) << "Negative cache hit for backend" << backend.identifier();
0714             break;
0715         case CacheHitType::Positive:
0716             qCDebug(Log) << "Positive cache hit for backend" << backend.identifier();
0717             reply->addAttributions(std::move(cache.attributions));
0718             reply->addResult(std::move(cache.data));
0719             break;
0720         case CacheHitType::Miss:
0721             qCDebug(Log) << "Cache miss for backend" << backend.identifier();
0722             reply->addAttribution(BackendPrivate::impl(backend)->attribution());
0723             if (BackendPrivate::impl(backend)->queryLocation(req, reply, nam())) {
0724                 return 1;
0725             }
0726             break;
0727     }
0728 
0729     return 0;
0730 }
0731 
0732 LocationReply* Manager::queryLocation(const LocationRequest &req) const
0733 {
0734     auto reply = d->makeReply<LocationReply>(req);
0735     int pendingOps = 0;
0736 
0737     // validate input
0738     if (!req.isValid()) {
0739         reply->addError(Reply::InvalidRequest, {});
0740         reply->setPendingOps(pendingOps);
0741         return reply;
0742     }
0743 
0744     d->loadNetworks();
0745 
0746     QSet<QString> triedBackends;
0747     bool foundNonGlobalCoverage = false;
0748     const auto loc = req.location();
0749     const auto isCountryOnly = !loc.hasCoordinate() && !loc.country().isEmpty() && loc.region().isEmpty();
0750     for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
0751         // pass 1: coordinate-based coverage, or nationwide country coverage
0752         for (const auto &backend : d->m_backends) {
0753             if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
0754                 continue;
0755             }
0756             const auto coverage = backend.coverageArea(coverageType);
0757             if (coverage.isEmpty() || !coverage.coversLocation(loc)) {
0758                 continue;
0759             }
0760             if (isCountryOnly && !coverage.hasNationWideCoverage(loc.country())) {
0761                 continue;
0762             }
0763 
0764             triedBackends.insert(backend.identifier());
0765             foundNonGlobalCoverage |= !coverage.isGlobal();
0766             pendingOps += d->queryLocationOnBackend(req, reply, backend);
0767         }
0768         if (pendingOps && foundNonGlobalCoverage) {
0769             break;
0770         }
0771 
0772         // pass 2: any country match
0773         for (const auto &backend : d->m_backends) {
0774             if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
0775                 continue;
0776             }
0777             const auto coverage = backend.coverageArea(coverageType);
0778             if (coverage.isEmpty() || !coverage.coversLocation(loc)) {
0779                 continue;
0780             }
0781 
0782             triedBackends.insert(backend.identifier());
0783             foundNonGlobalCoverage |= !coverage.isGlobal();
0784             pendingOps += d->queryLocationOnBackend(req, reply, backend);
0785         }
0786         if (pendingOps && foundNonGlobalCoverage) {
0787             break;
0788         }
0789     }
0790     reply->setPendingOps(pendingOps);
0791     return reply;
0792 }
0793 
0794 VehicleLayoutReply* Manager::queryVehicleLayout(const VehicleLayoutRequest &req) const
0795 {
0796     auto reply = d->makeReply<VehicleLayoutReply>(req);
0797     int pendingOps = 0;
0798 
0799     // validate input
0800     if (!req.isValid()) {
0801         reply->addError(Reply::InvalidRequest, {});
0802         reply->setPendingOps(pendingOps);
0803         return reply;
0804     }
0805 
0806     d->loadNetworks();
0807 
0808     for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular }) {
0809         for (const auto &backend : d->m_backends) {
0810             if (d->shouldSkipBackend(backend, req)) {
0811                 continue;
0812             }
0813             const auto coverage = backend.coverageArea(coverageType);
0814             if (coverage.isEmpty() || !coverage.coversLocation(req.stopover().stopPoint())) {
0815                 continue;
0816             }
0817             reply->addAttribution(BackendPrivate::impl(backend)->attribution());
0818 
0819             auto cache = Cache::lookupVehicleLayout(backend.identifier(), req.cacheKey());
0820             switch (cache.type) {
0821                 case CacheHitType::Negative:
0822                     qCDebug(Log) << "Negative cache hit for backend" << backend.identifier();
0823                     break;
0824                 case CacheHitType::Positive:
0825                     qCDebug(Log) << "Positive cache hit for backend" << backend.identifier();
0826                     if (cache.data.size() == 1) {
0827                         reply->addAttributions(std::move(cache.attributions));
0828                         reply->addResult(cache.data[0]);
0829                         break;
0830                     }
0831                     [[fallthrough]];
0832                 case CacheHitType::Miss:
0833                     qCDebug(Log) << "Cache miss for backend" << backend.identifier();
0834                     if (BackendPrivate::impl(backend)->queryVehicleLayout(req, reply, d->nam())) {
0835                         ++pendingOps;
0836                     }
0837                     break;
0838             }
0839         }
0840         if (pendingOps) {
0841             break;
0842         }
0843     }
0844 
0845     reply->setPendingOps(pendingOps);
0846     return reply;
0847 }
0848 
0849 const std::vector<Attribution>& Manager::attributions() const
0850 {
0851     d->loadNetworks();
0852     d->readCachedAttributions();
0853     return d->m_attributions;
0854 }
0855 
0856 QVariantList Manager::attributionsVariant() const
0857 {
0858     d->loadNetworks();
0859     d->readCachedAttributions();
0860     QVariantList l;
0861     l.reserve(d->m_attributions.size());
0862     std::transform(d->m_attributions.begin(), d->m_attributions.end(), std::back_inserter(l), [](const auto &attr) { return QVariant::fromValue(attr); });
0863     return l;
0864 }
0865 
0866 const std::vector<Backend>& Manager::backends() const
0867 {
0868     d->loadNetworks();
0869     return d->m_backends;
0870 }
0871 
0872 bool Manager::isBackendEnabled(const QString &backendId) const
0873 {
0874     if (std::binary_search(d->m_disabledBackends.cbegin(), d->m_disabledBackends.cend(), backendId)) {
0875         return false;
0876     }
0877     if (std::binary_search(d->m_enabledBackends.cbegin(), d->m_enabledBackends.cend(), backendId)) {
0878         return true;
0879     }
0880 
0881     return d->m_backendsEnabledByDefault;
0882 }
0883 
0884 static void sortedInsert(QStringList &l, const QString &value)
0885 {
0886     const auto it = std::lower_bound(l.begin(), l.end(), value);
0887     if (it == l.end() || (*it) != value) {
0888         l.insert(it, value);
0889     }
0890 }
0891 
0892 static void sortedRemove(QStringList &l, const QString &value)
0893 {
0894     const auto it = std::lower_bound(l.begin(), l.end(), value);
0895     if (it != l.end() && (*it) == value) {
0896         l.erase(it);
0897     }
0898 }
0899 
0900 void Manager::setBackendEnabled(const QString &backendId, bool enabled)
0901 {
0902     if (enabled) {
0903         sortedInsert(d->m_enabledBackends, backendId);
0904         sortedRemove(d->m_disabledBackends, backendId);
0905     } else {
0906         sortedRemove(d->m_enabledBackends, backendId);
0907         sortedInsert(d->m_disabledBackends, backendId);
0908     }
0909     Q_EMIT configurationChanged();
0910 }
0911 
0912 QStringList Manager::enabledBackends() const
0913 {
0914     return d->m_enabledBackends;
0915 }
0916 
0917 void Manager::setEnabledBackends(const QStringList &backendIds)
0918 {
0919     QSignalBlocker blocker(this); // no change signals during settings restore
0920     for (const auto &backendId : backendIds) {
0921         setBackendEnabled(backendId, true);
0922     }
0923 }
0924 
0925 QStringList Manager::disabledBackends() const
0926 {
0927     return d->m_disabledBackends;
0928 }
0929 
0930 void Manager::setDisabledBackends(const QStringList &backendIds)
0931 {
0932     QSignalBlocker blocker(this); // no change signals during settings restore
0933     for (const auto &backendId : backendIds) {
0934         setBackendEnabled(backendId, false);
0935     }
0936 }
0937 
0938 bool Manager::backendsEnabledByDefault() const
0939 {
0940     return d->m_backendsEnabledByDefault;
0941 }
0942 
0943 void Manager::setBackendsEnabledByDefault(bool byDefault)
0944 {
0945     d->m_backendsEnabledByDefault = byDefault;
0946 
0947     Q_EMIT configurationChanged();
0948 }
0949 
0950 QVariantList Manager::backendsVariant() const
0951 {
0952     d->loadNetworks();
0953     QVariantList l;
0954     l.reserve(d->m_backends.size());
0955     std::transform(d->m_backends.begin(), d->m_backends.end(), std::back_inserter(l), [](const auto &b) { return QVariant::fromValue(b); });
0956     return l;
0957 }