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

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "hafasquerybackend.h"
0008 #include "cache.h"
0009 #include "logging.h"
0010 
0011 #include <KPublicTransport/Journey>
0012 #include <KPublicTransport/JourneyReply>
0013 #include <KPublicTransport/JourneyRequest>
0014 #include <KPublicTransport/Location>
0015 #include <KPublicTransport/LocationReply>
0016 #include <KPublicTransport/LocationRequest>
0017 #include <KPublicTransport/Stopover>
0018 #include <KPublicTransport/StopoverReply>
0019 #include <KPublicTransport/StopoverRequest>
0020 
0021 #include <QDateTime>
0022 #include <QDebug>
0023 #include <QNetworkAccessManager>
0024 #include <QNetworkReply>
0025 #include <QNetworkRequest>
0026 #include <QStringDecoder>
0027 #include <QUrl>
0028 #include <QUrlQuery>
0029 
0030 using namespace KPublicTransport;
0031 
0032 HafasQueryBackend::HafasQueryBackend() = default;
0033 HafasQueryBackend::~HafasQueryBackend() = default;
0034 
0035 void HafasQueryBackend::init()
0036 {
0037     m_parser.setLocationIdentifierTypes(locationIdentifierType(), standardLocationIdentifierType());
0038     m_parser.setLineModeMap(m_lineModeMap);
0039     m_parser.setStandardLocationIdentfierCountries(std::move(m_uicCountryCodes));
0040 }
0041 
0042 AbstractBackend::Capabilities HafasQueryBackend::capabilities() const
0043 {
0044     return (m_endpoint.startsWith(QLatin1String("https://")) ? Secure : NoCapability) | CanQueryArrivals;
0045 }
0046 
0047 bool HafasQueryBackend::needsLocationQuery(const Location &loc, QueryType type) const
0048 {
0049     switch (type) {
0050         case QueryType::Departure:
0051             return locationIdentifier(loc).isEmpty();
0052         case QueryType::Journey:
0053             return locationIdentifier(loc).isEmpty(); // TODO coordinate-based search doesn't actually seem to work? && !loc.hasCoordinate();
0054     }
0055     return false;
0056 }
0057 
0058 bool HafasQueryBackend::queryLocation(const LocationRequest &request, LocationReply *reply, QNetworkAccessManager *nam) const
0059 {
0060     if ((request.types() & Location::Stop) == 0) {
0061         return false;
0062     }
0063 
0064     if (request.hasCoordinate()) {
0065         return queryLocationByCoordinate(request, reply, nam);
0066     }
0067     if (!request.name().isEmpty()) {
0068         return queryLocationByName(request, reply, nam);
0069     }
0070     return false;
0071 }
0072 
0073 static QByteArray readReplyAsUtf8(QNetworkReply *reply)
0074 {
0075     const auto data = reply->readAll();
0076     const auto contentType = reply->header(QNetworkRequest::ContentTypeHeader).toString();
0077     const auto charsetStart = contentType.indexOf(QLatin1String("charset="));
0078     if (charsetStart < 0) {
0079         return data;
0080     }
0081     auto codec = QStringDecoder(QStringView(contentType).mid(charsetStart + 8).toUtf8().constData());
0082     if (!codec.isValid()) {
0083         return data;
0084     }
0085     return QString(codec.decode(data)).toUtf8();
0086 }
0087 
0088 bool HafasQueryBackend::queryLocationByName(const LocationRequest &request, LocationReply *reply, QNetworkAccessManager *nam) const
0089 {
0090     QUrl url(m_endpoint);
0091     url.setPath(url.path() + QLatin1String("/ajax-getstop.exe/") + preferredLanguage());
0092 
0093     QUrlQuery query;
0094     query.addQueryItem(QStringLiteral("getstop"), QStringLiteral("1"));
0095     query.addQueryItem(QStringLiteral("REQ0JourneyStopsS0A"), QStringLiteral("255"));
0096     query.addQueryItem(QStringLiteral("REQ0JourneyStopsS0G"), request.name()); // TODO apps are seen to append '?' here
0097     query.addQueryItem(QStringLiteral("REQ0JourneyStopsB"), QString::number(std::max(1, request.maximumResults())));
0098     query.addQueryItem(QStringLiteral("js"), QStringLiteral("true"));
0099     url.setQuery(query);
0100 
0101     const QNetworkRequest netRequest(url);
0102     logRequest(request, netRequest);
0103     auto netReply = nam->get(netRequest);
0104     netReply->setParent(reply);
0105     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply]() {
0106         const auto data = readReplyAsUtf8(netReply);
0107         logReply(reply, netReply, data);
0108         netReply->deleteLater();
0109 
0110         if (netReply->error() != QNetworkReply::NoError) {
0111             addError(reply, Reply::NetworkError, netReply->errorString());
0112             return;
0113         }
0114 
0115         auto res = m_parser.parseGetStopResponse(data);
0116         if (m_parser.error() != Reply::NoError) {
0117             addError(reply, m_parser.error(), m_parser.errorMessage());
0118         } else {
0119             Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), res, {});
0120             addResult(reply, std::move(res));
0121         }
0122     });
0123 
0124     return true;
0125 }
0126 
0127 bool HafasQueryBackend::queryLocationByCoordinate(const LocationRequest &request, LocationReply *reply, QNetworkAccessManager *nam) const
0128 {
0129     QUrl url(m_endpoint);
0130     url.setPath(url.path() + QLatin1String("/query.exe/") + preferredLanguage() + QLatin1Char('y'));
0131 
0132     QUrlQuery query;
0133     query.addQueryItem(QStringLiteral("performLocating"), QStringLiteral("2"));
0134     query.addQueryItem(QStringLiteral("tpl"), QStringLiteral("stop2json"));
0135     query.addQueryItem(QStringLiteral("look_x"), QString::number((int)(request.longitude() * 1000000)));
0136     query.addQueryItem(QStringLiteral("look_y"), QString::number((int)(request.latitude() * 1000000)));
0137     query.addQueryItem(QStringLiteral("look_maxdist"), QString::number(std::max(1, request.maximumDistance())));
0138     query.addQueryItem(QStringLiteral("look_maxno"), QString::number(std::max(1, request.maximumResults())));
0139     url.setQuery(query);
0140 
0141     const QNetworkRequest netRequest(url);
0142     logRequest(request, netRequest);
0143     auto netReply = nam->get(netRequest);
0144     netReply->setParent(reply);
0145     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply]() {
0146         netReply->deleteLater();
0147         const auto data = netReply->readAll();
0148         logReply(reply, netReply, data);
0149 
0150         if (netReply->error() != QNetworkReply::NoError) {
0151             addError(reply, Reply::NetworkError, netReply->errorString());
0152             return;
0153         }
0154         auto res = m_parser.parseQueryLocationResponse(data);
0155         if (m_parser.error() != Reply::NoError) {
0156             addError(reply, m_parser.error(), m_parser.errorMessage());
0157         } else {
0158             Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), res, {});
0159             addResult(reply, std::move(res));
0160         }
0161     });
0162     return true;
0163 }
0164 
0165 bool HafasQueryBackend::queryStopover(const StopoverRequest &request, StopoverReply *reply, QNetworkAccessManager *nam) const
0166 {
0167     const auto stationId = locationIdentifier(request.stop());
0168     if (stationId.isEmpty()) {
0169         qCDebug(Log) << "no station identifier found for departure stop" << backendId();
0170         return false;
0171     }
0172 
0173     QUrl url(m_endpoint);
0174     url.setPath(url.path() + QLatin1String("/stboard.exe/") + preferredLanguage());
0175 
0176     QUrlQuery query;
0177     query.addQueryItem(QStringLiteral("boardType"), request.mode() == StopoverRequest::QueryDeparture ? QStringLiteral("dep") : QStringLiteral("arr"));
0178     query.addQueryItem(QStringLiteral("disableEquivs"), QStringLiteral("0"));
0179     query.addQueryItem(QStringLiteral("maxJourneys"), QString::number(request.maximumResults()));
0180     query.addQueryItem(QStringLiteral("input"), stationId);
0181     query.addQueryItem(QStringLiteral("date"), request.dateTime().date().toString(QStringLiteral("dd.MM.yy")));
0182     query.addQueryItem(QStringLiteral("time"), request.dateTime().time().toString(QStringLiteral("hh:mm")));
0183     query.addQueryItem(QStringLiteral("L"), QStringLiteral("vs_java3"));
0184     query.addQueryItem(QStringLiteral("start"), QStringLiteral("yes"));
0185     url.setQuery(query);
0186 
0187     const QNetworkRequest netRequest(url);
0188     logRequest(request, netRequest);
0189     auto netReply = nam->get(netRequest);
0190     netReply->setParent(reply);
0191     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply]() {
0192         netReply->deleteLater();
0193         const auto data = netReply->readAll();
0194         logReply(reply, netReply, data);
0195 
0196         if (netReply->error() != QNetworkReply::NoError) {
0197             addError(reply, Reply::NetworkError, netReply->errorString());
0198             return;
0199         }
0200         auto res = m_parser.parseStationBoardResponse(data, reply->request().mode() == StopoverRequest::QueryArrival);
0201         if (m_parser.error() != Reply::NoError) {
0202             addError(reply, m_parser.error(), m_parser.errorMessage());
0203         } else {
0204             addResult(reply, this, std::move(res));
0205         }
0206     });
0207 
0208     return true;
0209 }
0210 
0211 QString HafasQueryBackend::locationId(const Location &loc) const
0212 {
0213     const auto id = locationIdentifier(loc);
0214     if (!id.isEmpty()) {
0215         return QLatin1String("A=1@L=") + id;
0216     }
0217 
0218     if (loc.hasCoordinate()) {
0219         return QLatin1String("A=1@X=") + QString::number((int)(loc.longitude() * 1000000)) + QLatin1String("@Y=") + QString::number((int)(loc.latitude() * 1000000));
0220     }
0221 
0222     if (!loc.name().isEmpty()) {
0223         return QLatin1String("A=1@G=") + loc.name();
0224     }
0225 
0226     return {};
0227 }
0228 
0229 bool HafasQueryBackend::queryJourney(const JourneyRequest &request, JourneyReply *reply, QNetworkAccessManager *nam) const
0230 {
0231 #if Q_BYTE_ORDER == Q_BIG_ENDIAN
0232 #warning Hafas binary journey reponse parsing not implemented for big endian yet!
0233     return false;
0234 #endif
0235     if ((request.modes() & JourneySection::PublicTransport) == 0) {
0236         return false;
0237     }
0238 
0239     const auto fromId = locationId(request.from());
0240     const auto toId = locationId(request.to());
0241     if (fromId.isEmpty() || toId.isEmpty()) {
0242         return false;
0243     }
0244 
0245     QUrl url(m_endpoint);
0246     url.setPath(url.path() + QLatin1String("/query.exe/") + preferredLanguage());
0247     QUrlQuery query;
0248     query.addQueryItem(QStringLiteral("REQ0JourneyStopsS0ID"), fromId);
0249     query.addQueryItem(QStringLiteral("REQ0JourneyStopsZ0ID"), toId);
0250     query.addQueryItem(QStringLiteral("REQ0JourneyDate"), request.dateTime().date().toString(QStringLiteral("dd.MM.yy")));
0251     query.addQueryItem(QStringLiteral("REQ0JourneyTime"), request.dateTime().time().toString(QStringLiteral("hh:mm")));
0252     query.addQueryItem(QStringLiteral("REQ0HafasSearchForw"), request.dateTimeMode() == JourneyRequest::Departure ? QStringLiteral("1") : QStringLiteral("0"));
0253     query.addQueryItem(QStringLiteral("REQ0JourneyProduct_prod_list_1"), QStringLiteral("1111111111"));
0254     // no idea what this stuff does, but it seems necessary...
0255     query.addQueryItem(QStringLiteral("start"), QStringLiteral("Suchen"));
0256     query.addQueryItem(QStringLiteral("h2g-direct"), QStringLiteral("11"));
0257     query.addQueryItem(QStringLiteral("clientType"), QStringLiteral("ANDROID"));
0258 
0259     url.setQuery(query);
0260 
0261     const QNetworkRequest netRequest(url);
0262     logRequest(request, netRequest);
0263     auto netReply = nam->get(netRequest);
0264     netReply->setParent(reply);
0265     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply]() {
0266         netReply->deleteLater();
0267         const auto data = netReply->readAll();
0268         logReply(reply, netReply, data);
0269 
0270         if (netReply->error() != QNetworkReply::NoError) {
0271             addError(reply, Reply::NetworkError, netReply->errorString());
0272             return;
0273         }
0274 
0275         auto res = m_parser.parseQueryJourneyResponse(data);
0276         if (m_parser.error() != Reply::NoError) {
0277             addError(reply, m_parser.error(), m_parser.errorMessage());
0278         } else {
0279             addResult(reply, this, std::move(res));
0280         }
0281     });
0282 
0283     return true;
0284 }