File indexing completed on 2024-05-12 04:42:42
0001 /* 0002 SPDX-FileCopyrightText: 2024 Jonah Brüchert <jbb@kaidan.im> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "zpcgbackend.h" 0008 0009 #include <QUrl> 0010 #include <QUrlQuery> 0011 #include <QNetworkAccessManager> 0012 #include <QNetworkRequest> 0013 #include <QNetworkReply> 0014 #include <QJsonDocument> 0015 #include <QJsonArray> 0016 #include <QFile> 0017 0018 #include <array> 0019 #include <chrono> 0020 0021 #include "datatypes/location.h" 0022 #include "datatypes/stopover.h" 0023 #include "locationrequest.h" 0024 #include "locationreply.h" 0025 #include "journeyreply.h" 0026 #include "journeyrequest.h" 0027 #include "localbackendutils.h" 0028 #include "networkreplycollection.h" 0029 #include "logging.h" 0030 0031 using namespace KPublicTransport::LocalBackendUtils; 0032 using namespace KPublicTransport; 0033 0034 using namespace std::chrono_literals; 0035 0036 AbstractBackend::Capabilities ZPCGBackend::capabilities() const 0037 { 0038 return Secure; 0039 } 0040 0041 bool ZPCGBackend::needsLocationQuery(const Location &loc, QueryType type) const 0042 { 0043 Q_UNUSED(type); 0044 return loc.identifier(identifierName()).isEmpty(); 0045 } 0046 0047 bool ZPCGBackend::queryJourney(const JourneyRequest &request, JourneyReply *reply, QNetworkAccessManager *nam) const 0048 { 0049 // Ignore requests for which we don't have the needed identifier 0050 if (request.from().identifier(identifierName()).isEmpty() || request.to().identifier(identifierName()).isEmpty()) { 0051 return false; 0052 } 0053 0054 if (m_stations.empty()) { 0055 if (!m_fetchStationsTask) { 0056 auto mutThis = const_cast<ZPCGBackend *>(this); 0057 mutThis->m_fetchStationsTask = mutThis->downloadStationData(reply, nam); 0058 } 0059 0060 connect(m_fetchStationsTask, &AbstractAsyncTask::finished, reply, [=, this]() { 0061 queryJourney(request, reply, nam); 0062 }); 0063 0064 return true; 0065 } 0066 0067 QUrl url = baseUrl(); 0068 QUrlQuery query; 0069 query.addQueryItem(QStringLiteral("r"), QStringLiteral("api/search")); 0070 query.addQueryItem(QStringLiteral("from"), request.from().identifier(identifierName())); 0071 query.addQueryItem(QStringLiteral("to"), request.to().identifier(identifierName())); 0072 query.addQueryItem(QStringLiteral("date"), request.dateTime().date().toString(QStringLiteral("yyyy-MM-dd"))); 0073 url.setQuery(query); 0074 0075 auto netReply = nam->get(QNetworkRequest(url)); 0076 connect(netReply, &QNetworkReply::finished, reply, [=, this]() { 0077 auto data = netReply->readAll(); 0078 auto doc = QJsonDocument::fromJson(data); 0079 const auto journeysJson = doc.array(); 0080 0081 logReply(reply, netReply, data); 0082 0083 if (journeysJson.empty()) { 0084 addResult(reply, this, std::vector<Journey>()); 0085 } 0086 0087 std::vector<QNetworkReply *> replies; 0088 auto journeys = std::make_shared<std::vector<Journey>>(); 0089 0090 for (const auto &journeyJson : journeysJson) { 0091 const QString from = journeyJson[u"f"].toString(); 0092 const QString to = journeyJson[u"t"].toString(); 0093 0094 const auto journeyJsonArray = journeyJson[u"s"].toArray(); 0095 for (const auto &something : journeyJsonArray) { 0096 const QString timetableId = something[u"i"].toString(); 0097 const QString trainNumber = something[u"n"].toString(); 0098 const QString trainType = something[u"t"].toString(); 0099 0100 auto departureTime = parseDateTime(something[u"d"].toString(), request.dateTime().date()); 0101 auto arrivalTime = parseDateTime(something[u"a"].toString(), request.dateTime().date(), departureTime); 0102 0103 if (departureTime.isNull() 0104 || arrivalTime.isNull() 0105 || (arrivalTime - departureTime) == 0s 0106 || from.isEmpty() 0107 || to.isEmpty()) { 0108 qCDebug(Log) << "zpcg: Skipped one incomplete result"; 0109 continue; 0110 } 0111 0112 if (!LocalBackendUtils::isInSelectedTimeframe(departureTime, arrivalTime, request)) { 0113 continue; 0114 } 0115 0116 QUrl url = baseUrl(); 0117 QUrlQuery query; 0118 query.addQueryItem(QStringLiteral("r"), QStringLiteral("api/details")); 0119 query.addQueryItem(QStringLiteral("timetable"), timetableId); 0120 query.addQueryItem(QStringLiteral("locale"), preferredLanguage()); 0121 url.setQuery(query); 0122 0123 auto netReply = nam->get(QNetworkRequest(url)); 0124 replies.push_back(netReply); 0125 0126 connect(netReply, &QNetworkReply::finished, reply, [=, this]() { 0127 auto data = netReply->readAll(); 0128 auto doc = QJsonDocument::fromJson(data); 0129 const auto detailsJson = doc.array(); 0130 0131 logReply(reply, netReply, data); 0132 0133 for (const auto &resultJson : detailsJson) { 0134 const auto routeJson = resultJson[u"d"].toArray(); 0135 0136 Journey journey; 0137 0138 Line line; 0139 line.setName(trainNumber); 0140 line.setMode(matchTrainType(trainType)); 0141 0142 Route route; 0143 route.setLine(line); 0144 0145 if (!routeJson.empty()) { 0146 route.setDestination(stationToLocation(routeJson.last()[u"s"].toString())); 0147 } 0148 0149 JourneySection section; 0150 section.setMode(JourneySection::PublicTransport); 0151 section.setFrom(stationToLocation(from)); 0152 section.setTo(stationToLocation(to)); 0153 section.setRoute(route); 0154 0155 std::vector<Stopover> stopovers; 0156 0157 0158 bool inRoute = false; 0159 0160 QDateTime previousDateTime; 0161 for (const auto &stopoverJson : routeJson) { 0162 const auto name = stopoverJson[u"s"].toString(); 0163 0164 if (name == to) { 0165 auto arrivalTime = parseDateTime(stopoverJson[u"a"].toString(), request.dateTime().date(), previousDateTime); 0166 previousDateTime = arrivalTime; 0167 section.setScheduledArrivalTime(arrivalTime); 0168 inRoute = false; 0169 } 0170 0171 if (inRoute) { 0172 auto location = stationToLocation(name); 0173 0174 auto arrivalTime = parseDateTime(stopoverJson[u"a"].toString(), request.dateTime().date(), previousDateTime); 0175 previousDateTime = arrivalTime; 0176 auto departureTime = parseDateTime(stopoverJson[u"d"].toString(), request.dateTime().date(), previousDateTime); 0177 previousDateTime = departureTime; 0178 0179 Stopover stopover; 0180 stopover.setStopPoint(location); 0181 stopover.setScheduledArrivalTime(arrivalTime); 0182 stopover.setScheduledDepartureTime(departureTime); 0183 0184 stopovers.push_back(std::move(stopover)); 0185 } 0186 0187 if (name == from) { 0188 auto departureTime = parseDateTime(stopoverJson[u"d"].toString(), request.dateTime().date()); 0189 previousDateTime = departureTime; 0190 section.setScheduledDepartureTime(departureTime); 0191 inRoute = true; 0192 } 0193 } 0194 0195 section.setIntermediateStops(std::move(stopovers)); 0196 journey.setSections({section}); 0197 journeys->push_back(std::move(journey)); 0198 } 0199 }); 0200 } 0201 } 0202 0203 auto *allReplies = new NetworkReplyCollection(replies); 0204 connect(allReplies, &NetworkReplyCollection::allFinished, reply, [=, this]() { 0205 allReplies->deleteLater(); 0206 0207 bool isEmpty = journeys->empty(); 0208 addResult(reply, this, std::move(*journeys)); 0209 0210 if (!isEmpty) { 0211 Attribution osmAttribution; 0212 osmAttribution.setLicense(QStringLiteral("ODbL")); 0213 osmAttribution.setLicenseUrl(QUrl(QStringLiteral("https://opendatacommons.org/licenses/odbl/"))); 0214 osmAttribution.setName(QStringLiteral("OpenStreetMap®")); 0215 osmAttribution.setUrl(QUrl(QStringLiteral("https://www.openstreetmap.org"))); 0216 0217 Attribution attribution; 0218 attribution.setName(QStringLiteral("ŽPCG")); 0219 attribution.setUrl(QUrl(QStringLiteral("https://www.zcg-prevoz.me"))); 0220 0221 addAttributions(reply, {attribution, osmAttribution}); 0222 } 0223 }); 0224 connect(allReplies, &NetworkReplyCollection::errorOccured, reply, [=, this]() { 0225 addError(reply, Reply::NetworkError, netReply->errorString()); 0226 }); 0227 }); 0228 0229 connect(netReply, &QNetworkReply::errorOccurred, reply, [=, this]() { 0230 addError(reply, Reply::NetworkError, netReply->errorString()); 0231 }); 0232 0233 return true; 0234 } 0235 0236 bool ZPCGBackend::queryLocation(const LocationRequest &request, LocationReply *reply, QNetworkAccessManager *nam) const 0237 { 0238 if (m_stations.empty()) { 0239 if (!m_fetchStationsTask) { 0240 auto mutThis = const_cast<ZPCGBackend *>(this); 0241 mutThis->m_fetchStationsTask = mutThis->downloadStationData(reply, nam); 0242 } 0243 0244 connect(m_fetchStationsTask, &AbstractAsyncTask::finished, reply, [=, this]() { 0245 queryLocation(request, reply, nam); 0246 }); 0247 0248 return true; 0249 } 0250 0251 std::vector<Location> locations; 0252 0253 const auto searchableName = makeSearchableName(request.name()); 0254 for (const auto &[name, station] : m_stations) { 0255 if (name.contains(searchableName) && !station->idName.isEmpty()) { 0256 auto location = stationToLocation(name); 0257 locations.push_back(std::move(location)); 0258 } 0259 } 0260 0261 addResult(reply, std::move(locations)); 0262 0263 return false; 0264 } 0265 0266 QDateTime ZPCGBackend::parseDateTime(const QString &timeString, const QDate &date, const QDateTime &knownPreviousTime) const 0267 { 0268 auto time = QTime::fromString(timeString); 0269 0270 auto dateTime = date.startOfDay(); 0271 dateTime.setTime(time); 0272 dateTime.setTimeZone(QTimeZone("Europe/Podgorica")); 0273 0274 if (!knownPreviousTime.isNull() && dateTime < knownPreviousTime) { 0275 dateTime.setDate(dateTime.date().addDays(1)); 0276 } 0277 0278 return dateTime; 0279 } 0280 0281 std::unordered_map<QString, std::shared_ptr<ZPCG::Station>> ZPCGBackend::loadAuxStationData() 0282 { 0283 QFile file(QStringLiteral(":/org.kde.kpublictransport/networks/stations/me-rs.json")); 0284 if (!file.open(QFile::ReadOnly)) { 0285 qCWarning(Log) << file.errorString(); 0286 qFatal("The bundled station data of KPublicTransport can not be read. This is a bug."); 0287 } 0288 0289 const auto stationsJson = QJsonDocument::fromJson(file.readAll()).array(); 0290 std::unordered_map<QString, std::shared_ptr<ZPCG::Station>> stations; 0291 for (const auto &stationJson : stationsJson) { 0292 auto stationJsonObject = stationJson.toObject(); 0293 0294 // OSM name tags in different languages. 0295 // They will be tried from first to last. 0296 std::vector<QString> keyPrecedence = { 0297 QString(u"name:" % preferredLanguage()), 0298 QStringLiteral(u"name:en"), 0299 QStringLiteral(u"alt_name:en"), 0300 QStringLiteral(u"name:sr-Latn"), 0301 QStringLiteral(u"name"), 0302 QStringLiteral(u"alt_name"), 0303 QStringLiteral(u"name:sr") 0304 }; 0305 0306 auto findName = [=]() { 0307 for (const auto &key : keyPrecedence) { 0308 if (stationJsonObject.contains(key)) { 0309 return stationJsonObject[key].toString(); 0310 } 0311 } 0312 0313 return QString(); 0314 }; 0315 0316 auto stationName = findName(); 0317 0318 ZPCG::Station station { 0319 .name = stationName, 0320 .idName = QString(), // Will be replaced with timetable names after http request finishes 0321 .latitude = float(stationJson[u"longitude"].toDouble()), 0322 .longitude = float(stationJson[u"latitude"].toDouble()) 0323 }; 0324 0325 auto sharedStation = std::make_shared<ZPCG::Station>(std::move(station)); 0326 0327 // Add search links for all considered languages 0328 for (const auto &key : keyPrecedence) { 0329 if (stationJsonObject.contains(key)) { 0330 stations.insert({makeSearchableName(stationJsonObject[key].toString()), sharedStation}); 0331 } 0332 } 0333 } 0334 0335 return stations; 0336 } 0337 0338 AsyncTask<void> *ZPCGBackend::downloadStationData(Reply *reply, QNetworkAccessManager *nam) 0339 { 0340 auto task = new AsyncTask<void>(this); 0341 0342 QUrl url = baseUrl(); 0343 QUrlQuery query; 0344 query.addQueryItem(QStringLiteral("r"), QStringLiteral("api/stations")); 0345 query.addQueryItem(QStringLiteral("locale"), QStringLiteral("sr")); 0346 url.setQuery(query); 0347 0348 auto netReply = nam->get(QNetworkRequest(url)); 0349 connect(netReply, &QNetworkReply::finished, reply, [=, this]() { 0350 const auto array = QJsonDocument::fromJson(netReply->readAll()).array(); 0351 0352 m_stations = loadAuxStationData(); 0353 0354 for (const auto &nameJson : array) { 0355 const auto searchName = makeSearchableName(nameJson.toString()); 0356 0357 if (m_stations.contains(searchName)) { 0358 m_stations.at(searchName)->idName = nameJson.toString(); // Use the official names 0359 } else { 0360 qCWarning(Log) << "Missing station data for" << searchName << "."; 0361 qCWarning(Log) << "To fix this, look for the station on OpenStreetMap," 0362 << "fix its properties and regenerate the data in lib/networks/stations/"; 0363 qCWarning(Log) << "Usually, the issue is a name mismatch, or the railway=station property being set" 0364 << "on the station building instead of as a point"; 0365 } 0366 } 0367 0368 task->reportFinished(); 0369 }); 0370 0371 return task; 0372 } 0373 0374 Location ZPCGBackend::stationToLocation(const QString &name) const 0375 { 0376 auto searchableName = makeSearchableName(name); 0377 0378 Location loc; 0379 0380 if (m_stations.contains(searchableName)) { 0381 auto station = m_stations.at(searchableName); 0382 0383 loc.setName(station->name); 0384 loc.setLatitude(station->latitude); 0385 loc.setLongitude(station->longitude); 0386 if (!station->idName.isEmpty()) { 0387 loc.setIdentifier(identifierName(), station->idName); 0388 } 0389 } else { 0390 loc.setName(name); 0391 loc.setIdentifier(identifierName(), name); 0392 } 0393 0394 loc.setType(Location::Stop); 0395 0396 return loc; 0397 } 0398 0399 Line::Mode ZPCGBackend::matchTrainType(QStringView trainType) 0400 { 0401 if (trainType == u"fast") { 0402 return Line::LongDistanceTrain; 0403 } else if (trainType == u"local") { 0404 return Line::LocalTrain; 0405 } 0406 0407 return Line::Train; 0408 } 0409 0410 QUrl ZPCGBackend::baseUrl() const 0411 { 0412 return QUrl(QStringLiteral("https://zpcg.me/")); 0413 } 0414 0415 QString ZPCGBackend::identifierName() const 0416 { 0417 return QStringLiteral("zpcgname"); 0418 }