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 "tripreply.h"
19#include "triprequest.h"
20#include "vehiclelayoutrequest.h"
21#include "vehiclelayoutreply.h"
22#include "datatypes/attributionutil_p.h"
23#include "datatypes/backend.h"
24#include "datatypes/backend_p.h"
25#include "datatypes/disruption.h"
26#include "datatypes/json_p.h"
27#include "geo/geojson_p.h"
28
29#include <KPublicTransport/Journey>
30#include <KPublicTransport/Location>
31#include <KPublicTransport/Stopover>
32
33#include "backends/accessibilitycloudbackend.h"
34#include "backends/cache.h"
35#include "backends/deutschebahnbackend.h"
36#include "backends/efabackend.h"
37#include "backends/hafasmgatebackend.h"
38#include "backends/hafasquerybackend.h"
39#include "backends/ivvassbackend.h"
40#include "backends/motis2backend.h"
41#include "backends/navitiabackend.h"
42#include "backends/oebbbackend.h"
43#include "backends/openjourneyplannerbackend.h"
44#include "backends/opentripplannergraphqlbackend.h"
45#include "backends/opentripplannerrestbackend.h"
46#include "backends/ltglinkbackend.h"
47#include "gbfs/gbfsbackend.h"
48
49#include <QCoreApplication>
50#include <QDirIterator>
51#include <QJsonArray>
52#include <QJsonDocument>
53#include <QJsonObject>
54#include <QMetaProperty>
55#include <QNetworkAccessManager>
56#include <QStandardPaths>
57#include <QTimer>
58#include <QTimeZone>
59
60#include <functional>
61
62using namespace Qt::Literals::StringLiterals;
63using namespace KPublicTransport;
64
65static inline void initResources() {
66 Q_INIT_RESOURCE(asset_attributions);
67 Q_INIT_RESOURCE(gbfs);
68 Q_INIT_RESOURCE(geometry);
69 Q_INIT_RESOURCE(images);
70 Q_INIT_RESOURCE(networks);
71 Q_INIT_RESOURCE(network_certs);
72 Q_INIT_RESOURCE(otp);
73 Q_INIT_RESOURCE(stations);
74}
75
76namespace KPublicTransport {
77class ManagerPrivate {
78public:
79 [[nodiscard]] QNetworkAccessManager* nam();
80 void loadNetworks();
81 [[nodiscard]] std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &obj);
82 template <typename Backend, typename Backend2, typename ...Backends>
83 [[nodiscard]] static std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &backendType, const QJsonObject &obj);
84 template <typename Backend>
85 [[nodiscard]] static std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &backendType, const QJsonObject &obj);
86 template <typename T>
87 [[nodiscard]] static std::unique_ptr<AbstractBackend> loadNetwork(const QJsonObject &obj);
88
89 template <typename RequestT>
90 [[nodiscard]] bool shouldSkipBackend(const Backend &backend, const RequestT &req) const;
91
92 void resolveLocation(LocationRequest &&locReq, const AbstractBackend *backend, const std::function<void(const Location &loc)> &callback);
93 [[nodiscard]] bool queryJourney(const AbstractBackend *backend, const JourneyRequest &req, JourneyReply *reply);
94 [[nodiscard]] bool queryStopover(const AbstractBackend *backend, const StopoverRequest &req, StopoverReply *reply);
95
96 template <typename RepT, typename ReqT>
97 [[nodiscard]] RepT* makeReply(const ReqT &request);
98
99 void readCachedAttributions();
100
101 [[nodiscard]] int queryLocationOnBackend(const LocationRequest &req, LocationReply *reply, const Backend &backend);
102
103 Manager *q = nullptr;
104 QNetworkAccessManager *m_nam = nullptr;
105 std::vector<Backend> m_backends;
106 std::vector<Attribution> m_attributions;
107
108 // we store both explicitly to have a third state, backends with the enabled state being the "default" (whatever that might eventually be)
109 QStringList m_enabledBackends;
110 QStringList m_disabledBackends;
111
112 bool m_allowInsecure = false;
113 bool m_hasReadCachedAttributions = false;
114 bool m_backendsEnabledByDefault = true;
115
116private:
117 [[nodiscard]] bool shouldSkipBackend(const Backend &backend) const;
118};
119}
120
121QNetworkAccessManager* ManagerPrivate::nam()
122{
123 if (!m_nam) {
124 m_nam = new QNetworkAccessManager(q);
125 m_nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
126 m_nam->setStrictTransportSecurityEnabled(true);
127 m_nam->enableStrictTransportSecurityStore(true, QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/org.kde.kpublictransport/hsts/"_L1);
128 }
129 return m_nam;
130}
131
132
133void ManagerPrivate::loadNetworks()
134{
135 if (!m_backends.empty()) {
136 return;
137 }
138
139 QStringList searchDirs;
140#ifndef Q_OS_ANDROID
142#endif
143 searchDirs.push_back(u":/"_s);
144
145 std::vector<Attribution> attributions;
146 for (const auto &searchDir : searchDirs) {
147 QDirIterator it(searchDir + "/org.kde.kpublictransport/networks"_L1, {u"*.json"_s}, QDir::Files);
148 while (it.hasNext()) {
149 it.next();
150 const auto id = it.fileInfo().baseName();
151 if (std::any_of(m_backends.begin(), m_backends.end(), [&id](const auto &backend) { return backend.identifier() == id; })) {
152 // already seen in another location
153 continue;
154 }
155
156 QFile f(it.filePath());
157 if (!f.open(QFile::ReadOnly)) {
158 qCWarning(Log) << "Failed to open public transport network configuration:" << f.errorString();
159 continue;
160 }
161
162 QJsonParseError error;
163 const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
164 if (error.error != QJsonParseError::NoError) {
165 qCWarning(Log) << "Failed to parse public transport network configuration:" << error.errorString() << it.fileName();
166 continue;
167 }
168
169 auto net = loadNetwork(doc.object());
170 if (net) {
171 net->setBackendId(id);
172 net->init();
173 if (!net->attribution().isEmpty()) {
174 attributions.push_back(net->attribution());
175 }
176
177 auto b = BackendPrivate::fromJson(doc.object());
178 BackendPrivate::setImpl(b, std::move(net));
179 m_backends.push_back(std::move(b));
180 } else {
181 qCWarning(Log) << "Failed to load public transport network configuration config:" << it.fileName();
182 }
183 }
184 }
185
186 std::stable_sort(m_backends.begin(), m_backends.end(), [](const auto &lhs, const auto &rhs) {
187 return lhs.identifier() < rhs.identifier();
188 });
189
190 AttributionUtil::sort(attributions);
191 if (m_attributions.empty()) {
192 // first load
193 m_attributions = std::move(attributions);
194 } else {
195 // reload
196 AttributionUtil::merge(m_attributions, attributions);
197 }
198
199 qCDebug(Log) << m_backends.size() << "public transport network configurations loaded";
200}
201
202std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &obj)
203{
204 const auto type = obj.value("type"_L1).toObject();
205 // backends need to be topologically sorted according to their preference/priority here
206 return loadNetwork<
207 NavitiaBackend,
208 OpenTripPlannerGraphQLBackend,
209 OpenTripPlannerRestBackend,
210 DeutscheBahnBackend,
211 OebbBackend,
212 HafasMgateBackend,
213 HafasQueryBackend,
214 EfaBackend,
215 IvvAssBackend,
216 OpenJourneyPlannerBackend,
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 // FIXME this is not correct for negative cache hits!
644 //if (pendingOps == 0) {
645 // reply->addError(Reply::NoBackend, u"No viable backend found."_s);
646 //}
647 reply->setPendingOps(pendingOps);
648 return reply;
649}
650
652{
653 auto reply = d->makeReply<StopoverReply>(req);
654 int pendingOps = 0;
655
656 // validate input
657 if (!req.isValid()) {
658 reply->addError(Reply::InvalidRequest, {});
659 reply->setPendingOps(pendingOps);
660 return reply;
661 }
662
663 d->loadNetworks();
664
665 // first time/direct query
666 if (req.contexts().empty()) {
667 QSet<QString> triedBackends;
668 bool foundNonGlobalCoverage = false;
669 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
670 for (const auto &backend: d->m_backends) {
671 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
672 continue;
673 }
674 if (req.mode() == StopoverRequest::QueryArrival && (BackendPrivate::impl(backend)->capabilities() & AbstractBackend::CanQueryArrivals) == 0) {
675 qCDebug(Log) << "Skipping backend due to not supporting arrival queries:" << backend.identifier();
676 continue;
677 }
678 const auto coverage = backend.coverageArea(coverageType);
679 if (coverage.isEmpty() || !coverage.coversLocation(req.stop())) {
680 continue;
681 }
682 triedBackends.insert(backend.identifier());
683 foundNonGlobalCoverage |= !coverage.isGlobal();
684
685 if (d->queryStopover(BackendPrivate::impl(backend), req, reply)) {
686 ++pendingOps;
687 }
688 }
689
690 if (pendingOps && foundNonGlobalCoverage) {
691 break;
692 }
693 }
694
695 // subsequent earlier/later query
696 } else {
697 for (const auto &context : req.contexts()) {
698 // backend supports this itself
699 if ((context.type == RequestContext::Next && context.backend->hasCapability(AbstractBackend::CanQueryNextDeparture))
700 ||(context.type == RequestContext::Previous && context.backend->hasCapability(AbstractBackend::CanQueryPreviousDeparture)))
701 {
702 if (d->queryStopover(context.backend, req, reply)) {
703 ++pendingOps;
704 continue;
705 }
706 }
707
708 // backend doesn't support this, let's try to emulate
709 if (context.type == RequestContext::Next) {
710 auto r = req;
711 r.setDateTime(context.dateTime);
712 if (d->queryStopover(context.backend, r, reply)) {
713 ++pendingOps;
714 continue;
715 }
716 }
717 }
718 }
719
720 if (req.downloadAssets()) {
721 reply->addAttributions(AssetRepository::instance()->attributions());
722 }
723
724 // FIXME this is not correct for negative cache hits!
725 //if (pendingOps == 0) {
726 // reply->addError(Reply::NoBackend, u"No viable backend found."_s);
727 //}
728 reply->setPendingOps(pendingOps);
729 return reply;
730}
731
732int ManagerPrivate::queryLocationOnBackend(const LocationRequest &req, LocationReply *reply, const Backend &backend)
733{
734 auto cache = Cache::lookupLocation(backend.identifier(), req.cacheKey());
735 switch (cache.type) {
736 case CacheHitType::Negative:
737 qCDebug(Log) << "Negative cache hit for backend" << backend.identifier();
738 break;
739 case CacheHitType::Positive:
740 qCDebug(Log) << "Positive cache hit for backend" << backend.identifier();
741 reply->addAttributions(std::move(cache.attributions));
742 reply->addResult(std::move(cache.data));
743 break;
744 case CacheHitType::Miss:
745 qCDebug(Log) << "Cache miss for backend" << backend.identifier();
746 reply->addAttribution(BackendPrivate::impl(backend)->attribution());
747 if (BackendPrivate::impl(backend)->queryLocation(req, reply, nam())) {
748 return 1;
749 }
750 break;
751 }
752
753 return 0;
754}
755
757{
758 auto reply = d->makeReply<LocationReply>(req);
759 int pendingOps = 0;
760
761 // validate input
762 if (!req.isValid()) {
763 reply->addError(Reply::InvalidRequest, {});
764 reply->setPendingOps(pendingOps);
765 return reply;
766 }
767
768 d->loadNetworks();
769
770 QSet<QString> triedBackends;
771 bool foundNonGlobalCoverage = false;
772 const auto loc = req.location();
773 const auto isCountryOnly = !loc.hasCoordinate() && !loc.country().isEmpty() && loc.region().isEmpty();
774 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
775 // pass 1: coordinate-based coverage, or nationwide country coverage
776 for (const auto &backend : d->m_backends) {
777 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
778 continue;
779 }
780 const auto coverage = backend.coverageArea(coverageType);
781 if (coverage.isEmpty() || !coverage.coversLocation(loc)) {
782 continue;
783 }
784 if (isCountryOnly && !coverage.hasNationWideCoverage(loc.country())) {
785 continue;
786 }
787
788 triedBackends.insert(backend.identifier());
789 foundNonGlobalCoverage |= !coverage.isGlobal();
790 pendingOps += d->queryLocationOnBackend(req, reply, backend);
791 }
792 if (pendingOps && foundNonGlobalCoverage) {
793 break;
794 }
795
796 // pass 2: any country match
797 for (const auto &backend : d->m_backends) {
798 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
799 continue;
800 }
801 const auto coverage = backend.coverageArea(coverageType);
802 if (coverage.isEmpty() || !coverage.coversLocation(loc)) {
803 continue;
804 }
805
806 triedBackends.insert(backend.identifier());
807 foundNonGlobalCoverage |= !coverage.isGlobal();
808 pendingOps += d->queryLocationOnBackend(req, reply, backend);
809 }
810 if (pendingOps && foundNonGlobalCoverage) {
811 break;
812 }
813 }
814
815 // FIXME this is not correct for negative cache hits!
816 //if (pendingOps == 0) {
817 // reply->addError(Reply::NoBackend, u"No viable backend found."_s);
818 //}
819 reply->setPendingOps(pendingOps);
820 return reply;
821}
822
823// Check whether a given JourneySection matches a trip request in fallback journey query mode
824// This is essentially the same as JourneySection::isSame, with one special case for accepting
825// all specialized modes for an requested generic mode. Line::isSame takes care of most of this
826// on its own already, we just need to handle all rail-based modes explicitly here.
827[[nodiscard]] static bool isSameTrip(KPublicTransport::JourneySection req, const KPublicTransport::JourneySection &res)
828{
829 if (req.route().line().mode() == Line::Train && Line::modeIsRailBound(res.route().line().mode())) {
830 auto route = req.route();
831 auto line = route.line();
832 line.setMode(res.route().line().mode());
833 route.setLine(line);
834 req.setRoute(route);
835 }
836 return JourneySection::isSame(req, res);
837}
838
840{
841 auto reply = d->makeReply<TripReply>(req);
842 int pendingOps = 0;
843
844 // validate input
845 if (!req.isValid()) {
846 reply->addError(Reply::InvalidRequest, {});
847 reply->setPendingOps(pendingOps);
848 return reply;
849 }
850
851 d->loadNetworks();
852
853 // try to find a viable backend that can do a trip query directly
854 QSet<QString> triedBackends;
855 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
856 const auto checkBackend = [&](const Backend &backend, bool bothLocationMatch) {
857 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
858 return;
859 }
860 const auto coverage = backend.coverageArea(coverageType);
861 if (coverage.isEmpty()) {
862 return;
863 }
864
865 if (bothLocationMatch) {
866 if (!coverage.coversLocation(req.journeySection().from()) || !coverage.coversLocation(req.journeySection().to())) {
867 return;
868 }
869 } else {
870 if (!coverage.coversLocation(req.journeySection().from()) && !coverage.coversLocation(req.journeySection().to())) {
871 return;
872 }
873 }
874
875 triedBackends.insert(backend.identifier());
876
877 if (BackendPrivate::impl(backend)->queryTrip(req, reply, d->nam())) {
878 ++pendingOps;
879 }
880 };
881
882 // look for coverage areas which contain both locations first
883 for (const auto &backend: d->m_backends) {
884 checkBackend(backend, true);
885 }
886 if (pendingOps) {
887 break;
888 }
889
890 // if we didn't find one, try with just a single one
891 for (const auto &backend: d->m_backends) {
892 checkBackend(backend, false);
893 }
894 if (pendingOps) {
895 break;
896 }
897 }
898
899 // emulate a trip query via a journey query
900 if (pendingOps == 0) {
901 JourneyRequest jnyReq(req.journeySection().from(), req.journeySection().to());
902 // start searching slightly earlier, so leading walking section because our coordinates
903 // aren't exactly at the right spot wont make the routing service consider the train we
904 // are looking for as impossible to reach on time
905 jnyReq.setDateTime(req.journeySection().scheduledDepartureTime().addSecs(-600));
906 jnyReq.setDateTimeMode(JourneyRequest::Departure);
907 jnyReq.setIncludeIntermediateStops(true);
908 jnyReq.setIncludePaths(true);
909 jnyReq.setModes(JourneySection::PublicTransport);
910 jnyReq.setBackendIds(req.backendIds());
911 auto jnyReply = queryJourney(jnyReq);
912 jnyReply->setParent(reply);
913 connect(jnyReply, &Reply::finished, reply, [jnyReply, reply]() {
914 jnyReply->deleteLater();
915 if (jnyReply->error() != Reply::NoError) {
916 reply->addError(jnyReply->error(), jnyReply->errorString());
917 return;
918 }
919 for (const auto &journey : jnyReply->result()) {
920 if (std::ranges::count_if(journey.sections(), [](const auto &sec) { return sec.mode() == JourneySection::PublicTransport; }) != 1) {
921 continue;
922 }
923 const auto it = std::ranges::find_if(journey.sections(), [](const auto &sec) {
924 return sec.mode() == JourneySection::PublicTransport;
925 });
926 assert(it != journey.sections().end());
927 qCDebug(Log) << "Got journey information:" << (*it).route().line().name() << (*it).scheduledDepartureTime();
928 if (isSameTrip(reply->request().journeySection(), *it)) {
929 qCDebug(Log) << "Found journey information:" << (*it).route().line().name() << (*it).expectedDeparturePlatform() << (*it).expectedDepartureTime();
930 reply->addAttributions(jnyReply->attributions());
931 reply->addResult(nullptr, JourneySection(*it));
932 return;
933 }
934 }
935
936 reply->addError(Reply::NotFoundError, u"Not found."_s);
937 });
938
939 reply->setPendingOps(1);
940 } else {
941 reply->setPendingOps(pendingOps);
942 }
943
944 return reply;
945}
946
948{
949 auto reply = d->makeReply<VehicleLayoutReply>(req);
950 int pendingOps = 0;
951 int negativeCacheHit = 0;
952
953 // validate input
954 if (!req.isValid()) {
955 reply->addError(Reply::InvalidRequest, {});
956 reply->setPendingOps(pendingOps);
957 return reply;
958 }
959
960 d->loadNetworks();
961
962 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular }) {
963 for (const auto &backend : d->m_backends) {
964 if (d->shouldSkipBackend(backend, req)) {
965 continue;
966 }
967 const auto coverage = backend.coverageArea(coverageType);
968 if (coverage.isEmpty() || !coverage.coversLocation(req.stopover().stopPoint())) {
969 continue;
970 }
971 reply->addAttribution(BackendPrivate::impl(backend)->attribution());
972
973 auto cache = Cache::lookupVehicleLayout(backend.identifier(), req.cacheKey());
974 switch (cache.type) {
975 case CacheHitType::Negative:
976 ++negativeCacheHit;
977 qCDebug(Log) << "Negative cache hit for backend" << backend.identifier();
978 break;
979 case CacheHitType::Positive:
980 qCDebug(Log) << "Positive cache hit for backend" << backend.identifier();
981 if (cache.data.size() == 1) {
982 reply->addAttributions(std::move(cache.attributions));
983 reply->addResult(cache.data[0]);
984 break;
985 }
986 [[fallthrough]];
987 case CacheHitType::Miss:
988 qCDebug(Log) << "Cache miss for backend" << backend.identifier();
989 if (BackendPrivate::impl(backend)->queryVehicleLayout(req, reply, d->nam())) {
990 ++pendingOps;
991 }
992 break;
993 }
994 }
995 if (pendingOps) {
996 break;
997 }
998 }
999
1000 if (pendingOps == 0 && negativeCacheHit == 0) {
1001 reply->addError(Reply::NoBackend, u"No viable backend found."_s);
1002 }
1003 reply->setPendingOps(pendingOps);
1004 return reply;
1005}
1006
1008{
1009 if (d->m_backends.empty()) { // not loaded yet, nothing to do
1010 return;
1011 }
1012 d->m_backends.clear();
1013 d->loadNetworks();
1014 Q_EMIT backendsChanged();
1015}
1016
1017const std::vector<Attribution>& Manager::attributions() const
1018{
1019 d->loadNetworks();
1020 d->readCachedAttributions();
1021 return d->m_attributions;
1022}
1023
1024QVariantList Manager::attributionsVariant() const
1025{
1026 d->loadNetworks();
1027 d->readCachedAttributions();
1028 QVariantList l;
1029 l.reserve(d->m_attributions.size());
1030 std::transform(d->m_attributions.begin(), d->m_attributions.end(), std::back_inserter(l), [](const auto &attr) { return QVariant::fromValue(attr); });
1031 return l;
1032}
1033
1034const std::vector<Backend>& Manager::backends() const
1035{
1036 d->loadNetworks();
1037 return d->m_backends;
1038}
1039
1040bool Manager::isBackendEnabled(const QString &backendId) const
1041{
1042 if (std::binary_search(d->m_disabledBackends.cbegin(), d->m_disabledBackends.cend(), backendId)) {
1043 return false;
1044 }
1045 if (std::binary_search(d->m_enabledBackends.cbegin(), d->m_enabledBackends.cend(), backendId)) {
1046 return true;
1047 }
1048
1049 return d->m_backendsEnabledByDefault;
1050}
1051
1052static void sortedInsert(QStringList &l, const QString &value)
1053{
1054 const auto it = std::lower_bound(l.begin(), l.end(), value);
1055 if (it == l.end() || (*it) != value) {
1056 l.insert(it, value);
1057 }
1058}
1059
1060static void sortedRemove(QStringList &l, const QString &value)
1061{
1062 const auto it = std::lower_bound(l.begin(), l.end(), value);
1063 if (it != l.end() && (*it) == value) {
1064 l.erase(it);
1065 }
1066}
1067
1068void Manager::setBackendEnabled(const QString &backendId, bool enabled)
1069{
1070 if (enabled) {
1071 sortedInsert(d->m_enabledBackends, backendId);
1072 sortedRemove(d->m_disabledBackends, backendId);
1073 } else {
1074 sortedRemove(d->m_enabledBackends, backendId);
1075 sortedInsert(d->m_disabledBackends, backendId);
1076 }
1077 Q_EMIT configurationChanged();
1078}
1079
1081{
1082 return d->m_enabledBackends;
1083}
1084
1086{
1087 QSignalBlocker blocker(this); // no change signals during settings restore
1088 for (const auto &backendId : backendIds) {
1089 setBackendEnabled(backendId, true);
1090 }
1091}
1092
1094{
1095 return d->m_disabledBackends;
1096}
1097
1099{
1100 QSignalBlocker blocker(this); // no change signals during settings restore
1101 for (const auto &backendId : backendIds) {
1102 setBackendEnabled(backendId, false);
1103 }
1104}
1105
1107{
1108 return d->m_backendsEnabledByDefault;
1109}
1110
1112{
1113 d->m_backendsEnabledByDefault = byDefault;
1114
1115 Q_EMIT configurationChanged();
1116}
1117
1118QVariantList Manager::backendsVariant() const
1119{
1120 d->loadNetworks();
1121 QVariantList l;
1122 l.reserve(d->m_backends.size());
1123 std::transform(d->m_backends.begin(), d->m_backends.end(), std::back_inserter(l), [](const auto &b) { return QVariant::fromValue(b); });
1124 return l;
1125}
1126
1127bool Manager::eventFilter(QObject *object, QEvent *event)
1128{
1130 reload();
1131 }
1132
1133 return QObject::eventFilter(object, event);
1134}
1135
1136#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.
void setBackendIds(const QStringList &backendIds)
Set identifiers of backends that should be queried.
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.
A segment of a journey plan.
Definition journey.h:39
static bool isSame(const JourneySection &lhs, const JourneySection &rhs)
Checks if two instances refer to the same journey section (which does not necessarily mean they are e...
Definition journey.cpp:692
KPublicTransport::Location from
Departure location of this segment.
Definition journey.h:90
QDateTime scheduledDepartureTime
Planned departure time.
Definition journey.h:63
@ RentedVehicle
free floating or dock-based rental bike service, electric scooters, car sharing services,...
Definition journey.h:52
KPublicTransport::Route route
Route to take on this segment.
Definition journey.h:94
KPublicTransport::Location to
Arrival location of this segment.
Definition journey.h:92
KPublicTransport::Line::Mode mode
Type of transport.
Definition line.h:60
static bool modeIsRailBound(KPublicTransport::Line::Mode mode)
true if mode is bounds to rail tracks.
Definition line.cpp:139
LocationRequest request() const
The request this is the reply for.
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:756
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:1111
void setEnabledBackends(const QStringList &backendIds)
Sets the explicitly enabled backends.
Definition manager.cpp:1085
void setBackendEnabled(const QString &backendId, bool enabled)
Sets whether the backend with the given identifier should be used.
Definition manager.cpp:1068
void reload()
Reload backend configuration.
Definition manager.cpp:1007
StopoverReply * queryStopover(const StopoverRequest &req) const
Query arrivals or departures from a specific station.
Definition manager.cpp:651
QStringList disabledBackends
Definition manager.h:54
Q_INVOKABLE bool isBackendEnabled(const QString &backendId) const
Returns whether the use of the backend with a given identifier is enabled.
Definition manager.cpp:1040
Q_INVOKABLE KPublicTransport::TripReply * queryTrip(const TripRequest &req) const
Query trip information.
Definition manager.cpp:839
void setDisabledBackends(const QStringList &backendIds)
Sets the explicitly disabled backends.
Definition manager.cpp:1098
VehicleLayoutReply * queryVehicleLayout(const VehicleLayoutRequest &req) const
Query vehicle and platform layout information.
Definition manager.cpp:947
QVariantList backends
QML-compatible access to backends().
Definition manager.h:59
bool allowInsecureBackends
Allow usage of insecure backends (default: off).
Definition manager.h:49
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:52
QVariantList attributions
QML-compatible access to attributions().
Definition manager.h:47
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
@ NoError
Nothing went wrong.
Definition reply.h:34
@ NotFoundError
The requested journey/departure/place could not be found.
Definition reply.h:36
KPublicTransport::Line line
Line this route belongs to.
Definition line.h:151
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 trip query.
Definition tripreply.h:23
Request for a single trip.
Definition triprequest.h:29
bool isValid() const
Returns true if this is a valid request, that is it has enough parameters set to perform a query.
QStringList backendIds
Identifiers of backends that should be queried.
Definition triprequest.h:37
KPublicTransport::JourneySection journeySection
A JourneySection for which the full trip is requested.
Definition triprequest.h:32
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()
QDateTime addSecs(qint64 s) const const
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 Feb 28 2025 11:53:27 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.