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 }