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 }