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

0001 /*
0002     SPDX-FileCopyrightText: 2024 Volker Krause <vkrause@kde.org>
0003     SPDX-License-Identifier: LGPL-2.0-or-later
0004 */
0005 
0006 #include "motisbackend.h"
0007 #include "abstractbackend.h"
0008 #include "cache.h"
0009 #include "motisparser.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 <QJsonArray>
0022 #include <QJsonDocument>
0023 #include <QJsonObject>
0024 #include <QNetworkAccessManager>
0025 #include <QNetworkReply>
0026 #include <QNetworkRequest>
0027 
0028 using namespace KPublicTransport;
0029 using namespace Qt::Literals::StringLiterals;
0030 
0031 MotisBackend::MotisBackend() = default;
0032 MotisBackend::~MotisBackend() = default;
0033 
0034 AbstractBackend::Capabilities MotisBackend::capabilities() const
0035 {
0036     // TODO
0037     // - CanQueryNextJourney?
0038     // - CanQueryPreviousJourney?
0039     // - CanQueryNextDeparture?
0040     // - CanQueryPreviousDeparture?
0041     // - CanQueryArrivals?
0042     return (m_endpoint.scheme() == "https"_L1) ? AbstractBackend::Secure : AbstractBackend::NoCapability;
0043 }
0044 
0045 bool MotisBackend::needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const
0046 {
0047     if (type == QueryType::Journey && m_intermodal) {
0048         return !loc.hasCoordinate() && loc.identifier(m_locationIdentifierType).isEmpty();
0049     }
0050     return loc.identifier(m_locationIdentifierType).isEmpty();
0051 }
0052 
0053 bool MotisBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
0054 {
0055     QJsonObject query;
0056     if (req.hasCoordinate()) {
0057         query = QJsonObject{
0058             {"destination"_L1, QJsonObject{
0059                 {"type"_L1, "Module"_L1},
0060                 {"target"_L1, "/lookup/geo_station"_L1}
0061             }},
0062             {"content_type"_L1, "LookupGeoStationRequest"_L1},
0063             {"content"_L1, QJsonObject{
0064                 {"pos"_L1, QJsonObject{
0065                     {"lat"_L1, req.latitude()},
0066                     {"lng"_L1, req.longitude()},
0067                 }},
0068                 {"max_radius"_L1, req.maximumDistance()},
0069             }}
0070         };
0071     } else {
0072         query = QJsonObject{
0073             {"destination"_L1, QJsonObject{
0074                 {"type"_L1, "Module"_L1},
0075                 {"target"_L1, "/guesser"_L1}
0076             }},
0077             {"content_type"_L1, "StationGuesserRequest"_L1},
0078             {"content"_L1, QJsonObject{
0079                 {"input"_L1, req.name()},
0080                 {"guess_count"_L1, req.maximumResults()}
0081             }}
0082         };
0083     }
0084 
0085     auto netReply = makeRequest(req, reply, query, nam);
0086     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply]() {
0087         netReply->deleteLater();
0088         const auto data = netReply->readAll();
0089         logReply(reply, netReply, data);
0090 
0091         qDebug().noquote() << data << netReply->error();
0092         MotisParser p(m_locationIdentifierType);
0093         auto result = p.parseStations(data);
0094         if (netReply->error() == QNetworkReply::NoError && !p.hasError()) {
0095             Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), result, {});
0096             addResult(reply, std::move(result));
0097         } else if (p.hasError()) {
0098             addError(reply, Reply::InvalidRequest, p.errorMessage());
0099         } else {
0100             addError(reply, Reply::NetworkError, netReply->errorString());
0101         }
0102     });
0103 
0104     return true;
0105 }
0106 
0107 bool MotisBackend::queryStopover(const StopoverRequest &req, StopoverReply *reply, QNetworkAccessManager *nam) const
0108 {
0109     // TODO arrival/departure filtering needs to be done on the result
0110     QJsonObject query{
0111         {"destination"_L1, QJsonObject{
0112             {"type"_L1, "Module"_L1},
0113             {"target"_L1, "/railviz/get_station"_L1}
0114         }},
0115         {"content_type"_L1, "RailVizStationRequest"_L1},
0116         {"content"_L1, QJsonObject{
0117             {"time"_L1, req.dateTime().toSecsSinceEpoch()}, // TODO timezone?
0118             {"direction"_L1, "BOTH"_L1}, // TODO paging?
0119             {"station_id"_L1, req.stop().identifier(m_locationIdentifierType)},
0120             {"event_count"_L1, req.maximumResults()},
0121         }}
0122     };
0123 
0124     auto netReply = makeRequest(req, reply, query, nam);
0125     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply]() {
0126         netReply->deleteLater();
0127         const auto data = netReply->readAll();
0128         logReply(reply, netReply, data);
0129 
0130         qDebug().noquote() << data << netReply->error();
0131         MotisParser p(m_locationIdentifierType);
0132         auto result = p.parseEvents(data);
0133         if (netReply->error() == QNetworkReply::NoError && !p.hasError()) {
0134             // TODO caching?
0135             addResult(reply, this, std::move(result));
0136         } else if (p.hasError()) {
0137             addError(reply, Reply::InvalidRequest, p.errorMessage());
0138         } else {
0139             addError(reply, Reply::NetworkError, netReply->errorString());
0140         }
0141     });
0142 
0143     return true;
0144 }
0145 
0146 [[nodiscard]] static QJsonArray ivModes(const std::vector<IndividualTransport> &ivs)
0147 {
0148     // TODO allow external configuration of the durection limits and PPR profiles
0149     QJsonArray modes;
0150     for (const auto &iv : ivs) {
0151         if (iv.mode() == IndividualTransport::Walk) {
0152             modes.push_back(QJsonObject{
0153                 {"mode_type"_L1, "FootPPR"_L1},
0154                 {"mode"_L1, QJsonObject{
0155                     {"search_options"_L1, QJsonObject{
0156                         {"profile"_L1, "default"_L1},
0157                         {"duration_limit"_L1, 900},
0158                     }},
0159                 }}
0160             });
0161         }
0162         if (iv.mode() == IndividualTransport::Bike && iv.qualifier() != IndividualTransport::Rent) {
0163             // TODO neither bike parking nor taking the bike on public transport is explicitly supported
0164             modes.push_back(QJsonObject{
0165                 {"mode_type"_L1, "Bike"_L1},
0166                 {"mode"_L1, QJsonObject{
0167                     {"max_duration"_L1, 900}, // TODO docs and fbs disagree on this
0168                 }}
0169             });
0170         }
0171         if (iv.mode() == IndividualTransport::Car && iv.qualifier() != IndividualTransport::Park && iv.qualifier() != IndividualTransport::Rent) {
0172             modes.push_back(QJsonObject{
0173                 {"mode_type"_L1, "Car"_L1},
0174                 {"mode"_L1, QJsonObject{
0175                     {"max_duration"_L1, 900}, // TODO docs and fbs disagree on this
0176                 }}
0177             });
0178         }
0179         if (iv.mode() == IndividualTransport::Car && iv.qualifier() == IndividualTransport::Park) {
0180             modes.push_back(QJsonObject{
0181                 {"mode_type"_L1, "CarParking"_L1},
0182                 {"mode"_L1, QJsonObject{
0183                     {"max_car_duration"_L1, 900},
0184                     {"ppr_search_options"_L1, QJsonObject{
0185                         {"profile"_L1, "default"_L1},
0186                         {"duration_limit"_L1, 900},
0187                     }},
0188                 }}
0189             });
0190         }
0191     }
0192     return modes;
0193 }
0194 
0195 [[nodiscard]] static QJsonObject encodeLocation(const Location &loc, const QString &locationIdentifierType, bool intermodal)
0196 {
0197     if (loc.hasCoordinate() && intermodal) {
0198         return QJsonObject({
0199             {"lat"_L1, loc.latitude()},
0200             {"lng"_L1, loc.longitude()}
0201         });
0202     }
0203     return QJsonObject({
0204         {"id"_L1, loc.identifier(locationIdentifierType)},
0205         {"name"_L1, loc.name()}
0206     });
0207 }
0208 
0209 bool MotisBackend::queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const
0210 {
0211     // backward search for MOTIS is really backward, so we need to swap everything
0212     const auto from = req.dateTimeMode() == JourneyRequest::Departure ? req.from() : req.to();
0213     const auto to = req.dateTimeMode() == JourneyRequest::Departure ? req.to() : req.from();
0214     const auto &startModes = req.dateTimeMode() == JourneyRequest::Departure ? req.accessModes() : req.egressModes();
0215     const auto &destModes = req.dateTimeMode() == JourneyRequest::Departure ? req.egressModes() : req.accessModes();
0216 
0217     // ### HACK in this request the JSON key order matters!!
0218     // see https://github.com/motis-project/motis/issues/433
0219     // thefore the '!' prefix hack and post-processing below...
0220     // can be removed once the fix hits the Motis demo server
0221     QJsonObject query{
0222         {"destination"_L1, QJsonObject{
0223             {"type"_L1, "Module"_L1},
0224             {"target"_L1, "/intermodal"_L1}
0225         }},
0226         {"content_type"_L1, "IntermodalRoutingRequest"_L1},
0227         {"content"_L1, QJsonObject{
0228             // TODO how can we make the ontrip start options available? OntripTrainStart in particular
0229             {"!start_type"_L1, from.hasCoordinate() && m_intermodal ? "IntermodalPretripStart"_L1 : "PretripStart"_L1},
0230             {"!start"_L1, QJsonObject{
0231                 {from.hasCoordinate() && m_intermodal ? "position"_L1: "station"_L1, encodeLocation(from, m_locationIdentifierType, m_intermodal)},
0232                 {"interval"_L1, QJsonObject{
0233                     {"begin"_L1, req.dateTime().toSecsSinceEpoch()}, // TODO timezone?
0234                     {"end"_L1, req.dateTime().toSecsSinceEpoch() + 1800}, // TODO configure this
0235                 }},
0236                 {"min_connection_count"_L1, req.maximumResults()},
0237                 {"extend_interval_earlier"_L1, true}, // TODO paging support
0238                 {"extend_interval_later"_L1, true},
0239             }},
0240             {"!start_modes"_L1, ivModes(startModes)},
0241             {"destination_type"_L1, to.hasCoordinate() && m_intermodal ? "InputPosition"_L1 : "InputStation"_L1},
0242             {"destination"_L1, encodeLocation(to, m_locationIdentifierType, m_intermodal)},
0243             {"destination_modes"_L1, ivModes(destModes)},
0244             {"search_type"_L1, "Default"_L1},
0245             {"search_dir"_L1, req.dateTimeMode() == JourneyRequest::Departure ? "Forward"_L1 : "Backward"_L1},
0246             {"router"_L1, ""_L1}
0247         }}
0248     };
0249 
0250     auto netReply = makeRequest(req, reply, query, nam);
0251     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply]() {
0252         netReply->deleteLater();
0253         const auto data = netReply->readAll();
0254         logReply(reply, netReply, data);
0255 
0256         // TODO result caching?
0257         // TODO paging support
0258         qDebug().noquote() << data << netReply->error();
0259         MotisParser p(m_locationIdentifierType);
0260         auto result = p.parseConnections(data);
0261         if (netReply->error() == QNetworkReply::NoError && !p.hasError()) {
0262             addResult(reply, this, std::move(result));
0263         } else if (p.hasError()) {
0264             addError(reply, Reply::InvalidRequest, p.errorMessage());
0265         } else {
0266             addError(reply, Reply::NetworkError, netReply->errorString());
0267         }
0268 
0269     });
0270 
0271     return true;
0272 }
0273 
0274 template <typename Request>
0275 QNetworkReply* MotisBackend::makeRequest(const Request &req, Reply *reply, const QJsonObject &query, QNetworkAccessManager *nam) const
0276 {
0277     QNetworkRequest netReq(m_endpoint);
0278     netReq.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"_L1);
0279     // TODO user agent
0280     auto postData = QJsonDocument(query).toJson(QJsonDocument::Compact);
0281     // ### HACK see above
0282     postData = postData.replace("\"!", "\"");
0283     logRequest(req, netReq, postData);
0284     qDebug().noquote() << QJsonDocument(query).toJson();
0285     auto netReply = nam->post(netReq, postData);
0286     netReply->setParent(reply);
0287     return netReply;
0288 }
0289 
0290 #include "moc_motisbackend.cpp"