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

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 "ltglinkbackend.h"
0008 
0009 #include <QNetworkAccessManager>
0010 #include <QNetworkRequest>
0011 #include <QNetworkReply>
0012 #include <QUrl>
0013 #include <QUrlQuery>
0014 #include <QJsonDocument>
0015 #include <QJsonArray>
0016 #include <QTimeZone>
0017 
0018 #include "datatypes/stopover.h"
0019 #include "journeyrequest.h"
0020 #include "journeyreply.h"
0021 #include "locationrequest.h"
0022 #include "locationreply.h"
0023 #include "datatypes/journey.h"
0024 
0025 #include "localbackendutils.h"
0026 
0027 using namespace KPublicTransport;
0028 
0029 AbstractBackend::Capabilities LTGLinkBackend::capabilities() const
0030 {
0031     return Secure;
0032 }
0033 
0034 bool LTGLinkBackend::needsLocationQuery(const Location &loc, QueryType type) const
0035 {
0036     Q_UNUSED(type);
0037     return loc.identifier(QStringLiteral("ltglinkint")).isEmpty();
0038 }
0039 
0040 bool LTGLinkBackend::queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const
0041 {
0042     if (m_stations.empty()) {
0043         if (!m_stationDataTask) {
0044             auto mutThis = const_cast<LTGLinkBackend *>(this);
0045             mutThis->m_stationDataTask = mutThis->downloadStationData(reply, nam);
0046         }
0047 
0048         connect(m_stationDataTask, &AbstractAsyncTask::finished, reply, [=, this]() {
0049             queryJourney(req, reply, nam);
0050             m_stationDataTask->deleteLater();
0051         });
0052 
0053         return true;
0054     }
0055 
0056     QUrl url(QStringLiteral("https://bilietas.ltglink.lt/api/v2021/en-gb/journeys/search"));
0057     QUrlQuery urlQuery;
0058     urlQuery.addQueryItem(QStringLiteral("departureDate"), req.dateTime().date().toString(Qt::ISODate));
0059     urlQuery.addQueryItem(QStringLiteral("currencyId"), QStringLiteral("CURRENCY.EUR"));
0060     urlQuery.addQueryItem(QStringLiteral("Passengers"), QStringLiteral("BONUS_SCHEME_GROUP.ADULT%2C1"));
0061     urlQuery.addQueryItem(QStringLiteral("OriginStopId"), req.from().identifier(QStringLiteral("ltglinkint")));
0062     urlQuery.addQueryItem(QStringLiteral("DestinationStopId"), req.to().identifier(QStringLiteral("ltglinkint")));
0063     url.setQuery(urlQuery);
0064 
0065     auto netReply = nam->get(QNetworkRequest(url));
0066     connect(netReply, &QNetworkReply::finished, this, [=, this]() {
0067         auto bytes = netReply->readAll();
0068         auto jsonValue = QJsonDocument::fromJson(bytes);
0069 
0070         logReply(reply, netReply, bytes);
0071 
0072         const auto journeysJson = jsonValue[u"Journeys"].toArray();
0073 
0074         auto journeys = std::make_shared<std::vector<Journey>>();
0075         auto runningRequests = std::make_shared<int>(0);
0076 
0077         for (const auto &j : journeysJson) {
0078             auto journeyId = j[u"Id"].toString();
0079             auto sectionsJson = j[u"Legs"].toArray();
0080 
0081             QUrl routeUrl(QStringLiteral("https://bilietas.ltglink.lt/api/v2021/en-gb/journeys/%1/route?includeIntermediateStops=true").arg(journeyId));
0082             auto routeReply = nam->get(QNetworkRequest(routeUrl));
0083 
0084             (*runningRequests)++;
0085 
0086             connect(routeReply, &QNetworkReply::finished, this, [=, this]() {
0087                 auto bytes = routeReply->readAll();
0088                 auto route = QJsonDocument::fromJson(bytes);
0089 
0090                 logReply(reply, netReply, bytes);
0091 
0092                 auto sectionStopsJson = route[u"Legs"].toArray();
0093 
0094                 std::vector<JourneySection> sections;
0095                 for (const auto sectionJson : sectionsJson) {
0096                     auto originId = sectionJson[u"Origin"][u"Stop"][u"Id"].toInt();
0097                     auto originTimeZone = QTimeZone(sectionJson[u"Origin"][u"TimeZone"].toString().toUtf8());
0098 
0099                     auto actualDepartureTime = QDateTime::fromString(sectionJson[u"Origin"][u"ActualDepartureDateTime"].toString(), Qt::ISODate);
0100                     actualDepartureTime.setTimeZone(originTimeZone);
0101                     auto scheduledDepartureTime = QDateTime::fromString(sectionJson[u"Origin"][u"PlannedDepartureDateTime"].toString(), Qt::ISODate);
0102                     scheduledDepartureTime.setTimeZone(originTimeZone);
0103 
0104                     auto destinationId = sectionJson[u"Destination"][u"Stop"][u"Id"].toInt();
0105                     auto destinationTimeZone = QTimeZone(sectionJson[u"Destination"][u"TimeZone"].toString().toUtf8());
0106 
0107                     auto actualArrivalTime = QDateTime::fromString(sectionJson[u"Destination"][u"ActualArrivalDateTime"].toString(), Qt::ISODate);
0108                     actualArrivalTime.setTimeZone(destinationTimeZone);
0109                     auto scheduledArrivalTime = QDateTime::fromString(sectionJson[u"Destination"][u"PlannedArrivalDateTime"].toString(), Qt::ISODate);
0110                     scheduledArrivalTime.setTimeZone(destinationTimeZone);
0111 
0112                     auto note = sectionJson[u"Trip"][u"Name"].toString();
0113 
0114                     // Filter out results that don't match the selected time frame
0115                     if (!LocalBackendUtils::isInSelectedTimeframe(actualDepartureTime, actualArrivalTime, req)) {
0116                         continue;
0117                     }
0118 
0119                     auto lineNumber = sectionJson[u"Line"][u"Number"].toString();
0120                     auto transportationType = sectionJson[u"Line"][u"TransportationType"][u"Id"].toString();
0121 
0122                     Line line;
0123                     line.setName(lineNumber);
0124                     line.setMode(transportationType == u"LINE_TRANSPORTATION_TYPE.TRAIN" ? Line::Mode::Train : Line::Mode::Unknown);
0125 
0126                     Route route;
0127                     route.setLine(line);
0128 
0129                     JourneySection section;
0130                     section.setFrom(lookupStation(originId));
0131                     section.setTo(lookupStation(destinationId));
0132                     section.setScheduledDepartureTime(scheduledDepartureTime);
0133                     section.setExpectedDepartureTime(actualDepartureTime);
0134                     section.setScheduledArrivalTime(scheduledArrivalTime);
0135                     section.setExpectedArrivalTime(actualArrivalTime);
0136                     section.setMode(JourneySection::PublicTransport);
0137                     section.setRoute(route);
0138 
0139                     if (!note.isEmpty()) {
0140                         section.setNotes({note});
0141                     }
0142 
0143                     sections.push_back(std::move(section));
0144                 }
0145 
0146                 for (int i = 0; i < sectionStopsJson.size() && i < int(sections.size()); i++) {
0147                     auto sectionStopJson = sectionStopsJson[i];
0148 
0149                     std::vector<Stopover> stopovers;
0150                     for (const auto &stopJson : sectionStopJson[u"IntermediateStops"].toArray()) {
0151                         QTimeZone arrivalTimeZone(stopJson[u"ArrivalDateTimeZone"].toString().toUtf8());
0152                         QTimeZone departureTimeZone(stopJson[u"DepartureDateTimeZone"].toString().toUtf8());
0153 
0154                         // Unfortunately no coordinates included in reply :(
0155                         Location location;
0156                         location.setName(stopJson[u"StopName"].toString());
0157                         location.setCountry(QLocale::territoryToString(arrivalTimeZone.territory()));
0158                         location.setTimeZone(arrivalTimeZone);
0159 
0160                         auto arrivalDateTime = QDateTime::fromString(stopJson[u"ArrivalDateTime"].toString(), Qt::ISODate);
0161                         arrivalDateTime.setTimeZone(arrivalTimeZone);
0162 
0163                         auto departureDateTime = QDateTime::fromString(stopJson[u"DepartureDateTime"].toString(), Qt::ISODate);
0164                         arrivalDateTime.setTimeZone(departureTimeZone);
0165 
0166                         Stopover stopover;
0167                         stopover.setStopPoint(location);
0168                         stopover.setScheduledArrivalTime(arrivalDateTime);
0169                         stopover.setScheduledDepartureTime(departureDateTime);
0170 
0171                         stopovers.push_back(std::move(stopover));
0172                     }
0173 
0174                     sections[i].setIntermediateStops(std::move(stopovers));
0175                 }
0176 
0177                 Journey journey;
0178                 journey.setSections(std::move(sections));
0179 
0180                 journeys->push_back(std::move(journey));
0181                 (*runningRequests)--;
0182 
0183                 if (*runningRequests == 0) {
0184                     if (!journeys->empty()) {
0185                         Attribution attribution;
0186                         attribution.setName(QStringLiteral("LTG Link"));
0187                         attribution.setUrl(QUrl(QStringLiteral("https://ltglink.lt")));
0188                         addAttributions(reply, {attribution});
0189                     }
0190 
0191                     addResult(reply, this, std::move(*journeys));
0192                 }
0193             });
0194 
0195             connect(routeReply, &QNetworkReply::errorOccurred, reply, [=, this]() {
0196                 addError(reply, Reply::NetworkError, netReply->errorString());
0197             });
0198         };
0199 
0200         netReply->deleteLater();
0201     });
0202 
0203     connect(netReply, &QNetworkReply::errorOccurred, reply, [=, this]() {
0204         addError(reply, Reply::NetworkError, netReply->errorString());
0205     });
0206 
0207     return true;
0208 }
0209 
0210 bool LTGLinkBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
0211 {
0212     if (m_stations.empty()) {
0213         if (!m_stationDataTask) {
0214             auto mutThis = const_cast<LTGLinkBackend *>(this);
0215             mutThis->m_stationDataTask = mutThis->downloadStationData(reply, nam);
0216         }
0217 
0218         connect(m_stationDataTask, &AbstractAsyncTask::finished, reply, [=, this]() {
0219             queryLocation(req, reply, nam);
0220             m_stationDataTask->deleteLater();
0221         });
0222 
0223         return true;
0224     }
0225 
0226     std::vector<Location> locations;
0227     QString name = LocalBackendUtils::makeSearchableName(req.name());
0228 
0229     for (auto [id, station] : std::as_const(m_stations)) {
0230         if (station.searchableName.contains(name)) {
0231             auto loc = stationToLocation(station);
0232             locations.push_back(std::move(loc));
0233         }
0234     }
0235 
0236     addResult(reply, std::move(locations));
0237 
0238     return false;
0239 }
0240 
0241 AsyncTask<void> *LTGLinkBackend::downloadStationData(Reply *reply, QNetworkAccessManager *nam)
0242 {
0243     auto task = new AsyncTask<void>(this);
0244 
0245     QUrl url(QStringLiteral("https://cms.ltglink.turnit.com/api/turnit/search"));
0246     QUrlQuery urlQuery;
0247 
0248     // The API falls back to lithuanian, so we want to force english most of the time
0249     urlQuery.addQueryItem(QStringLiteral("locale"), preferredLanguage());
0250 
0251     url.setQuery(urlQuery);
0252 
0253     auto *netReply = nam->get(QNetworkRequest(url));
0254     QObject::connect(netReply, &QNetworkReply::finished, this, [=, this]() {
0255         const auto bytes = netReply->readAll();
0256 
0257         logReply(reply, netReply, bytes);
0258 
0259         if (!m_stations.empty()) {
0260             return;
0261         }
0262 
0263         const auto jsonValue = QJsonDocument::fromJson(bytes);
0264 
0265         const auto countries = jsonValue[u"Stops"][u"Countries"].toArray();
0266         for (const auto &country : countries) {
0267             const auto cities = country[u"Cities"].toArray();
0268             for (const auto &city : cities) {
0269                 const auto stations = city[u"BusStops"].toArray();
0270 
0271                 for (const auto &station : stations) {
0272                     m_stations.insert({
0273                         station[u"BusStopId"].toInt(),
0274                         LTGLink::Station {
0275                             .id = station[u"BusStopId"].toInt(),
0276                             .name = station[u"BusStopName"].toString(),
0277                             .searchableName = LocalBackendUtils::makeSearchableName(station[u"BusStopName"].toString()),
0278                             .latitude = float(station[u"Coordinates"][u"Latitude"].toDouble()),
0279                             .longitude = float(station[u"Coordinates"][u"Longitude"].toDouble())
0280                         }
0281                     });
0282                 }
0283             }
0284         }
0285 
0286         task->reportFinished();
0287 
0288         netReply->deleteLater();
0289     });
0290 
0291     return task;
0292 }
0293 
0294 Location LTGLinkBackend::stationToLocation(const LTGLink::Station &station)
0295 {
0296     Location loc;
0297     loc.setCoordinate(station.latitude, station.longitude);
0298     loc.setIdentifier(QStringLiteral("ltglinkint"), QString::number(station.id));
0299     loc.setName(station.name);
0300     loc.setType(Location::Stop);
0301     return loc;
0302 }
0303 
0304 Location LTGLinkBackend::lookupStation(int ltglinkint) const
0305 {
0306     const auto &station = m_stations.at(ltglinkint);
0307     return stationToLocation(station);
0308 }