KPublicTransport

manager.cpp
1/*
2 SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "manager.h"
8#include "assetrepository_p.h"
9#include "backends/srbijavozbackend.h"
10#include "journeyreply.h"
11#include "journeyrequest.h"
12#include "requestcontext_p.h"
13#include "locationreply.h"
14#include "locationrequest.h"
15#include "logging.h"
16#include "stopoverreply.h"
17#include "stopoverrequest.h"
18#include "vehiclelayoutrequest.h"
19#include "vehiclelayoutreply.h"
20#include "datatypes/attributionutil_p.h"
21#include "datatypes/backend.h"
22#include "datatypes/backend_p.h"
23#include "datatypes/disruption.h"
24#include "datatypes/json_p.h"
25#include "geo/geojson_p.h"
26
27#include <KPublicTransport/Journey>
28#include <KPublicTransport/Location>
29#include <KPublicTransport/Stopover>
30
31#include "backends/accessibilitycloudbackend.h"
32#include "backends/cache.h"
33#include "backends/deutschebahnbackend.h"
34#include "backends/efabackend.h"
35#include "backends/hafasmgatebackend.h"
36#include "backends/hafasquerybackend.h"
37#include "backends/ivvassbackend.h"
38#include "backends/motisbackend.h"
39#include "backends/motis2backend.h"
40#include "backends/navitiabackend.h"
41#include "backends/oebbbackend.h"
42#include "backends/openjourneyplannerbackend.h"
43#include "backends/opentripplannergraphqlbackend.h"
44#include "backends/opentripplannerrestbackend.h"
45#include "backends/ltglinkbackend.h"
46#include "gbfs/gbfsbackend.h"
47
48#include <QCoreApplication>
49#include <QDirIterator>
50#include <QJsonArray>
51#include <QJsonDocument>
52#include <QJsonObject>
53#include <QMetaProperty>
54#include <QNetworkAccessManager>
55#include <QStandardPaths>
56#include <QTimer>
57#include <QTimeZone>
58
59#include <functional>
60
61using namespace Qt::Literals::StringLiterals;
62using namespace KPublicTransport;
63
64static inline void initResources() {
65 Q_INIT_RESOURCE(asset_attributions);
66 Q_INIT_RESOURCE(gbfs);
67 Q_INIT_RESOURCE(geometry);
68 Q_INIT_RESOURCE(images);
69 Q_INIT_RESOURCE(networks);
70 Q_INIT_RESOURCE(network_certs);
71 Q_INIT_RESOURCE(otp);
72 Q_INIT_RESOURCE(stations);
73}
74
75namespace KPublicTransport {
76class ManagerPrivate {
77public:
78 [[nodiscard]] QNetworkAccessManager* nam();
79 void loadNetworks();
80 [[nodiscard]] std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &obj);
81 template <typename Backend, typename Backend2, typename ...Backends>
82 [[nodiscard]] static std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &backendType, const QJsonObject &obj);
83 template <typename Backend>
84 [[nodiscard]] static std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &backendType, const QJsonObject &obj);
85 template <typename T>
86 [[nodiscard]] static std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &obj);
87
88 template <typename RequestT>
89 [[nodiscard]] bool shouldSkipBackend(const Backend &backend, const RequestT &req) const;
90
91 void resolveLocation(LocationRequest &&locReq, const AbstractBackend *backend, const std::function<void(const Location &loc)> &callback);
92 [[nodiscard]] bool queryJourney(const AbstractBackend *backend, const JourneyRequest &req, JourneyReply *reply);
93 [[nodiscard]] bool queryStopover(const AbstractBackend *backend, const StopoverRequest &req, StopoverReply *reply);
94
95 template <typename RepT, typename ReqT>
96 [[nodiscard]] RepT* makeReply(const ReqT &request);
97
98 void readCachedAttributions();
99
100 [[nodiscard]] int queryLocationOnBackend(const LocationRequest &req, LocationReply *reply, const Backend &backend);
101
102 Manager *q = nullptr;
103 QNetworkAccessManager *m_nam = nullptr;
104 std::vector<Backend> m_backends;
105 std::vector<Attribution> m_attributions;
106
107 // we store both explicitly to have a third state, backends with the enabled state being the "default" (whatever that might eventually be)
108 QStringList m_enabledBackends;
109 QStringList m_disabledBackends;
110
111 bool m_allowInsecure = false;
112 bool m_hasReadCachedAttributions = false;
113 bool m_backendsEnabledByDefault = true;
114
115private:
116 [[nodiscard]] bool shouldSkipBackend(const Backend &backend) const;
117};
118}
119
120QNetworkAccessManager* ManagerPrivate::nam()
121{
122 if (!m_nam) {
123 m_nam = new QNetworkAccessManager(q);
124 m_nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
125 m_nam->setStrictTransportSecurityEnabled(true);
126 m_nam->enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/org.kde.kpublictransport/hsts/"_L1);
127 }
128 return m_nam;
129}
130
131
132void ManagerPrivate::loadNetworks()
133{
134 if (!m_backends.empty()) {
135 return;
136 }
137
138 QStringList searchDirs;
139#ifndef Q_OS_ANDROID
141#endif
142 searchDirs.push_back(u":/"_s);
143
144 std::vector<Attribution> attributions;
145 for (const auto &searchDir : searchDirs) {
146 QDirIterator it(searchDir + "/org.kde.kpublictransport/networks"_L1, {u"*.json"_s}, QDir::Files);
147 while (it.hasNext()) {
148 it.next();
149 const auto id = it.fileInfo().baseName();
150 if (std::any_of(m_backends.begin(), m_backends.end(), [&id](const auto &backend) { return backend.identifier() == id; })) {
151 // already seen in another location
152 continue;
153 }
154
155 QFile f(it.filePath());
156 if (!f.open(QFile::ReadOnly)) {
157 qCWarning(Log) << "Failed to open public transport network configuration:" << f.errorString();
158 continue;
159 }
160
161 QJsonParseError error;
162 const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
163 if (error.error != QJsonParseError::NoError) {
164 qCWarning(Log) << "Failed to parse public transport network configuration:" << error.errorString() << it.fileName();
165 continue;
166 }
167
168 auto net = loadNetwork(doc.object());
169 if (net) {
170 net->setBackendId(id);
171 net->init();
172 if (!net->attribution().isEmpty()) {
173 attributions.push_back(net->attribution());
174 }
175
176 auto b = BackendPrivate::fromJson(doc.object());
177 BackendPrivate::setImpl(b, std::move(net));
178 m_backends.push_back(std::move(b));
179 } else {
180 qCWarning(Log) << "Failed to load public transport network configuration config:" << it.fileName();
181 }
182 }
183 }
184
185 std::stable_sort(m_backends.begin(), m_backends.end(), [](const auto &lhs, const auto &rhs) {
186 return lhs.identifier() < rhs.identifier();
187 });
188
189 AttributionUtil::sort(attributions);
190 if (m_attributions.empty()) {
191 // first load
192 m_attributions = std::move(attributions);
193 } else {
194 // reload
195 AttributionUtil::merge(m_attributions, attributions);
196 }
197
198 qCDebug(Log) << m_backends.size() << "public transport network configurations loaded";
199}
200
201std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &obj)
202{
203 const auto type = obj.value("type"_L1).toObject();
204 // backends need to be topologically sorted according to their preference/priority here
205 return loadNetwork<
206 NavitiaBackend,
207 OpenTripPlannerGraphQLBackend,
208 OpenTripPlannerRestBackend,
209 DeutscheBahnBackend,
210 OebbBackend,
211 HafasMgateBackend,
212 HafasQueryBackend,
213 EfaBackend,
214 IvvAssBackend,
215 OpenJourneyPlannerBackend,
216 MotisBackend,
217 Motis2Backend,
218 GBFSBackend,
219 AccessibilityCloudBackend,
220 LTGLinkBackend,
221 SrbijavozBackend
222 >(type, obj);
223}
224
225template <typename Backend, typename Backend2, typename ...Backends>
226std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &backendType, const QJsonObject &obj)
227{
228 if (backendType.value(QLatin1String(Backend::type())).toBool()) {
229 return loadNetwork<Backend>(obj);
230 }
231 return loadNetwork<Backend2, Backends...>(backendType, obj);
232}
233
234template <typename Backend>
235std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &backendType, const QJsonObject &obj)
236{
237 if (backendType.value(QLatin1String(Backend::type())).toBool()) {
238 return ManagerPrivate::loadNetwork<Backend>(obj);
239 }
240 qCWarning(Log) << "Unknown backend type:" << backendType;
241 return {};
242}
243
244static void applyBackendOptions(AbstractBackend *backend, const QMetaObject *mo, const QJsonObject &obj)
245{
246 const auto opts = obj.value("options"_L1).toObject();
247 for (auto it = opts.begin(); it != opts.end(); ++it) {
248 const auto idx = mo->indexOfProperty(it.key().toUtf8().constData());
249 if (idx < 0) {
250 qCWarning(Log) << "Unknown backend setting:" << it.key();
251 continue;
252 }
253 const auto mp = mo->property(idx);
254 if (it.value().isObject()) {
255 mp.writeOnGadget(backend, it.value().toObject());
256 } else if (it.value().isArray()) {
257 const auto a = it.value().toArray();
258 if (mp.userType() == QMetaType::QStringList) {
259 QStringList l;
260 l.reserve(a.size());
261 std::transform(a.begin(), a.end(), std::back_inserter(l), [](const auto &v) { return v.toString(); });
262 mp.writeOnGadget(backend, l);
263 } else {
264 mp.writeOnGadget(backend, it.value().toArray());
265 }
266 } else {
267 mp.writeOnGadget(backend, it.value().toVariant());
268 }
269 }
270
271 const auto attrObj = obj.value("attribution"_L1).toObject();
272 const auto attr = Attribution::fromJson(attrObj);
273 backend->setAttribution(attr);
274
275 const auto tzId = obj.value("timezone"_L1).toString();
276 if (!tzId.isEmpty()) {
277 QTimeZone tz(tzId.toUtf8());
278 if (tz.isValid()) {
279 backend->setTimeZone(tz);
280 } else {
281 qCWarning(Log) << "Invalid timezone:" << tzId;
282 }
283 }
284
285 const auto langArray = obj.value("supportedLanguages"_L1).toArray();
286 QStringList langs;
287 langs.reserve(langArray.size());
288 std::transform(langArray.begin(), langArray.end(), std::back_inserter(langs), [](const auto &v) { return v.toString(); });
289 backend->setSupportedLanguages(langs);
290}
291
292template<typename T> std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &obj)
293{
294 std::unique_ptr<AbstractBackend> backend(new T);
295 applyBackendOptions(backend.get(), &T::staticMetaObject, obj);
296 return backend;
297}
298
299bool ManagerPrivate::shouldSkipBackend(const Backend &backend) const
300{
301 if (!backend.isSecure() && !m_allowInsecure) {
302 qCDebug(Log) << "Skipping insecure backend:" << backend.identifier();
303 return true;
304 }
305 return !q->isBackendEnabled(backend.identifier());
306}
307
308template <typename RequestT>
309bool ManagerPrivate::shouldSkipBackend(const Backend &backend, const RequestT &req) const
310{
311 if (!req.backendIds().isEmpty() && !req.backendIds().contains(backend.identifier())) {
312 //qCDebug(Log) << "Skipping backend" << backend.identifier() << "due to explicit request";
313 return true;
314 }
315 return shouldSkipBackend(backend);
316}
317
318// IMPORTANT callback must not be called directly, but only via queued invocation,
319// our callers rely on that to not mess up sync/async response handling
320void ManagerPrivate::resolveLocation(LocationRequest &&locReq, const AbstractBackend *backend, const std::function<void(const Location&)> &callback)
321{
322 // apply all changes to locReq *before* we call cacheKey() on it!
323 locReq.setMaximumResults(1);
324
325 // check if this location query is cached already
326 const auto cacheEntry = Cache::lookupLocation(backend->backendId(), locReq.cacheKey());
327 switch (cacheEntry.type) {
328 case CacheHitType::Negative:
329 QTimer::singleShot(0, q, [callback]() { callback({}); });
330 return;
331 case CacheHitType::Positive:
332 if (!cacheEntry.data.empty()) {
333 const auto loc = cacheEntry.data[0];
334 QTimer::singleShot(0, q, [callback, loc]() { callback(loc); });
335 return;
336 }
337 break;
338 case CacheHitType::Miss:
339 break;
340 }
341
342 // actually do the location query
343 auto locReply = new LocationReply(locReq, q);
344 if (backend->queryLocation(locReq, locReply, nam())) {
345 locReply->setPendingOps(1);
346 } else {
347 locReply->setPendingOps(0);
348 }
349 QObject::connect(locReply, &Reply::finished, q, [callback, locReply]() {
350 locReply->deleteLater();
351 if (locReply->result().empty()) {
352 callback({});
353 } else {
354 callback(locReply->result()[0]);
355 }
356 });
357}
358
359static Location::Types locationTypesForJourneyRequest(const JourneyRequest &req)
360{
361 Location::Types t = Location::Place;
362 if (req.modes() & JourneySection::PublicTransport) {
363 t |= Location::Stop;
364 }
367 }
368 return t;
369}
370
371bool ManagerPrivate::queryJourney(const AbstractBackend* backend, const JourneyRequest &req, JourneyReply *reply)
372{
373 auto cache = Cache::lookupJourney(backend->backendId(), req.cacheKey());
374 switch (cache.type) {
375 case CacheHitType::Negative:
376 qCDebug(Log) << "Negative cache hit for backend" << backend->backendId();
377 return false;
378 case CacheHitType::Positive:
379 qCDebug(Log) << "Positive cache hit for backend" << backend->backendId();
380 reply->addAttributions(std::move(cache.attributions));
381 reply->addResult(backend, std::move(cache.data));
382 return false;
383 case CacheHitType::Miss:
384 qCDebug(Log) << "Cache miss for backend" << backend->backendId();
385 break;
386 }
387
388 // resolve locations if needed
389 if (backend->needsLocationQuery(req.from(), AbstractBackend::QueryType::Journey)) {
390 LocationRequest fromReq(req.from());
391 fromReq.setTypes(locationTypesForJourneyRequest(req));
392 resolveLocation(std::move(fromReq), backend, [reply, backend, req, this](const Location &loc) {
393 auto jnyRequest = req;
394 const auto fromLoc = Location::merge(jnyRequest.from(), loc);
395 jnyRequest.setFrom(fromLoc);
396
397 if (backend->needsLocationQuery(jnyRequest.to(), AbstractBackend::QueryType::Journey)) {
398 LocationRequest toReq(jnyRequest.to());
399 toReq.setTypes(locationTypesForJourneyRequest(req));
400 resolveLocation(std::move(toReq), backend, [jnyRequest, reply, backend, this](const Location &loc) {
401 auto jnyReq = jnyRequest;
402 const auto toLoc = Location::merge(jnyRequest.to(), loc);
403 jnyReq.setTo(toLoc);
404 if (!backend->queryJourney(jnyReq, reply, nam())) {
405 reply->addError(Reply::NotFoundError, {});
406 }
407 });
408
409 return;
410 }
411
412 if (!backend->queryJourney(jnyRequest, reply, nam())) {
413 reply->addError(Reply::NotFoundError, {});
414 }
415 });
416
417 return true;
418 }
419
420 if (backend->needsLocationQuery(req.to(), AbstractBackend::QueryType::Journey)) {
421 LocationRequest toReq(req.to());
422 toReq.setTypes(locationTypesForJourneyRequest(req));
423 resolveLocation(std::move(toReq), backend, [req, toReq, reply, backend, this](const Location &loc) {
424 const auto toLoc = Location::merge(req.to(), loc);
425 auto jnyRequest = req;
426 jnyRequest.setTo(toLoc);
427 if (!backend->queryJourney(jnyRequest, reply, nam())) {
428 reply->addError(Reply::NotFoundError, {});
429 }
430 });
431 return true;
432 }
433
434 return backend->queryJourney(req, reply, nam());
435}
436
437bool ManagerPrivate::queryStopover(const AbstractBackend *backend, const StopoverRequest &req, StopoverReply *reply)
438{
439 auto cache = Cache::lookupStopover(backend->backendId(), req.cacheKey());
440 switch (cache.type) {
441 case CacheHitType::Negative:
442 qCDebug(Log) << "Negative cache hit for backend" << backend->backendId();
443 return false;
444 case CacheHitType::Positive:
445 qCDebug(Log) << "Positive cache hit for backend" << backend->backendId();
446 reply->addAttributions(std::move(cache.attributions));
447 reply->addResult(backend, std::move(cache.data));
448 return false;
449 case CacheHitType::Miss:
450 qCDebug(Log) << "Cache miss for backend" << backend->backendId();
451 break;
452 }
453
454 // check if we first need to resolve the location first
455 if (backend->needsLocationQuery(req.stop(), AbstractBackend::QueryType::Departure)) {
456 qCDebug(Log) << "Backend needs location query first:" << backend->backendId();
457 LocationRequest locReq(req.stop());
458 locReq.setTypes(Location::Stop); // Stopover can never refer to other location types
459 locReq.setMaximumDistance(250);
460 resolveLocation(std::move(locReq), backend, [reply, req, backend, this](const Location &loc) {
461 const auto depLoc = Location::merge(req.stop(), loc);
462 auto depRequest = req;
463 depRequest.setStop(depLoc);
464 if (!backend->queryStopover(depRequest, reply, nam())) {
465 reply->addError(Reply::NotFoundError, {});
466 }
467 });
468 return true;
469 }
470
471 return backend->queryStopover(req, reply, nam());
472}
473
474void ManagerPrivate::readCachedAttributions()
475{
476 if (m_hasReadCachedAttributions) {
477 return;
478 }
479
480 Cache::allCachedAttributions(m_attributions);
481 m_hasReadCachedAttributions = true;
482}
483
484template<typename RepT, typename ReqT>
485RepT* ManagerPrivate::makeReply(const ReqT &request)
486{
487 auto reply = new RepT(request, q);
488 QObject::connect(reply, &Reply::finished, q, [this, reply]() {
489 AttributionUtil::merge(m_attributions, reply->attributions());
490 });
491 return reply;
492}
493
494
495
496Manager::Manager(QObject *parent)
497 : QObject(parent)
498 , d(new ManagerPrivate)
499{
500 initResources();
501 qRegisterMetaType<Disruption::Effect>();
502 d->q = this;
503
504 if (!AssetRepository::instance()) {
505 auto assetRepo = new AssetRepository(this);
506 assetRepo->setNetworkAccessManagerProvider(std::bind(&ManagerPrivate::nam, d.get()));
507 }
508
509 Cache::expire();
510
512}
513
514Manager::~Manager() = default;
515
517{
518 if (d->m_nam == nam) {
519 return;
520 }
521
522 if (d->m_nam && d->m_nam->parent() == this) {
523 delete d->m_nam;
524 }
525
526 d->m_nam = nam;
527}
528
530{
531 return d->m_allowInsecure;
532}
533
535{
536 if (d->m_allowInsecure == insecure) {
537 return;
538 }
539 d->m_allowInsecure = insecure;
540 Q_EMIT configurationChanged();
541}
542
544{
545 auto reply = d->makeReply<JourneyReply>(req);
546 int pendingOps = 0;
547
548 // validate input
549 req.validate();
550 if (!req.isValid()) {
551 reply->addError(Reply::InvalidRequest, {});
552 reply->setPendingOps(pendingOps);
553 return reply;
554 }
555
556 d->loadNetworks();
557
558 // first time/direct query
559 if (req.contexts().empty()) {
560 QSet<QString> triedBackends;
561 bool foundNonGlobalCoverage = false;
562 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
563 const auto checkBackend = [&](const Backend &backend, bool bothLocationMatch) {
564 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
565 return;
566 }
567 const auto coverage = backend.coverageArea(coverageType);
568 if (coverage.isEmpty()) {
569 return;
570 }
571
572 if (bothLocationMatch) {
573 if (!coverage.coversLocation(req.from()) || !coverage.coversLocation(req.to())) {
574 return;
575 }
576 } else {
577 if (!coverage.coversLocation(req.from()) && !coverage.coversLocation(req.to())) {
578 return;
579 }
580 }
581
582 triedBackends.insert(backend.identifier());
583 foundNonGlobalCoverage |= !coverage.isGlobal();
584
585 if (d->queryJourney(BackendPrivate::impl(backend), req, reply)) {
586 ++pendingOps;
587 }
588 };
589
590 // look for coverage areas which contain both locations first
591 for (const auto &backend: d->m_backends) {
592 checkBackend(backend, true);
593 }
594 if (pendingOps && foundNonGlobalCoverage) {
595 break;
596 }
597
598 // if we didn't find one, try with just a single one
599 for (const auto &backend: d->m_backends) {
600 checkBackend(backend, false);
601 }
602 if (pendingOps && foundNonGlobalCoverage) {
603 break;
604 }
605 }
606
607 // subsequent earlier/later query
608 } else {
609 for (const auto &context : req.contexts()) {
610 // backend supports this itself
611 if ((context.type == RequestContext::Next && context.backend->hasCapability(AbstractBackend::CanQueryNextJourney))
612 ||(context.type == RequestContext::Previous && context.backend->hasCapability(AbstractBackend::CanQueryPreviousJourney)))
613 {
614 if (d->queryJourney(context.backend, req, reply)) {
615 ++pendingOps;
616 continue;
617 }
618 }
619
620 // backend doesn't support this, let's try to emulate
621 if (context.type == RequestContext::Next && req.dateTimeMode() == JourneyRequest::Departure) {
622 auto r = req;
623 r.setDepartureTime(context.dateTime);
624 if (d->queryJourney(context.backend, r, reply)) {
625 ++pendingOps;
626 continue;
627 }
628 } else if (context.type == RequestContext::Previous && req.dateTimeMode() == JourneyRequest::Departure) {
629 auto r = req;
630 r.setArrivalTime(context.dateTime);
631 if (d->queryJourney(context.backend, r, reply)) {
632 ++pendingOps;
633 continue;
634 }
635 }
636 }
637 }
638
639 if (req.downloadAssets()) {
640 reply->addAttributions(AssetRepository::instance()->attributions());
641 }
642
643 if (pendingOps == 0) {
644 reply->addError(Reply::NoBackend, u"No viable backend found."_s);
645 }
646 reply->setPendingOps(pendingOps);
647 return reply;
648}
649
651{
652 auto reply = d->makeReply<StopoverReply>(req);
653 int pendingOps = 0;
654
655 // validate input
656 if (!req.isValid()) {
657 reply->addError(Reply::InvalidRequest, {});
658 reply->setPendingOps(pendingOps);
659 return reply;
660 }
661
662 d->loadNetworks();
663
664 // first time/direct query
665 if (req.contexts().empty()) {
666 QSet<QString> triedBackends;
667 bool foundNonGlobalCoverage = false;
668 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
669 for (const auto &backend: d->m_backends) {
670 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
671 continue;
672 }
673 if (req.mode() == StopoverRequest::QueryArrival && (BackendPrivate::impl(backend)->capabilities() & AbstractBackend::CanQueryArrivals) == 0) {
674 qCDebug(Log) << "Skipping backend due to not supporting arrival queries:" << backend.identifier();
675 continue;
676 }
677 const auto coverage = backend.coverageArea(coverageType);
678 if (coverage.isEmpty() || !coverage.coversLocation(req.stop())) {
679 continue;
680 }
681 triedBackends.insert(backend.identifier());
682 foundNonGlobalCoverage |= !coverage.isGlobal();
683
684 if (d->queryStopover(BackendPrivate::impl(backend), req, reply)) {
685 ++pendingOps;
686 }
687 }
688
689 if (pendingOps && foundNonGlobalCoverage) {
690 break;
691 }
692 }
693
694 // subsequent earlier/later query
695 } else {
696 for (const auto &context : req.contexts()) {
697 // backend supports this itself
698 if ((context.type == RequestContext::Next && context.backend->hasCapability(AbstractBackend::CanQueryNextDeparture))
699 ||(context.type == RequestContext::Previous && context.backend->hasCapability(AbstractBackend::CanQueryPreviousDeparture)))
700 {
701 if (d->queryStopover(context.backend, req, reply)) {
702 ++pendingOps;
703 continue;
704 }
705 }
706
707 // backend doesn't support this, let's try to emulate
708 if (context.type == RequestContext::Next) {
709 auto r = req;
710 r.setDateTime(context.dateTime);
711 if (d->queryStopover(context.backend, r, reply)) {
712 ++pendingOps;
713 continue;
714 }
715 }
716 }
717 }
718
719 if (req.downloadAssets()) {
720 reply->addAttributions(AssetRepository::instance()->attributions());
721 }
722
723 if (pendingOps == 0) {
724 reply->addError(Reply::NoBackend, u"No viable backend found."_s);
725 }
726 reply->setPendingOps(pendingOps);
727 return reply;
728}
729
730int ManagerPrivate::queryLocationOnBackend(const LocationRequest &req, LocationReply *reply, const Backend &backend)
731{
732 auto cache = Cache::lookupLocation(backend.identifier(), req.cacheKey());
733 switch (cache.type) {
734 case CacheHitType::Negative:
735 qCDebug(Log) << "Negative cache hit for backend" << backend.identifier();
736 break;
737 case CacheHitType::Positive:
738 qCDebug(Log) << "Positive cache hit for backend" << backend.identifier();
739 reply->addAttributions(std::move(cache.attributions));
740 reply->addResult(std::move(cache.data));
741 break;
742 case CacheHitType::Miss:
743 qCDebug(Log) << "Cache miss for backend" << backend.identifier();
744 reply->addAttribution(BackendPrivate::impl(backend)->attribution());
745 if (BackendPrivate::impl(backend)->queryLocation(req, reply, nam())) {
746 return 1;
747 }
748 break;
749 }
750
751 return 0;
752}
753
755{
756 auto reply = d->makeReply<LocationReply>(req);
757 int pendingOps = 0;
758
759 // validate input
760 if (!req.isValid()) {
761 reply->addError(Reply::InvalidRequest, {});
762 reply->setPendingOps(pendingOps);
763 return reply;
764 }
765
766 d->loadNetworks();
767
768 QSet<QString> triedBackends;
769 bool foundNonGlobalCoverage = false;
770 const auto loc = req.location();
771 const auto isCountryOnly = !loc.hasCoordinate() && !loc.country().isEmpty() && loc.region().isEmpty();
772 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
773 // pass 1: coordinate-based coverage, or nationwide country coverage
774 for (const auto &backend : d->m_backends) {
775 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
776 continue;
777 }
778 const auto coverage = backend.coverageArea(coverageType);
779 if (coverage.isEmpty() || !coverage.coversLocation(loc)) {
780 continue;
781 }
782 if (isCountryOnly && !coverage.hasNationWideCoverage(loc.country())) {
783 continue;
784 }
785
786 triedBackends.insert(backend.identifier());
787 foundNonGlobalCoverage |= !coverage.isGlobal();
788 pendingOps += d->queryLocationOnBackend(req, reply, backend);
789 }
790 if (pendingOps && foundNonGlobalCoverage) {
791 break;
792 }
793
794 // pass 2: any country match
795 for (const auto &backend : d->m_backends) {
796 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
797 continue;
798 }
799 const auto coverage = backend.coverageArea(coverageType);
800 if (coverage.isEmpty() || !coverage.coversLocation(loc)) {
801 continue;
802 }
803
804 triedBackends.insert(backend.identifier());
805 foundNonGlobalCoverage |= !coverage.isGlobal();
806 pendingOps += d->queryLocationOnBackend(req, reply, backend);
807 }
808 if (pendingOps && foundNonGlobalCoverage) {
809 break;
810 }
811 }
812
813 if (pendingOps == 0) {
814 reply->addError(Reply::NoBackend, u"No viable backend found."_s);
815 }
816 reply->setPendingOps(pendingOps);
817 return reply;
818}
819
821{
822 auto reply = d->makeReply<VehicleLayoutReply>(req);
823 int pendingOps = 0;
824
825 // validate input
826 if (!req.isValid()) {
827 reply->addError(Reply::InvalidRequest, {});
828 reply->setPendingOps(pendingOps);
829 return reply;
830 }
831
832 d->loadNetworks();
833
834 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular }) {
835 for (const auto &backend : d->m_backends) {
836 if (d->shouldSkipBackend(backend, req)) {
837 continue;
838 }
839 const auto coverage = backend.coverageArea(coverageType);
840 if (coverage.isEmpty() || !coverage.coversLocation(req.stopover().stopPoint())) {
841 continue;
842 }
843 reply->addAttribution(BackendPrivate::impl(backend)->attribution());
844
845 auto cache = Cache::lookupVehicleLayout(backend.identifier(), req.cacheKey());
846 switch (cache.type) {
847 case CacheHitType::Negative:
848 qCDebug(Log) << "Negative cache hit for backend" << backend.identifier();
849 break;
850 case CacheHitType::Positive:
851 qCDebug(Log) << "Positive cache hit for backend" << backend.identifier();
852 if (cache.data.size() == 1) {
853 reply->addAttributions(std::move(cache.attributions));
854 reply->addResult(cache.data[0]);
855 break;
856 }
857 [[fallthrough]];
858 case CacheHitType::Miss:
859 qCDebug(Log) << "Cache miss for backend" << backend.identifier();
860 if (BackendPrivate::impl(backend)->queryVehicleLayout(req, reply, d->nam())) {
861 ++pendingOps;
862 }
863 break;
864 }
865 }
866 if (pendingOps) {
867 break;
868 }
869 }
870
871 if (pendingOps == 0) {
872 reply->addError(Reply::NoBackend, u"No viable backend found."_s);
873 }
874 reply->setPendingOps(pendingOps);
875 return reply;
876}
877
879{
880 if (d->m_backends.empty()) { // not loaded yet, nothing to do
881 return;
882 }
883 d->m_backends.clear();
884 d->loadNetworks();
885 Q_EMIT backendsChanged();
886}
887
888const std::vector<Attribution>& Manager::attributions() const
889{
890 d->loadNetworks();
891 d->readCachedAttributions();
892 return d->m_attributions;
893}
894
895QVariantList Manager::attributionsVariant() const
896{
897 d->loadNetworks();
898 d->readCachedAttributions();
899 QVariantList l;
900 l.reserve(d->m_attributions.size());
901 std::transform(d->m_attributions.begin(), d->m_attributions.end(), std::back_inserter(l), [](const auto &attr) { return QVariant::fromValue(attr); });
902 return l;
903}
904
905const std::vector<Backend>& Manager::backends() const
906{
907 d->loadNetworks();
908 return d->m_backends;
909}
910
911bool Manager::isBackendEnabled(const QString &backendId) const
912{
913 if (std::binary_search(d->m_disabledBackends.cbegin(), d->m_disabledBackends.cend(), backendId)) {
914 return false;
915 }
916 if (std::binary_search(d->m_enabledBackends.cbegin(), d->m_enabledBackends.cend(), backendId)) {
917 return true;
918 }
919
920 return d->m_backendsEnabledByDefault;
921}
922
923static void sortedInsert(QStringList &l, const QString &value)
924{
925 const auto it = std::lower_bound(l.begin(), l.end(), value);
926 if (it == l.end() || (*it) != value) {
927 l.insert(it, value);
928 }
929}
930
931static void sortedRemove(QStringList &l, const QString &value)
932{
933 const auto it = std::lower_bound(l.begin(), l.end(), value);
934 if (it != l.end() && (*it) == value) {
935 l.erase(it);
936 }
937}
938
939void Manager::setBackendEnabled(const QString &backendId, bool enabled)
940{
941 if (enabled) {
942 sortedInsert(d->m_enabledBackends, backendId);
943 sortedRemove(d->m_disabledBackends, backendId);
944 } else {
945 sortedRemove(d->m_enabledBackends, backendId);
946 sortedInsert(d->m_disabledBackends, backendId);
947 }
948 Q_EMIT configurationChanged();
949}
950
952{
953 return d->m_enabledBackends;
954}
955
957{
958 QSignalBlocker blocker(this); // no change signals during settings restore
959 for (const auto &backendId : backendIds) {
960 setBackendEnabled(backendId, true);
961 }
962}
963
965{
966 return d->m_disabledBackends;
967}
968
970{
971 QSignalBlocker blocker(this); // no change signals during settings restore
972 for (const auto &backendId : backendIds) {
973 setBackendEnabled(backendId, false);
974 }
975}
976
978{
979 return d->m_backendsEnabledByDefault;
980}
981
983{
984 d->m_backendsEnabledByDefault = byDefault;
985
986 Q_EMIT configurationChanged();
987}
988
989QVariantList Manager::backendsVariant() const
990{
991 d->loadNetworks();
992 QVariantList l;
993 l.reserve(d->m_backends.size());
994 std::transform(d->m_backends.begin(), d->m_backends.end(), std::back_inserter(l), [](const auto &b) { return QVariant::fromValue(b); });
995 return l;
996}
997
998bool Manager::eventFilter(QObject *object, QEvent *event)
999{
1001 reload();
1002 }
1003
1004 return QObject::eventFilter(object, event);
1005}
1006
1007#include "moc_manager.cpp"
static Attribution fromJson(const QJsonObject &obj)
Deserialize an Attribution object from JSON.
Information about a backend service queried for location/departure/journey data.
Definition backend.h:22
bool isSecure
Supports secrure network access.
Definition backend.h:35
QString identifier
Internal identifier of this backend.
Definition backend.h:27
Journey query response.
Describes a journey search.
KPublicTransport::Location to
The journey destination.
void setArrivalTime(const QDateTime &dt)
Sets the desired arrival time.
void setDepartureTime(const QDateTime &dt)
Set the desired departure time.
bool downloadAssets
Download graphic assets such as line logos for the data requested here.
@ Departure
dateTime() represents the desired departure time.
bool isValid() const
Returns true if this is a valid request, that is, it has enough parameters set to perform a query.
QString cacheKey() const
Unique string representation used for caching results.
KPublicTransport::Location from
The starting point of the journey search.
KPublicTransport::JourneySection::Modes modes
Modes of transportation that should be considered for this query.
DateTimeMode dateTimeMode
Controls whether to search for journeys starting or ending at the given time.
@ RentedVehicle
free floating or dock-based rental bike service, electric scooters, car sharing services,...
Definition journey.h:45
Describes a location search.
bool isValid() const
Returns true if this is a valid request, that is it has enough parameters set to perform a query.
QString cacheKey() const
Unique string representation used for caching results.
KPublicTransport::Location location
Location object containing the search parameters.
QString region
Region (as in ISO 3166-2) of the location, if known.
Definition location.h:65
@ RentedVehicleStation
a pick-up/drop-off point for dock-based rental bike/scooter systems
Definition location.h:38
@ Place
a location that isn't of any specific type
Definition location.h:36
@ Stop
a public transport stop (train station, bus stop, etc)
Definition location.h:37
static Location merge(const Location &lhs, const Location &rhs)
Merge two departure instances.
Definition location.cpp:407
QString country
Country of the location as ISO 3166-1 alpha 2 code, if known.
Definition location.h:67
LocationReply * queryLocation(const LocationRequest &req) const
Query location information based on coordinates or (parts of) the name.
Definition manager.cpp:754
JourneyReply * queryJourney(const JourneyRequest &req) const
Query a journey.
Definition manager.cpp:543
void setBackendsEnabledByDefault(bool byDefault)
Set wheter backends are enabled by default.
Definition manager.cpp:982
void setEnabledBackends(const QStringList &backendIds)
Sets the explicitly enabled backends.
Definition manager.cpp:956
void setBackendEnabled(const QString &backendId, bool enabled)
Sets whether the backend with the given identifier should be used.
Definition manager.cpp:939
void reload()
Reload backend configuration.
Definition manager.cpp:878
StopoverReply * queryStopover(const StopoverRequest &req) const
Query arrivals or departures from a specific station.
Definition manager.cpp:650
QStringList disabledBackends
Definition manager.h:52
Q_INVOKABLE bool isBackendEnabled(const QString &backendId) const
Returns whether the use of the backend with a given identifier is enabled.
Definition manager.cpp:911
void setDisabledBackends(const QStringList &backendIds)
Sets the explicitly disabled backends.
Definition manager.cpp:969
VehicleLayoutReply * queryVehicleLayout(const VehicleLayoutRequest &req) const
Query vehicle and platform layout information.
Definition manager.cpp:820
QVariantList backends
QML-compatible access to backends().
Definition manager.h:57
bool allowInsecureBackends
Allow usage of insecure backends (default: off).
Definition manager.h:47
void setNetworkAccessManager(QNetworkAccessManager *nam)
Set the network access manager to use for network operations.
Definition manager.cpp:516
void setAllowInsecureBackends(bool insecure)
Allow usage of insecure backends, that is services not using transport encryption.
Definition manager.cpp:534
QStringList enabledBackends
Definition manager.h:50
QVariantList attributions
QML-compatible access to attributions().
Definition manager.h:45
void finished()
Emitted whenever the corresponding search has been completed.
const std::vector< Attribution > & attributions() const
Returns the attributions for the provided data.
Definition reply.cpp:84
@ InvalidRequest
Incomplete or otherwise invalid request.
Definition reply.h:37
@ NoBackend
No backend was found to satisfy this request, e.g. due to no backend covering the requested area.
Definition reply.h:38
@ NotFoundError
The requested journey/departure/place could not be found.
Definition reply.h:36
Departure or arrival query reply.
Describes an arrival or departure search.
@ QueryArrival
Search for arrivals.
bool downloadAssets
Enable downloading of graphic assets such as line logos for the data requested here.
bool isValid() const
Returns true if this is a valid request, ie.
QString cacheKey() const
Unique string representation used for caching results.
KPublicTransport::Location stop
The location at which to search for departures/arrivals.
Mode mode
Controls whether to search for arrivals or departures.
KPublicTransport::Location stopPoint
The stop point of this departure.
Definition stopover.h:64
Reply to a vehicle layout query.
Describes a query for vehicle layout information.
QString cacheKey() const
Unique string representation used for caching results.
bool isValid() const
Returns true if this is a valid request, that is it has enough parameters set to perform a query.
KPublicTransport::Stopover stopover
The stopover vehicle and platform layout information are requested for.
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
Query operations and data types for accessing realtime public transport information from online servi...
QCoreApplication * instance()
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonValue value(QLatin1StringView key) const const
QJsonArray toArray() const const
QJsonObject toObject() const const
QString toString() const const
iterator begin()
iterator end()
iterator erase(const_iterator begin, const_iterator end)
iterator insert(const_iterator before, parameter_type value)
void push_back(parameter_type value)
void reserve(qsizetype size)
int indexOfProperty(const char *name) const const
QMetaProperty property(int index) const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
virtual bool event(QEvent *e)
virtual bool eventFilter(QObject *watched, QEvent *event)
void installEventFilter(QObject *filterObj)
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
QStringList standardLocations(StandardLocation type)
QString writableLocation(StandardLocation type)
bool isEmpty() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Jan 24 2025 11:50:52 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.