File indexing completed on 2024-05-12 04:42:33
0001 /* 0002 SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "efabackend.h" 0008 #include "efacompactparser.h" 0009 #include "efamodeoftransport.h" 0010 #include "efaxmlparser.h" 0011 #include "cache.h" 0012 #include "logging.h" 0013 0014 #include <KPublicTransport/Journey> 0015 #include <KPublicTransport/JourneyReply> 0016 #include <KPublicTransport/JourneyRequest> 0017 #include <KPublicTransport/Location> 0018 #include <KPublicTransport/LocationReply> 0019 #include <KPublicTransport/LocationRequest> 0020 #include <KPublicTransport/Stopover> 0021 #include <KPublicTransport/StopoverReply> 0022 #include <KPublicTransport/StopoverRequest> 0023 0024 #include <QNetworkAccessManager> 0025 #include <QNetworkReply> 0026 #include <QNetworkRequest> 0027 #include <QUrl> 0028 #include <QUrlQuery> 0029 0030 using namespace KPublicTransport; 0031 0032 EfaBackend::EfaBackend() = default; 0033 EfaBackend::~EfaBackend() = default; 0034 0035 AbstractBackend::Capabilities EfaBackend::capabilities() const 0036 { 0037 return (m_endpoint.startsWith(QLatin1String("https")) ? Secure : NoCapability) 0038 | CanQueryNextJourney | CanQueryPreviousJourney | CanQueryNextDeparture | CanQueryPreviousDeparture; 0039 } 0040 0041 bool EfaBackend::needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const 0042 { 0043 Q_UNUSED(type); 0044 return !loc.hasCoordinate() && loc.identifier(locationIdentifierType()).isEmpty(); 0045 } 0046 0047 bool EfaBackend::queryLocation(const LocationRequest& request, LocationReply *reply, QNetworkAccessManager *nam) const 0048 { 0049 if ((request.name().isEmpty() && !request.hasCoordinate()) || (request.types() & Location::Stop) == 0) { 0050 return false; 0051 } 0052 0053 QUrl url(m_endpoint); 0054 url.setPath(url.path() + (m_stopfinderRequestCommand.isEmpty() ? QStringLiteral("XML_STOPFINDER_REQUEST") : m_stopfinderRequestCommand)); 0055 0056 auto query = commonQuery(); 0057 query.addQueryItem(QStringLiteral("locationServerActive"), QStringLiteral("1")); 0058 0059 if (request.hasCoordinate()) { 0060 query.addQueryItem(QStringLiteral("type_sf"), QStringLiteral("coord")); 0061 query.addQueryItem(QStringLiteral("name_sf"), QString::number(request.longitude()) + QLatin1Char(':') + QString::number(request.latitude()) + QLatin1String(":WGS84[DD.ddddd]")); 0062 } else { 0063 query.addQueryItem(QStringLiteral("type_sf"), QStringLiteral("any")); 0064 query.addQueryItem(QStringLiteral("name_sf"), request.name()); 0065 } 0066 0067 query.addQueryItem(QStringLiteral("anyObjFilter_sf"), QStringLiteral("2")); // bitfield, "2" is the flag for stops 0068 query.addQueryItem(QStringLiteral("anyMaxSizeHitList"), QString::number(std::max(1, request.maximumResults()))); 0069 url.setQuery(query); 0070 0071 QNetworkRequest netRequest(url); 0072 applySslConfiguration(netRequest); 0073 logRequest(request, netRequest); 0074 auto netReply = nam->get(netRequest); 0075 netReply->setParent(reply); 0076 QObject::connect(netReply, &QNetworkReply::finished, reply, [this, reply, netReply]() { 0077 netReply->deleteLater(); 0078 const auto data = netReply->readAll(); 0079 logReply(reply, netReply, data); 0080 0081 if (netReply->error() != QNetworkReply::NoError) { 0082 addError(reply, Reply::NetworkError, netReply->errorString()); 0083 return; 0084 } 0085 auto p = make_parser(); 0086 auto res = p->parseStopFinderResponse(data); 0087 if (p->error() != Reply::NoError) { 0088 addError(reply, p->error(), p->errorMessage()); 0089 } else { 0090 Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), res, {}); 0091 addResult(reply, std::move(res)); 0092 } 0093 }); 0094 0095 return true; 0096 } 0097 0098 bool EfaBackend::queryStopover(const StopoverRequest &request, StopoverReply *reply, QNetworkAccessManager *nam) const 0099 { 0100 const auto stopId = request.stop().identifier(locationIdentifierType()); 0101 if (stopId.isEmpty() && !request.stop().hasCoordinate()) { 0102 return false; 0103 } 0104 0105 QUrl url(m_endpoint); 0106 url.setPath(url.path() + (m_dmRequestCommand.isEmpty() ? QStringLiteral("XML_DM_REQUEST") : m_dmRequestCommand)); 0107 0108 QDateTime dt = request.dateTime(); 0109 if (timeZone().isValid()) { 0110 dt = dt.toTimeZone(timeZone()); 0111 } 0112 0113 const auto ctx = requestContextData(request).value<EfaRequestContext>(); 0114 auto query = commonQuery(); 0115 if (ctx.isEmpty()) { 0116 if (stopId.isEmpty()) { 0117 query.addQueryItem(QStringLiteral("type_dm"), QStringLiteral("coord")); 0118 query.addQueryItem(QStringLiteral("name_dm"), QString::number(request.stop().longitude()) + QLatin1Char(':') + QString::number(request.stop().latitude()) + QLatin1String(":WGS84[DD.ddddd]")); 0119 } else { 0120 query.addQueryItem(QStringLiteral("type_dm"), QStringLiteral("stop")); 0121 query.addQueryItem(QStringLiteral("name_dm"), stopId); 0122 } 0123 query.addQueryItem(QStringLiteral("itdDate"), dt.date().toString(QStringLiteral("yyyyMMdd"))); 0124 query.addQueryItem(QStringLiteral("itdTime"), dt.time().toString(QStringLiteral("hhmm"))); 0125 query.addQueryItem(QStringLiteral("useRealtime"), QStringLiteral("1")); 0126 query.addQueryItem(QStringLiteral("limit"), QString::number(request.maximumResults())); 0127 0128 // not exactly sure what these do, but without this the result is missing departure times 0129 query.addQueryItem(QStringLiteral("mode"), QStringLiteral("direct")); 0130 query.addQueryItem(QStringLiteral("ptOptionsActive"), QStringLiteral("1")); 0131 query.addQueryItem(QStringLiteral("merge_dep"), QStringLiteral("1")); 0132 0133 // enable support for previous/next queries 0134 query.addQueryItem(QStringLiteral("stateless"), QStringLiteral("1")); 0135 query.addQueryItem(QStringLiteral("sessionID"), QStringLiteral("0")); 0136 query.addQueryItem(QStringLiteral("requestID"), QStringLiteral("0")); 0137 } else { 0138 query.addQueryItem(QStringLiteral("stateless"), QStringLiteral("1")); 0139 query.addQueryItem(QStringLiteral("sessionID"), ctx.sessionId); 0140 query.addQueryItem(QStringLiteral("requestID"), ctx.requestId); 0141 if (requestContext(request).type == RequestContext::Next) { 0142 query.addQueryItem(QStringLiteral("command"), QStringLiteral("dmNext")); 0143 } else { 0144 query.addQueryItem(QStringLiteral("command"), QStringLiteral("dmPrev")); 0145 } 0146 } 0147 0148 url.setQuery(query); 0149 0150 QNetworkRequest netRequest(url); 0151 applySslConfiguration(netRequest); 0152 logRequest(request, netRequest); 0153 auto netReply = nam->get(netRequest); 0154 netReply->setParent(reply); 0155 QObject::connect(netReply, &QNetworkReply::finished, reply, [this, reply, netReply]() { 0156 netReply->deleteLater(); 0157 const auto data = netReply->readAll(); 0158 logReply(reply, netReply, data); 0159 0160 if (netReply->error() != QNetworkReply::NoError) { 0161 addError(reply, Reply::NetworkError, netReply->errorString()); 0162 return; 0163 } 0164 auto p = make_parser(); 0165 auto res = p->parseDmResponse(data); 0166 if (p->error() != Reply::NoError) { 0167 addError(reply, p->error(), p->errorMessage()); 0168 } else { 0169 setNextRequestContext(reply, QVariant::fromValue(p->requestContext())); 0170 setPreviousRequestContext(reply, QVariant::fromValue(p->requestContext())); 0171 addResult(reply, this, std::move(res)); 0172 } 0173 }); 0174 0175 return true; 0176 } 0177 0178 struct { 0179 IndividualTransport::Mode mode; 0180 IndividualTransport::Qualifier qualifier; 0181 int itMot; 0182 } static constexpr const itMotMap[] = { 0183 { IndividualTransport::Walk, IndividualTransport::None, 100 }, 0184 { IndividualTransport::Bike, IndividualTransport::Park, 101 }, 0185 { IndividualTransport::Bike, IndividualTransport::None, 102 }, 0186 { IndividualTransport::Car, IndividualTransport::Rent, 103 }, // ### 103 is "Kiss & Ride", unclear which of this two this practically maps to... 0187 { IndividualTransport::Car, IndividualTransport::Dropoff, 103 }, 0188 { IndividualTransport::Car, IndividualTransport::Park, 104 }, 0189 // TODO 105 Taxi 0190 // TODO 106 and 107 exist as well but no idea yet what they mean... 0191 }; 0192 0193 static void addItModeOptions(QUrlQuery &query, const QString ¶mName, const std::vector<IndividualTransport> &modes) 0194 { 0195 for (const auto &mode : modes) { 0196 const auto it = std::find_if(std::begin(itMotMap), std::end(itMotMap), [mode](const auto &m) { 0197 return m.mode == mode.mode() && m.qualifier == mode.qualifier(); 0198 }); 0199 if (it != std::end(itMotMap)) { 0200 query.addQueryItem(paramName, QString::number((*it).itMot));; 0201 break; 0202 } 0203 } 0204 } 0205 0206 bool EfaBackend::queryJourney(const JourneyRequest &request, JourneyReply *reply, QNetworkAccessManager *nam) const 0207 { 0208 if ((request.modes() & JourneySection::PublicTransport) == 0) { 0209 return false; 0210 } 0211 0212 const auto fromId = request.from().identifier(locationIdentifierType()); 0213 const auto toId = request.to().identifier(locationIdentifierType()); 0214 if ((fromId.isEmpty() && !request.from().hasCoordinate()) || (toId.isEmpty() && !request.to().hasCoordinate())) { 0215 return false; 0216 } 0217 0218 QUrl url(m_endpoint); 0219 url.setPath(url.path() + (m_tripRequestCommand.isEmpty() ? QStringLiteral("XML_TRIP_REQUEST2") : m_tripRequestCommand)); 0220 0221 const auto ctx = requestContextData(request).value<EfaRequestContext>(); 0222 auto query = commonQuery(); 0223 if (ctx.isEmpty()) { 0224 query.addQueryItem(QStringLiteral("locationServerActive"), QStringLiteral("1")); 0225 query.addQueryItem(QStringLiteral("useRealtime"), QStringLiteral("1")); 0226 0227 if (fromId.isEmpty()) { 0228 query.addQueryItem(QStringLiteral("type_origin"), QStringLiteral("coord")); 0229 query.addQueryItem(QStringLiteral("name_origin"), QString::number(request.from().longitude()) + QLatin1Char(':') + QString::number(request.from().latitude()) + QLatin1String(":WGS84[DD.ddddd]")); 0230 } else { 0231 query.addQueryItem(QStringLiteral("type_origin"), QStringLiteral("stop")); 0232 query.addQueryItem(QStringLiteral("name_origin"), fromId); 0233 } 0234 if (toId.isEmpty()) { 0235 query.addQueryItem(QStringLiteral("type_destination"), QStringLiteral("coord")); 0236 query.addQueryItem(QStringLiteral("name_destination"), QString::number(request.to().longitude()) + QLatin1Char(':') + QString::number(request.to().latitude()) + QLatin1String(":WGS84[DD.ddddd]")); 0237 } else { 0238 query.addQueryItem(QStringLiteral("type_destination"), QStringLiteral("stop")); 0239 query.addQueryItem(QStringLiteral("name_destination"), toId); 0240 } 0241 0242 QDateTime dt = request.dateTime(); 0243 if (timeZone().isValid()) { 0244 dt = dt.toTimeZone(timeZone()); 0245 } 0246 query.addQueryItem(QStringLiteral("itdDate"), dt.date().toString(QStringLiteral("yyyyMMdd"))); 0247 query.addQueryItem(QStringLiteral("itdTime"), dt.time().toString(QStringLiteral("hhmm"))); 0248 query.addQueryItem(QStringLiteral("itdTripDateTimeDepArr"), request.dateTimeMode() == JourneyRequest::Departure ? QStringLiteral("dep") : QStringLiteral("arr")); 0249 0250 EfaModeOfTransport::lineModesToQuery(request.lineModes(), query); 0251 query.addQueryItem(QStringLiteral("itOptionsActive"), QStringLiteral("1")); 0252 addItModeOptions(query, QStringLiteral("trITDepMOT"), request.accessModes()); 0253 addItModeOptions(query, QStringLiteral("trITArrMOT"), request.egressModes()); 0254 0255 query.addQueryItem(QStringLiteral("calcNumberOfTrips"), QString::number(std::max(1, request.maximumResults()))); 0256 query.addQueryItem(QStringLiteral("calcCO2"), QStringLiteral("1")); 0257 0258 // saves several 100kb due to not encoding that path coordinates (which we don't even need) as XML 0259 query.addQueryItem(QStringLiteral("coordListOutputFormat"), QStringLiteral("STRING")); 0260 0261 // enable support for previous/next queries 0262 query.addQueryItem(QStringLiteral("stateless"), QStringLiteral("1")); 0263 query.addQueryItem(QStringLiteral("sessionID"), QStringLiteral("0")); 0264 query.addQueryItem(QStringLiteral("requestID"), QStringLiteral("0")); 0265 } else { 0266 query.addQueryItem(QStringLiteral("stateless"), QStringLiteral("1")); 0267 query.addQueryItem(QStringLiteral("sessionID"), ctx.sessionId); 0268 query.addQueryItem(QStringLiteral("requestID"), ctx.requestId); 0269 if (requestContext(request).type == RequestContext::Next) { 0270 query.addQueryItem(QStringLiteral("command"), QStringLiteral("tripNext")); 0271 } else { 0272 query.addQueryItem(QStringLiteral("command"), QStringLiteral("tripPrev")); 0273 } 0274 } 0275 0276 url.setQuery(query); 0277 0278 QNetworkRequest netRequest(url); 0279 applySslConfiguration(netRequest); 0280 logRequest(request, netRequest); 0281 auto netReply = nam->get(netRequest); 0282 netReply->setParent(reply); 0283 QObject::connect(netReply, &QNetworkReply::finished, reply, [this, reply, netReply]() { 0284 netReply->deleteLater(); 0285 const auto data = netReply->readAll(); 0286 logReply(reply, netReply, data); 0287 0288 if (netReply->error() != QNetworkReply::NoError) { 0289 addError(reply, Reply::NetworkError, netReply->errorString()); 0290 return; 0291 } 0292 auto p = make_parser(); 0293 auto res = p->parseTripResponse(data); 0294 if (p->error() != Reply::NoError) { 0295 addError(reply, p->error(), p->errorMessage()); 0296 } else { 0297 setNextRequestContext(reply, QVariant::fromValue(p->requestContext())); 0298 setPreviousRequestContext(reply, QVariant::fromValue(p->requestContext())); 0299 addResult(reply, this, std::move(res)); 0300 } 0301 }); 0302 0303 return true; 0304 } 0305 0306 QString EfaBackend::locationIdentifierType() const 0307 { 0308 return m_locationIdentifierType.isEmpty() ? backendId() : m_locationIdentifierType; 0309 } 0310 0311 std::unique_ptr<EfaParser> EfaBackend::make_parser() const 0312 { 0313 std::unique_ptr<EfaParser> p; 0314 if (m_compactXmlResponse) { 0315 p = std::make_unique<EfaCompactParser>(); 0316 } else { 0317 p = std::make_unique<EfaXmlParser>(); 0318 } 0319 p->setLocationIdentifierType(locationIdentifierType()); 0320 return p; 0321 } 0322 0323 QUrlQuery EfaBackend::commonQuery() const 0324 { 0325 QUrlQuery query; 0326 query.addQueryItem(QStringLiteral("outputFormat"), QStringLiteral("XML")); 0327 query.addQueryItem(QStringLiteral("coordOutputFormat"), QStringLiteral("WGS84[DD.ddddd]")); 0328 query.addQueryItem(QStringLiteral("language"), preferredLanguage()); 0329 if (!m_mId.isEmpty()) { 0330 query.addQueryItem(QStringLiteral("mId"), m_mId); 0331 } 0332 return query; 0333 } 0334 0335 void EfaBackend::setXmlOutputFormat(const QString &format) 0336 { 0337 m_compactXmlResponse = format == QLatin1String("compact"); 0338 }