diff --git a/autotests/data/otp/fi-digitransit-departure.in.json b/autotests/data/otp/fi-digitransit-departure.in.json index b136e50..58c234b 100644 --- a/autotests/data/otp/fi-digitransit-departure.in.json +++ b/autotests/data/otp/fi-digitransit-departure.in.json @@ -1,117 +1,147 @@ { "nearest": { "edges": [ { "node": { "place": { "stop": { "gtfsId": "MATKA:4_TPE", "name": "TAMPERE", "lat": 61.498776, "lon": 23.773019, "timezone": null, "parentStation": null }, "stoptimes": [ { "stop": { "platformCode": null }, "serviceDay": 1578520800, "scheduledArrival": 93300, "realtimeArrival": 93300, "scheduledDeparture": 95400, "realtimeDeparture": 95400, "realtime": false, "trip": { "tripHeadsign": "ROVANIEMI", "route": { "type": 102, "shortName": "273", "color": null, "textColor": null, - "alerts": [] + "alerts": [ + { + "alertHeaderTextTranslations": [ + {"language":"fi","text":"A- ja L-junat Helsinkiin eivät pysähdy Ilmalassa su 12.1. klo 6.30-23.40"}, + {"language":"sv","text":"A- och L-tåg mot Helsingfors stannar inte i Ilmala söndag 12.1 kl. 6.30-23.40"}, + {"language":"en","text":"A and L trains to Helsinki do not stop at Ilmala on Sunday 12 Jan 6.30am-11.40pm"} + ], + "alertDescriptionTextTranslations": [ + {"language":"fi","text":"A- ja L-junat Helsinkiin eivät pysähdy Ilmalassa su 12.1. klo 6.30-23.40."}, + {"language":"sv","text":"A- och L-tåg mot Helsingfors stannar inte i Ilmala söndag 12.1 kl. 6.30-23.40."}, + {"language":"en","text":"A and L trains to Helsinki do not stop at Ilmala on Sunday 12 Jan 6.30am-11.40pm."} + ], + "alertEffect":"DETOUR", + "alertCause":"MAINTENANCE", + "alertSeverityLevel":"WARNING" + }, { + "alertHeaderTextTranslations":[ + {"language":"fi","text":"L- ja I-junat Helsinkiin eivät pysähdy Ilmalassa la 11.1. klo 23.10-23.40"}, + {"language":"sv","text":"L- och I-tåg mot Helsingfors stannar inte i Ilmala lördag 11.1 kl. 23.10-23.40"}, + {"language":"en","text":"L and I trains to Helsinki do not stop at Ilmala on Saturday 11 Jan 11.10pm-11.40pm"} + ], + "alertDescriptionTextTranslations":[ + {"language":"fi","text":"L- ja I-junat Helsinkiin eivät pysähdy Ilmalassa la 11.1. klo 23.10-23.40."}, + {"language":"sv","text":"L- och I-tåg mot Helsingfors stannar inte i Ilmala lördag 11.1 kl. 23.10-23.40."}, + {"language":"en","text":"L and I trains to Helsinki do not stop at Ilmala on Saturday 11 Jan 11.10pm-11.40pm."} + ], + "alertEffect":"DETOUR", + "alertCause":"MAINTENANCE", + "alertSeverityLevel":"WARNING" + } + ] } } } ] } } }, { "node": { "place": { "stop": { "gtfsId": "MATKA:4_TPE", "name": "TAMPERE", "lat": 61.498776, "lon": 23.773019, "timezone": null, "parentStation": null }, "stoptimes": [ { "stop": { "platformCode": null }, "serviceDay": 1578520800, "scheduledArrival": 97500, "realtimeArrival": 97500, "scheduledDeparture": 99900, "realtimeDeparture": 99900, "realtime": true, "trip": { "tripHeadsign": "HELSINKI", "route": { "type": 102, "shortName": "266", "color": null, "textColor": null, "alerts": [] } } } ] } } }, { "node": { "place": { "stop": { "gtfsId": "MATKA:4_TPE", "name": "TAMPERE", "lat": 61.498776, "lon": 23.773019, "timezone": null, "parentStation": null }, "stoptimes": [ { "stop": { "platformCode": null }, "serviceDay": 1578607200, "scheduledArrival": 18240, "realtimeArrival": 18240, "scheduledDeparture": 18240, "realtimeDeparture": 18240, "realtime": false, "trip": { "tripHeadsign": "HELSINKI", "route": { "type": 2, "shortName": "160", "color": null, "textColor": null, "alerts": [] } } } ] } } } ] } } diff --git a/autotests/data/otp/fi-digitransit-departure.out.json b/autotests/data/otp/fi-digitransit-departure.out.json index 63a95cb..f3f3d03 100644 --- a/autotests/data/otp/fi-digitransit-departure.out.json +++ b/autotests/data/otp/fi-digitransit-departure.out.json @@ -1,64 +1,68 @@ [ { "disruptionEffect": "NormalService", + "notes": [ + "A- och L-tåg mot Helsingfors stannar inte i Ilmala söndag 12.1 kl. 6.30-23.40.", + "L- och I-tåg mot Helsingfors stannar inte i Ilmala lördag 11.1 kl. 23.10-23.40." + ], "route": { "direction": "ROVANIEMI", "line": { "mode": "LongDistanceTrain", "name": "273" } }, "scheduledArrivalTime": "2020-01-09T23:55:00", "scheduledDepartureTime": "2020-01-10T00:30:00", "stopPoint": { "identifier": { "gtfs": "MATKA:4_TPE" }, "latitude": 61.498775482177734, "longitude": 23.773019790649414, "name": "TAMPERE" } }, { "disruptionEffect": "NormalService", "expectedArrivalTime": "2020-01-10T01:05:00", "expectedDepartureTime": "2020-01-10T01:45:00", "route": { "direction": "HELSINKI", "line": { "mode": "LongDistanceTrain", "name": "266" } }, "scheduledArrivalTime": "2020-01-10T01:05:00", "scheduledDepartureTime": "2020-01-10T01:45:00", "stopPoint": { "identifier": { "gtfs": "MATKA:4_TPE" }, "latitude": 61.498775482177734, "longitude": 23.773019790649414, "name": "TAMPERE" } }, { "disruptionEffect": "NormalService", "route": { "direction": "HELSINKI", "line": { "mode": "Train", "name": "160" } }, "scheduledArrivalTime": "2020-01-10T03:04:00", "scheduledDepartureTime": "2020-01-10T03:04:00", "stopPoint": { "identifier": { "gtfs": "MATKA:4_TPE" }, "latitude": 61.498775482177734, "longitude": 23.773019790649414, "name": "TAMPERE" } } ] diff --git a/autotests/otpparsertest.cpp b/autotests/otpparsertest.cpp index 6ffd91c..016a3d8 100644 --- a/autotests/otpparsertest.cpp +++ b/autotests/otpparsertest.cpp @@ -1,169 +1,170 @@ /* 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"); + QLocale::setDefault(QLocale(QLocale::Swedish, QLocale::Finland)); 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); 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); 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); 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); 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/opentripplannerparser.cpp b/src/lib/backends/opentripplannerparser.cpp index db820a2..8b12feb 100644 --- a/src/lib/backends/opentripplannerparser.cpp +++ b/src/lib/backends/opentripplannerparser.cpp @@ -1,192 +1,228 @@ /* 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; 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(m_identifierType, id); } return loc; } 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())); } // deduplicate elements, which we get due to searching for stops rather than stations std::stable_sort(locs.begin(), locs.end(), [this](const auto &lhs, const auto &rhs) { return lhs.identifier(m_identifierType) < rhs.identifier(m_identifierType); }); locs.erase(std::unique(locs.begin(), locs.end(), [this](const auto &lhs, const auto &rhs) { return lhs.identifier(m_identifierType) == rhs.identifier(m_identifierType); }), locs.end()); return locs; } 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) +void OpenTripPlannerParser::parseAlerts(const QJsonArray& alertsArray) const { + m_alerts.reserve(alertsArray.size()); + for (const auto &alertValue : alertsArray) { + const auto alertObj = alertValue.toObject(); + const auto descsArray = alertObj.value(QLatin1String("alertDescriptionTextTranslations")).toArray(); + if (descsArray.empty()) { + continue; + } + + // find the best language + const auto uiLangs = QLocale().uiLanguages(); + int minIdx = 0, minWeight = std::numeric_limits::max(); + for (int i = 0; i < descsArray.size(); ++i) { + const auto lang = descsArray.at(i).toObject().value(QLatin1String("language")).toString(); + for (int j = 0; j < uiLangs.size() && j < minWeight; ++j) { + if (uiLangs.at(j).startsWith(lang)) { + minIdx = i; + minWeight = j; + break; + } + } + } + + m_alerts.push_back(descsArray.at(minIdx).toObject().value(QLatin1String("text")).toString()); + } +} + +Line OpenTripPlannerParser::parseLine(const QJsonObject &obj) const +{ + parseAlerts(obj.value(QLatin1String("alerts")).toArray()); + 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 OpenTripPlannerParser::parseRoute(const QJsonObject &obj) const { 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 OpenTripPlannerParser::parseDeparture(const QJsonObject &obj) const { 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())); + + dep.addNotes(m_alerts); + m_alerts.clear(); + return dep; } 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) 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; } 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); } + section.addNotes(m_alerts); + m_alerts.clear(); + return section; } 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) 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 b8648dc..7e4aa7e 100644 --- a/src/lib/backends/opentripplannerparser.h +++ b/src/lib/backends/opentripplannerparser.h @@ -1,61 +1,70 @@ /* 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 #include +class QJsonArray; class QJsonObject; namespace KPublicTransport { class Departure; class Journey; class JourneySection; +class Line; class Location; +class Route; /** Parser for OTP responses as defined by the GraphQL files in the otp/ subdir. * @internal only exported for unit tests */ class KPUBLICTRANSPORT_EXPORT OpenTripPlannerParser { 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 parseAlerts(const QJsonArray &alertsArray) const; + Line parseLine(const QJsonObject &obj) const; + Route parseRoute(const QJsonObject &obj) const; + Departure parseDeparture(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; + mutable QStringList m_alerts; }; } #endif // KPUBLICTRANSPORT_OPENTRIPPLANNERPARSER_H