Commit d2435c73 authored by Volker Krause's avatar Volker Krause
Browse files

Add accessibility.cloud backend

This gives us aggregated real-time status data for station equipment.
parent 22b1a284
......@@ -3,7 +3,7 @@ Upstream-Name: KPublicTransport
Upstream-Contact: Volker Krause <vkrause@kde.org>
Source: https://invent.kde.org/libraries/kpublictransport
Files: autotests/data/departures/* autotests/data/deutschebahn/* autotests/data/efa/* autotests/data/hafas/* autotests/data/journeys/* autotests/data/navitia/* autotests/data/otp/*
Files: autotests/data/departures/* autotests/data/deutschebahn/* autotests/data/efa/* autotests/data/hafas/* autotests/data/journeys/* autotests/data/navitia/* autotests/data/otp/* autotests/data/a11y-cloud/*
Copyright: none
License: CC0-1.0
......
......@@ -30,3 +30,4 @@ ecm_add_test(otpparsertest.cpp LINK_LIBRARIES Qt5::Test KPublicTransport)
ecm_add_test(publictransportmanagertest.cpp LINK_LIBRARIES Qt5::Test KPublicTransport)
ecm_add_test(cachetest.cpp LINK_LIBRARIES Qt5::Test KPublicTransport)
ecm_add_test(gbfstest.cpp LINK_LIBRARIES Qt5::Test KPublicTransport)
ecm_add_test(accessibilitycloudtest.cpp LINK_LIBRARIES Qt5::Test KPublicTransport)
/*
SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "backends/accessibilitycloudparser.cpp"
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QTest>
#define s(x) QStringLiteral(x)
using namespace KPublicTransport;
class AccessibilityCloudTest : public QObject
{
Q_OBJECT
private:
QByteArray readFile(const QString &fn)
{
QFile f(fn);
f.open(QFile::ReadOnly);
return f.readAll();
}
private Q_SLOTS:
void testParseLocation_data()
{
QTest::addColumn<QString>("inFileName");
QTest::addColumn<QString>("refFileName");
QTest::addColumn<QString>("attrFileName");
QTest::newRow("equipment")
<< s(SOURCE_DIR "/data/a11y-cloud/equipment.in.json")
<< s(SOURCE_DIR "/data/a11y-cloud/equipment.out.json")
<< s(SOURCE_DIR "/data/a11y-cloud/equipment.attribution.json");
}
void testParseLocation()
{
QFETCH(QString, inFileName);
QFETCH(QString, refFileName);
QFETCH(QString, attrFileName);
AccessibilityCloudParser p;
QVERIFY(p.parseLocations(readFile(inFileName)));
const auto jsonRes = Location::toJson(p.locations);
const auto ref = QJsonDocument::fromJson(readFile(refFileName)).array();
if (jsonRes != ref) {
qDebug().noquote() << QJsonDocument(jsonRes).toJson();
}
QVERIFY(!jsonRes.empty());
QCOMPARE(jsonRes, ref);
const auto attrRes = Attribution::toJson(p.attributions);
const auto attrRef = QJsonDocument::fromJson(readFile(attrFileName)).array();
if (attrRes != attrRef) {
qDebug().noquote() << QJsonDocument(attrRes).toJson();
}
QVERIFY(!attrRes.empty());
QCOMPARE(attrRes, attrRef);
}
};
QTEST_GUILESS_MAIN(AccessibilityCloudTest)
#include "accessibilitycloudtest.moc"
[
{
"license": "CC-BY 4.0 International",
"licenseUrl": "https://creativecommons.org/licenses/by/4.0/",
"name": "DB Station&Service AG"
}
]
{
"type": "FeatureCollection",
"featureCount": 4,
"totalFeatureCount": 4,
"related": {
"licenses": {
"ae4M5zruvz2uPSz6p": {
"_id": "ae4M5zruvz2uPSz6p",
"name": "Creative Commons Attribution 4.0 International Public License",
"shortName": "Creative Commons",
"websiteURL": "https://creativecommons.org/licenses/by/4.0/",
"fullTextURL": "https://creativecommons.org/licenses/by/4.0/legalcode",
"consideredAs": "CCBY",
"organizationId": "LPb4y2ri7b6fLxLFa"
},
"ns5HmC6xFrakRdoJ5": {
"_id": "ns5HmC6xFrakRdoJ5",
"name": "CC-BY 4.0 International",
"shortName": "CC-BY 4.0 Int.",
"websiteURL": "https://creativecommons.org/licenses/by/4.0/",
"fullTextURL": "https://creativecommons.org/licenses/by/4.0/legalcode",
"consideredAs": "CCBY",
"organizationId": "6ZhFGZ97omm6uKyfn"
}
}
},
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
13.36899555,
52.52497575
]
},
"properties": {
"category": "escalator",
"originalId": "10317782",
"originalPlaceInfoId": "1071",
"placeSourceId": "Q9jzJMxydegZYfFbK",
"description": "zu Gleis 13/14",
"isWorking": true,
"sourceId": "nwccXsd9WMekvFs3r",
"sourceImportId": "QNMBuRsJsFFfZ6cnA",
"placeInfoId": "hBkEGMEzkJ659qQwG",
"longDescription": "zu Gleis 13/14",
"shortDescription": "→ Gleis 13/14",
"lastUpdate": "2020-12-20T15:31:13.365Z",
"sourceName": "FaSta",
"organizationName": "DB Station&Service AG",
"sourceOrganizationId": "6ZhFGZ97omm6uKyfn",
"_id": "58LtBfCipQBBAns5S",
"distance": 2.710552149272396
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
13.3688684,
52.52516555
]
},
"properties": {
"category": "escalator",
"originalId": "10317780",
"originalPlaceInfoId": "1071",
"placeSourceId": "Q9jzJMxydegZYfFbK",
"description": "zu Gleis 15/16 (S-Bahn)",
"isWorking": false,
"sourceId": "nwccXsd9WMekvFs3r",
"sourceImportId": "QNMBuRsJsFFfZ6cnA",
"placeInfoId": "hBkEGMEzkJ659qQwG",
"longDescription": "zu Gleis 15/16 (S-Bahn)",
"shortDescription": "→ Gleis 15/16 (S-Bahn)",
"allowPublicStatusSubscriptions": true,
"languages": [],
"hasRaisedText": false,
"hasBrailleText": false,
"hasSpeech": false,
"isHighContrast": false,
"hasLargePrint": false,
"isVoiceActivated": false,
"hasHeadPhoneJack": false,
"isEasyToUnderstand": false,
"lastUpdate": "2020-12-20T15:31:13.264Z",
"organizationName": "DB Station&Service AG",
"sourceName": "FaSta",
"sourceOrganizationId": "6ZhFGZ97omm6uKyfn",
"_id": "Myc6zj47M88ec6thn",
"distance": 20.42806378086071
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
13.368924,
52.525847
]
},
"properties": {
"sourceId": "Wkd92kW7X2rAsbTdH",
"originalId": "10315353",
"placeSourceId": "f2EwSR5vmzfS5Hjox",
"category": "elevator",
"originalPlaceInfoId": "900003201",
"customData": {
"operator": "S-Bahn"
},
"sourceImportId": "FoDK5KT334Y83tiqo",
"sourceName": "VBB Anlagen (S-Bahn + BVG)",
"organizationName": "VBB Verkehrsverbund Berlin-Brandenburg GmbH",
"lastUpdate": "2020-12-18T17:40:02.398Z",
"placeInfoId": "q4cSsptF8LzuZcp5R",
"description": "zwischen OG 2 S-Bahnsteig Gl. 15 und 16 und OG 1 und EG und UG 1 und Tunnelbahnsteig UG 2 Gl. 5 und 6",
"longDescription": "zwischen Obergeschoß 2 S-Bahnsteig Gleis 15 und 16 und OG 1 und Erdgeschoß und Untergeschoss 1 und Tunnelbahnsteig Untergeschoss 2 Gleis 5 und 6",
"shortDescription": "OG 2 S-Bahnsteig Gl. 15/16 ⟷ OG 1 ⟷ EG ⟷ UG 1 ⟷ Tunnelbahnsteig UG 2 Gl. 5/6",
"disruptionSourceImportId": "7pNB4ZeDGjAENN24A",
"isWorking": false,
"lastDisruptionProperties": {
"originalId": "10315353",
"originalPlaceInfoIdField": "properties.bhfId",
"isEquipmentWorking": false,
"stateExplanation": "Außer Betrieb",
"sourceId": "vhoBSMAD7JEXNwgCy",
"sourceImportId": "7pNB4ZeDGjAENN24A",
"lastUpdate": "2020-12-18T17:40:02.375Z"
},
"ownerId": "sakKuZ2ACs6cWeKhT",
"transitNetworkId": "Qr3iwpT9EDkGLN7gg",
"_id": "GABc8hiTh6HRyMfRS",
"distance": 94.22921559136768
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
13.369897,
52.525683
]
},
"properties": {
"sourceId": "Wkd92kW7X2rAsbTdH",
"originalId": "5258",
"placeSourceId": "f2EwSR5vmzfS5Hjox",
"category": "elevator",
"originalPlaceInfoId": "900003201",
"customData": {
"operator": "BVG"
},
"sourceImportId": "guzxMQ6uJBPrK2xHg",
"lastUpdate": "2020-12-20T15:30:06.091Z",
"placeInfoId": "q4cSsptF8LzuZcp5R",
"disruptionSourceImportId": "3wBsLuvb5daRZiG75",
"isWorking": true,
"lastDisruptionProperties": {
"originalId": "5258",
"isEquipmentWorking": true,
"stateExplanation": "In Betrieb",
"sourceId": "PDphRFL4SwhE8N4tb",
"sourceImportId": "3wBsLuvb5daRZiG75",
"lastUpdate": "2020-12-20T15:30:06.077Z"
},
"organizationName": "VBB Verkehrsverbund Berlin-Brandenburg GmbH",
"sourceName": "VBB Anlagen (S-Bahn + BVG)",
"description": "zwischen Friedrich-List-Ufer / Europaplatz und UG 1 und U-Bahnsteig",
"longDescription": "zwischen Friedrich-List-Ufer / Europaplatz und Untergeschoss 1 und U-Bahnsteig",
"shortDescription": "Friedrich-List-Ufer / Europaplatz ⟷ UG 1 ⟷ U-Bahnsteig",
"ownerId": "gRbaDotqJfePW2TLb",
"transitNetworkId": "Qr3iwpT9EDkGLN7gg",
"_id": "pSaPPAY2xALhFGrMX",
"distance": 97.11699076048566
}
}
]
}
[
{
"equipment": {
"disruptionEffect": "NormalService",
"type": "Escalator"
},
"identifier": {
"a11ycloud": "10317782"
},
"latitude": 52.52497482299805,
"longitude": 13.368995666503906,
"name": "zu Gleis 13/14",
"type": "Equipment"
},
{
"equipment": {
"disruptionEffect": "NoService",
"type": "Escalator"
},
"identifier": {
"a11ycloud": "10317780"
},
"latitude": 52.52516555786133,
"longitude": 13.368868827819824,
"name": "zu Gleis 15/16 (S-Bahn)",
"type": "Equipment"
},
{
"equipment": {
"disruptionEffect": "NoService",
"notes": [
"Außer Betrieb"
],
"type": "Elevator"
},
"identifier": {
"a11ycloud": "10315353"
},
"latitude": 52.525848388671875,
"longitude": 13.368924140930176,
"name": "zwischen OG 2 S-Bahnsteig Gl. 15 und 16 und OG 1 und EG und UG 1 und Tunnelbahnsteig UG 2 Gl. 5 und 6",
"type": "Equipment"
},
{
"equipment": {
"disruptionEffect": "NormalService",
"notes": [
"In Betrieb"
],
"type": "Elevator"
},
"identifier": {
"a11ycloud": "5258"
},
"latitude": 52.52568435668945,
"longitude": 13.36989688873291,
"name": "zwischen Friedrich-List-Ufer / Europaplatz und UG 1 und U-Bahnsteig",
"type": "Equipment"
}
]
......@@ -18,6 +18,8 @@ set(kpublictransport_srcs
vehiclelayoutrequest.cpp
backends/abstractbackend.cpp
backends/accessibilitycloudbackend.cpp
backends/accessibilitycloudparser.cpp
backends/cache.cpp
backends/deutschebahnbackend.cpp
backends/deutschebahnvehiclelayoutparser.cpp
......
/*
SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "accessibilitycloudbackend.h"
#include "accessibilitycloudparser.h"
#include "cache.h"
#include <KPublicTransport/Location>
#include <KPublicTransport/LocationRequest>
#include <KPublicTransport/LocationReply>
#include <QDebug>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QUrl>
#include <QUrlQuery>
using namespace KPublicTransport;
AccessibilityCloudBackend::AccessibilityCloudBackend() = default;
AccessibilityCloudBackend::~AccessibilityCloudBackend() = default;
AbstractBackend::Capabilities AccessibilityCloudBackend::capabilities() const
{
return AbstractBackend::Secure; // hardcoded below
}
bool AccessibilityCloudBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const
{
if ((req.types() & Location::Equipment) == 0 || !req.hasCoordinate()) {
return false;
}
QUrl url;
url.setScheme(QStringLiteral("https"));
url.setHost(QStringLiteral("accessibility-cloud.freetls.fastly.net"));
url.setPath(QStringLiteral("/equipment-infos"));
QUrlQuery query;
query.addQueryItem(QStringLiteral("latitude"), QString::number(req.latitude()));
query.addQueryItem(QStringLiteral("longitude"), QString::number(req.longitude()));
query.addQueryItem(QStringLiteral("accuracy"), QString::number(req.maximumDistance()));
query.addQueryItem(QStringLiteral("appToken"), m_token);
url.setQuery(query);
QNetworkRequest netReq(url);
netReq.setRawHeader("Accept", "application/json");
logRequest(req, netReq);
auto netReply = nam->get(netReq);
QObject::connect(netReply, &QNetworkReply::finished, reply, [this, netReply, reply] {
netReply->deleteLater();
const auto data = netReply->readAll();
logReply(reply, netReply, data);
if (netReply->error()) {
addError(reply, Reply::NetworkError, netReply->errorString());
return;
}
AccessibilityCloudParser parser;
if (parser.parseLocations(data)) {
if (parser.locations.empty()) {
addError(reply, Reply::NotFoundError, {});
} else {
Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), parser.locations, parser.attributions, std::chrono::minutes(5));
addAttributions(reply, std::move(parser.attributions));
addResult(reply, std::move(parser.locations));
}
} else {
addError(reply, Reply::UnknownError, {});
}
});
return true;
}
/*
SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPUBLICTRANSPORT_ACCESSIBILITYCLOUDBACKEND_H
#define KPUBLICTRANSPORT_ACCESSIBILITYCLOUDBACKEND_H
#include "abstractbackend.h"
namespace KPublicTransport {
/** Backend for querying equipment status from accessibility.cloud. */
class AccessibilityCloudBackend : public AbstractBackend
{
Q_GADGET
Q_PROPERTY(QString token MEMBER m_token)
public:
explicit AccessibilityCloudBackend();
~AccessibilityCloudBackend();
static inline constexpr const char* type() { return "a11y_cloud"; }
Capabilities capabilities() const override;
bool queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const override;
private:
QString m_token;
};
}
#endif // KPUBLICTRANSPORT_ACCESSIBILITYCLOUDBACKEND_H
/*
SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "accessibilitycloudparser.h"
#include <KPublicTransport/Attribution>
#include <KPublicTransport/Equipment>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrl>
using namespace KPublicTransport;
bool AccessibilityCloudParser::parseLocations(const QByteArray &data)
{
QJsonParseError error;
const auto obj = QJsonDocument::fromJson(data, &error).object();
if (error.error != QJsonParseError::NoError) {
qDebug() << error.errorString();
return false;
}
const auto licensesObj = obj.value(QLatin1String("related")).toObject().value(QLatin1String("licenses")).toObject();
QHash<QString, Attribution> licenses;
for (auto it = licensesObj.begin(); it != licensesObj.end(); ++it) {
const auto obj = it.value().toObject();
const auto id = obj.value(QLatin1String("organizationId")).toString();
Attribution attr;
attr.setLicense(obj.value(QLatin1String("name")).toString());
attr.setLicenseUrl(QUrl(obj.value(QLatin1String("websiteURL")).toString()));
licenses.insert(id, attr);
}
const auto features = obj.value(QLatin1String("features")).toArray();
for (const auto &featureV : features) {
const auto feature = featureV.toObject();
const auto coordinates = feature.value(QLatin1String("geometry")).toObject().value(QLatin1String("coordinates")).toArray();
if (coordinates.size() != 2) {
continue;
}
Equipment e;
const auto properties = feature.value(QLatin1String("properties")).toObject();
const auto type = properties.value(QLatin1String("category")).toString();
if (type == QLatin1String("elevator")) {
e.setType(Equipment::Elevator);
} else if (type == QLatin1String("escalator")) {
e.setType(Equipment::Escalator);
}
if (e.type() == Equipment::Unknown) {
continue;
}
const auto working = properties.value(QLatin1String("isWorking")).toBool();
e.setDisruptionEffect(working ? Disruption::NormalService : Disruption::NoService);
const auto explanation = properties.value(QLatin1String("lastDisruptionProperties")).toObject().value(QLatin1String("stateExplanation")).toString();
e.addNote(explanation);
Location loc;
loc.setType(Location::Equipment);
loc.setCoordinate(coordinates.at(1).toDouble(), coordinates.at(0).toDouble());
loc.setIdentifier(QStringLiteral("a11ycloud"), properties.value(QLatin1String("originalId")).toString());
loc.setName(properties.value(QLatin1String("description")).toString());
loc.setData(e);
locations.push_back(std::move(loc));
const auto orgId = properties.value(QLatin1String("sourceOrganizationId")).toString();
const auto attrIt = licenses.find(orgId);
if (attrIt != licenses.end()) {
attrIt.value().setName(properties.value(QLatin1String("organizationName")).toString());
attributions.push_back(std::move(attrIt.value()));
licenses.erase(attrIt);
}
}
return true;
}
/*
SPDX-FileCopyrightText: 2020 Volker Krause <vkrause@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPUBLICTRANSPORT_ACCESSIBILITYCLOUDPARSER_H
#define KPUBLICTRANSPORT_ACCESSIBILITYCLOUDPARSER_H
#include <KPublicTransport/Attribution>
#include <KPublicTransport/Location>
namespace KPublicTransport {
/** Parser for Accessibility Cloud JSON responses. */
class AccessibilityCloudParser
{
public:
bool parseLocations(const QByteArray &data);
std::vector<Location> locations;
std::vector<Attribution> attributions;
};
}
#endif // KPUBLICTRANSPORT_ACCESSIBILITYCLOUDPARSER_H
......@@ -27,6 +27,7 @@
#include <KPublicTransport/Location>
#include <KPublicTransport/Stopover>
#include "backends/accessibilitycloudbackend.h"
#include "backends/cache.h"
#include "backends/deutschebahnbackend.h"
#include "backends/efabackend.h"
......@@ -187,7 +188,8 @@ std::unique_ptr<AbstractBackend> ManagerPrivate::loadNetwork(const QJsonObject &
HafasQueryBackend,
EfaBackend,
DeutscheBahnBackend,
GBFSBackend
GBFSBackend,
AccessibilityCloudBackend
>(type, obj);