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 }