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