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

0001 /*
0002     SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "gbfsbackend.h"
0008 #include "gbfsservice.h"
0009 #include "gbfsstore.h"
0010 #include "gbfsjob.h"
0011 #include "gbfsreader.h"
0012 #include "gbfsvehicletypes.h"
0013 
0014 #include <KPublicTransport/Attribution>
0015 #include <KPublicTransport/Location>
0016 #include <KPublicTransport/LocationReply>
0017 #include <KPublicTransport/LocationRequest>
0018 #include <KPublicTransport/RentalVehicle>
0019 
0020 #include <QCoreApplication>
0021 #include <QDebug>
0022 #include <QJsonArray>
0023 #include <QJsonDocument>
0024 #include <QJsonObject>
0025 #include <QTimer>
0026 
0027 #include <cmath>
0028 #include <functional>
0029 
0030 using namespace KPublicTransport;
0031 
0032 GBFSBackend::GBFSBackend()
0033 {
0034     QTimer::singleShot(std::chrono::seconds(10), Qt::CoarseTimer, QCoreApplication::instance(), []() { GBFSStore::expire(); });
0035 }
0036 
0037 GBFSBackend::~GBFSBackend() = default;
0038 
0039 AbstractBackend::Capabilities GBFSBackend::capabilities() const
0040 {
0041     return Secure;
0042 }
0043 
0044 static bool isServiceApplicable(const GBFSService &s, const LocationRequest &req)
0045 {
0046     if (req.hasCoordinate()) {
0047         return s.boundingBox.contains(QPointF(req.longitude(), req.latitude()));
0048     } else {
0049         // TODO
0050     }
0051     return false;
0052 }
0053 
0054 struct QueryContext {
0055     bool stillStarting = true;
0056     bool hasError = false;
0057     int pendingJobs = 0;
0058     std::vector<Location> result;
0059     std::vector<Attribution> attributions;
0060     QString errorMessage;
0061 };
0062 
0063 static QString stationIdToString(const QJsonValue &id)
0064 {
0065     if (id.isDouble()) {
0066         return QString::number(id.toInt());
0067     }
0068     return id.toString();
0069 }
0070 
0071 static RentalVehicle::VehicleType gbfs2kptVehicleType(const GBFSVehicleType &vehicle)
0072 {
0073     static constexpr struct {
0074         GBFSVehicleType::FormFactor formFactor;
0075         GBFSVehicleType::PropulsionType propulsion;
0076         RentalVehicle::VehicleType type;
0077     } const type_map[] = {
0078         { GBFSVehicleType::UndefinedFormFactor, GBFSVehicleType::UndefinedPropulsion, RentalVehicle::Unknown },
0079         { GBFSVehicleType::Bicycle, GBFSVehicleType::Human, RentalVehicle::Bicycle },
0080         { GBFSVehicleType::Bicycle, GBFSVehicleType::ElectricAssist, RentalVehicle::Pedelec },
0081         { GBFSVehicleType::Scooter, GBFSVehicleType::Electric, RentalVehicle::ElectricKickScooter },
0082         { GBFSVehicleType::Scooter, GBFSVehicleType::ElectricAssist, RentalVehicle::ElectricKickScooter },
0083         { GBFSVehicleType::Scooter, GBFSVehicleType::UndefinedPropulsion, RentalVehicle::ElectricKickScooter },
0084         { GBFSVehicleType::Moped, GBFSVehicleType::Electric, RentalVehicle::ElectricMoped },
0085         { GBFSVehicleType::Moped, GBFSVehicleType::UndefinedPropulsion, RentalVehicle::ElectricMoped },
0086         { GBFSVehicleType::Car, GBFSVehicleType::Electric, RentalVehicle::Car },
0087         { GBFSVehicleType::Car, GBFSVehicleType::Combustion, RentalVehicle::Car },
0088     };
0089 
0090     for (const auto &map : type_map) {
0091         if (map.formFactor == vehicle.formFactor && map.propulsion == vehicle.propulsionType) {
0092             return map.type;
0093         }
0094     }
0095 
0096     qDebug() << "unhandled vehicle type:" << vehicle.formFactor << vehicle.propulsionType;
0097     return RentalVehicle::Unknown;
0098 }
0099 
0100 // we get some address values just being " , "...
0101 static QString cleanAddress(const QString &input)
0102 {
0103     if (std::any_of(input.begin(), input.end(), std::mem_fn(qOverload<>(&QChar::isLetter)))) {
0104         return input;
0105     }
0106     return {};
0107 }
0108 
0109 static void appendResults(const GBFSService &service, const LocationRequest &req, QueryContext *context)
0110 {
0111     GBFSStore store(service.systemId);
0112     GBFSVehicleTypes vehicleTypes(service);
0113 
0114     RentalVehicleNetwork network;
0115     const auto sysInfoDoc = store.loadData(GBFS::SystemInformation);
0116     const auto sysInfo = GBFSReader::dataObject(sysInfoDoc);
0117     network.setName(sysInfo.value(QLatin1String("name")).toString());
0118 
0119     const auto stationsDoc = store.loadData(GBFS::StationInformation);
0120     const auto stations = GBFSReader::dataValue(stationsDoc, QLatin1String("stations")).toArray();
0121 
0122     std::vector<QString> selectedStationIds;
0123     for (const auto &stationV : stations) {
0124         const auto station = stationV.toObject();
0125         const auto lat = GBFSReader::readLatitude(station);
0126         const auto lon = GBFSReader::readLongitude(station);
0127         if (std::isnan(lat) || std::isnan(lon) || Location::distance(lat, lon, req.latitude(), req.longitude()) > req.maximumDistance()) {
0128             continue;
0129         }
0130         Location loc;
0131         loc.setType(Location::RentedVehicleStation);
0132         loc.setCoordinate(lat, lon);
0133         loc.setName(station.value(QLatin1String("name")).toString());
0134         const auto stationId = stationIdToString(station.value(QLatin1String("station_id")));
0135         loc.setIdentifier(service.systemId, stationId);
0136         loc.setStreetAddress(cleanAddress(station.value(QLatin1String("address")).toString()));
0137         loc.setPostalCode(station.value(QLatin1String("post_code")).toString());
0138         loc.setLocality(station.value(QLatin1String("city")).toString()); // non-standard extension
0139         // TODO cover more properties
0140 
0141         RentalVehicleStation s;
0142         s.setNetwork(network);
0143         s.setCapacity(station.value(QLatin1String("capacity")).toInt(-1));
0144         const auto vehicleCapacities = station.value(QLatin1String("vehicle_capacity")).toObject();
0145         for (auto it = vehicleCapacities.begin(); it != vehicleCapacities.end(); ++it) {
0146             const auto type = gbfs2kptVehicleType(vehicleTypes.vehicleType(it.key()));
0147             s.setCapacity(type, it.value().toInt(-1));
0148         }
0149 
0150         loc.setData(s);
0151         selectedStationIds.push_back(stationId);
0152         context->result.push_back(loc);
0153     }
0154 
0155     const auto statusDoc = store.loadData(GBFS::StationStatus);
0156     const auto status = GBFSReader::dataValue(statusDoc, QLatin1String("stations")).toArray();
0157     for (const auto &statV : status) {
0158         const auto stat = statV.toObject();
0159         const auto id = stationIdToString(stat.value(QLatin1String("station_id")));
0160         const auto it = std::find(selectedStationIds.begin(), selectedStationIds.end(), id);
0161         if (it == selectedStationIds.end()) {
0162             continue;
0163         }
0164 
0165         auto &loc = context->result[context->result.size() - selectedStationIds.size() + std::distance(selectedStationIds.begin(), it)];
0166         auto s = loc.rentalVehicleStation();
0167 
0168         s.setAvailableVehicles(stat.value(QLatin1String("num_bikes_available")).toInt(-1));
0169         const auto availableVehicleTypes = stat.value(QLatin1String("vehicle_types_available")).toArray();
0170         for (const auto &v : availableVehicleTypes) {
0171             const auto obj = v.toObject();
0172             const auto type = gbfs2kptVehicleType(vehicleTypes.vehicleType(obj.value(QLatin1String("vehicle_type_id")).toString()));
0173             s.setAvailableVehicles(type, obj.value(QLatin1String("count")).toInt(-1));
0174         }
0175 
0176         loc.setData(s);
0177     }
0178 
0179     const auto floatingDoc = store.loadData(GBFS::FreeBikeStatus);
0180     const auto floating = GBFSReader::dataValue(floatingDoc, QLatin1String("bikes")).toArray();
0181     for (const auto &bikeV : floating) {
0182         const auto bike = bikeV.toObject();
0183         if (bike.value(QLatin1String("is_reserved")).toBool() || bike.value(QLatin1String("is_disabled")).toBool()) {
0184             continue;
0185         }
0186         const auto lat = GBFSReader::readLatitude(bike);
0187         const auto lon = GBFSReader::readLongitude(bike);
0188         const bool selectedByCoord = !std::isnan(lat) && !std::isnan(lon) && Location::distance(lat, lon, req.latitude(), req.longitude()) <= req.maximumDistance();
0189 
0190         // GBFS v2.1: docked vehicle status - TODO do we want to drop the corresponding station in that case?
0191         const auto id = stationIdToString(bike.value(QLatin1String("station_id")));
0192         const auto it = std::find(selectedStationIds.begin(), selectedStationIds.end(), id);
0193         if (it == selectedStationIds.end() && !selectedByCoord) {
0194             continue;
0195         }
0196 
0197         Location loc;
0198         loc.setName(network.name());
0199         loc.setType(Location::RentedVehicle);
0200         if (selectedByCoord) {
0201             loc.setCoordinate(lat, lon);
0202         } else {
0203             const auto &station = context->result[context->result.size() - selectedStationIds.size() + std::distance(selectedStationIds.begin(), it)];
0204             loc.setCoordinate(station.latitude(), station.longitude());
0205         }
0206         const auto bikeId = bike.value(QLatin1String("bike_id")).toString();
0207         loc.setIdentifier(service.systemId, bikeId);
0208 
0209         // TODO deep rental links
0210         RentalVehicle vehicle;
0211         vehicle.setNetwork(network);
0212 
0213         auto vehicleTypeId = bike.value(QLatin1String("vehicle_type_id")).toString();
0214         if (vehicleTypeId.isEmpty()) { // non-compliant format used eg. by Lime
0215             vehicleTypeId = bike.value(QLatin1String("vehicle_type")).toString();
0216         }
0217         const auto vehicleType = vehicleTypes.vehicleType(vehicleTypeId);
0218         vehicle.setType(gbfs2kptVehicleType(vehicleType));
0219         if (!vehicleType.name.isEmpty()) {
0220             loc.setName(vehicleType.name);
0221         }
0222 
0223         const auto range = bike.value(QLatin1String("current_range_meters")).toInt();
0224         if (range > 0) { // there's too many reporting 0 for unknown that we can assume 0 means actually empty...
0225             vehicle.setRemainingRange(range);
0226         }
0227 
0228         loc.setData(vehicle);
0229         context->result.push_back(loc);
0230     }
0231 
0232     Attribution attr;
0233     attr.setLicense(sysInfo.value(QLatin1String("license_id")).toString());
0234     attr.setLicenseUrl(QUrl(sysInfo.value(QLatin1String("license_url")).toString()));
0235     attr.setName(sysInfo.value(QLatin1String("attribution_organization_name")).toString());
0236     if (attr.name().isEmpty()) {
0237         attr.setName(network.name());
0238     }
0239     attr.setUrl(QUrl(sysInfo.value(QLatin1String("attribution_url")).toString()));
0240     if (attr.url().isEmpty()) {
0241         attr.setUrl(QUrl(sysInfo.value(QLatin1String("url")).toString()));
0242     }
0243 
0244     if (attr.hasLicense()) {
0245         context->attributions.push_back(std::move(attr));
0246     }
0247 }
0248 
0249 bool GBFSBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
0250 {
0251     if ((req.types() & (Location::RentedVehicleStation | Location::RentedVehicle)) == 0) {
0252         return false;
0253     }
0254 
0255     // (1) find all applicable services
0256     // (2) fetch updates where needed
0257     // (3) look for matching locations in their data
0258 
0259     QueryContext *context = nullptr;
0260     const auto &services = GBFSServiceRepository::services();
0261     for (const auto &s : services) {
0262         if (!isServiceApplicable(s, req) || s.discoveryUrl.scheme() != QLatin1String("https")) {
0263             continue;
0264         }
0265         qDebug() << "  " << s.systemId << "applicable for request";
0266 
0267         if (!context) {
0268             context = new QueryContext;
0269         }
0270 
0271         context->pendingJobs++;
0272         auto updateJob = new GBFSJob(nam, reply);
0273         updateJob->setRequestedData({GBFS::StationInformation, GBFS::StationStatus, GBFS::FreeBikeStatus, GBFS::VehicleTypes});
0274         QObject::connect(updateJob, &GBFSJob::finished, reply, [this, context, reply, updateJob, req]() {
0275             context->pendingJobs--;
0276             updateJob->deleteLater();
0277             if (updateJob->error() != GBFSJob::NoError) {
0278                 context->errorMessage = updateJob->errorMessage();
0279                 context->hasError = true;
0280             } else {
0281                 appendResults(updateJob->service(), req, context);
0282             }
0283 
0284             if (context->pendingJobs == 0 &&  !context->stillStarting) {
0285                 if (context->hasError && context->result.empty()) {
0286                     addError(reply, Reply::NetworkError, context->errorMessage);
0287                 } else {
0288                     addResult(reply, std::move(context->result));
0289                 }
0290                 delete context;
0291             }
0292         });
0293         updateJob->discoverAndUpdate(s);
0294     }
0295 
0296     if (context && context->pendingJobs == 0) {
0297         if (!context->result.empty()) {
0298             addAttributions(reply, std::move(context->attributions));
0299             addResult(reply, std::move(context->result));
0300         }
0301         delete context;
0302         return false;
0303     } else if (context) {
0304         context->stillStarting = false;
0305         return true;
0306     }
0307     return false;
0308 }