File indexing completed on 2024-12-08 07:19:11

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