File indexing completed on 2024-05-12 04:42:41
0001 /* 0002 SPDX-FileCopyrightText: 2024 Jonah Brüchert <jbb@kaidan.im> 0003 SPDX-FileCopyrightText: 2024 Stefan Vesovic <asphyxia@spline.de> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "srbijavozbackend.h" 0009 0010 #include "datatypes/location.h" 0011 #include "datatypes/stopover.h" 0012 #include "locationrequest.h" 0013 #include "logging.h" 0014 #include "localbackendutils.h" 0015 #include "locationreply.h" 0016 #include "locationrequest.h" 0017 #include "journeyreply.h" 0018 0019 #include <QFile> 0020 #include <QJsonDocument> 0021 #include <QJsonArray> 0022 #include <QNetworkAccessManager> 0023 #include <QNetworkRequest> 0024 #include <QNetworkReply> 0025 #include <QUrlQuery> 0026 0027 #include <memory> 0028 0029 using namespace Qt::Literals::StringLiterals; 0030 0031 using namespace KPublicTransport; 0032 0033 SrbijavozBackend::SrbijavozBackend() = default; 0034 0035 AbstractBackend::Capabilities SrbijavozBackend::capabilities() const 0036 { 0037 return Secure; 0038 } 0039 0040 bool SrbijavozBackend::needsLocationQuery(const Location &loc, QueryType type) const 0041 { 0042 Q_UNUSED(type); 0043 return loc.identifier(identifierName()).isEmpty(); 0044 } 0045 0046 bool SrbijavozBackend::queryJourney(const JourneyRequest &request, JourneyReply *reply, QNetworkAccessManager *nam) const 0047 { 0048 // Ignore requests for which we don't have the needed identifier 0049 if (request.from().identifier(identifierName()).isEmpty() || request.to().identifier(identifierName()).isEmpty()) { 0050 return false; 0051 } 0052 0053 if (m_stationsById.empty()) { 0054 if (!m_fetchStationsTask) { 0055 auto mutThis = const_cast<SrbijavozBackend *>(this); 0056 mutThis->m_fetchStationsTask = mutThis->downloadStationData(reply, nam); 0057 } 0058 0059 QObject::connect(m_fetchStationsTask, &AbstractAsyncTask::finished, reply, [=, this]() { 0060 queryJourney(request, reply, nam); 0061 }); 0062 0063 return true; 0064 } 0065 0066 QUrl url(u"https://webapi1.srbvoz.rs/ekarta/api/listavozova/ListaVozova_Web"_s); 0067 0068 QUrlQuery query; 0069 query.addQueryItem(u"stanicaod"_s, request.from().identifier(identifierName())); 0070 query.addQueryItem(u"stanicado"_s, request.to().identifier(identifierName())); 0071 query.addQueryItem(u"datum"_s, request.dateTime().date().toString(QStringLiteral("MM-dd-yyyy"))); 0072 query.addQueryItem(u"brojputnika"_s, QString::number(1)); 0073 query.addQueryItem(u"razred"_s, QString::number(2)); 0074 0075 url.setQuery(query); 0076 0077 auto netReply = nam->get(QNetworkRequest(url)); 0078 QObject::connect(netReply, &QNetworkReply::finished, reply, [=, this] { 0079 auto bytes = netReply->readAll(); 0080 auto connections = QJsonDocument::fromJson(bytes).array(); 0081 0082 std::vector<Journey> journeys; 0083 for (auto train : connections) { 0084 int startId = train[u"odsifra"].toInt(); 0085 int endId = train[u"dosifra"].toInt(); 0086 QString departureTimeString = train[u"vremep"].toString(); 0087 QString arrivalTimeString = train[u"vremed"].toString(); 0088 int trainNumber = train[u"brvoz"].toInt(); 0089 0090 QString notes; 0091 if (preferredLanguage() == u"sr") { 0092 notes = train[u"napomena"].toString().trimmed(); 0093 } else { 0094 notes = train[u"napomenaE"].toString().trimmed(); 0095 } 0096 0097 auto departureTime = parseDateTime(departureTimeString, request.dateTime().date()); 0098 auto arrivalTime = parseDateTime(arrivalTimeString, request.dateTime().date(), departureTime); 0099 0100 const auto stops = train[u"etTrasaVoza"].toArray(); 0101 0102 if (!LocalBackendUtils::isInSelectedTimeframe(departureTime, arrivalTime, request)) { 0103 continue; 0104 } 0105 0106 Line line; 0107 line.setName(QString::number(trainNumber)); 0108 line.setMode(Line::Train); 0109 0110 Route route; 0111 route.setLine(line); 0112 0113 if (!stops.empty()) { 0114 if (auto destinationId = stops.last()[u"sifrA_STANICE"].toInt(); m_stationsById.contains(destinationId)) { 0115 route.setDestination(stationToLocation(*m_stationsById.at(destinationId))); 0116 } 0117 } 0118 0119 Journey journey; 0120 0121 JourneySection journeySection; 0122 journeySection.setFrom(stationToLocation(*m_stationsById.at(startId))); 0123 journeySection.setTo(stationToLocation(*m_stationsById.at(endId))); 0124 journeySection.setScheduledDepartureTime(departureTime); 0125 journeySection.setScheduledArrivalTime(arrivalTime); 0126 journeySection.setMode(JourneySection::PublicTransport); 0127 0128 if (!notes.isEmpty()) { 0129 journeySection.setNotes(QList<QString>{notes}); 0130 } 0131 0132 std::vector<Stopover> stopovers; 0133 0134 bool inRoute = false; 0135 QDateTime mostRecentTime = departureTime; 0136 for (const auto &stopover : stops) { 0137 int stationId = stopover[u"sifrA_STANICE"].toInt(); 0138 0139 if (stationId == endId) { 0140 inRoute = false; 0141 } 0142 0143 if (inRoute) { 0144 Stopover stop; 0145 0146 auto arrivalTime = parseDateTime(stopover[u"vremE_DOLASKA"].toString(), request.dateTime().date(), mostRecentTime); 0147 mostRecentTime = arrivalTime; 0148 auto departureTime = parseDateTime(stopover[u"vremE_POLASKA"].toString(), request.dateTime().date(), mostRecentTime); 0149 mostRecentTime = departureTime; 0150 0151 stop.setScheduledArrivalTime(arrivalTime); 0152 stop.setScheduledDepartureTime(arrivalTime); 0153 stop.setStopPoint(stationToLocation(*m_stationsById.at(stationId))); 0154 stopovers.push_back(std::move(stop)); 0155 } 0156 0157 if (stationId == startId) { 0158 inRoute = true; 0159 } 0160 } 0161 0162 journeySection.setIntermediateStops(std::move(stopovers)); 0163 journeySection.setRoute(route); 0164 0165 journey.setSections({journeySection}); 0166 0167 journeys.push_back(std::move(journey)); 0168 } 0169 0170 bool isEmpty = journeys.empty(); 0171 0172 addResult(reply, this, std::move(journeys)); 0173 0174 if (!isEmpty) { 0175 Attribution osmAttribution; 0176 osmAttribution.setLicense(QStringLiteral("ODbL")); 0177 osmAttribution.setLicenseUrl(QUrl(QStringLiteral("https://opendatacommons.org/licenses/odbl/"))); 0178 osmAttribution.setName(QStringLiteral("OpenStreetMap®")); 0179 osmAttribution.setUrl(QUrl(QStringLiteral("https://www.openstreetmap.org"))); 0180 0181 Attribution attribution; 0182 attribution.setName(QStringLiteral("Srbija Voz")); 0183 attribution.setUrl(QUrl(QStringLiteral("https://srbijavoz.rs"))); 0184 0185 addAttributions(reply, {attribution, osmAttribution}); 0186 } 0187 }); 0188 0189 QObject::connect(netReply, &QNetworkReply::errorOccurred, reply, [=, this]() { 0190 addError(reply, Reply::NetworkError, netReply->errorString()); 0191 }); 0192 0193 return true; 0194 } 0195 0196 bool SrbijavozBackend::queryLocation(const LocationRequest &request, LocationReply *reply, QNetworkAccessManager *nam) const 0197 { 0198 if (m_stationsById.empty()) { 0199 if (!m_fetchStationsTask) { 0200 auto mutThis = const_cast<SrbijavozBackend *>(this); 0201 mutThis->m_fetchStationsTask = mutThis->downloadStationData(reply, nam); 0202 } 0203 0204 QObject::connect(m_fetchStationsTask, &AbstractAsyncTask::finished, reply, [=, this]() { 0205 queryLocation(request, reply, nam); 0206 }); 0207 0208 return true; 0209 } 0210 0211 std::vector<Location> locations; 0212 0213 const auto searchableName = makeSearchableName(request.name()); 0214 for (const auto &[name, station] : m_stationsBySearchName) { 0215 if (name.contains(searchableName)) { 0216 auto location = stationToLocation(*station); 0217 0218 // Skip stations that we don't have an identifier for 0219 // Those would be coming from OSM data when a station is either not used by Srb Voz, 0220 // or not correctly matched 0221 if (!location.identifier(identifierName()).isEmpty()) { 0222 locations.push_back(std::move(location)); 0223 } 0224 } 0225 } 0226 0227 addResult(reply, std::move(locations)); 0228 0229 return false; 0230 } 0231 0232 void SrbijavozBackend::loadAuxData() 0233 { 0234 QFile file(QStringLiteral(":/org.kde.kpublictransport/networks/stations/me-rs.json")); 0235 if (!file.open(QFile::ReadOnly)) { 0236 qCWarning(Log) << file.errorString(); 0237 qFatal("The bundled station data of KPublicTransport can not be read. This is a bug."); 0238 } 0239 0240 const auto stationsJson = QJsonDocument::fromJson(file.readAll()).array(); 0241 std::unordered_map<int, std::shared_ptr<SrbStation>> stationsById; 0242 std::unordered_map<QString, std::shared_ptr<SrbStation>> stationsBySearchName; 0243 for (const auto &stationJson : stationsJson) { 0244 auto stationJsonObject = stationJson.toObject(); 0245 0246 // OSM name tags in different languages. 0247 // They will be tried from first to last. 0248 std::vector<QString> keyPrecedence = { 0249 u"name:" % preferredLanguage(), 0250 u"int_name"_s, 0251 u"name:en"_s, 0252 u"alt_name:en"_s, 0253 u"name:sr-Latn"_s, 0254 u"name"_s, 0255 u"alt_name"_s, 0256 u"name:sr"_s 0257 }; 0258 0259 auto findName = [=]() { 0260 for (const auto &key : keyPrecedence) { 0261 if (stationJsonObject.contains(key)) { 0262 if (auto name = stationJsonObject[key].toString(); !name.isEmpty()) { 0263 return name; 0264 } 0265 } 0266 } 0267 0268 return QString(); 0269 }; 0270 0271 auto stationName = findName(); 0272 0273 SrbStation station { 0274 .name = stationName, 0275 .longitude = float(stationJson[u"latitude"].toDouble()), 0276 .latitude = float(stationJson[u"longitude"].toDouble()), 0277 .id = -1 0278 }; 0279 0280 auto sharedStation = std::make_shared<SrbStation>(std::move(station)); 0281 0282 // Add search links for all considered languages 0283 for (const auto &key : keyPrecedence) { 0284 if (stationJsonObject.contains(key)) { 0285 stationsBySearchName.insert({makeSearchableName(stationJsonObject[key].toString()), sharedStation}); 0286 } 0287 } 0288 } 0289 0290 m_stationsBySearchName = std::move(stationsBySearchName); 0291 } 0292 0293 void SrbijavozBackend::applyStationQuirks() 0294 { 0295 auto addMapping = [&](const QString &from, QString to) { 0296 to = makeSearchableName(to); 0297 if (!m_stationsBySearchName.contains(to)) { 0298 qCWarning(Log) << "srbijavoz: Error in manual mapping from" << from << "to" << to; 0299 return; 0300 } 0301 0302 auto station = m_stationsBySearchName.at(to); 0303 m_stationsBySearchName.insert({makeSearchableName(from), station}); 0304 }; 0305 0306 // Stations of which we can't easily normalize the spelling 0307 addMapping(u"Kos Mitrovica Sever"_s, u"Kosovska Mitrovica Sever"_s); 0308 addMapping(u"Ban.milosevo Polje"_s, u"Banatsko Milosevo"_s); 0309 addMapping(u"Subotica Predgrade"_s, u"Subotica predgrađe"_s); 0310 addMapping(u"Skenderovo"_s, u"Skenderevo"_s); 0311 addMapping(u"Gugalj"_s, u"Гугаљ"_s); 0312 addMapping(u"Donje Jerinje"_s, u"Jarinjë"_s); 0313 addMapping(u"Jerina"_s, u"Jarinjë"_s); 0314 addMapping(u"Brvenik"_s, u"Brevnik"_s); 0315 addMapping(u"Palanka"_s, u"Smederevska Palanka"_s); 0316 addMapping(u"Petrovac-glozan"_s, u"Bački Petrovac - Gložan"_s); 0317 addMapping(u"Osipaonica Stajali."_s, u"Osipaonica staјalište"_s); 0318 addMapping(u"Pancevo Gl.stanica"_s, u"Pančevo glavna"_s); 0319 addMapping(u"Resnik Kragujev."_s, u"Resnik Kragujevački"_s); 0320 addMapping(u"Karlovacki Vinograd"_s, u"Karlovački vinogradi"_s); 0321 0322 // That one station that I'm not sure enough to add it to OSM 0323 m_stationsBySearchName.insert({makeSearchableName(u"Subotica Javna Skl."_s), std::make_shared<SrbStation>(SrbStation { 0324 .name = u"Subotica Javna Skladista"_s, 0325 .longitude = 19.696104, 0326 .latitude = 46.094215, 0327 .id = -1 0328 })}); 0329 } 0330 0331 AsyncTask<void> *SrbijavozBackend::downloadStationData(Reply *reply, QNetworkAccessManager *nam) 0332 { 0333 loadAuxData(); 0334 applyStationQuirks(); 0335 0336 auto *task = new AsyncTask<void>(reply); 0337 0338 const QUrl url(u"https://webapi1.srbvoz.rs/ekarta/api/stanica"_s); 0339 auto netReply = nam->get(QNetworkRequest(url)); 0340 QObject::connect(netReply, &QNetworkReply::finished, reply, [=, this] { 0341 auto bytes = netReply->readAll(); 0342 0343 auto stationsJson = QJsonDocument::fromJson(bytes).array(); 0344 for (auto stationJson : stationsJson) { 0345 QString stationName = stationJson[u"naziv"].toString(); 0346 uint32_t stationId = stationJson[u"sifra"].toInt(); 0347 0348 QString searchName = makeSearchableName(stationName); 0349 if (!m_stationsBySearchName.contains(searchName)) { 0350 qCWarning(Log) << "Missing station data for" << stationName << "."; 0351 qCWarning(Log) << "To fix this, look for the station on OpenStreetMap," 0352 << "fix its properties and regenerate the data in lib/networks/stations/"; 0353 qCWarning(Log) << "Usually, the issue is a name mismatch. If the name used by Srbijavoz can not be added" 0354 "to OSM, you can add a mapping in the backend."; 0355 0356 SrbStation station; 0357 station.id = int(stationId); 0358 station.name = stationName; 0359 0360 auto sharedStation = std::make_shared<SrbStation>(std::move(station)); 0361 m_stationsBySearchName.insert({searchName, sharedStation}); 0362 m_stationsById.insert({station.id, sharedStation}); 0363 0364 continue; 0365 } 0366 0367 auto &station = m_stationsBySearchName.at(searchName); 0368 station->id = int(stationId); 0369 0370 m_stationsById.insert({int(stationId), station}); 0371 } 0372 0373 task->reportFinished(); 0374 }); 0375 0376 return task; 0377 } 0378 0379 Location SrbijavozBackend::stationToLocation(const SrbStation &station) const 0380 { 0381 Location loc; 0382 if (station.latitude != NAN && station.longitude != NAN) { 0383 loc.setLatitude(station.latitude); 0384 loc.setLongitude(station.longitude); 0385 } else { 0386 // So we have at least some knowledge of the position 0387 loc.setCountry(u"Serbia"_s); 0388 } 0389 loc.setName(station.name); 0390 loc.setType(Location::Stop); 0391 0392 if (station.id != -1) { 0393 loc.setIdentifier(identifierName(), QString::number(station.id)); 0394 } 0395 0396 return loc; 0397 } 0398 0399 QString SrbijavozBackend::makeSearchableName(QString name) const 0400 { 0401 auto out = LocalBackendUtils::makeSearchableName( 0402 name.replace(QRegularExpression(QStringLiteral("station|halt")), QString())); 0403 0404 auto normalizeEnd = [&](QStringView end, QStringView normalizedEnd) { 0405 if (out.endsWith(end)) { 0406 out = out.mid(0, out.size() - end.size()) % normalizedEnd; 0407 } 0408 }; 0409 0410 normalizeEnd(u"ce", u"ca"); 0411 normalizeEnd(u"ci", u"c"); 0412 normalizeEnd(u"je", u"ja"); 0413 normalizeEnd(u".", u""); 0414 0415 return out; 0416 } 0417 0418 QString SrbijavozBackend::identifierName() const 0419 { 0420 return u"srbvozid"_s; 0421 } 0422 0423 QDateTime SrbijavozBackend::parseDateTime(const QString &timeString, const QDate &date, const QDateTime &knownPreviousTime) const 0424 { 0425 auto time = QTime::fromString(timeString.trimmed()); 0426 0427 auto dateTime = date.startOfDay(); 0428 dateTime.setTime(time); 0429 dateTime.setTimeZone(QTimeZone("Europe/Belgrade")); 0430 0431 if (!knownPreviousTime.isNull() && dateTime < knownPreviousTime) { 0432 dateTime.setDate(dateTime.date().addDays(1)); 0433 } 0434 0435 return dateTime; 0436 }