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

KDE's Doxygen guidelines are available online.