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 }