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

KDE's Doxygen guidelines are available online.