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 &paramName, 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 }