KPublicTransport

journeyreply.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 "journeyreply.h"
8#include "reply_p.h"
9#include "journeyrequest.h"
10#include "requestcontext_p.h"
11#include "logging.h"
12#include "backends/abstractbackend.h"
13#include "backends/cache.h"
14#include "datatypes/journeyutil_p.h"
15#include "geo/pathfilter_p.h"
16
17#include <KPublicTransport/Journey>
18#include <KPublicTransport/Location>
19
20#include <QDateTime>
21#include <QTimeZone>
22
23using namespace KPublicTransport;
24
25namespace KPublicTransport {
26
27// result filter thresholds
28constexpr inline const auto MINIMUM_WAIT_TIME = 60; // seconds; shorter waiting sections are dropped
29constexpr inline const auto MINIMUM_WALK_TIME = 90; // seconds; shorter walking sections are dropped
30constexpr inline const auto MINIMUM_WALK_DISTANCE = 50; // meters; shorter walking sections are dropped
31
32constexpr inline const auto MAXIMUM_TRANSFER_SPEED = 30; // meter/second; anything above is discarded as nonsensical data
33constexpr inline const auto MAXIMUM_TRANSFER_DISTANCE = 100000; // meters; anything above is discarded as nonsensical data
34
35class JourneyReplyPrivate : public ReplyPrivate {
36public:
37 void finalizeResult() override;
38 bool needToWaitForAssets() const override;
39 static void postProcessJourneys(std::vector<Journey> &journeys);
40
41 JourneyRequest request;
42 JourneyRequest nextRequest;
43 JourneyRequest prevRequest;
44 std::vector<Journey> journeys;
45};
46}
47
48void JourneyReplyPrivate::finalizeResult()
49{
50 if (journeys.empty()) {
51 return;
52 }
53
55 errorMsg.clear();
56
57 // merge results, aligned by first transport departure
58 std::sort(journeys.begin(), journeys.end(), JourneyUtil::firstTransportDepartureLessThan);
59 for (auto it = journeys.begin(); it != journeys.end(); ++it) {
60 for (auto mergeIt = it + 1; mergeIt != journeys.end();) {
61 if (!JourneyUtil::firstTransportDepartureEqual(*it, *mergeIt)) {
62 break;
63 }
64
65 if (Journey::isSame(*it, *mergeIt)) {
66 *it = Journey::merge(*it, *mergeIt);
67 mergeIt = journeys.erase(mergeIt);
68 } else {
69 ++mergeIt;
70 }
71 }
72 }
73
74 // sort by departure time for display
75 std::sort(journeys.begin(), journeys.end(), [](const auto &lhs, const auto &rhs) {
76 return lhs.scheduledDepartureTime() < rhs.scheduledDepartureTime();
77 });
78
79 nextRequest.purgeLoops(request);
80 prevRequest.purgeLoops(request);
81}
82
83bool JourneyReplyPrivate::needToWaitForAssets() const
84{
85 return request.downloadAssets();
86}
87
88[[nodiscard]] bool hasNonTrivialPath(const JourneySection &section)
89{
90 const auto path = section.path();
91 if (path.isEmpty()) {
92 return false;
93 }
94 if (path.sections().size() > 1) {
95 return true;
96 }
97 return !path.sections()[0].description().isEmpty() || path.sections()[0].path().size() > 2;
98}
99
100static bool isPointlessSection(const JourneySection &section)
101{
102 if (section.mode() == JourneySection::Waiting) {
103 return section.duration() < MINIMUM_WAIT_TIME;
104 }
105 if (section.mode() == JourneySection::Walking && !hasNonTrivialPath(section)) {
106 return section.duration() < MINIMUM_WALK_TIME || (section.distance() > 0 && section.distance() < MINIMUM_WALK_DISTANCE);
107 }
108 return false;
109}
110
111static bool isImplausibleSection(const JourneySection &section)
112{
113 if ((section.mode() == JourneySection::Transfer || section.mode() == JourneySection::Walking)
114 && section.from().hasCoordinate() && section.to().hasCoordinate())
115 {
116 const auto distance = Location::distance(section.from(), section.to());
117 if (section.duration() > 0 && (distance / (float)section.duration()) > MAXIMUM_TRANSFER_SPEED) {
118 qCDebug(Log) << "discarding journey based on insane transfer/walking speed:" << (distance / (float)section.duration()) << "m/s";
119 return true;
120 }
121 if (distance > MAXIMUM_TRANSFER_DISTANCE) {
122 qCDebug(Log) << "discarding journey with insane transfer/walking distance:" << distance << "m" << section.from().name() << section.to().name();
123 return true;
124 }
125 }
126 return false;
127}
128
129void JourneyReplyPrivate::postProcessJourneys(std::vector<Journey> &journeys)
130{
131 // try to fill gaps in timezone data
132 for (auto &journey : journeys) {
133 JourneyUtil::propagateTimeZones(journey);
134 auto sections = journey.takeSections();
135 for (auto &section : sections) {
136 if (section.mode() == JourneySection::Walking) {
137 if (!section.from().timeZone().isValid() && section.to().timeZone().isValid()) {
138 auto from = section.from();
139 from.setTimeZone(section.to().timeZone());
140 section.setFrom(from);
141 auto dt = section.scheduledDepartureTime();
142 dt.setTimeZone(from.timeZone());
143 section.setScheduledDepartureTime(dt);
144 }
145 if (section.from().timeZone().isValid() && !section.to().timeZone().isValid()) {
146 auto to = section.to();
147 to.setTimeZone(section.from().timeZone());
148 section.setTo(to);
149 auto dt = section.scheduledArrivalTime();
150 dt.setTimeZone(to.timeZone());
151 section.setScheduledArrivalTime(dt);
152 }
153 }
154 }
155 journey.setSections(std::move(sections));
156 }
157
158 // clean up non-transport sections
159 for (auto &journey : journeys) {
160 auto sections = journey.takeSections();
161
162 // merge adjacent walking sections (yes, we do get that from backends...)
163 for (auto it = sections.begin(); it != sections.end();) {
164 if (it == sections.begin()) {
165 ++it;
166 continue;
167 }
168 auto prevIt = it - 1;
169 if ((*it).mode() == JourneySection::Walking && (*prevIt).mode() == JourneySection::Walking) {
170 (*prevIt).setTo((*it).to());
171 (*prevIt).setScheduledArrivalTime((*it).scheduledArrivalTime());
172 (*prevIt).setExpectedArrivalTime((*it).expectedArrivalTime());
173 (*prevIt).setDistance((*prevIt).distance() + (*it).distance());
174 it = sections.erase(it);
175 continue;
176 }
177
178 ++it;
179 }
180
181 // remove pointless sections such as 0-length walks
182 sections.erase(std::remove_if(sections.begin(), sections.end(), isPointlessSection), sections.end());
183
184 for (auto &section : sections) {
185 JourneyUtil::postProcessPath(section);
186 }
187
188 journey.setSections(std::move(sections));
189 }
190
191 // remove empty or implausible journeys
192 journeys.erase(std::remove_if(journeys.begin(), journeys.end(), [](const auto &journey) {
193 return journey.sections().empty() || std::any_of(journey.sections().begin(), journey.sections().end(), isImplausibleSection);
194 }), journeys.end());
195}
196
197JourneyReply::JourneyReply(const JourneyRequest &req, QObject *parent)
198 : Reply(new JourneyReplyPrivate, parent)
199{
201 d->request = req;
202 d->nextRequest = req;
203 d->prevRequest = req;
204}
205
206JourneyReply::~JourneyReply() = default;
207
209{
210 Q_D(const JourneyReply);
211 return d->request;
212}
213
214const std::vector<Journey>& JourneyReply::result() const
215{
216 Q_D(const JourneyReply);
217 return d->journeys;
218}
219
220std::vector<Journey>&& JourneyReply::takeResult()
221{
222 Q_D(JourneyReply);
223 return std::move(d->journeys);
224}
225
227{
228 Q_D(const JourneyReply);
229 if (d->nextRequest.contexts().empty()) {
230 return {};
231 }
232 return d->nextRequest;
233}
234
236{
237 Q_D(const JourneyReply);
238 if (d->prevRequest.contexts().empty()) {
239 return {};
240 }
241 return d->prevRequest;
242}
243
244void JourneyReply::addResult(const AbstractBackend *backend, std::vector<Journey> &&res)
245{
247 d->postProcessJourneys(res);
248
249 // update context for next/prev requests
250 // do this first, before res gets moved from below
251 if (d->request.dateTimeMode() == JourneyRequest::Departure && !res.empty()) {
252 // we create a context for later queries here in any case, since we can emulate that generically without backend support
253 auto context = d->nextRequest.context(backend);
254 context.type = RequestContext::Next;
255 for (const auto &jny : res) {
256 context.dateTime = std::max(context.dateTime, jny.scheduledDepartureTime());
257 }
258 d->nextRequest.setContext(backend, std::move(context));
259
260 context = d->prevRequest.context(backend);
261 context.type = RequestContext::Previous;
262 context.dateTime = res[0].scheduledArrivalTime(); // "invalid" is the minimum...
263 for (const auto &jny : res) {
264 context.dateTime = std::min(context.dateTime, jny.scheduledArrivalTime());
265 }
266 d->prevRequest.setContext(backend, std::move(context));
267 }
268
269 // if this is a backend with a static timezone, apply this to the result
270 if (backend->timeZone().isValid()) {
271 for (auto &jny : res) {
272 JourneyUtil::applyTimeZone(jny, backend->timeZone());
273 }
274 }
275
276 // apply line meta data
277 for (auto &jny : res) {
278 jny.applyMetaData(request().downloadAssets());
279 }
280
281 // cache negative hits, positive ones are too short-lived
282 if (res.empty()) {
283 Cache::addNegativeJourneyCacheEntry(backend->backendId(), request().cacheKey());
284 }
285
286 // apply static attributions if @p backend contributed to the results
287 addAttribution(backend->attribution());
288
289 // update result
290 if (!res.empty()) {
291 if (d->journeys.empty()) {
292 d->journeys = std::move(res);
293 } else {
294 d->journeys.insert(d->journeys.end(), res.begin(), res.end());
295 }
296 d->emitUpdated(this);
297 }
298
299 d->pendingOps--;
300 d->emitFinishedIfDone(this);
301}
302
303void JourneyReply::setNextContext(const AbstractBackend *backend, const QVariant &data)
304{
305 Q_D(JourneyReply);
306 auto context = d->nextRequest.context(backend);
307 context.type = RequestContext::Next;
308 context.backendData = data;
309 d->nextRequest.setContext(backend, std::move(context));
310}
311
312void JourneyReply::setPreviousContext(const AbstractBackend *backend, const QVariant &data)
313{
314 Q_D(JourneyReply);
315 auto context = d->prevRequest.context(backend);
316 context.type = RequestContext::Previous;
317 context.backendData = data;
318 d->prevRequest.setContext(backend, std::move(context));
319}
320
321void JourneyReply::addError(const AbstractBackend *backend, Reply::Error error, const QString &errorMsg)
322{
323 if (error == Reply::NotFoundError) {
324 Cache::addNegativeJourneyCacheEntry(backend->backendId(), request().cacheKey());
325 } else {
326 qCDebug(Log) << backend->backendId() << error << errorMsg;
327 }
328 Reply::addError(error, errorMsg);
329}
330
331#include "moc_journeyreply.cpp"
Journey query response.
const std::vector< Journey > & result() const
Returns the retrieved journeys.
JourneyRequest nextRequest() const
Returns a request object for querying journeys following the ones returned by this reply.
std::vector< Journey > && takeResult()
Returns the retrieved journeys for moving elsewhere.
JourneyRequest previousRequest() const
Returns a request object for querying journeys preceding the ones returned by this reply.
JourneyRequest request() const
The request this is the reply for.
Describes a journey search.
@ Departure
dateTime() represents the desired departure time.
A segment of a journey plan.
Definition journey.h:39
KPublicTransport::Path path
Movement path for this journey section.
Definition journey.h:148
KPublicTransport::Location from
Departure location of this segment.
Definition journey.h:90
QDateTime scheduledDepartureTime
Planned departure time.
Definition journey.h:63
KPublicTransport::Location to
Arrival location of this segment.
Definition journey.h:92
int duration
Duration of the section in seconds.
Definition journey.h:85
Mode mode
Mode of transport for this section.
Definition journey.h:60
QDateTime scheduledArrivalTime
Planned arrival time.
Definition journey.h:74
int distance
Distance of the section in meter.
Definition journey.h:87
static bool isSame(const Journey &lhs, const Journey &rhs)
Checks if two instances refer to the same journey (which does not necessarily mean they are exactly e...
Definition journey.cpp:1018
static Journey merge(const Journey &lhs, const Journey &rhs)
Merge two instances.
Definition journey.cpp:1050
QTimeZone timeZone() const
The timezone this location is in, if known.
Definition location.cpp:126
static double distance(double lat1, double lon1, double lat2, double lon2)
Compute the distance between two geo coordinates, in meters.
Definition location.cpp:458
QString name
Human-readable name of the location.
Definition location.h:52
Query response base class.
Definition reply.h:25
Error
Error types.
Definition reply.h:33
@ NoError
Nothing went wrong.
Definition reply.h:34
@ NotFoundError
The requested journey/departure/place could not be found.
Definition reply.h:36
int distance(const GeoCoordinates &coord1, const GeoCoordinates &coord2)
QString path(const QString &relativePath)
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...
void setTimeZone(const QTimeZone &toZone)
bool isEmpty() const const
qsizetype size() const const
bool isValid() const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:47:40 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.