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 }