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"