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 &section)
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 &section)
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 &section : 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 &section : 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 }