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 }