File indexing completed on 2024-05-12 04:42:41

0001 /*
0002     SPDX-FileCopyrightText: 2023 Jonah BrĂ¼chert <jbb@kaidan.im>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "pasazieruvilciensbackend.h"
0008 
0009 #include <QNetworkAccessManager>
0010 #include <QNetworkRequest>
0011 #include <QNetworkReply>
0012 #include <QUrlQuery>
0013 #include <QPointer>
0014 
0015 #include <QJsonDocument>
0016 #include <QJsonArray>
0017 
0018 #include "datatypes/stopover.h"
0019 #include "journeyreply.h"
0020 #include "journeyrequest.h"
0021 #include "locationrequest.h"
0022 #include "locationreply.h"
0023 #include "localbackendutils.h"
0024 
0025 using namespace KPublicTransport;
0026 
0027 PasazieruVilciensBackend::PasazieruVilciensBackend()
0028     : AbstractBackend()
0029 {
0030 }
0031 
0032 AbstractBackend::Capabilities PasazieruVilciensBackend::capabilities() const
0033 {
0034     return Secure;
0035 }
0036 
0037 bool PasazieruVilciensBackend::needsLocationQuery(const Location &loc, QueryType type) const
0038 {
0039     Q_UNUSED(type);
0040     return loc.identifier(QStringLiteral("pvint")).isEmpty();
0041 }
0042 
0043 bool PasazieruVilciensBackend::queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const
0044 {
0045     if (m_stations.empty()) {
0046         if (!m_stationDataTask) {
0047             auto mutThis = const_cast<PasazieruVilciensBackend *>(this);
0048             mutThis->m_stationDataTask = mutThis->downloadStationData(reply, nam);
0049         }
0050 
0051         connect(m_stationDataTask, &AbstractAsyncTask::finished, reply, [=, this]() {
0052             m_stationDataTask->deleteLater();
0053             queryJourney(req, reply, nam);
0054         });
0055 
0056         return true;
0057     }
0058 
0059     auto tripResult = fetchTrip(req, reply, nam);
0060     auto joinedTripResult = fetchJoinedTrip(req, reply, nam);
0061 
0062     auto processResults = [=, this]() {
0063         std::vector<Journey> results = tripResult->result().value();
0064         results.insert(results.end(), joinedTripResult->result()->begin(), joinedTripResult->result()->end());
0065         std::sort(results.begin(), results.end(), [](const Journey &a, const Journey &b) {
0066             return a.scheduledDepartureTime() < b.scheduledDepartureTime();
0067         });
0068 
0069         bool isEmpty = results.empty();
0070 
0071         addResult(reply, this, std::move(results));
0072 
0073         if (!isEmpty) {
0074             Attribution attribution;
0075             attribution.setName(QStringLiteral("Pasazieru vilciens"));
0076             attribution.setUrl(QUrl(QStringLiteral("https://pv.lv")));
0077             addAttributions(reply, {attribution});
0078         }
0079     };
0080 
0081     connect(tripResult.get(), &PendingQuery::finished, this, [=, this]() {
0082         if (joinedTripResult->result()) {
0083             processResults();
0084         } else {
0085             connect(joinedTripResult.get(), &PendingQuery::finished, this, processResults);
0086         }
0087     });
0088 
0089     return true;
0090 }
0091 
0092 bool PasazieruVilciensBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
0093 {
0094     if (m_stations.empty()) {
0095         if (!m_stationDataTask) {
0096             auto mutThis = const_cast<PasazieruVilciensBackend *>(this);
0097             mutThis->m_stationDataTask = mutThis->downloadStationData(reply, nam);
0098         }
0099 
0100         connect(m_stationDataTask, &AbstractAsyncTask::finished, reply, [=, this]() {
0101             queryLocation(req, reply, nam);
0102             m_stationDataTask->deleteLater();
0103         });
0104 
0105         return true;
0106     }
0107 
0108     std::vector<Location> locations;
0109     QString name = LocalBackendUtils::makeSearchableName(req.name());
0110 
0111     for (auto [id, station] : std::as_const(m_stations)) {
0112         if (station.searchableName.contains(name)) {
0113             auto loc = stationToLocation(station);
0114             locations.push_back(std::move(loc));
0115         }
0116     }
0117 
0118     addResult(reply, std::move(locations));
0119 
0120     return false;
0121 }
0122 
0123 AsyncTask<void> *PasazieruVilciensBackend::downloadStationData(Reply *reply, QNetworkAccessManager *nam)
0124 {
0125     auto task = new AsyncTask<void>(this);
0126 
0127     auto *netReply = nam->get(QNetworkRequest(QUrl(QStringLiteral("https://pvapi.pv.lv/api/getallStations/"))));
0128     QObject::connect(netReply, &QNetworkReply::finished, this, [=, this]() {
0129         const auto bytes = netReply->readAll();
0130 
0131         logReply(reply, netReply, bytes);
0132 
0133         if (!m_stations.empty()) {
0134             return;
0135         }
0136 
0137         const auto jsonValue = QJsonDocument::fromJson(bytes);
0138         const auto data = jsonValue[QStringLiteral("data")].toArray();
0139 
0140         for (const auto &stationJson : data) {
0141             const QString name = stationJson[u"name"].toString();
0142             m_stations.insert({int(stationJson[u"id"].toDouble()),
0143                 PV::Station {
0144                     .id = int(stationJson[QStringLiteral("id")].toDouble()),
0145                     .name = name,
0146                     .searchableName = LocalBackendUtils::makeSearchableName(name),
0147                     .latitude = static_cast<float>(stationJson[u"latitude"].toDouble()),
0148                     .longitude = static_cast<float>(stationJson[u"longitude"].toDouble())
0149                 }});
0150         }
0151 
0152         task->reportFinished();
0153 
0154         netReply->deleteLater();
0155     });
0156 
0157     return task;
0158 }
0159 
0160 std::shared_ptr<PendingQuery> PasazieruVilciensBackend::fetchTrip(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const
0161 {
0162     auto pendingQuery = std::make_shared<PendingQuery>();
0163 
0164     auto reqUrl = QUrl(QStringLiteral("https://pvapi.pv.lv/api/gettrip"));
0165     QUrlQuery query;
0166     query.addQueryItem(QStringLiteral("stationAId"), req.from().identifier(QStringLiteral("pvint")));
0167     query.addQueryItem(QStringLiteral("stationBId"), req.to().identifier(QStringLiteral("pvint")));
0168     query.addQueryItem(QStringLiteral("date"), req.dateTime().date().toString(Qt::ISODate));
0169     reqUrl.setQuery(query);
0170 
0171     auto netReply = nam->get(QNetworkRequest(reqUrl));
0172     QObject::connect(netReply, &QNetworkReply::finished, netReply, [=, this]() {
0173         auto jsonValue = QJsonDocument::fromJson(netReply->readAll());
0174         const auto data = jsonValue[u"data"].toArray();
0175 
0176         if (data.empty()) {
0177             pendingQuery->reportFinished({});
0178         }
0179 
0180         auto journeys = std::make_shared<std::vector<Journey>>();
0181         auto runningRequests = std::make_shared<int>();
0182 
0183         bool foundAny = false;
0184         for (const auto tripJson : data) {
0185             QTimeZone tz("Europe/Riga");
0186 
0187             auto arrival = QDateTime::fromSecsSinceEpoch(qint64(tripJson[QStringLiteral("arrival")].toDouble())).toTimeZone(tz);
0188             auto departure = QDateTime::fromSecsSinceEpoch(qint64(tripJson[QStringLiteral("departure")].toDouble())).toTimeZone(tz);
0189 
0190             // Filter for requested arrival / departure time frame
0191             if (!LocalBackendUtils::isInSelectedTimeframe(departure, arrival, req)) {
0192                 continue;
0193             }
0194 
0195             foundAny = true;
0196 
0197             QUrl detailsUrl(QStringLiteral("https://pvapi.pv.lv/api/getroute"));
0198             QUrlQuery query;
0199             query.addQueryItem(QStringLiteral("stationAId"), req.from().identifier(QStringLiteral("pvint")));
0200             query.addQueryItem(QStringLiteral("stationBId"), req.to().identifier(QStringLiteral("pvint")));
0201             query.addQueryItem(QStringLiteral("getRouteId"), QString::number(tripJson[QStringLiteral("getRouteId")].toInt()));
0202             query.addQueryItem(QStringLiteral("lang"), QStringLiteral("en"));
0203             detailsUrl.setQuery(query);
0204 
0205             auto trainNr = tripJson[u"trainNr"].toString();
0206             bool isBus = tripJson[u"bus"].toBool();
0207             auto routeName = tripJson[u"name"].toString();
0208             QString platform = tripJson[u"roadNumber"].toString();
0209 
0210             auto detailsReply = nam->get(QNetworkRequest(detailsUrl));
0211             (*runningRequests)++;
0212             QObject::connect(detailsReply, &QNetworkReply::finished, this, [=, this]() {
0213                 const auto jsonValue = QJsonDocument::fromJson(detailsReply->readAll());
0214                 auto stopsJson = prepareStops(jsonValue[u"data"][u"route"].toArray());
0215                 auto stops = parseStopovers({stopsJson.begin() + 1, stopsJson.end() - 1}, departure);
0216 
0217                 JourneySection section;
0218                 section.setFrom(req.from());
0219                 section.setTo(req.to());
0220                 section.setScheduledDeparturePlatform(platform);
0221 
0222                 section.setScheduledDepartureTime(departure);
0223                 section.setScheduledArrivalTime(arrival);
0224 
0225                 section.setMode(JourneySection::Mode::PublicTransport);
0226                 section.setIntermediateStops(std::move(stops));
0227 
0228                 Line line;
0229                 line.setName(trainNr);
0230                 line.setMode(isBus ? Line::Mode::Bus : Line::Mode::Train);
0231 
0232                 Route route;
0233                 route.setName(routeName);
0234                 route.setLine(line);
0235 
0236                 section.setRoute(route);
0237 
0238                 Journey journey;
0239                 journey.setSections({section});
0240                 journeys->push_back(std::move(journey));
0241 
0242                 (*runningRequests)--;
0243 
0244                 if (*runningRequests == 0) {
0245                     pendingQuery->reportFinished(std::move(*journeys));
0246                 }
0247             });
0248 
0249             connect(detailsReply, &QNetworkReply::errorOccurred, reply, [=, this]() {
0250                 addError(reply, Reply::NetworkError, netReply->errorString());
0251             });
0252         }
0253 
0254         if (!foundAny) {
0255             pendingQuery->reportFinished({});
0256         }
0257 
0258         netReply->deleteLater();
0259     });
0260     connect(netReply, &QNetworkReply::errorOccurred, reply, [=, this]() {
0261         addError(reply, Reply::NetworkError, netReply->errorString());
0262     });
0263 
0264 
0265     return pendingQuery;
0266 }
0267 
0268 std::shared_ptr<PendingQuery> PasazieruVilciensBackend::fetchJoinedTrip(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const
0269 {
0270     auto pendingQuery = std::make_shared<PendingQuery>();
0271 
0272     QUrl reqUrl(QStringLiteral("https://pvapi.pv.lv/api/getjoinedtrip"));
0273     QUrlQuery query;
0274     query.addQueryItem(QStringLiteral("stationAId"), req.from().identifier(QStringLiteral("pvint")));
0275     query.addQueryItem(QStringLiteral("stationBId"), req.to().identifier(QStringLiteral("pvint")));
0276     query.addQueryItem(QStringLiteral("date"), req.dateTime().date().toString(Qt::ISODate));
0277     reqUrl.setQuery(query);
0278 
0279     auto netReply = nam->get(QNetworkRequest(reqUrl));
0280     QObject::connect(netReply, &QNetworkReply::finished, this, [=, this]() {
0281         auto joinedTrip = QJsonDocument::fromJson(netReply->readAll());
0282         auto data = joinedTrip[QStringLiteral("data")].toArray();
0283 
0284         auto journeys = std::make_shared<std::vector<Journey>>();
0285         auto runningRequests = std::make_shared<int>();
0286 
0287         bool foundAny = false;
0288         for (const auto &tripJson : data) {
0289             QTimeZone tz("Europe/Riga");
0290 
0291             auto arrival = QDateTime::fromSecsSinceEpoch(qint64(tripJson[QStringLiteral("arrival")].toDouble())).toTimeZone(tz);
0292             auto departure = QDateTime::fromSecsSinceEpoch(qint64(tripJson[QStringLiteral("departure")].toDouble())).toTimeZone(tz);
0293 
0294             auto route1Id = tripJson[u"leg1routeId"].toString();
0295             auto route2Id = tripJson[u"leg2routeId"].toString();
0296 
0297             auto startStationId = tripJson[u"startStationId"].toString();
0298             auto startTime = parseDateTime(tripJson[u"startTime"].toString(), departure.date());
0299             auto startTrainNr = tripJson[u"startTrainNumber"].toString();
0300 
0301             auto transferStationId = tripJson[u"transferPlaceId"].toString();
0302             auto transferArriveTime = parseDateTime(tripJson[u"transferArriveTime"].toString(), departure.date(), startTime);
0303             auto transferLeaveTime = parseDateTime(tripJson[u"transferLeaveTime"].toString(), departure.date(), transferArriveTime);
0304 
0305             auto endStationId = tripJson[u"endStationId"].toString();
0306             auto endTime = parseDateTime(tripJson[u"endTime"].toString(), departure.date(), transferLeaveTime);
0307             auto endTrainNr = tripJson[u"endTrainNumber"].toString();
0308 
0309             // Filter for requested arrival / departure time frame
0310             if (!LocalBackendUtils::isInSelectedTimeframe(departure, arrival, req)) {
0311                 continue;
0312             }
0313 
0314             foundAny = true;
0315 
0316             auto startTrainNumber = tripJson[QStringLiteral("startTrainNr")].toString();
0317             auto endTrainNumber = tripJson[QStringLiteral("endTrainNr")].toString();
0318 
0319             QUrl detailsUrl(QStringLiteral("https://pvapi.pv.lv/api/getjoinedroute"));
0320             QUrlQuery query;
0321             query.addQueryItem(QStringLiteral("stationAId"), startStationId);
0322             query.addQueryItem(QStringLiteral("stationBId"), endStationId);
0323             query.addQueryItem(QStringLiteral("stationXId"), transferStationId);
0324             query.addQueryItem(QStringLiteral("leg1RouteId"), route1Id);
0325             query.addQueryItem(QStringLiteral("leg2RouteId"), route2Id);
0326             query.addQueryItem(QStringLiteral("lang"), QStringLiteral("en"));
0327             detailsUrl.setQuery(query);
0328 
0329             auto detailsReply = nam->get(QNetworkRequest(detailsUrl));
0330             (*runningRequests)++;
0331 
0332             connect(detailsReply, &QNetworkReply::finished, this, [=, this]() {
0333                 auto jsonValue = QJsonDocument::fromJson(detailsReply->readAll());
0334                 auto stopovers = jsonValue[u"data"][u"route"].toArray();
0335 
0336                 auto [stopOversAJson, stopOversBJson] = splitJoinedSections(std::move(stopovers));
0337 
0338                 auto stopoversA = parseStopovers(std::move(stopOversAJson), departure);
0339                 auto stopoversB = parseStopovers(std::move(stopOversBJson), departure);
0340 
0341                 JourneySection sectionA;
0342                 sectionA.setFrom(lookupStation(startStationId.toInt()));
0343                 sectionA.setTo(lookupStation(transferStationId.toInt()));
0344                 sectionA.setScheduledDepartureTime(startTime);
0345                 sectionA.setScheduledArrivalTime(transferArriveTime);
0346                 sectionA.setIntermediateStops(std::move(stopoversA));
0347                 sectionA.setMode(JourneySection::Mode::PublicTransport);
0348 
0349                 Line lineA;
0350                 lineA.setName(startTrainNr);
0351                 lineA.setMode(Line::Mode::Train);
0352 
0353                 Route routeA;
0354                 routeA.setLine(lineA);
0355 
0356                 sectionA.setRoute(routeA);
0357 
0358                 JourneySection sectionB;
0359                 sectionB.setFrom(lookupStation(transferStationId.toInt()));
0360                 sectionB.setTo(lookupStation(endStationId.toInt()));
0361                 sectionB.setScheduledDepartureTime(transferLeaveTime);
0362                 sectionB.setScheduledArrivalTime(endTime);
0363                 sectionB.setIntermediateStops(std::move(stopoversB));
0364                 sectionB.setMode(JourneySection::Mode::PublicTransport);
0365 
0366                 Line lineB;
0367                 lineB.setName(endTrainNr);
0368                 lineB.setMode(Line::Mode::Train);
0369 
0370                 Route routeB;
0371                 routeB.setLine(lineB);
0372 
0373                 sectionB.setRoute(routeB);
0374 
0375                 Journey journey;
0376                 journey.setSections({sectionA, sectionB});
0377                 journeys->push_back(std::move(journey));
0378 
0379                 (*runningRequests)--;
0380 
0381                 if (*runningRequests == 0) {
0382                     pendingQuery->reportFinished(std::move(*journeys));
0383                 }
0384             });
0385             connect(detailsReply, &QNetworkReply::errorOccurred, reply, [=, this]() {
0386                 addError(reply, Reply::NetworkError, netReply->errorString());
0387             });
0388         }
0389 
0390         if (!foundAny) {
0391             pendingQuery->reportFinished({});
0392         }
0393 
0394         netReply->deleteLater();
0395     });
0396 
0397     connect(netReply, &QNetworkReply::errorOccurred, reply, [=, this]() {
0398         addError(reply, Reply::NetworkError, netReply->errorString());
0399     });
0400 
0401 
0402     return pendingQuery;
0403 }
0404 
0405 Location PasazieruVilciensBackend::stationToLocation(const PV::Station &station)
0406 {
0407     Location loc;
0408     loc.setCoordinate(station.latitude, station.longitude);
0409     loc.setIdentifier(QStringLiteral("pvint"), QString::number(station.id));
0410     loc.setName(station.name);
0411     loc.setType(Location::Stop);
0412     return loc;
0413 }
0414 
0415 Location PasazieruVilciensBackend::lookupStation(int pvint) const
0416 {
0417     const auto &station = m_stations.at(pvint);
0418     return stationToLocation(station);
0419 }
0420 
0421 std::vector<Stopover> PasazieruVilciensBackend::parseStopovers(std::vector<QJsonObject> &&stops, const QDateTime &startTime) const {
0422     std::vector<Stopover> stopovers;
0423 
0424     // Just to ensure timestamps can't go backwards
0425     QDateTime previousArrivalTime;
0426 
0427     for (auto &&stopoverJson : stops) {
0428         Stopover stopover;
0429 
0430         QDateTime arrivalDateTime = parseDateTime(stopoverJson[QStringLiteral("time")].toString(), startTime.date(), arrivalDateTime);
0431         previousArrivalTime = arrivalDateTime;
0432 
0433         stopover.setScheduledArrivalTime(arrivalDateTime);
0434         stopover.setScheduledDepartureTime(arrivalDateTime);
0435         stopover.setStopPoint(stationToLocation(m_stations.at(stopoverJson[QStringLiteral("stationId")].toInt())));
0436         stopovers.push_back(std::move(stopover));
0437     }
0438 
0439     return stopovers;
0440 }
0441 
0442 std::vector<QJsonObject> PasazieruVilciensBackend::prepareStops(QJsonArray &&data) const
0443 {
0444     // Drop items not part of the route and items that are unsupported
0445     std::vector<QJsonValueRef> filteredItems;
0446     std::copy_if(data.begin(), data.end(), std::back_inserter(filteredItems), [](const auto &item) {
0447         const auto obj = item.toObject();
0448         const auto type = obj[QStringLiteral("type")].toString();
0449         bool inRoute = obj[QStringLiteral("inRoute")].toBool();
0450         return (type == QStringLiteral("stop") || type == QStringLiteral("transfer")) && inRoute;
0451     });
0452 
0453     // Convert all elements in the array to objects
0454     std::vector<QJsonObject> items;
0455     std::transform(filteredItems.begin(), filteredItems.end(), std::back_inserter(items), [](auto &&item) {
0456         return item.toObject();
0457     });
0458 
0459     return items;
0460 }
0461 
0462 std::tuple<std::vector<QJsonObject>, std::vector<QJsonObject>> PasazieruVilciensBackend::splitJoinedSections(QJsonArray &&data) const
0463 {
0464     auto route = prepareStops(std::move(data));
0465 
0466     // find the transfer
0467     auto endTransferIt = std::find_if(route.begin(), route.end(), [](const auto &item) {
0468         return item[u"type"].toString() == u"transfer";
0469     });
0470     Q_ASSERT(endTransferIt != route.end());
0471 
0472     auto startTransferIt = std::find_if(endTransferIt + 1, route.end(), [](const auto &item) {
0473         return item[u"type"].toString() == u"transfer";
0474     });
0475     Q_ASSERT(startTransferIt != route.end());
0476 
0477     // Copy stopovers between start and transfer, transfer and end
0478     std::vector firstPart(route.begin() + 1, endTransferIt);
0479     std::vector secondPart(startTransferIt + 1, route.end() - 1);
0480 
0481     return {firstPart, secondPart};
0482 }
0483 
0484 QDateTime PasazieruVilciensBackend::parseDateTime(const QString &timeString, const QDate &date, const QDateTime &knownPreviousTime) const
0485 {
0486     auto time = QTime::fromString(timeString);
0487 
0488     auto dateTime = date.startOfDay();
0489     dateTime.setTime(time);
0490     dateTime.setTimeZone(QTimeZone("Europe/Riga"));
0491 
0492     if (!knownPreviousTime.isNull() && dateTime < knownPreviousTime) {
0493         dateTime.setDate(dateTime.date().addDays(1));
0494     }
0495 
0496     return dateTime;
0497 }