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);
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
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{
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
516void Manager::setNetworkAccessManager(QNetworkAccessManager *nam)
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
529bool Manager::allowInsecureBackends() const
530{
531 return d->m_allowInsecure;
532}
533
534void Manager::setAllowInsecureBackends(bool insecure)
535{
536 if (d->m_allowInsecure == insecure) {
537 return;
538 }
539 d->m_allowInsecure = insecure;
540 Q_EMIT configurationChanged();
541}
542
543JourneyReply* Manager::queryJourney(const JourneyRequest &req) const
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 reply->setPendingOps(pendingOps);
643 return reply;
644}
645
646StopoverReply* Manager::queryStopover(const StopoverRequest &req) const
647{
648 auto reply = d->makeReply<StopoverReply>(req);
649 int pendingOps = 0;
650
651 // validate input
652 if (!req.isValid()) {
653 reply->addError(Reply::InvalidRequest, {});
654 reply->setPendingOps(pendingOps);
655 return reply;
656 }
657
658 d->loadNetworks();
659
660 // first time/direct query
661 if (req.contexts().empty()) {
662 QSet<QString> triedBackends;
663 bool foundNonGlobalCoverage = false;
664 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
665 for (const auto &backend: d->m_backends) {
666 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
667 continue;
668 }
669 if (req.mode() == StopoverRequest::QueryArrival && (BackendPrivate::impl(backend)->capabilities() & AbstractBackend::CanQueryArrivals) == 0) {
670 qCDebug(Log) << "Skipping backend due to not supporting arrival queries:" << backend.identifier();
671 continue;
672 }
673 const auto coverage = backend.coverageArea(coverageType);
674 if (coverage.isEmpty() || !coverage.coversLocation(req.stop())) {
675 continue;
676 }
677 triedBackends.insert(backend.identifier());
678 foundNonGlobalCoverage |= !coverage.isGlobal();
679
680 if (d->queryStopover(BackendPrivate::impl(backend), req, reply)) {
681 ++pendingOps;
682 }
683 }
684
685 if (pendingOps && foundNonGlobalCoverage) {
686 break;
687 }
688 }
689
690 // subsequent earlier/later query
691 } else {
692 for (const auto &context : req.contexts()) {
693 // backend supports this itself
694 if ((context.type == RequestContext::Next && context.backend->hasCapability(AbstractBackend::CanQueryNextDeparture))
695 ||(context.type == RequestContext::Previous && context.backend->hasCapability(AbstractBackend::CanQueryPreviousDeparture)))
696 {
697 if (d->queryStopover(context.backend, req, reply)) {
698 ++pendingOps;
699 continue;
700 }
701 }
702
703 // backend doesn't support this, let's try to emulate
704 if (context.type == RequestContext::Next) {
705 auto r = req;
706 r.setDateTime(context.dateTime);
707 if (d->queryStopover(context.backend, r, reply)) {
708 ++pendingOps;
709 continue;
710 }
711 }
712 }
713 }
714
715 if (req.downloadAssets()) {
716 reply->addAttributions(AssetRepository::instance()->attributions());
717 }
718 reply->setPendingOps(pendingOps);
719 return reply;
720}
721
722int ManagerPrivate::queryLocationOnBackend(const LocationRequest &req, LocationReply *reply, const Backend &backend)
723{
724 auto cache = Cache::lookupLocation(backend.identifier(), req.cacheKey());
725 switch (cache.type) {
726 case CacheHitType::Negative:
727 qCDebug(Log) << "Negative cache hit for backend" << backend.identifier();
728 break;
729 case CacheHitType::Positive:
730 qCDebug(Log) << "Positive cache hit for backend" << backend.identifier();
731 reply->addAttributions(std::move(cache.attributions));
732 reply->addResult(std::move(cache.data));
733 break;
734 case CacheHitType::Miss:
735 qCDebug(Log) << "Cache miss for backend" << backend.identifier();
736 reply->addAttribution(BackendPrivate::impl(backend)->attribution());
737 if (BackendPrivate::impl(backend)->queryLocation(req, reply, nam())) {
738 return 1;
739 }
740 break;
741 }
742
743 return 0;
744}
745
747{
748 auto reply = d->makeReply<LocationReply>(req);
749 int pendingOps = 0;
750
751 // validate input
752 if (!req.isValid()) {
753 reply->addError(Reply::InvalidRequest, {});
754 reply->setPendingOps(pendingOps);
755 return reply;
756 }
757
758 d->loadNetworks();
759
760 QSet<QString> triedBackends;
761 bool foundNonGlobalCoverage = false;
762 const auto loc = req.location();
763 const auto isCountryOnly = !loc.hasCoordinate() && !loc.country().isEmpty() && loc.region().isEmpty();
764 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular, CoverageArea::Any }) {
765 // pass 1: coordinate-based coverage, or nationwide country coverage
766 for (const auto &backend : d->m_backends) {
767 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
768 continue;
769 }
770 const auto coverage = backend.coverageArea(coverageType);
771 if (coverage.isEmpty() || !coverage.coversLocation(loc)) {
772 continue;
773 }
774 if (isCountryOnly && !coverage.hasNationWideCoverage(loc.country())) {
775 continue;
776 }
777
778 triedBackends.insert(backend.identifier());
779 foundNonGlobalCoverage |= !coverage.isGlobal();
780 pendingOps += d->queryLocationOnBackend(req, reply, backend);
781 }
782 if (pendingOps && foundNonGlobalCoverage) {
783 break;
784 }
785
786 // pass 2: any country match
787 for (const auto &backend : d->m_backends) {
788 if (triedBackends.contains(backend.identifier()) || d->shouldSkipBackend(backend, req)) {
789 continue;
790 }
791 const auto coverage = backend.coverageArea(coverageType);
792 if (coverage.isEmpty() || !coverage.coversLocation(loc)) {
793 continue;
794 }
795
796 triedBackends.insert(backend.identifier());
797 foundNonGlobalCoverage |= !coverage.isGlobal();
798 pendingOps += d->queryLocationOnBackend(req, reply, backend);
799 }
800 if (pendingOps && foundNonGlobalCoverage) {
801 break;
802 }
803 }
804 reply->setPendingOps(pendingOps);
805 return reply;
806}
807
809{
810 auto reply = d->makeReply<VehicleLayoutReply>(req);
811 int pendingOps = 0;
812
813 // validate input
814 if (!req.isValid()) {
815 reply->addError(Reply::InvalidRequest, {});
816 reply->setPendingOps(pendingOps);
817 return reply;
818 }
819
820 d->loadNetworks();
821
822 for (const auto coverageType : { CoverageArea::Realtime, CoverageArea::Regular }) {
823 for (const auto &backend : d->m_backends) {
824 if (d->shouldSkipBackend(backend, req)) {
825 continue;
826 }
827 const auto coverage = backend.coverageArea(coverageType);
828 if (coverage.isEmpty() || !coverage.coversLocation(req.stopover().stopPoint())) {
829 continue;
830 }
831 reply->addAttribution(BackendPrivate::impl(backend)->attribution());
832
833 auto cache = Cache::lookupVehicleLayout(backend.identifier(), req.cacheKey());
834 switch (cache.type) {
835 case CacheHitType::Negative:
836 qCDebug(Log) << "Negative cache hit for backend" << backend.identifier();
837 break;
838 case CacheHitType::Positive:
839 qCDebug(Log) << "Positive cache hit for backend" << backend.identifier();
840 if (cache.data.size() == 1) {
841 reply->addAttributions(std::move(cache.attributions));
842 reply->addResult(cache.data[0]);
843 break;
844 }
845 [[fallthrough]];
846 case CacheHitType::Miss:
847 qCDebug(Log) << "Cache miss for backend" << backend.identifier();
848 if (BackendPrivate::impl(backend)->queryVehicleLayout(req, reply, d->nam())) {
849 ++pendingOps;
850 }
851 break;
852 }
853 }
854 if (pendingOps) {
855 break;
856 }
857 }
858
859 reply->setPendingOps(pendingOps);
860 return reply;
861}
862
864{
865 d->m_backends.clear();
866 d->loadNetworks();
867 Q_EMIT backendsChanged();
868}
869
870const std::vector<Attribution>& Manager::attributions() const
871{
872 d->loadNetworks();
873 d->readCachedAttributions();
874 return d->m_attributions;
875}
876
877QVariantList Manager::attributionsVariant() const
878{
879 d->loadNetworks();
880 d->readCachedAttributions();
881 QVariantList l;
882 l.reserve(d->m_attributions.size());
883 std::transform(d->m_attributions.begin(), d->m_attributions.end(), std::back_inserter(l), [](const auto &attr) { return QVariant::fromValue(attr); });
884 return l;
885}
886
887const std::vector<Backend>& Manager::backends() const
888{
889 d->loadNetworks();
890 return d->m_backends;
891}
892
893bool Manager::isBackendEnabled(const QString &backendId) const
894{
895 if (std::binary_search(d->m_disabledBackends.cbegin(), d->m_disabledBackends.cend(), backendId)) {
896 return false;
897 }
898 if (std::binary_search(d->m_enabledBackends.cbegin(), d->m_enabledBackends.cend(), backendId)) {
899 return true;
900 }
901
902 return d->m_backendsEnabledByDefault;
903}
904
905static void sortedInsert(QStringList &l, const QString &value)
906{
907 const auto it = std::lower_bound(l.begin(), l.end(), value);
908 if (it == l.end() || (*it) != value) {
909 l.insert(it, value);
910 }
911}
912
913static void sortedRemove(QStringList &l, const QString &value)
914{
915 const auto it = std::lower_bound(l.begin(), l.end(), value);
916 if (it != l.end() && (*it) == value) {
917 l.erase(it);
918 }
919}
920
921void Manager::setBackendEnabled(const QString &backendId, bool enabled)
922{
923 if (enabled) {
924 sortedInsert(d->m_enabledBackends, backendId);
925 sortedRemove(d->m_disabledBackends, backendId);
926 } else {
927 sortedRemove(d->m_enabledBackends, backendId);
928 sortedInsert(d->m_disabledBackends, backendId);
929 }
930 Q_EMIT configurationChanged();
931}
932
934{
935 return d->m_enabledBackends;
936}
937
939{
940 QSignalBlocker blocker(this); // no change signals during settings restore
941 for (const auto &backendId : backendIds) {
942 setBackendEnabled(backendId, true);
943 }
944}
945
947{
948 return d->m_disabledBackends;
949}
950
952{
953 QSignalBlocker blocker(this); // no change signals during settings restore
954 for (const auto &backendId : backendIds) {
955 setBackendEnabled(backendId, false);
956 }
957}
958
960{
961 return d->m_backendsEnabledByDefault;
962}
963
965{
966 d->m_backendsEnabledByDefault = byDefault;
967
968 Q_EMIT configurationChanged();
969}
970
971QVariantList Manager::backendsVariant() const
972{
973 d->loadNetworks();
974 QVariantList l;
975 l.reserve(d->m_backends.size());
976 std::transform(d->m_backends.begin(), d->m_backends.end(), std::back_inserter(l), [](const auto &b) { return QVariant::fromValue(b); });
977 return l;
978}
979
980bool Manager::eventFilter(QObject *object, QEvent *event)
981{
982 if (event->type() == QEvent::LanguageChange && object == QCoreApplication::instance()) {
983 reload();
984 }
985
986 return QObject::eventFilter(object, event);
987}
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:417
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:746
void setBackendsEnabledByDefault(bool byDefault)
Set wheter backends are enabled by default.
Definition manager.cpp:964
void setEnabledBackends(const QStringList &backendIds)
Sets the explicitly enabled backends.
Definition manager.cpp:938
void setBackendEnabled(const QString &backendId, bool enabled)
Sets whether the backend with the given identifier should be used.
Definition manager.cpp:921
void reload()
Reload backend configuration.
Definition manager.cpp:863
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:893
void setDisabledBackends(const QStringList &backendIds)
Sets the explicitly disabled backends.
Definition manager.cpp:951
VehicleLayoutReply * queryVehicleLayout(const VehicleLayoutRequest &req) const
Query vehicle and platform layout information.
Definition manager.cpp:808
QVariantList backends
QML-compatible access to backends().
Definition manager.h:57
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:35
@ NotFoundError
The requested journey/departure/place could not be found.
Definition reply.h:34
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
void enableStrictTransportSecurityStore(bool enabled, const QString &storeDir)
void setRedirectPolicy(QNetworkRequest::RedirectPolicy policy)
void setStrictTransportSecurityEnabled(bool enabled)
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-2024 The KDE developers.
Generated on Fri Dec 13 2024 11:46:17 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.