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 }