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 if (!section.from().hasCoordinate() || !section.to().hasCoordinate() || section.path().isEmpty()) {
186 continue;
187 }
188
189 // remove implausible paths
190 const auto pointDist = Location::distance(section.from(), section.to());
191 const auto pathDist = section.path().distance();
192 if (pathDist > pointDist * 10) {
193 qCDebug(Log) << "Dropping implausibly long path:" << pointDist << pathDist;
194 section.setPath({});
195 }
196
197 // filter spikes found in rail paths in nearly all backends and GTFS shapes
198 if (section.mode() == JourneySection::PublicTransport && Line::modeIsRailBound(section.route().line().mode())) {
199 auto path = section.path();
200 auto pathSecs = path.takeSections();
201 for (auto &pathSec : pathSecs) {
202 QPolygonF p = pathSec.path();
203 PathFilter::removeSpikes(p, 55.0);
204 pathSec.setPath(p);
205 }
206 path.setSections(std::move(pathSecs));
207 section.setPath(path);
208 }
209 }
210
211 journey.setSections(std::move(sections));
212 }
213
214 // remove empty or implausible journeys
215 journeys.erase(std::remove_if(journeys.begin(), journeys.end(), [](const auto &journey) {
216 return journey.sections().empty() || std::any_of(journey.sections().begin(), journey.sections().end(), isImplausibleSection);
217 }), journeys.end());
218}
219
220JourneyReply::JourneyReply(const JourneyRequest &req, QObject *parent)
221 : Reply(new JourneyReplyPrivate, parent)
222{
224 d->request = req;
225 d->nextRequest = req;
226 d->prevRequest = req;
227}
228
229JourneyReply::~JourneyReply() = default;
230
232{
233 Q_D(const JourneyReply);
234 return d->request;
235}
236
237const std::vector<Journey>& JourneyReply::result() const
238{
239 Q_D(const JourneyReply);
240 return d->journeys;
241}
242
243std::vector<Journey>&& JourneyReply::takeResult()
244{
246 return std::move(d->journeys);
247}
248
250{
251 Q_D(const JourneyReply);
252 if (d->nextRequest.contexts().empty()) {
253 return {};
254 }
255 return d->nextRequest;
256}
257
259{
260 Q_D(const JourneyReply);
261 if (d->prevRequest.contexts().empty()) {
262 return {};
263 }
264 return d->prevRequest;
265}
266
267void JourneyReply::addResult(const AbstractBackend *backend, std::vector<Journey> &&res)
268{
270 d->postProcessJourneys(res);
271
272 // update context for next/prev requests
273 // do this first, before res gets moved from below
274 if (d->request.dateTimeMode() == JourneyRequest::Departure && !res.empty()) {
275 // we create a context for later queries here in any case, since we can emulate that generically without backend support
276 auto context = d->nextRequest.context(backend);
277 context.type = RequestContext::Next;
278 for (const auto &jny : res) {
279 context.dateTime = std::max(context.dateTime, jny.scheduledDepartureTime());
280 }
281 d->nextRequest.setContext(backend, std::move(context));
282
283 context = d->prevRequest.context(backend);
284 context.type = RequestContext::Previous;
285 context.dateTime = res[0].scheduledArrivalTime(); // "invalid" is the minimum...
286 for (const auto &jny : res) {
287 context.dateTime = std::min(context.dateTime, jny.scheduledArrivalTime());
288 }
289 d->prevRequest.setContext(backend, std::move(context));
290 }
291
292 // if this is a backend with a static timezone, apply this to the result
293 if (backend->timeZone().isValid()) {
294 for (auto &jny : res) {
295 JourneyUtil::applyTimeZone(jny, backend->timeZone());
296 }
297 }
298
299 // apply line meta data
300 for (auto &jny : res) {
301 jny.applyMetaData(request().downloadAssets());
302 }
303
304 // cache negative hits, positive ones are too short-lived
305 if (res.empty()) {
306 Cache::addNegativeJourneyCacheEntry(backend->backendId(), request().cacheKey());
307 }
308
309 // apply static attributions if @p backend contributed to the results
310 addAttribution(backend->attribution());
311
312 // update result
313 if (!res.empty()) {
314 if (d->journeys.empty()) {
315 d->journeys = std::move(res);
316 } else {
317 d->journeys.insert(d->journeys.end(), res.begin(), res.end());
318 }
319 d->emitUpdated(this);
320 }
321
322 d->pendingOps--;
323 d->emitFinishedIfDone(this);
324}
325
326void JourneyReply::setNextContext(const AbstractBackend *backend, const QVariant &data)
327{
329 auto context = d->nextRequest.context(backend);
330 context.type = RequestContext::Next;
331 context.backendData = data;
332 d->nextRequest.setContext(backend, std::move(context));
333}
334
335void JourneyReply::setPreviousContext(const AbstractBackend *backend, const QVariant &data)
336{
338 auto context = d->prevRequest.context(backend);
339 context.type = RequestContext::Previous;
340 context.backendData = data;
341 d->prevRequest.setContext(backend, std::move(context));
342}
343
344void JourneyReply::addError(const AbstractBackend *backend, Reply::Error error, const QString &errorMsg)
345{
347 Cache::addNegativeJourneyCacheEntry(backend->backendId(), request().cacheKey());
348 } else {
349 qCDebug(Log) << backend->backendId() << error << errorMsg;
350 }
351 Reply::addError(error, errorMsg);
352}
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::Route route
Route to take on this segment.
Definition journey.h:87
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:803
static Journey merge(const Journey &lhs, const Journey &rhs)
Merge two instances.
Definition journey.cpp:835
KPublicTransport::Line::Mode mode
Type of transport.
Definition line.h:60
static bool modeIsRailBound(KPublicTransport::Line::Mode mode)
true if mode is bounds to rail tracks.
Definition line.cpp:139
QTimeZone timeZone() const
The timezone this location is in, if known.
Definition location.cpp:125
static double distance(double lat1, double lon1, double lat2, double lon2)
Compute the distance between two geo coordinates, in meters.
Definition location.cpp:474
QString name
Human-readable name of the location.
Definition location.h:52
int distance
The length of this path in meters.
Definition path.h:122
bool isEmpty() const
Returns true if this is an empty/not-set path.
Definition path.cpp:143
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
KPublicTransport::Line line
Line this route belongs to.
Definition line.h:151
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 Mon Nov 18 2024 12:07:52 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.