File indexing completed on 2024-12-08 07:19:10
0001 /* 0002 SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "journeyreply.h" 0008 #include "reply_p.h" 0009 #include "journeyrequest.h" 0010 #include "requestcontext_p.h" 0011 #include "logging.h" 0012 #include "backends/abstractbackend.h" 0013 #include "backends/cache.h" 0014 #include "datatypes/journeyutil_p.h" 0015 0016 #include <KPublicTransport/Journey> 0017 #include <KPublicTransport/Location> 0018 0019 #include <QDateTime> 0020 #include <QTimeZone> 0021 0022 using namespace KPublicTransport; 0023 0024 namespace KPublicTransport { 0025 class JourneyReplyPrivate : public ReplyPrivate { 0026 public: 0027 void finalizeResult() override; 0028 bool needToWaitForAssets() const override; 0029 static void postProcessJourneys(std::vector<Journey> &journeys); 0030 0031 JourneyRequest request; 0032 JourneyRequest nextRequest; 0033 JourneyRequest prevRequest; 0034 std::vector<Journey> journeys; 0035 }; 0036 } 0037 0038 void JourneyReplyPrivate::finalizeResult() 0039 { 0040 if (journeys.empty()) { 0041 return; 0042 } 0043 0044 error = Reply::NoError; 0045 errorMsg.clear(); 0046 0047 // merge results, aligned by first transport departure 0048 std::sort(journeys.begin(), journeys.end(), JourneyUtil::firstTransportDepartureLessThan); 0049 for (auto it = journeys.begin(); it != journeys.end(); ++it) { 0050 for (auto mergeIt = it + 1; mergeIt != journeys.end();) { 0051 if (!JourneyUtil::firstTransportDepartureEqual(*it, *mergeIt)) { 0052 break; 0053 } 0054 0055 if (Journey::isSame(*it, *mergeIt)) { 0056 *it = Journey::merge(*it, *mergeIt); 0057 mergeIt = journeys.erase(mergeIt); 0058 } else { 0059 ++mergeIt; 0060 } 0061 } 0062 } 0063 0064 // sort by departure time for display 0065 std::sort(journeys.begin(), journeys.end(), [](const auto &lhs, const auto &rhs) { 0066 return lhs.scheduledDepartureTime() < rhs.scheduledDepartureTime(); 0067 }); 0068 0069 nextRequest.purgeLoops(request); 0070 prevRequest.purgeLoops(request); 0071 } 0072 0073 bool JourneyReplyPrivate::needToWaitForAssets() const 0074 { 0075 return request.downloadAssets(); 0076 } 0077 0078 static bool isPointlessSection(const JourneySection §ion) 0079 { 0080 if (section.mode() == JourneySection::Waiting) { 0081 return section.duration() < 60; 0082 } 0083 if (section.mode() == JourneySection::Walking) { 0084 return section.duration() < 60 && section.path().isEmpty(); 0085 } 0086 return false; 0087 } 0088 0089 static bool isImplausibleSection(const JourneySection §ion) 0090 { 0091 if ((section.mode() == JourneySection::Transfer || section.mode() == JourneySection::Walking) 0092 && section.from().hasCoordinate() && section.to().hasCoordinate()) 0093 { 0094 const auto distance = Location::distance(section.from(), section.to()); 0095 if (section.duration() > 0 && (distance / section.duration()) > 30) { 0096 qCDebug(Log) << "discarding journey based on insane transfer/walking speed:" << (distance / section.duration()) << "m/s"; 0097 return true; 0098 } 0099 if (distance > 100000) { 0100 qCDebug(Log) << "discarding journey with insane transfer/walking distance:" << distance << "m" << section.from().name() << section.to().name(); 0101 return true; 0102 } 0103 } 0104 return false; 0105 } 0106 0107 void JourneyReplyPrivate::postProcessJourneys(std::vector<Journey> &journeys) 0108 { 0109 // try to fill gaps in timezone data 0110 for (auto &journey : journeys) { 0111 auto sections = journey.takeSections(); 0112 for (auto §ion : sections) { 0113 if (section.mode() == JourneySection::Walking) { 0114 if (!section.from().timeZone().isValid() && section.to().timeZone().isValid()) { 0115 auto from = section.from(); 0116 from.setTimeZone(section.to().timeZone()); 0117 section.setFrom(from); 0118 auto dt = section.scheduledDepartureTime(); 0119 dt.setTimeZone(from.timeZone()); 0120 section.setScheduledDepartureTime(dt); 0121 } 0122 if (section.from().timeZone().isValid() && !section.to().timeZone().isValid()) { 0123 auto to = section.to(); 0124 to.setTimeZone(section.from().timeZone()); 0125 section.setTo(to); 0126 auto dt = section.scheduledArrivalTime(); 0127 dt.setTimeZone(to.timeZone()); 0128 section.setScheduledArrivalTime(dt); 0129 } 0130 } 0131 } 0132 journey.setSections(std::move(sections)); 0133 } 0134 0135 // clean up non-transport sections 0136 for (auto &journey : journeys) { 0137 auto sections = journey.takeSections(); 0138 0139 // merge adjacent walking sections (yes, we do get that from backends...) 0140 for (auto it = sections.begin(); it != sections.end();) { 0141 if (it == sections.begin()) { 0142 ++it; 0143 continue; 0144 } 0145 auto prevIt = it - 1; 0146 if ((*it).mode() == JourneySection::Walking && (*prevIt).mode() == JourneySection::Walking) { 0147 (*prevIt).setTo((*it).to()); 0148 (*prevIt).setScheduledArrivalTime((*it).scheduledArrivalTime()); 0149 (*prevIt).setExpectedArrivalTime((*it).expectedArrivalTime()); 0150 (*prevIt).setDistance((*prevIt).distance() + (*it).distance()); 0151 it = sections.erase(it); 0152 continue; 0153 } 0154 0155 ++it; 0156 } 0157 0158 // remove pointless sections such as 0-length walks 0159 sections.erase(std::remove_if(sections.begin(), sections.end(), isPointlessSection), sections.end()); 0160 0161 // remove implausible paths 0162 for (auto §ion : sections) { 0163 if (!section.from().hasCoordinate() || !section.to().hasCoordinate() || section.path().isEmpty()) { 0164 continue; 0165 } 0166 0167 const auto pointDist = Location::distance(section.from(), section.to()); 0168 const auto pathDist = section.path().distance(); 0169 if (pathDist > pointDist * 10) { 0170 qCDebug(Log) << "Dropping implausibly long path:" << pointDist << pathDist; 0171 section.setPath({}); 0172 } 0173 } 0174 0175 journey.setSections(std::move(sections)); 0176 } 0177 0178 // remove empty or implausible journeys 0179 journeys.erase(std::remove_if(journeys.begin(), journeys.end(), [](const auto &journey) { 0180 return journey.sections().empty() || std::any_of(journey.sections().begin(), journey.sections().end(), isImplausibleSection); 0181 }), journeys.end()); 0182 } 0183 0184 JourneyReply::JourneyReply(const JourneyRequest &req, QObject *parent) 0185 : Reply(new JourneyReplyPrivate, parent) 0186 { 0187 Q_D(JourneyReply); 0188 d->request = req; 0189 d->nextRequest = req; 0190 d->prevRequest = req; 0191 } 0192 0193 JourneyReply::~JourneyReply() = default; 0194 0195 JourneyRequest JourneyReply::request() const 0196 { 0197 Q_D(const JourneyReply); 0198 return d->request; 0199 } 0200 0201 const std::vector<Journey>& JourneyReply::result() const 0202 { 0203 Q_D(const JourneyReply); 0204 return d->journeys; 0205 } 0206 0207 std::vector<Journey>&& JourneyReply::takeResult() 0208 { 0209 Q_D(JourneyReply); 0210 return std::move(d->journeys); 0211 } 0212 0213 JourneyRequest JourneyReply::nextRequest() const 0214 { 0215 Q_D(const JourneyReply); 0216 if (d->nextRequest.contexts().empty()) { 0217 return {}; 0218 } 0219 return d->nextRequest; 0220 } 0221 0222 JourneyRequest JourneyReply::previousRequest() const 0223 { 0224 Q_D(const JourneyReply); 0225 if (d->prevRequest.contexts().empty()) { 0226 return {}; 0227 } 0228 return d->prevRequest; 0229 } 0230 0231 void JourneyReply::addResult(const AbstractBackend *backend, std::vector<Journey> &&res) 0232 { 0233 Q_D(JourneyReply); 0234 d->postProcessJourneys(res); 0235 0236 // update context for next/prev requests 0237 // do this first, before res gets moved from below 0238 if (d->request.dateTimeMode() == JourneyRequest::Departure && !res.empty()) { 0239 // we create a context for later queries here in any case, since we can emulate that generically without backend support 0240 auto context = d->nextRequest.context(backend); 0241 context.type = RequestContext::Next; 0242 for (const auto &jny : res) { 0243 context.dateTime = std::max(context.dateTime, jny.scheduledDepartureTime()); 0244 } 0245 d->nextRequest.setContext(backend, std::move(context)); 0246 0247 context = d->prevRequest.context(backend); 0248 context.type = RequestContext::Previous; 0249 context.dateTime = res[0].scheduledArrivalTime(); // "invalid" is the minimum... 0250 for (const auto &jny : res) { 0251 context.dateTime = std::min(context.dateTime, jny.scheduledArrivalTime()); 0252 } 0253 d->prevRequest.setContext(backend, std::move(context)); 0254 } 0255 0256 // if this is a backend with a static timezone, apply this to the result 0257 if (backend->timeZone().isValid()) { 0258 for (auto &jny : res) { 0259 JourneyUtil::applyTimeZone(jny, backend->timeZone()); 0260 } 0261 } 0262 0263 // apply line meta data 0264 for (auto &jny : res) { 0265 jny.applyMetaData(request().downloadAssets()); 0266 } 0267 0268 // cache negative hits, positive ones are too short-lived 0269 if (res.empty()) { 0270 Cache::addNegativeDepartureCacheEntry(backend->backendId(), request().cacheKey()); 0271 } 0272 0273 // apply static attributions if @p backend contributed to the results 0274 addAttribution(backend->attribution()); 0275 0276 // update result 0277 if (!res.empty()) { 0278 if (d->journeys.empty()) { 0279 d->journeys = std::move(res); 0280 } else { 0281 d->journeys.insert(d->journeys.end(), res.begin(), res.end()); 0282 } 0283 d->emitUpdated(this); 0284 } 0285 0286 d->pendingOps--; 0287 d->emitFinishedIfDone(this); 0288 } 0289 0290 void JourneyReply::setNextContext(const AbstractBackend *backend, const QVariant &data) 0291 { 0292 Q_D(JourneyReply); 0293 auto context = d->nextRequest.context(backend); 0294 context.type = RequestContext::Next; 0295 context.backendData = data; 0296 d->nextRequest.setContext(backend, std::move(context)); 0297 } 0298 0299 void JourneyReply::setPreviousContext(const AbstractBackend *backend, const QVariant &data) 0300 { 0301 Q_D(JourneyReply); 0302 auto context = d->prevRequest.context(backend); 0303 context.type = RequestContext::Previous; 0304 context.backendData = data; 0305 d->prevRequest.setContext(backend, std::move(context)); 0306 } 0307 0308 void JourneyReply::addError(const AbstractBackend *backend, Reply::Error error, const QString &errorMsg) 0309 { 0310 if (error == Reply::NotFoundError) { 0311 Cache::addNegativeJourneyCacheEntry(backend->backendId(), request().cacheKey()); 0312 } else { 0313 qCDebug(Log) << backend->backendId() << error << errorMsg; 0314 } 0315 Reply::addError(error, errorMsg); 0316 }