diff --git a/autotests/otpparsertest.cpp b/autotests/otpparsertest.cpp index 3f413eb..6ffd91c 100644 --- a/autotests/otpparsertest.cpp +++ b/autotests/otpparsertest.cpp @@ -1,165 +1,169 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "backends/opentripplannerparser.h" #include #include #include #include #include #include #include #include #include #include #define s(x) QStringLiteral(x) using namespace KPublicTransport; class OtpParserTest : public QObject { Q_OBJECT private: QByteArray readFile(const QString &fn) { QFile f(fn); f.open(QFile::ReadOnly); return f.readAll(); } private Q_SLOTS: void initTestCase() { qputenv("TZ", "UTC"); qRegisterMetaType(); } void testParseLocationByCoordinate_data() { QTest::addColumn("inFileName"); QTest::addColumn("refFileName"); QTest::newRow("fi-digitransit-location") << s(SOURCE_DIR "/data/otp/fi-digitransit-location-by-coordinate.in.json") << s(SOURCE_DIR "/data/otp/fi-digitransit-location-by-coordinate.out.json"); } void testParseLocationByCoordinate() { QFETCH(QString, inFileName); QFETCH(QString, refFileName); - const auto res = OpenTripPlannerParser::parseLocationsByCoordinate(QJsonDocument::fromJson(readFile(inFileName)).object()); + OpenTripPlannerParser p(s("gtfs")); + const auto res = p.parseLocationsByCoordinate(QJsonDocument::fromJson(readFile(inFileName)).object()); const auto jsonRes = Location::toJson(res); const auto ref = QJsonDocument::fromJson(readFile(refFileName)).array(); if (jsonRes != ref) { qDebug().noquote() << QJsonDocument(jsonRes).toJson(); } QVERIFY(!jsonRes.empty()); QCOMPARE(jsonRes, ref); } void testParseLocationByName_data() { QTest::addColumn("inFileName"); QTest::addColumn("refFileName"); QTest::newRow("fi-digitransit-location") << s(SOURCE_DIR "/data/otp/fi-digitransit-location-by-name.in.json") << s(SOURCE_DIR "/data/otp/fi-digitransit-location-by-name.out.json"); } void testParseLocationByName() { QFETCH(QString, inFileName); QFETCH(QString, refFileName); - const auto res = OpenTripPlannerParser::parseLocationsByName(QJsonDocument::fromJson(readFile(inFileName)).object()); + OpenTripPlannerParser p(s("gtfs")); + const auto res = p.parseLocationsByName(QJsonDocument::fromJson(readFile(inFileName)).object()); const auto jsonRes = Location::toJson(res); const auto ref = QJsonDocument::fromJson(readFile(refFileName)).array(); if (jsonRes != ref) { qDebug().noquote() << QJsonDocument(jsonRes).toJson(); } QVERIFY(!jsonRes.empty()); QCOMPARE(jsonRes, ref); } void testParseDepartures_data() { QTest::addColumn("inFileName"); QTest::addColumn("refFileName"); QTest::newRow("fi-digitransit-departures") << s(SOURCE_DIR "/data/otp/fi-digitransit-departure.in.json") << s(SOURCE_DIR "/data/otp/fi-digitransit-departure.out.json"); } void testParseDepartures() { QFETCH(QString, inFileName); QFETCH(QString, refFileName); - const auto res = OpenTripPlannerParser::parseDepartures(QJsonDocument::fromJson(readFile(inFileName)).object()); + OpenTripPlannerParser p(s("gtfs")); + const auto res = p.parseDepartures(QJsonDocument::fromJson(readFile(inFileName)).object()); const auto jsonRes = Departure::toJson(res); const auto ref = QJsonDocument::fromJson(readFile(refFileName)).array(); if (jsonRes != ref) { qDebug().noquote() << QJsonDocument(jsonRes).toJson(); } QVERIFY(!jsonRes.empty()); QCOMPARE(jsonRes, ref); } void testParseJourney_data() { QTest::addColumn("inFileName"); QTest::addColumn("refFileName"); QTest::newRow("fi-digitransit-departures") << s(SOURCE_DIR "/data/otp/fi-digitransit-journey.in.json") << s(SOURCE_DIR "/data/otp/fi-digitransit-journey.out.json"); } void testParseJourney() { QFETCH(QString, inFileName); QFETCH(QString, refFileName); - const auto res = OpenTripPlannerParser::parseJourneys(QJsonDocument::fromJson(readFile(inFileName)).object()); + OpenTripPlannerParser p(s("gtfs")); + const auto res = p.parseJourneys(QJsonDocument::fromJson(readFile(inFileName)).object()); const auto jsonRes = Journey::toJson(res); const auto ref = QJsonDocument::fromJson(readFile(refFileName)).array(); if (jsonRes != ref) { qDebug().noquote() << QJsonDocument(jsonRes).toJson(); } QVERIFY(!jsonRes.empty()); QCOMPARE(jsonRes, ref); } }; QTEST_GUILESS_MAIN(OtpParserTest) #include "otpparsertest.moc" diff --git a/src/lib/backends/opentripplannerbackend.cpp b/src/lib/backends/opentripplannerbackend.cpp index 956d2b4..6856449 100644 --- a/src/lib/backends/opentripplannerbackend.cpp +++ b/src/lib/backends/opentripplannerbackend.cpp @@ -1,134 +1,137 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "opentripplannerbackend.h" #include "opentripplannerparser.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace KPublicTransport; OpenTripPlannerBackend::OpenTripPlannerBackend() = default; OpenTripPlannerBackend::~OpenTripPlannerBackend() = default; AbstractBackend::Capabilities OpenTripPlannerBackend::capabilities() const { return m_endpoint.startsWith(QLatin1String("https://")) ? Secure : NoCapability; } bool OpenTripPlannerBackend::needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const { Q_UNUSED(type); return !loc.hasCoordinate(); } bool OpenTripPlannerBackend::queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const { KGraphQLRequest gqlReq(QUrl(m_endpoint + QLatin1String("index/graphql"))); if (req.hasCoordinate()) { gqlReq.setQueryFromFile(QStringLiteral(":/org.kde.kpublictransport/otp/stationByCoordinate.graphql")); gqlReq.setVariable(QStringLiteral("lat"), req.latitude()); gqlReq.setVariable(QStringLiteral("lon"), req.longitude()); } else { gqlReq.setQueryFromFile(QStringLiteral(":/org.kde.kpublictransport/otp/stationByName.graphql")); gqlReq.setVariable(QStringLiteral("name"), req.name()); } if (isLoggingEnabled()) { logRequest(req, gqlReq.networkRequest(), gqlReq.rawData()); } KGraphQL::query(gqlReq, nam, [this, req, reply](const KGraphQLReply &gqlReply) { logReply(reply, gqlReply.networkReply(), gqlReply.rawData()); if (gqlReply.error() != KGraphQLReply::NoError) { addError(reply, this, Reply::NetworkError, gqlReply.errorString()); return; } + OpenTripPlannerParser p(backendId()); if (req.hasCoordinate()) { - addResult(reply, OpenTripPlannerParser::parseLocationsByCoordinate(gqlReply.data())); + addResult(reply, p.parseLocationsByCoordinate(gqlReply.data())); } else { - addResult(reply, OpenTripPlannerParser::parseLocationsByName(gqlReply.data())); + addResult(reply, p.parseLocationsByName(gqlReply.data())); } }); return true; } bool OpenTripPlannerBackend::queryDeparture(const DepartureRequest &req, DepartureReply *reply, QNetworkAccessManager *nam) const { KGraphQLRequest gqlReq(QUrl(m_endpoint + QLatin1String("index/graphql"))); gqlReq.setQueryFromFile(QStringLiteral(":/org.kde.kpublictransport/otp/departure.graphql")); gqlReq.setVariable(QStringLiteral("lat"), req.stop().latitude()); gqlReq.setVariable(QStringLiteral("lon"), req.stop().longitude()); gqlReq.setVariable(QStringLiteral("startTime"), req.dateTime().toSecsSinceEpoch()); // TODO timezone conversion? // TODO arrival/departure selection? if (isLoggingEnabled()) { logRequest(req, gqlReq.networkRequest(), gqlReq.rawData()); } KGraphQL::query(gqlReq, nam, [this, reply](const KGraphQLReply &gqlReply) { logReply(reply, gqlReply.networkReply(), gqlReply.rawData()); if (gqlReply.error() != KGraphQLReply::NoError) { addError(reply, this, Reply::NetworkError, gqlReply.errorString()); } else { - addResult(reply, this, OpenTripPlannerParser::parseDepartures(gqlReply.data())); + OpenTripPlannerParser p(backendId()); + addResult(reply, this, p.parseDepartures(gqlReply.data())); } }); return true; } bool OpenTripPlannerBackend::queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const { KGraphQLRequest gqlReq(QUrl(m_endpoint + QLatin1String("index/graphql"))); gqlReq.setQueryFromFile(QStringLiteral(":/org.kde.kpublictransport/otp/journey.graphql")); gqlReq.setVariable(QStringLiteral("fromLat"), req.from().latitude()); gqlReq.setVariable(QStringLiteral("fromLon"), req.from().longitude()); gqlReq.setVariable(QStringLiteral("toLat"), req.to().latitude()); gqlReq.setVariable(QStringLiteral("toLon"), req.to().longitude()); gqlReq.setVariable(QStringLiteral("date"), req.dateTime().date().toString(QStringLiteral("yyyy-MM-dd"))); gqlReq.setVariable(QStringLiteral("time"), req.dateTime().time().toString(QStringLiteral("hh:mm:ss"))); // TODO timezone conversion? gqlReq.setVariable(QStringLiteral("arriveBy"), req.dateTimeMode() == JourneyRequest::Arrival); if (isLoggingEnabled()) { logRequest(req, gqlReq.networkRequest(), gqlReq.rawData()); } KGraphQL::query(gqlReq, nam, [this, reply](const KGraphQLReply &gqlReply) { logReply(reply, gqlReply.networkReply(), gqlReply.rawData()); if (gqlReply.error() != KGraphQLReply::NoError) { addError(reply, this, Reply::NetworkError, gqlReply.errorString()); } else { - addResult(reply, this, OpenTripPlannerParser::parseJourneys(gqlReply.data())); + OpenTripPlannerParser p(backendId()); + addResult(reply, this, p.parseJourneys(gqlReply.data())); } }); return true; } diff --git a/src/lib/backends/opentripplannerparser.cpp b/src/lib/backends/opentripplannerparser.cpp index 832e63a..b0662c6 100644 --- a/src/lib/backends/opentripplannerparser.cpp +++ b/src/lib/backends/opentripplannerparser.cpp @@ -1,176 +1,183 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "opentripplannerparser.h" #include "gtfs/hvt.h" #include #include #include #include #include #include using namespace KPublicTransport; -static Location parseLocation(const QJsonObject &obj) +OpenTripPlannerParser::OpenTripPlannerParser(const QString &identifierType) + : m_identifierType(identifierType) +{ +} + +OpenTripPlannerParser::~OpenTripPlannerParser() = default; + +Location OpenTripPlannerParser::parseLocation(const QJsonObject &obj) const { const auto parentObj = obj.value(QLatin1String("parentStation")).toObject(); if (!parentObj.isEmpty()) { return parseLocation(parentObj); } Location loc; loc.setName(obj.value(QLatin1String("name")).toString()); loc.setLatitude(obj.value(QLatin1String("lat")).toDouble()); loc.setLongitude(obj.value(QLatin1String("lon")).toDouble()); // TODO time zone const auto id = obj.value(QLatin1String("gtfsId")).toString(); if (!id.isEmpty()) { - loc.setIdentifier(QLatin1String("gtfs"), id); + loc.setIdentifier(m_identifierType, id); } return loc; } -std::vector OpenTripPlannerParser::parseLocationsByCoordinate(const QJsonObject &obj) +std::vector OpenTripPlannerParser::parseLocationsByCoordinate(const QJsonObject &obj) const { std::vector locs; const auto stopArray = obj.value(QLatin1String("stopsByRadius")).toObject().value(QLatin1String("edges")).toArray(); locs.reserve(stopArray.size()); for (const auto &stop : stopArray) { locs.push_back(parseLocation(stop.toObject().value(QLatin1String("node")).toObject().value(QLatin1String("stop")).toObject())); } return locs; } -std::vector OpenTripPlannerParser::parseLocationsByName(const QJsonObject &obj) +std::vector OpenTripPlannerParser::parseLocationsByName(const QJsonObject &obj) const { std::vector locs; const auto stationArray = obj.value(QLatin1String("stations")).toArray(); locs.reserve(stationArray.size()); for (const auto &station : stationArray) { locs.push_back(parseLocation(station.toObject())); } return locs; } static Line parseLine(const QJsonObject &obj) { Line line; // TODO alerts need to be propagated to journey section / departure line.setName(obj.value(QLatin1String("shortName")).toString()); line.setMode(Gtfs::Hvt::typeToMode(obj.value(QLatin1String("type")).toInt())); // TODO parse color return line; } static Route parseRoute(const QJsonObject &obj) { Route route; route.setLine(parseLine(obj.value(QLatin1String("route")).toObject())); route.setDirection(obj.value(QLatin1String("tripHeadsign")).toString()); return route; } static Departure parseDeparture(const QJsonObject &obj) { Departure dep; const auto baseTime = obj.value(QLatin1String("serviceDay")).toDouble(); // ### 64bit dep.setScheduledArrivalTime(QDateTime::fromSecsSinceEpoch(baseTime + obj.value(QLatin1String("scheduledArrival")).toDouble())); dep.setScheduledDepartureTime(QDateTime::fromSecsSinceEpoch(baseTime + obj.value(QLatin1String("scheduledDeparture")).toDouble())); if (obj.value(QLatin1String("realtime")).toBool()) { dep.setExpectedArrivalTime(QDateTime::fromSecsSinceEpoch(baseTime + obj.value(QLatin1String("realtimeArrival")).toDouble())); dep.setExpectedDepartureTime(QDateTime::fromSecsSinceEpoch(baseTime + obj.value(QLatin1String("realtimeDeparture")).toDouble())); } dep.setScheduledPlatform(obj.value(QLatin1String("stop")).toObject().value(QLatin1String("platformCode")).toString()); dep.setRoute(parseRoute(obj.value(QLatin1String("trip")).toObject())); return dep; } -static void parseDeparturesForStop(const QJsonObject &obj, std::vector &deps) +void OpenTripPlannerParser::parseDeparturesForStop(const QJsonObject &obj, std::vector &deps) const { const auto loc = parseLocation(obj.value(QLatin1String("stop")).toObject()); const auto stopTimes = obj.value(QLatin1String("stoptimes")).toArray(); for (const auto &stopTime : stopTimes) { auto dep = parseDeparture(stopTime.toObject()); dep.setStopPoint(loc); deps.push_back(dep); } } -std::vector OpenTripPlannerParser::parseDepartures(const QJsonObject &obj) +std::vector OpenTripPlannerParser::parseDepartures(const QJsonObject &obj) const { std::vector deps; const auto depsArray = obj.value(QLatin1String("nearest")).toObject().value(QLatin1String("edges")).toArray(); for (const auto &depsV : depsArray) { parseDeparturesForStop(depsV.toObject().value(QLatin1String("node")).toObject().value(QLatin1String("place")).toObject(), deps); } return deps; } -static JourneySection parseJourneySection(const QJsonObject &obj) +JourneySection OpenTripPlannerParser::parseJourneySection(const QJsonObject &obj) const { JourneySection section; section.setScheduledDepartureTime(QDateTime::fromMSecsSinceEpoch(obj.value(QLatin1String("startTime")).toDouble())); // ### sic! double to get 64 bit precision... section.setScheduledArrivalTime(QDateTime::fromMSecsSinceEpoch(obj.value(QLatin1String("endTime")).toDouble())); if (obj.value(QLatin1String("realTime")).toBool()) { section.setExpectedDepartureTime(section.scheduledDepartureTime().addSecs(obj.value(QLatin1String("departureDelay")).toInt())); section.setExpectedArrivalTime(section.scheduledArrivalTime().addSecs(obj.value(QLatin1String("arrivalDelay")).toInt())); } section.setFrom(parseLocation(obj.value(QLatin1String("from")).toObject())); // TODO handle the nested structure correctly, TODO parse platforms section.setTo(parseLocation(obj.value(QLatin1String("to")).toObject())); section.setDistance(obj.value(QLatin1String("distance")).toDouble()); if (obj.value(QLatin1String("transitLeg")).toBool()) { section.setMode(JourneySection::PublicTransport); section.setRoute(parseRoute(obj.value(QLatin1String("trip")).toObject())); } else { section.setMode(JourneySection::Walking); } return section; } -static Journey parseJourney(const QJsonObject &obj) +Journey OpenTripPlannerParser::parseJourney(const QJsonObject &obj) const { std::vector sections; const auto sectionsArray = obj.value(QLatin1String("legs")).toArray(); for (const auto §ionObj : sectionsArray) { sections.push_back(parseJourneySection(sectionObj.toObject())); } Journey journey; journey.setSections(std::move(sections)); return journey; } -std::vector OpenTripPlannerParser::parseJourneys(const QJsonObject& obj) +std::vector OpenTripPlannerParser::parseJourneys(const QJsonObject& obj) const { std::vector journeys; const auto journeysArray = obj.value(QLatin1String("plan")).toObject().value(QLatin1String("itineraries")).toArray(); journeys.reserve(journeysArray.size()); for (const auto &journeyObj : journeysArray) { journeys.push_back(parseJourney(journeyObj.toObject())); } return journeys; } diff --git a/src/lib/backends/opentripplannerparser.h b/src/lib/backends/opentripplannerparser.h index 807ae36..b8648dc 100644 --- a/src/lib/backends/opentripplannerparser.h +++ b/src/lib/backends/opentripplannerparser.h @@ -1,46 +1,61 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KPUBLICTRANSPORT_OPENTRIPPLANNERPARSER_H #define KPUBLICTRANSPORT_OPENTRIPPLANNERPARSER_H #include "kpublictransport_export.h" +#include + #include class QJsonObject; namespace KPublicTransport { class Departure; class Journey; +class JourneySection; class Location; /** Parser for OTP responses as defined by the GraphQL files in the otp/ subdir. * @internal only exported for unit tests */ -namespace OpenTripPlannerParser +class KPUBLICTRANSPORT_EXPORT OpenTripPlannerParser { - KPUBLICTRANSPORT_EXPORT std::vector parseLocationsByCoordinate(const QJsonObject &obj); - KPUBLICTRANSPORT_EXPORT std::vector parseLocationsByName(const QJsonObject &obj); - KPUBLICTRANSPORT_EXPORT std::vector parseDepartures(const QJsonObject &obj); - KPUBLICTRANSPORT_EXPORT std::vector parseJourneys(const QJsonObject &obj); -} +public: + explicit OpenTripPlannerParser(const QString &identifierType); + ~OpenTripPlannerParser(); + + std::vector parseLocationsByCoordinate(const QJsonObject &obj) const; + std::vector parseLocationsByName(const QJsonObject &obj) const; + std::vector parseDepartures(const QJsonObject &obj) const; + std::vector parseJourneys(const QJsonObject &obj) const; + +private: + Location parseLocation(const QJsonObject &obj) const; + void parseDeparturesForStop(const QJsonObject &obj, std::vector &deps) const; + JourneySection parseJourneySection(const QJsonObject &obj) const; + Journey parseJourney(const QJsonObject &obj) const; + + QString m_identifierType; +}; } #endif // KPUBLICTRANSPORT_OPENTRIPPLANNERPARSER_H