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 }