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

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "navitiabackend.h"
0008 #include "logging.h"
0009 #include "navitiaparser.h"
0010 #include "navitiaphysicalmode.h"
0011 #include "cache.h"
0012 
0013 #include <KPublicTransport/Journey>
0014 #include <KPublicTransport/JourneyReply>
0015 #include <KPublicTransport/JourneyRequest>
0016 #include <KPublicTransport/Location>
0017 #include <KPublicTransport/LocationReply>
0018 #include <KPublicTransport/LocationRequest>
0019 #include <KPublicTransport/Stopover>
0020 #include <KPublicTransport/StopoverReply>
0021 #include <KPublicTransport/StopoverRequest>
0022 
0023 #include <QNetworkAccessManager>
0024 #include <QNetworkReply>
0025 #include <QNetworkRequest>
0026 #include <QUrl>
0027 #include <QUrlQuery>
0028 
0029 using namespace KPublicTransport;
0030 
0031 NavitiaBackend::NavitiaBackend() = default;
0032 
0033 AbstractBackend::Capabilities NavitiaBackend::capabilities() const
0034 {
0035     return Secure | CanQueryNextJourney | CanQueryPreviousJourney | CanQueryArrivals; // https is hardcoded below
0036 }
0037 
0038 bool NavitiaBackend::needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const
0039 {
0040     Q_UNUSED(type);
0041     return !loc.hasCoordinate();
0042 }
0043 
0044 struct {
0045     IndividualTransport::Mode mode;
0046     IndividualTransport::Qualifier qualifier;
0047     const char *name;
0048 } static constexpr const mode_table[] = {
0049     { IndividualTransport::Walk, IndividualTransport::None, "walking" },
0050     { IndividualTransport::Bike, IndividualTransport::None, "bike" },
0051     { IndividualTransport::Bike, IndividualTransport::Park, "bike" }, // ### ?
0052     { IndividualTransport::Bike, IndividualTransport::Rent, "bss" },
0053     { IndividualTransport::Car, IndividualTransport::None, "car" },
0054     { IndividualTransport::Car, IndividualTransport::Park, "car" },
0055     { IndividualTransport::Car, IndividualTransport::Dropoff, "car_no_park" },
0056     // TODO: ridesharing, taxi
0057 };
0058 
0059 static void addModeArgument(const QString &arg, const std::vector<IndividualTransport> &modes, QUrlQuery &query)
0060 {
0061     for (const auto &mode : modes) {
0062         const auto it = std::find_if(std::begin(mode_table), std::end(mode_table), [mode](const auto &entry) {
0063             return entry.mode == mode.mode() && entry.qualifier == mode.qualifier();
0064         });
0065         if (it != std::end(mode_table)) {
0066             query.addQueryItem(arg, QLatin1String((*it).name));
0067         }
0068     }
0069 }
0070 
0071 bool NavitiaBackend::queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const
0072 {
0073     if ((req.modes() & JourneySection::PublicTransport) == 0) {
0074         return false;
0075     }
0076     if (!req.from().hasCoordinate() || !req.to().hasCoordinate()) {
0077         return false;
0078     }
0079 
0080     QUrl url = requestContextData(req).value<QUrl>();
0081     if (!url.isValid()) {
0082         url.setHost(m_endpoint);
0083         url.setPath(QStringLiteral("/v1") +
0084             (m_coverage.isEmpty() ? QString() : (QStringLiteral("/coverage/") + m_coverage)) +
0085             QStringLiteral("/journeys"));
0086 
0087         QUrlQuery query;
0088         query.addQueryItem(QStringLiteral("from"), QString::number(req.from().longitude()) + QLatin1Char(';') + QString::number(req.from().latitude()));
0089         query.addQueryItem(QStringLiteral("to"), QString::number(req.to().longitude()) + QLatin1Char(';') + QString::number(req.to().latitude()));
0090         if (req.dateTime().isValid()) {
0091             query.addQueryItem(QStringLiteral("datetime"), req.dateTime().toString(QStringLiteral("yyyyMMddThhmmss")));
0092             query.addQueryItem(QStringLiteral("datetime_represents"), req.dateTimeMode() == JourneyRequest::Arrival ? QStringLiteral("arrival") : QStringLiteral("departure"));
0093         }
0094         query.addQueryItem(QStringLiteral("count"), QString::number(std::max(1, req.maximumResults())));
0095         NavitiaPhysicalMode::lineModesToQuery(req.lineModes(), query);
0096         addModeArgument(QStringLiteral("first_section_mode[]"), req.accessModes(), query);
0097         addModeArgument(QStringLiteral("last_section_mode[]"), req.egressModes(), query);
0098 
0099         // TODO: disable reply parts we don't care about
0100         query.addQueryItem(QStringLiteral("disable_geojson"), req.includePaths() ? QStringLiteral("false") : QStringLiteral("true"));
0101         query.addQueryItem(QStringLiteral("depth"), QStringLiteral("0")); // ### also has no effect?
0102         query.addQueryItem(QStringLiteral("add_poi_infos[]"), QStringLiteral("bss_stands"));
0103         url.setQuery(query);
0104     }
0105     url.setScheme(QStringLiteral("https")); // force https in any case
0106 
0107     QNetworkRequest netReq(url);
0108     netReq.setRawHeader("Authorization", m_auth.toUtf8());
0109     logRequest(req, netReq);
0110     auto netReply = nam->get(netReq);
0111     netReply->setParent(reply);
0112     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, reply, netReply] {
0113         const auto data = netReply->readAll();
0114         logReply(reply, netReply, data);
0115 
0116         switch (netReply->error()) {
0117             case QNetworkReply::NoError:
0118             {
0119                 NavitiaParser parser;
0120                 addResult(reply, this, parser.parseJourneys(data));
0121                 if (parser.nextLink.isValid()) {
0122                     setNextRequestContext(reply, parser.nextLink);
0123                 }
0124                 if (parser.prevLink.isValid()) {
0125                     setPreviousRequestContext(reply, parser.prevLink);
0126                 }
0127                 addAttributions(reply, std::move(parser.attributions));
0128                 break;
0129             }
0130             case QNetworkReply::ContentNotFoundError:
0131                 addError(reply, Reply::NotFoundError, NavitiaParser::parseErrorMessage(data));
0132                 break;
0133             default:
0134                 addError(reply, Reply::NetworkError, netReply->errorString());
0135                 break;
0136         }
0137         netReply->deleteLater();
0138     });
0139 
0140     return true;
0141 }
0142 
0143 bool NavitiaBackend::queryStopover(const StopoverRequest &req, StopoverReply *reply, QNetworkAccessManager *nam) const
0144 {
0145     if (!req.stop().hasCoordinate()) {
0146         return false;
0147     }
0148     const auto loc = req.stop();
0149 
0150     QUrl url;
0151     url.setScheme(QStringLiteral("https"));
0152     url.setHost(m_endpoint);
0153     url.setPath(
0154         QStringLiteral("/v1/coverage/") +
0155         (m_coverage.isEmpty() ? QString::number(loc.longitude()) + QLatin1Char(';') + QString::number(loc.latitude()) : m_coverage) +
0156         QStringLiteral("/coord/") + QString::number(loc.longitude()) + QLatin1Char(';') + QString::number(loc.latitude()) +
0157         (req.mode() == StopoverRequest::QueryDeparture ? QStringLiteral("/departures") : QStringLiteral("/arrivals"))
0158     );
0159 
0160     QUrlQuery query;
0161     query.addQueryItem(QStringLiteral("from_datetime"), req.dateTime().toString(QStringLiteral("yyyyMMddThhmmss")));
0162     query.addQueryItem(QStringLiteral("disable_geojson"), QStringLiteral("true"));
0163     query.addQueryItem(QStringLiteral("depth"), QStringLiteral("0"));
0164     query.addQueryItem(QStringLiteral("count"), QString::number(std::max(1, req.maximumResults())));
0165     NavitiaPhysicalMode::lineModesToQuery(req.lineModes(), query);
0166     url.setQuery(query);
0167 
0168     QNetworkRequest netReq(url);
0169     netReq.setRawHeader("Authorization", m_auth.toUtf8());
0170     logRequest(req, netReq);
0171     auto netReply = nam->get(netReq);
0172     netReply->setParent(reply);
0173     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply] {
0174         const auto data = netReply->readAll();
0175         logReply(reply, netReply, data);
0176 
0177         switch (netReply->error()) {
0178             case QNetworkReply::NoError:
0179             {
0180                 NavitiaParser p;
0181                 addResult(reply, this, p.parseDepartures(data));
0182                 if (p.nextLink.isValid()) {
0183                     setNextRequestContext(reply, p.nextLink);
0184                 }
0185                 if (p.prevLink.isValid()) {
0186                     setPreviousRequestContext(reply, p.prevLink);
0187                 }
0188                 addAttributions(reply, std::move(p.attributions));
0189                 break;
0190             }
0191             case QNetworkReply::ContentNotFoundError:
0192                 addError(reply, Reply::NotFoundError, NavitiaParser::parseErrorMessage(data));
0193                 break;
0194             default:
0195                 addError(reply, Reply::NetworkError, netReply->errorString());
0196                 break;
0197         }
0198         netReply->deleteLater();
0199     });
0200 
0201     return true;
0202 }
0203 
0204 bool NavitiaBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
0205 {
0206     if ((req.types() & (Location::Stop | Location::RentedVehicleStation)) == 0) {
0207         return false;
0208     }
0209 
0210     QUrl url;
0211     url.setScheme(QStringLiteral("https"));
0212     url.setHost(m_endpoint);
0213 
0214     QUrlQuery query;
0215     query.addQueryItem(QStringLiteral("disable_geojson"), QStringLiteral("true"));
0216     query.addQueryItem(QStringLiteral("depth"), QStringLiteral("1")); // 1 so administrative region elements are included
0217     query.addQueryItem(QStringLiteral("count"), QString::number(std::max(1, req.maximumResults())));
0218     if (req.types() & Location::Stop) {
0219         query.addQueryItem(QStringLiteral("type[]"), QStringLiteral("stop_point"));
0220     }
0221     if (req.types() & Location::RentedVehicleStation) {
0222         query.addQueryItem(QStringLiteral("type[]"), QStringLiteral("poi"));
0223         query.addQueryItem(QStringLiteral("add_poi_infos[]"), QStringLiteral("bss_stands"));
0224         // filter is exclusive, so we cannot use this when also looking for other objects
0225         if ((req.types() & ~(Location::RentedVehicleStation | Location::RentedVehicle)) == 0) {
0226             query.addQueryItem(QStringLiteral("filter"), QStringLiteral("poi_type.id=poi_type:amenity:bicycle_rental"));
0227         }
0228     }
0229 
0230     if (req.hasCoordinate()) {
0231         url.setPath(QStringLiteral("/v1") +
0232             (m_coverage.isEmpty() ? QString() : (QStringLiteral("/coverage/") + m_coverage)) +
0233             QStringLiteral("/coord/") + QString::number(req.longitude()) + QLatin1Char(';') + QString::number(req.latitude()) +
0234             QStringLiteral("/places_nearby"));
0235         query.addQueryItem(QStringLiteral("distance"), QString::number(std::max(1, req.maximumDistance())));
0236     } else if (!req.name().isEmpty()) {
0237         url.setPath(QStringLiteral("/v1") +
0238             (m_coverage.isEmpty() ? QString() : (QStringLiteral("/coverage/") + m_coverage)) +
0239             QStringLiteral("/places"));
0240         query.addQueryItem(QStringLiteral("q"), req.name());
0241     } else {
0242         return false;
0243     }
0244 
0245     url.setQuery(query);
0246     QNetworkRequest netReq(url);
0247     netReq.setRawHeader("Authorization", m_auth.toUtf8());
0248     logRequest(req, netReq);
0249     auto netReply = nam->get(netReq);
0250     netReply->setParent(reply);
0251     QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply] {
0252         const auto data = netReply->readAll();
0253         logReply(reply, netReply, data);
0254 
0255         switch (netReply->error()) {
0256             case QNetworkReply::NoError:
0257             {
0258                 std::vector<Location> res;
0259                 NavitiaParser p;
0260                 if (reply->request().hasCoordinate()) {
0261                     res = p.parsePlacesNearby(data);
0262                 } else {
0263                     res = p.parsePlaces(data);
0264                 }
0265                 Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), res, p.attributions);
0266                 addResult(reply, std::move(res));
0267                 addAttributions(reply, std::move(p.attributions));
0268                 break;
0269             }
0270             case QNetworkReply::ContentNotFoundError:
0271                 addError(reply, Reply::NotFoundError, NavitiaParser::parseErrorMessage(data));
0272                 break;
0273             default:
0274                 addError(reply, Reply::NetworkError, netReply->errorString());
0275                 break;
0276         }
0277         netReply->deleteLater();
0278     });
0279 
0280     return true;
0281 }