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

KDE's Doxygen guidelines are available online.