File indexing completed on 2024-05-12 04:42:40
0001 /* 0002 SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "opentripplannergraphqlbackend.h" 0008 #include "opentripplannerparser.h" 0009 #include "cache.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/RentalVehicle> 0018 #include <KPublicTransport/Stopover> 0019 #include <KPublicTransport/StopoverReply> 0020 #include <KPublicTransport/StopoverRequest> 0021 0022 #include <kgraphql.h> 0023 0024 #include <QDebug> 0025 #include <QFile> 0026 #include <QJsonArray> 0027 #include <QNetworkRequest> 0028 #include <QUrl> 0029 0030 using namespace KPublicTransport; 0031 0032 OpenTripPlannerGraphQLBackend::OpenTripPlannerGraphQLBackend() = default; 0033 OpenTripPlannerGraphQLBackend::~OpenTripPlannerGraphQLBackend() = default; 0034 0035 AbstractBackend::Capabilities OpenTripPlannerGraphQLBackend::capabilities() const 0036 { 0037 return (m_endpoint.startsWith(QLatin1String("https://")) ? Secure : NoCapability) 0038 | (m_apiVersion == QLatin1String("otp2") ? (CanQueryNextJourney | CanQueryPreviousJourney) : NoCapability); 0039 } 0040 0041 bool OpenTripPlannerGraphQLBackend::needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const 0042 { 0043 Q_UNUSED(type); 0044 return !loc.hasCoordinate(); 0045 } 0046 0047 bool OpenTripPlannerGraphQLBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const 0048 { 0049 if ((req.types() & (Location::Stop | Location::RentedVehicle | Location::RentedVehicleStation)) == 0) { 0050 return false; 0051 } 0052 0053 auto gqlReq = graphQLRequest(); 0054 if (req.hasCoordinate()) { 0055 gqlReq.setQueryFromFile(graphQLPath(QStringLiteral("stationByCoordinate.graphql"))); 0056 gqlReq.setVariable(QStringLiteral("lat"), req.latitude()); 0057 gqlReq.setVariable(QStringLiteral("lon"), req.longitude()); 0058 gqlReq.setVariable(QStringLiteral("radius"), req.maximumDistance()); 0059 gqlReq.setVariable(QStringLiteral("maxResults"), req.maximumResults()); 0060 QJsonArray placeTypeFilter; 0061 if (req.types() & Location::Stop) { 0062 placeTypeFilter.push_back(m_apiVersion == QLatin1String("entur") ? QStringLiteral("stopPlace") : QStringLiteral("STOP")); 0063 } 0064 if (req.types() & (Location::RentedVehicleStation | Location::RentedVehicle)) { 0065 placeTypeFilter.push_back(m_apiVersion == QLatin1String("entur") ? QStringLiteral("bicycleRent") : QStringLiteral("BICYCLE_RENT")); 0066 } 0067 // TODO: also supports BIKE_PARK, CAR_PARK 0068 gqlReq.setVariable(QStringLiteral("placeType"), placeTypeFilter); 0069 } else { 0070 gqlReq.setQueryFromFile(graphQLPath(QStringLiteral("stationByName.graphql"))); 0071 gqlReq.setVariable(QStringLiteral("name"), req.name()); 0072 } 0073 0074 if (isLoggingEnabled()) { 0075 logRequest(req, gqlReq.networkRequest(), gqlReq.rawData()); 0076 } 0077 KGraphQL::query(gqlReq, nam, [this, req, reply](const KGraphQLReply &gqlReply) { 0078 logReply(reply, gqlReply.networkReply(), gqlReply.rawData()); 0079 if (gqlReply.error() != KGraphQLReply::NoError) { 0080 addError(reply, Reply::NetworkError, gqlReply.errorString()); 0081 return; 0082 } 0083 0084 OpenTripPlannerParser p(backendId(), m_ifoptPrefix); 0085 p.setKnownRentalVehicleNetworks(m_rentalNetworks); 0086 std::vector<Location> res; 0087 if (req.hasCoordinate()) { 0088 res = p.parseLocationsByCoordinate(gqlReply.data()); 0089 } else { 0090 res = p.parseLocationsByName(gqlReply.data()); 0091 } 0092 // only cache results if there is no realtime data involved 0093 if ((req.types() & (Location::RentedVehicle | Location::RentedVehicleStation)) == 0) { 0094 Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), res, {}); 0095 } 0096 addResult(reply, std::move(res)); 0097 }, reply); 0098 0099 return true; 0100 } 0101 0102 struct { 0103 const char *otpMode; 0104 const char *enturMode; 0105 Line::Mode mode; 0106 } static constexpr const otp_mode_map[] = { 0107 { "AIRPLANE", "air", Line::Air }, 0108 { "BUS", "bus", Line::Bus }, 0109 { "CABLE_CAR", "cableway", Line::Tramway }, 0110 { "CARPOOL", nullptr, Line::RideShare }, 0111 { "COACH", "coach", Line::Coach }, 0112 { "FERRY", "water", Line::Ferry }, 0113 { "FUNICULAR", "funicular", Line::Funicular }, 0114 { "GONDOLA", "lift", Line::Tramway }, 0115 { "RAIL", "rail", Line::LongDistanceTrain }, 0116 { "RAIL", "rail", Line::Train }, 0117 { "RAIL", "rail", Line::LocalTrain }, 0118 { "RAIL", "rail", Line::RapidTransit }, 0119 { "SUBWAY", "metro", Line::Metro }, 0120 { "TRAM", "tram", Line::Tramway }, 0121 }; 0122 0123 bool OpenTripPlannerGraphQLBackend::queryStopover(const StopoverRequest &req, StopoverReply *reply, QNetworkAccessManager *nam) const 0124 { 0125 if (!req.stop().hasCoordinate()) { 0126 return false; 0127 } 0128 0129 auto gqlReq = graphQLRequest(); 0130 gqlReq.setQueryFromFile(graphQLPath(QStringLiteral("departure.graphql"))); 0131 gqlReq.setVariable(QStringLiteral("lat"), req.stop().latitude()); 0132 gqlReq.setVariable(QStringLiteral("lon"), req.stop().longitude()); 0133 auto dt = req.dateTime(); 0134 if (timeZone().isValid()) { 0135 dt = dt.toTimeZone(timeZone()); 0136 } 0137 gqlReq.setVariable(QStringLiteral("startTime"), dt.toSecsSinceEpoch()); 0138 gqlReq.setVariable(QStringLiteral("startDateTime"), dt.toString(Qt::ISODate)); 0139 gqlReq.setVariable(QStringLiteral("maxResults"), req.maximumResults()); 0140 // TODO arrival/departure selection? 0141 0142 // for unconstrained searches we need all modes, the "TRANSIT" special mode results in an empty result... 0143 const auto isEntur = m_apiVersion == QLatin1String("entur"); 0144 QStringList modes; 0145 for (const auto &m : otp_mode_map) { 0146 if ((!isEntur || m.enturMode) && (req.lineModes().empty() || std::binary_search(req.lineModes().begin(), req.lineModes().end(), m.mode))) { 0147 modes.push_back(QLatin1String(isEntur ? m.enturMode : m.otpMode)); 0148 } 0149 } 0150 modes.removeDuplicates(); 0151 QJsonArray modesArray; 0152 std::copy(modes.begin(), modes.end(), std::back_inserter(modesArray)); 0153 gqlReq.setVariable(QStringLiteral("modes"), modesArray); 0154 0155 if (isLoggingEnabled()) { 0156 logRequest(req, gqlReq.networkRequest(), gqlReq.rawData()); 0157 } 0158 KGraphQL::query(gqlReq, nam, [this, reply](const KGraphQLReply &gqlReply) { 0159 logReply(reply, gqlReply.networkReply(), gqlReply.rawData()); 0160 if (gqlReply.error() != KGraphQLReply::NoError) { 0161 addError(reply, Reply::NetworkError, gqlReply.errorString()); 0162 } else { 0163 OpenTripPlannerParser p(backendId(), m_ifoptPrefix); 0164 addResult(reply, this, p.parseDepartures(gqlReply.data())); 0165 } 0166 }, reply); 0167 0168 return true; 0169 } 0170 0171 static QString modeName(IndividualTransport::Mode mode) 0172 { 0173 switch (mode) { 0174 case IndividualTransport::Walk: return QStringLiteral("WALK"); 0175 case IndividualTransport::Bike: return QStringLiteral("BICYCLE"); 0176 case IndividualTransport::Car: return QStringLiteral("CAR"); 0177 } 0178 return {}; 0179 } 0180 0181 static QString qualifierName(IndividualTransport::Qualifier qualifier) 0182 { 0183 switch (qualifier) { 0184 case IndividualTransport::None: return {}; 0185 case IndividualTransport::Park: return QStringLiteral("PARK"); 0186 case IndividualTransport::Rent: return QStringLiteral("RENT"); 0187 case IndividualTransport::Dropoff: return QStringLiteral("DROPOFF"); 0188 case IndividualTransport::Pickup: return QStringLiteral("PICKUP"); 0189 } 0190 return {}; 0191 } 0192 0193 static void addEnturModes(QStringList &modes, const std::vector<IndividualTransport> &its) 0194 { 0195 for (const auto &it : its) { 0196 switch (it.mode()) { 0197 case IndividualTransport::Bike: 0198 // TODO park/rent variants only supported by Entur v3 0199 modes.push_back(QStringLiteral("bicycle")); 0200 break; 0201 case IndividualTransport::Car: 0202 switch (it.qualifier()) { 0203 case IndividualTransport::None: 0204 modes.push_back(QStringLiteral("car")); 0205 break; 0206 case IndividualTransport::Park: 0207 modes.push_back(QStringLiteral("car_park")); 0208 break; 0209 case IndividualTransport::Pickup: 0210 modes.push_back(QStringLiteral("car_pickup")); 0211 break; 0212 case IndividualTransport::Dropoff: 0213 modes.push_back(QStringLiteral("car_dropoff")); 0214 break; 0215 case IndividualTransport::Rent: // not supported 0216 break; 0217 } 0218 break; 0219 case IndividualTransport::Walk: 0220 modes.push_back(QStringLiteral("foot")); 0221 break; 0222 } 0223 } 0224 } 0225 0226 bool OpenTripPlannerGraphQLBackend::queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const 0227 { 0228 if (!req.from().hasCoordinate() || !req.to().hasCoordinate()) { 0229 return false; 0230 } 0231 0232 auto gqlReq = graphQLRequest(); 0233 gqlReq.setQueryFromFile(graphQLPath(QStringLiteral("journey.graphql"))); 0234 gqlReq.setVariable(QStringLiteral("fromLat"), req.from().latitude()); 0235 gqlReq.setVariable(QStringLiteral("fromLon"), req.from().longitude()); 0236 gqlReq.setVariable(QStringLiteral("toLat"), req.to().latitude()); 0237 gqlReq.setVariable(QStringLiteral("toLon"), req.to().longitude()); 0238 0239 auto dt = req.dateTime(); 0240 const auto context = requestContextData(req).value<OpenTripPlannerRequestContext>(); 0241 if (context.dateTime.isValid()) { 0242 dt = context.dateTime; 0243 } 0244 if (timeZone().isValid()) { 0245 dt = dt.toTimeZone(timeZone()); 0246 } 0247 0248 gqlReq.setVariable(QStringLiteral("date"), dt.toString(QStringLiteral("yyyy-MM-dd"))); 0249 gqlReq.setVariable(QStringLiteral("time"), dt.toString(QStringLiteral("hh:mm:ss"))); 0250 gqlReq.setVariable(QStringLiteral("dateTime"), dt.toString(Qt::ISODate)); 0251 gqlReq.setVariable(QStringLiteral("arriveBy"), req.dateTimeMode() == JourneyRequest::Arrival); 0252 gqlReq.setVariable(QStringLiteral("maxResults"), req.maximumResults()); 0253 gqlReq.setVariable(QStringLiteral("lang"), preferredLanguage()); 0254 gqlReq.setVariable(QStringLiteral("withIntermediateStops"), req.includeIntermediateStops()); 0255 gqlReq.setVariable(QStringLiteral("withPaths"), req.includePaths()); 0256 // TODO set context.searchWindow? 0257 0258 if (m_apiVersion == QLatin1String("entur")) { 0259 gqlReq.setVariable(QStringLiteral("allowBikeRental"), (req.modes() & JourneySection::RentedVehicle) != 0); 0260 QStringList modes; 0261 modes.push_back(QStringLiteral("foot")); 0262 if (req.modes() & JourneySection::PublicTransport) { 0263 if (req.lineModes().empty()) { 0264 modes.push_back(QStringLiteral("transit")); 0265 } else { 0266 for (const auto &m : otp_mode_map) { 0267 if (m.enturMode && std::binary_search(req.lineModes().begin(), req.lineModes().end(), m.mode)) { 0268 modes.push_back(QLatin1String(m.enturMode)); 0269 } 0270 } 0271 } 0272 } 0273 if (req.modes() & JourneySection::RentedVehicle) { 0274 modes.push_back(QStringLiteral("bicycle")); 0275 } 0276 addEnturModes(modes, req.accessModes()); 0277 addEnturModes(modes, req.egressModes()); 0278 0279 modes.removeDuplicates(); 0280 QJsonArray modesArray; 0281 std::copy(modes.begin(), modes.end(), std::back_inserter(modesArray)); 0282 gqlReq.setVariable(QStringLiteral("modes"), modesArray); 0283 } else { 0284 struct Mode { 0285 QString mode; 0286 QString qualifier; 0287 }; 0288 std::vector<Mode> modes; 0289 0290 if (req.modes() & JourneySection::PublicTransport) { 0291 if (req.lineModes().empty()) { 0292 for (const auto &mode : m_supportedTransitModes) { 0293 modes.push_back({ mode, {} }); 0294 } 0295 } else { 0296 for (const auto &m : otp_mode_map) { 0297 if (std::binary_search(req.lineModes().begin(), req.lineModes().end(), m.mode)) { 0298 modes.push_back({ QLatin1String(m.otpMode), {} }); 0299 } 0300 } 0301 } 0302 } 0303 if (req.modes() & JourneySection::RentedVehicle) { 0304 for (const auto &mode : m_supportedRentalModes) { 0305 modes.push_back({ mode, QStringLiteral("RENT") }); 0306 } 0307 } 0308 for (const auto &it : req.accessModes()) { 0309 modes.push_back({ modeName(it.mode()), qualifierName(it.qualifier()) }); 0310 } 0311 const auto modeLessThan = [](const Mode &lhs, const Mode &rhs) { 0312 if (lhs.mode == rhs.mode) { 0313 return lhs.qualifier < rhs.qualifier; 0314 } 0315 return lhs.mode < rhs.mode; 0316 }; 0317 const auto modeEqual = [](const Mode &lhs, const Mode &rhs) { 0318 return lhs.mode == rhs.mode && lhs.qualifier == rhs.qualifier; 0319 }; 0320 std::sort(modes.begin(), modes.end(), modeLessThan); 0321 modes.erase(std::unique(modes.begin(), modes.end(), modeEqual), modes.end()); 0322 0323 QJsonArray modesArray; 0324 for (const auto &mode : modes) { 0325 QJsonObject modeObj; 0326 modeObj.insert(QLatin1String("mode"), mode.mode); 0327 if (!mode.qualifier.isEmpty()) { 0328 modeObj.insert(QLatin1String("qualifier"), mode.qualifier); 0329 } 0330 modesArray.push_back(modeObj); 0331 } 0332 gqlReq.setVariable(QStringLiteral("modes"), modesArray); 0333 } 0334 0335 if (isLoggingEnabled()) { 0336 logRequest(req, gqlReq.networkRequest(), gqlReq.rawData()); 0337 } 0338 KGraphQL::query(gqlReq, nam, [this, reply](const KGraphQLReply &gqlReply) { 0339 logReply(reply, gqlReply.networkReply(), gqlReply.rawData()); 0340 if (gqlReply.error() != KGraphQLReply::NoError) { 0341 addError(reply, Reply::NetworkError, gqlReply.errorString()); 0342 } else { 0343 OpenTripPlannerParser p(backendId(), m_ifoptPrefix); 0344 p.setKnownRentalVehicleNetworks(m_rentalNetworks); 0345 addResult(reply, this, p.parseJourneys(gqlReply.data())); 0346 if (p.m_nextJourneyContext.dateTime.isValid()) { 0347 setNextRequestContext(reply, p.m_nextJourneyContext); 0348 } 0349 if (p.m_prevJourneyContext.dateTime.isValid()) { 0350 setPreviousRequestContext(reply, p.m_prevJourneyContext); 0351 } 0352 } 0353 }, reply); 0354 0355 return true; 0356 } 0357 0358 KGraphQLRequest OpenTripPlannerGraphQLBackend::graphQLRequest() const 0359 { 0360 KGraphQLRequest req(graphQLEndpoint()); 0361 for (const auto &header : m_extraHeaders) { 0362 req.networkRequest().setRawHeader(header.first, header.second); 0363 } 0364 applySslConfiguration(req.networkRequest()); 0365 return req; 0366 } 0367 0368 QUrl OpenTripPlannerGraphQLBackend::graphQLEndpoint() const 0369 { 0370 if (m_apiVersion == QLatin1String("entur")) { 0371 return QUrl(m_endpoint); 0372 } 0373 return QUrl(m_endpoint + QLatin1String("index/graphql")); 0374 } 0375 0376 static QString graphQLBasePath() 0377 { 0378 return QStringLiteral(":/org.kde.kpublictransport/otp/"); 0379 } 0380 0381 QString OpenTripPlannerGraphQLBackend::graphQLPath(const QString &fileName) const 0382 { 0383 if (!m_apiVersion.isEmpty()) { 0384 const QString versionedPath = graphQLBasePath() + m_apiVersion + QLatin1Char('/') + fileName; 0385 if (QFile::exists(versionedPath)) { 0386 return versionedPath; 0387 } 0388 } 0389 return graphQLBasePath() + fileName; 0390 } 0391 0392 void OpenTripPlannerGraphQLBackend::setExtraHttpHeaders(const QJsonValue &v) 0393 { 0394 const auto headers = v.toArray(); 0395 m_extraHeaders.reserve(headers.size()); 0396 for (const auto &header : headers) { 0397 const auto headerObj = header.toObject(); 0398 const auto name = headerObj.value(QLatin1String("name")).toString().toUtf8(); 0399 const auto val = headerObj.value(QLatin1String("value")).toString().toUtf8(); 0400 if (name.isEmpty() || val.isEmpty()) { 0401 continue; 0402 } 0403 m_extraHeaders.push_back(std::make_pair(name, val)); 0404 } 0405 } 0406 0407 void OpenTripPlannerGraphQLBackend::setRentalVehicleNetworks(const QJsonObject &obj) 0408 { 0409 m_rentalNetworks.reserve(obj.size()); 0410 for (auto it = obj.begin(); it != obj.end(); ++it) { 0411 auto n = RentalVehicleNetwork::fromJson(it.value().toObject()); 0412 m_rentalNetworks.insert(it.key(), std::move(n)); 0413 } 0414 }