diff --git a/autotests/data/otp/no-entur-journey.in.json b/autotests/data/otp/no-entur-journey.in.json new file mode 100644 index 0000000..207a4c0 --- /dev/null +++ b/autotests/data/otp/no-entur-journey.in.json @@ -0,0 +1,405 @@ +{ + "plan": { + "itineraries": [ + { + "legs": [ + { + "startTime": "2020-01-12T13:27:23+0100", + "endTime": "2020-01-12T13:29:17+0100", + "realTime": false, + "expectedStartTime": "2020-01-12T13:27:23+0100", + "expectedEndTime": "2020-01-12T13:29:17+0100", + "distance": 103.79500000000002, + "mode": "foot", + "transitLeg": false, + "from": { + "name": "Origin", + "lat": 59.94956970214844, + "lon": 10.764439582824707, + "stop": null + }, + "to": { + "name": "Nydalen", + "lat": 59.949532, + "lon": 10.765338, + "stop": { + "platformCode": "1", + "description": "Retning Majorstuen", + "gtfsId": "NSR:Quay:11151", + "timezone": "Europe/Oslo" + } + }, + "line": null, + "rentedBike": false + }, + { + "startTime": "2020-01-12T13:27:00+0100", + "endTime": "2020-01-12T13:38:00+0100", + "realTime": true, + "expectedStartTime": "2020-01-12T13:29:17+0100", + "expectedEndTime": "2020-01-12T13:40:17+0100", + "distance": 6101.852721737851, + "mode": "metro", + "transitLeg": true, + "from": { + "name": "Nydalen", + "lat": 59.949532, + "lon": 10.765338, + "stop": { + "platformCode": "1", + "description": "Retning Majorstuen", + "gtfsId": "NSR:Quay:11151", + "timezone": "Europe/Oslo" + } + }, + "to": { + "name": "Nationaltheatret", + "lat": 59.91532, + "lon": 10.73222, + "stop": { + "platformCode": "1", + "description": "Retning øst", + "gtfsId": "NSR:Quay:7333", + "timezone": "Europe/Oslo" + } + }, + "line": { + "presentation": { + "color": "EC700C", + "textColor": "FFFFFF" + }, + "transportMode": "metro", + "type": "metro", + "shortName": "4", + "name": "Vestli - Bergkrystallen", + "alerts": [] + }, + "rentedBike": false + }, + { + "startTime": "2020-01-12T13:40:17+0100", + "endTime": "2020-01-12T13:44:29+0100", + "realTime": false, + "expectedStartTime": "2020-01-12T13:40:17+0100", + "expectedEndTime": "2020-01-12T13:44:29+0100", + "distance": 326.353, + "mode": "foot", + "transitLeg": false, + "from": { + "name": "Nationaltheatret", + "lat": 59.91532, + "lon": 10.73222, + "stop": { + "platformCode": "1", + "description": "Retning øst", + "gtfsId": "NSR:Quay:7333", + "timezone": "Europe/Oslo" + } + }, + "to": { + "name": "Nationaltheatret stasjon", + "lat": 59.915362, + "lon": 10.727551, + "stop": { + "platformCode": "3", + "description": "tog retning Oslo S", + "gtfsId": "NSR:Quay:477", + "timezone": "Europe/Oslo" + } + }, + "line": null, + "rentedBike": false + }, + { + "startTime": "2020-01-12T13:48:00+0100", + "endTime": "2020-01-12T14:17:00+0100", + "realTime": true, + "expectedStartTime": "2020-01-12T13:48:00+0100", + "expectedEndTime": "2020-01-12T14:17:00+0100", + "distance": 45260.24760969986, + "mode": "rail", + "transitLeg": true, + "from": { + "name": "Nationaltheatret stasjon", + "lat": 59.915362, + "lon": 10.727551, + "stop": { + "platformCode": "3", + "description": "tog retning Oslo S", + "gtfsId": "NSR:Quay:477", + "timezone": "Europe/Oslo" + } + }, + "to": { + "name": "Oslo Lufthavn stasjon", + "lat": 60.192955, + "lon": 11.097078, + "stop": { + "platformCode": "1", + "description": "", + "gtfsId": "NSR:Quay:451", + "timezone": "Europe/Oslo" + } + }, + "line": { + "presentation": { + "color": "DC0000", + "textColor": "FFFFFF" + }, + "transportMode": "rail", + "type": "local", + "shortName": "L12", + "name": "L12", + "alerts": [] + }, + "rentedBike": false + }, + { + "startTime": "2020-01-12T14:17:00+0100", + "endTime": "2020-01-12T14:17:28+0100", + "realTime": false, + "expectedStartTime": "2020-01-12T14:17:00+0100", + "expectedEndTime": "2020-01-12T14:17:28+0100", + "distance": 34.513999999999996, + "mode": "foot", + "transitLeg": false, + "from": { + "name": "Oslo Lufthavn stasjon", + "lat": 60.192955, + "lon": 11.097078, + "stop": { + "platformCode": "1", + "description": "", + "gtfsId": "NSR:Quay:451", + "timezone": "Europe/Oslo" + } + }, + "to": { + "name": "Destination", + "lat": 60.193180084228516, + "lon": 11.096989631652832, + "stop": null + }, + "line": null, + "rentedBike": false + } + ], + "endTime": "2020-01-12T14:17:28+0100", + "distance": 51826.76233143771, + "duration": 3005, + "startTime": "2020-01-12T13:27:23+0100" + }, + { + "legs": [ + { + "startTime": "2020-01-12T13:32:15+0100", + "endTime": "2020-01-12T13:34:09+0100", + "realTime": false, + "expectedStartTime": "2020-01-12T13:32:15+0100", + "expectedEndTime": "2020-01-12T13:34:09+0100", + "distance": 103.79500000000002, + "mode": "foot", + "transitLeg": false, + "from": { + "name": "Origin", + "lat": 59.94956970214844, + "lon": 10.764439582824707, + "stop": null + }, + "to": { + "name": "Nydalen", + "lat": 59.949532, + "lon": 10.765338, + "stop": { + "platformCode": "1", + "description": "Retning Majorstuen", + "gtfsId": "NSR:Quay:11151", + "timezone": "Europe/Oslo" + } + }, + "line": null, + "rentedBike": false + }, + { + "startTime": "2020-01-12T13:33:00+0100", + "endTime": "2020-01-12T13:44:00+0100", + "realTime": true, + "expectedStartTime": "2020-01-12T13:34:09+0100", + "expectedEndTime": "2020-01-12T13:45:09+0100", + "distance": 6101.852721737851, + "mode": "metro", + "transitLeg": true, + "from": { + "name": "Nydalen", + "lat": 59.949532, + "lon": 10.765338, + "stop": { + "platformCode": "1", + "description": "Retning Majorstuen", + "gtfsId": "NSR:Quay:11151", + "timezone": "Europe/Oslo" + } + }, + "to": { + "name": "Nationaltheatret", + "lat": 59.91532, + "lon": 10.73222, + "stop": { + "platformCode": "1", + "description": "Retning øst", + "gtfsId": "NSR:Quay:7333", + "timezone": "Europe/Oslo" + } + }, + "line": { + "presentation": { + "color": "EC700C", + "textColor": "FFFFFF" + }, + "transportMode": "metro", + "type": "metro", + "shortName": "5", + "name": "Sognsvann - Vestli", + "alerts": [ + { + "alertHeaderTextTranslations": [ + { + "language": "no", + "text": "T-bane 5: Kjører kun mellom Sognsvann og Manglerud etter kl. 21:00 søndag-torsdag" + }, + { + "language": "en", + "text": "Metro 5: Service only between Sognsvann and Manglerud after 9 PM Sundays through Thursdays" + } + ], + "alertDescriptionTextTranslations": [ + { + "language": "no", + "text": "Før kl. 21:00 kjører linje 5 som normalt. Etter kl. 21:00 vil de bli endring i avgangstiden. Reiseplanlegger er oppdatert." + }, + { + "language": "en", + "text": "There will also be a change in departure times." + } + ] + } + ] + }, + "rentedBike": false + }, + { + "startTime": "2020-01-12T13:45:09+0100", + "endTime": "2020-01-12T13:49:21+0100", + "realTime": false, + "expectedStartTime": "2020-01-12T13:45:09+0100", + "expectedEndTime": "2020-01-12T13:49:21+0100", + "distance": 326.353, + "mode": "foot", + "transitLeg": false, + "from": { + "name": "Nationaltheatret", + "lat": 59.91532, + "lon": 10.73222, + "stop": { + "platformCode": "1", + "description": "Retning øst", + "gtfsId": "NSR:Quay:7333", + "timezone": "Europe/Oslo" + } + }, + "to": { + "name": "Nationaltheatret stasjon", + "lat": 59.915362, + "lon": 10.727551, + "stop": { + "platformCode": "3", + "description": "tog retning Oslo S", + "gtfsId": "NSR:Quay:477", + "timezone": "Europe/Oslo" + } + }, + "line": null, + "rentedBike": false + }, + { + "startTime": "2020-01-12T13:54:00+0100", + "endTime": "2020-01-12T14:22:00+0100", + "realTime": true, + "expectedStartTime": "2020-01-12T13:54:00+0100", + "expectedEndTime": "2020-01-12T14:22:00+0100", + "distance": 45265.3972513732, + "mode": "rail", + "transitLeg": true, + "from": { + "name": "Nationaltheatret stasjon", + "lat": 59.915362, + "lon": 10.727551, + "stop": { + "platformCode": "3", + "description": "tog retning Oslo S", + "gtfsId": "NSR:Quay:477", + "timezone": "Europe/Oslo" + } + }, + "to": { + "name": "Oslo Lufthavn stasjon", + "lat": 60.192985, + "lon": 11.096881, + "stop": { + "platformCode": "2", + "description": "", + "gtfsId": "NSR:Quay:447", + "timezone": "Europe/Oslo" + } + }, + "line": { + "presentation": { + "color": "757575", + "textColor": "FFFFFF" + }, + "transportMode": "rail", + "type": "airportLinkRail", + "shortName": "F2", + "name": "F2", + "alerts": [] + }, + "rentedBike": false + }, + { + "startTime": "2020-01-12T14:22:00+0100", + "endTime": "2020-01-12T14:22:18+0100", + "realTime": false, + "expectedStartTime": "2020-01-12T14:22:00+0100", + "expectedEndTime": "2020-01-12T14:22:18+0100", + "distance": 22.506, + "mode": "foot", + "transitLeg": false, + "from": { + "name": "Oslo Lufthavn stasjon", + "lat": 60.192985, + "lon": 11.096881, + "stop": { + "platformCode": "2", + "description": "", + "gtfsId": "NSR:Quay:447", + "timezone": "Europe/Oslo" + } + }, + "to": { + "name": "Destination", + "lat": 60.193180084228516, + "lon": 11.096989631652832, + "stop": null + }, + "line": null, + "rentedBike": false + } + ], + "endTime": "2020-01-12T14:22:18+0100", + "distance": 51819.90397311105, + "duration": 3003, + "startTime": "2020-01-12T13:32:15+0100" + } + ] + } +} diff --git a/autotests/data/otp/no-entur-journey.out.json b/autotests/data/otp/no-entur-journey.out.json new file mode 100644 index 0000000..27a075e --- /dev/null +++ b/autotests/data/otp/no-entur-journey.out.json @@ -0,0 +1,223 @@ +[ + { + "sections": [ + { + "disruptionEffect": "NormalService", + "distance": 103, + "from": { + "latitude": 59.94956970214844, + "longitude": 10.764439582824707, + "name": "Origin" + }, + "mode": "Walking", + "scheduledArrivalTime": "2020-01-12T13:29:17+01:00", + "scheduledDepartureTime": "2020-01-12T13:27:23+01:00", + "to": { + "latitude": 59.94953155517578, + "longitude": 10.765337944030762, + "name": "Nydalen" + } + }, + { + "disruptionEffect": "NormalService", + "distance": 6101, + "expectedArrivalTime": "2020-01-12T13:40:17+01:00", + "expectedDepartureTime": "2020-01-12T13:29:17+01:00", + "from": { + "latitude": 59.94953155517578, + "longitude": 10.765337944030762, + "name": "Nydalen" + }, + "mode": "PublicTransport", + "route": { + "line": { + "color": "#ec700c", + "mode": "Metro", + "name": "4", + "textColor": "#ffffff" + } + }, + "scheduledArrivalTime": "2020-01-12T13:38:00+01:00", + "scheduledDepartureTime": "2020-01-12T13:27:00+01:00", + "to": { + "latitude": 59.915321350097656, + "longitude": 10.732219696044922, + "name": "Nationaltheatret" + } + }, + { + "disruptionEffect": "NormalService", + "distance": 326, + "from": { + "latitude": 59.915321350097656, + "longitude": 10.732219696044922, + "name": "Nationaltheatret" + }, + "mode": "Walking", + "scheduledArrivalTime": "2020-01-12T13:44:29+01:00", + "scheduledDepartureTime": "2020-01-12T13:40:17+01:00", + "to": { + "latitude": 59.91536331176758, + "longitude": 10.727551460266113, + "name": "Nationaltheatret stasjon" + } + }, + { + "disruptionEffect": "NormalService", + "distance": 45260, + "expectedArrivalTime": "2020-01-12T14:17:00+01:00", + "expectedDepartureTime": "2020-01-12T13:48:00+01:00", + "from": { + "latitude": 59.91536331176758, + "longitude": 10.727551460266113, + "name": "Nationaltheatret stasjon" + }, + "mode": "PublicTransport", + "route": { + "line": { + "color": "#dc0000", + "mode": "LocalTrain", + "name": "L12", + "textColor": "#ffffff" + } + }, + "scheduledArrivalTime": "2020-01-12T14:17:00+01:00", + "scheduledDepartureTime": "2020-01-12T13:48:00+01:00", + "to": { + "latitude": 60.192955017089844, + "longitude": 11.097078323364258, + "name": "Oslo Lufthavn stasjon" + } + }, + { + "disruptionEffect": "NormalService", + "distance": 34, + "from": { + "latitude": 60.192955017089844, + "longitude": 11.097078323364258, + "name": "Oslo Lufthavn stasjon" + }, + "mode": "Walking", + "scheduledArrivalTime": "2020-01-12T14:17:28+01:00", + "scheduledDepartureTime": "2020-01-12T14:17:00+01:00", + "to": { + "latitude": 60.193180084228516, + "longitude": 11.096989631652832, + "name": "Destination" + } + } + ] + }, + { + "sections": [ + { + "disruptionEffect": "NormalService", + "distance": 103, + "from": { + "latitude": 59.94956970214844, + "longitude": 10.764439582824707, + "name": "Origin" + }, + "mode": "Walking", + "scheduledArrivalTime": "2020-01-12T13:34:09+01:00", + "scheduledDepartureTime": "2020-01-12T13:32:15+01:00", + "to": { + "latitude": 59.94953155517578, + "longitude": 10.765337944030762, + "name": "Nydalen" + } + }, + { + "disruptionEffect": "NormalService", + "distance": 6101, + "expectedArrivalTime": "2020-01-12T13:45:09+01:00", + "expectedDepartureTime": "2020-01-12T13:34:09+01:00", + "from": { + "latitude": 59.94953155517578, + "longitude": 10.765337944030762, + "name": "Nydalen" + }, + "mode": "PublicTransport", + "notes": [ + "Før kl. 21:00 kjører linje 5 som normalt. Etter kl. 21:00 vil de bli endring i avgangstiden. Reiseplanlegger er oppdatert." + ], + "route": { + "line": { + "color": "#ec700c", + "mode": "Metro", + "name": "5", + "textColor": "#ffffff" + } + }, + "scheduledArrivalTime": "2020-01-12T13:44:00+01:00", + "scheduledDepartureTime": "2020-01-12T13:33:00+01:00", + "to": { + "latitude": 59.915321350097656, + "longitude": 10.732219696044922, + "name": "Nationaltheatret" + } + }, + { + "disruptionEffect": "NormalService", + "distance": 326, + "from": { + "latitude": 59.915321350097656, + "longitude": 10.732219696044922, + "name": "Nationaltheatret" + }, + "mode": "Walking", + "scheduledArrivalTime": "2020-01-12T13:49:21+01:00", + "scheduledDepartureTime": "2020-01-12T13:45:09+01:00", + "to": { + "latitude": 59.91536331176758, + "longitude": 10.727551460266113, + "name": "Nationaltheatret stasjon" + } + }, + { + "disruptionEffect": "NormalService", + "distance": 45265, + "expectedArrivalTime": "2020-01-12T14:22:00+01:00", + "expectedDepartureTime": "2020-01-12T13:54:00+01:00", + "from": { + "latitude": 59.91536331176758, + "longitude": 10.727551460266113, + "name": "Nationaltheatret stasjon" + }, + "mode": "PublicTransport", + "route": { + "line": { + "color": "#757575", + "mode": "RapidTransit", + "name": "F2", + "textColor": "#ffffff" + } + }, + "scheduledArrivalTime": "2020-01-12T14:22:00+01:00", + "scheduledDepartureTime": "2020-01-12T13:54:00+01:00", + "to": { + "latitude": 60.19298553466797, + "longitude": 11.096880912780762, + "name": "Oslo Lufthavn stasjon" + } + }, + { + "disruptionEffect": "NormalService", + "distance": 22, + "from": { + "latitude": 60.19298553466797, + "longitude": 11.096880912780762, + "name": "Oslo Lufthavn stasjon" + }, + "mode": "Walking", + "scheduledArrivalTime": "2020-01-12T14:22:18+01:00", + "scheduledDepartureTime": "2020-01-12T14:22:00+01:00", + "to": { + "latitude": 60.193180084228516, + "longitude": 11.096989631652832, + "name": "Destination" + } + } + ] + } +] diff --git a/autotests/otpparsertest.cpp b/autotests/otpparsertest.cpp index 8a2b7f3..02cdfe1 100644 --- a/autotests/otpparsertest.cpp +++ b/autotests/otpparsertest.cpp @@ -1,173 +1,176 @@ /* 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"); QTest::newRow("it-torino-location") << s(SOURCE_DIR "/data/otp/it-torino-location-by-name.in.json") << s(SOURCE_DIR "/data/otp/it-torino-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") + QTest::newRow("fi-digitransit-journey") << s(SOURCE_DIR "/data/otp/fi-digitransit-journey.in.json") << s(SOURCE_DIR "/data/otp/fi-digitransit-journey.out.json"); + QTest::newRow("no-entur-journey") + << s(SOURCE_DIR "/data/otp/no-entur-journey.in.json") + << s(SOURCE_DIR "/data/otp/no-entur-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/coverage/public_transport_coverage.geojson b/src/coverage/public_transport_coverage.geojson index 3b192dd..8156175 100644 --- a/src/coverage/public_transport_coverage.geojson +++ b/src/coverage/public_transport_coverage.geojson @@ -1,43 +1,44 @@ { "type": "FeatureCollection", "name": "public_transport_coverage", "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, "features": [ -{ "type": "Feature", "properties": { "id": 35, "pto": "at_4_linz" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 14.179221045381826, 48.39236715318026 ], [ 14.499168910281343, 48.41236389473648 ], [ 14.509167281059453, 48.179068576580583 ], [ 14.189219416159936, 48.125743932430659 ], [ 14.179221045381826, 48.39236715318026 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 37, "pto": "no_entur" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 5.145530714531472, 58.771104725548177 ], [ 8.553822180056004, 56.884779328487923 ], [ 31.563211551729452, 69.899381679248691 ], [ 25.389960744695124, 85.024530552359565 ], [ -0.124317535375894, 75.990504981089785 ], [ 5.145530714531472, 58.771104725548177 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 1, "pto": "de_bb_vbb" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 11.049980606188592, 53.255023340553734 ], [ 14.450175166469796, 53.658123819796209 ], [ 14.907763336584974, 51.429550154798044 ], [ 13.148976578464548, 51.250906431986493 ], [ 11.049980606188592, 53.255023340553734 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 2, "pto": "at_oebb" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 9.14562225603507, 47.724658258492276 ], [ 17.052572342480296, 49.537661624446251 ], [ 17.325824045961401, 46.267315839926397 ], [ 9.622728404970328, 46.492856928513973 ], [ 9.14562225603507, 47.724658258492276 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 3, "pto": "fr_sncf" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -5.61972872135432, 48.662605574012751 ], [ 2.460714510158212, 51.369098637063651 ], [ 8.732491704343463, 49.000917206894115 ], [ 10.098750221748965, 41.063605820062165 ], [ -2.314684307821011, 42.807211927989179 ], [ -5.61972872135432, 48.662605574012751 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 4, "pto": "be_sncb" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 1.797103230275546, 51.108858919462591 ], [ 5.551061156670661, 49.1245310727546 ], [ 6.598526020014879, 50.464765618399994 ], [ 5.154195587329063, 51.837530128745527 ], [ 1.797103230275546, 51.108858919462591 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 5, "pto": "dk_dsb" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 8.010326488000569, 54.535890700621387 ], [ 12.421389701338327, 54.236615025380182 ], [ 13.891744105784246, 57.645755325953914 ], [ 7.85418265743994, 58.036114902355479 ], [ 8.010326488000569, 54.535890700621387 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 6, "pto": "nl_ns" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 3.169867740621071, 51.273135241198254 ], [ 6.130094528332992, 50.612776957785606 ], [ 7.730568791579436, 53.46240186551708 ], [ 5.290821439069613, 53.891797399558804 ], [ 3.169867740621071, 51.273135241198254 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 7, "pto": "ch_sbb" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 5.865517482105258, 45.912758561552828 ], [ 10.432724526003646, 45.70456678747199 ], [ 10.532483084417382, 47.851544457680639 ], [ 6.38165958868067, 47.77347254240032 ], [ 5.865517482105258, 45.912758561552828 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 8, "pto": "de_st_insa" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 10.659154257631855, 52.856620740432575 ], [ 11.95624739520955, 53.110824923173936 ], [ 13.325039148432243, 51.768105393822154 ], [ 12.138752962305908, 50.855577558340357 ], [ 10.515757026341859, 51.396576203661709 ], [ 10.659154257631855, 52.856620740432575 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 9, "pto": "de_sh_sh" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 8.361539529008056, 55.09883199333072 ], [ 11.421766805855931, 54.512206956235275 ], [ 10.838400796744356, 53.273776322367127 ], [ 9.828102121746653, 53.407396469705532 ], [ 8.038895758605562, 54.411177088735506 ], [ 8.361539529008056, 55.09883199333072 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 10, "pto": "de_he_rmv" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 8.30613605328238, 50.995715761646508 ], [ 10.219185479810285, 50.669812963260149 ], [ 9.016604153764634, 49.362942741730869 ], [ 8.250732577556699, 49.493303861085408 ], [ 7.638035316590351, 50.167922653745165 ], [ 8.30613605328238, 50.995715761646508 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 11, "pto": "uk_traveline" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -5.811973172814544, 48.997931607538135 ], [ 2.896149600068872, 51.474792875274431 ], [ -0.962539532825575, 61.564743513315996 ], [ -12.134487461509844, 55.611585729458554 ], [ -6.046623187652719, 53.586643008818015 ], [ -5.811973172814544, 48.997931607538135 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 12, "pto": "de_by_vgn" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 10.687399166825326, 50.301542801083542 ], [ 9.887850968117467, 49.44115941334357 ], [ 10.278934326181094, 48.580776025603591 ], [ 12.660197439724062, 48.641611214635709 ], [ 12.277804822950738, 50.258089094632034 ], [ 10.687399166825326, 50.301542801083542 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 13, "pto": "ie_tfi" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -8.02240890287003, 56.489622185444389 ], [ -5.493403187391912, 53.465244216419009 ], [ -7.683469992548218, 49.319760620944578 ], [ -12.897914766729905, 52.057344127389953 ], [ -8.02240890287003, 56.489622185444389 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 14, "pto": "de_by_mvv" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 10.737287190891561, 48.512878748688493 ], [ 11.14567953730689, 47.716178925353667 ], [ 12.618569967001518, 48.111181358771773 ], [ 12.089668403611174, 48.807456834627416 ], [ 10.737287190891561, 48.512878748688493 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 15, "pto": "de_nw_vrr" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 5.883520599125128, 51.905578088746957 ], [ 6.049697797861855, 51.111043357286981 ], [ 7.617437462538399, 50.983317696676302 ], [ 7.620784940787705, 51.790059954759037 ], [ 5.883520599125128, 51.905578088746957 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 16, "pto": "de_bw_vvs" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 8.732147719557286, 49.065212659823992 ], [ 9.706263890105323, 48.994915616588564 ], [ 9.686179020609487, 48.241733010494727 ], [ 8.450959546615584, 48.255122923491946 ], [ 8.732147719557286, 49.065212659823992 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 17, "pto": "se_resrobot" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 9.648855582634145, 58.488221096548784 ], [ 14.550433670364932, 69.881782928135777 ], [ 27.247606695497335, 68.291377272010365 ], [ 16.87086159487578, 54.57738751591252 ], [ 12.751450223272247, 54.655604187525249 ], [ 9.648855582634145, 58.488221096548784 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 18, "pto": "pl_pkp" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 13.559689163270411, 54.39488194881617 ], [ 14.837228132944924, 50.301542801083549 ], [ 23.023906428410172, 48.580776025603591 ], [ 25.578984367759197, 52.178742919788959 ], [ 22.007089697444741, 55.672420918490687 ], [ 13.559689163270411, 54.39488194881617 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 19, "pto": "de_db;railteam" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 37.806857363215244, 82.970039311331831 ], [ 35.303923871608035, 33.954258434023984 ], [ -14.337590378601625, 34.788569597893044 ], [ -36.34254732564834, 86.307283966808114 ], [ 37.806857363215244, 82.970039311331831 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 20, "pto": "au_nsw" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 140.818213877174429, -28.775512199381694 ], [ 154.271481394563182, -27.993345483254437 ], [ 151.090670082312357, -39.517268434195962 ], [ 140.766069429432605, -34.250679212272466 ], [ 140.818213877174429, -28.775512199381694 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 21, "pto": "de_nw_avv" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 5.848828953449205, 51.187998412694462 ], [ 5.819497701594432, 50.702403243098793 ], [ 6.344201206996464, 50.565524067776522 ], [ 6.572333165866914, 51.191257440678328 ], [ 5.848828953449205, 51.187998412694462 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 22, "pto": "de_ni_gvh" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 6.855868600463042, 53.884844069341568 ], [ 6.60655295969748, 52.007643950636158 ], [ 10.71292821936556, 51.039712639428686 ], [ 11.675970988597241, 53.195559650754426 ], [ 9.617894816787405, 53.9532836570027 ], [ 6.855868600463042, 53.884844069341568 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 23, "pto": "it_atm" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 8.177272911204046, 45.908582039406539 ], [ 9.731886982719276, 45.913998813174885 ], [ 9.769804399097698, 44.630223430076938 ], [ 8.090604530910515, 44.70064148906544 ], [ 8.177272911204046, 45.908582039406539 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 24, "pto": "us_ca_bart" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -123.273859767224778, 38.344169128288151 ], [ -121.422731872390273, 38.357205240223607 ], [ -121.318442976906653, 36.460450953615016 ], [ -122.804559737548431, 36.408306505873199 ], [ -123.273859767224778, 38.344169128288151 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 25, "pto": "de_bw_kvv" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 8.019884762033065, 49.188041573263455 ], [ 8.741216289128197, 49.075061936489519 ], [ 8.467457938483658, 48.214678548749546 ], [ 7.620110662679135, 48.358075780039542 ], [ 8.019884762033065, 49.188041573263455 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 26, "pto": "us_il_chicago" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -88.182275950973789, 42.491825409085131 ], [ -87.022061988718363, 42.496170779730285 ], [ -87.039443471298966, 41.414173489087581 ], [ -88.204002804199547, 41.414173489087581 ], [ -88.182275950973789, 42.491825409085131 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 27, "pto": "de_by_bayern" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 8.759140943039405, 50.529674759954013 ], [ 12.87203425867521, 50.666553935276283 ], [ 14.345114907381536, 47.655212078186359 ], [ 9.906318793359377, 46.996888425445917 ], [ 8.759140943039405, 50.529674759954013 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 28, "pto": "at_3_vor" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 14.335297600350632, 47.987181555425558 ], [ 16.276731838568946, 46.674489843012545 ], [ 17.57408836742573, 48.554583347005945 ], [ 14.960973089444686, 49.213996239923702 ], [ 14.335297600350632, 47.987181555425558 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 29, "pto": "at_4_ooevv" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 12.667596388610967, 48.084559971007586 ], [ 13.661316283054463, 48.881989515931387 ], [ 15.415661281886806, 48.397397715554625 ], [ 13.986422174446471, 47.164448957633979 ], [ 12.667596388610967, 48.084559971007586 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 30, "pto": "at_5_svv" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 12.786545699444453, 48.028814778255196 ], [ 11.905121703417906, 47.034234998954972 ], [ 14.14441509872859, 46.897256945518414 ], [ 13.370191318435003, 48.076459318580959 ], [ 12.786545699444453, 48.028814778255196 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 31, "pto": "at_6_vvst" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 13.444668396874395, 47.719826571372188 ], [ 16.159593361010543, 47.888712748718689 ], [ 16.130914576178121, 46.620473152795931 ], [ 13.884409764304898, 46.598167431259597 ], [ 13.444668396874395, 47.719826571372188 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 32, "pto": "at_7_vvt" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 13.008113561091935, 46.617286621147848 ], [ 12.587491383549715, 47.781432849901059 ], [ 10.162544909953702, 47.552487291630513 ], [ 10.063501038488257, 46.804155818336035 ], [ 13.008113561091935, 46.617286621147848 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 33, "pto": "at_8_vvv" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 9.537785226631346, 47.584789820373231 ], [ 10.270240565751523, 47.611655942850611 ], [ 10.161362069395821, 46.829710378114207 ], [ 9.461428878537738, 47.06019342884121 ], [ 9.537785226631346, 47.584789820373231 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 34, "pto": "lu" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 5.72342428215266, 50.209342896306865 ], [ 6.534121311294683, 50.118218036442059 ], [ 6.462145199810804, 49.37720667482418 ], [ 5.692270649896344, 49.468858406956855 ], [ 5.72342428215266, 50.209342896306865 ] ] ] ] } }, +{ "type": "Feature", "properties": { "id": 35, "pto": "at_4_linz" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 14.179221045381826, 48.39236715318026 ], [ 14.499168910281343, 48.41236389473648 ], [ 14.509167281059453, 48.179068576580583 ], [ 14.189219416159936, 48.125743932430659 ], [ 14.179221045381826, 48.39236715318026 ] ] ] ] } }, { "type": "Feature", "properties": { "id": 36, "pto": "at_9_wien" }, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ 16.16556403996632, 48.355706460327191 ], [ 16.565498871090714, 48.35237367006782 ], [ 16.598826773684415, 48.105747190874439 ], [ 16.182227991263169, 48.122411142171288 ], [ 16.16556403996632, 48.355706460327191 ] ] ] ] } } ] } diff --git a/src/lib/backends/opentripplannerbackend.cpp b/src/lib/backends/opentripplannerbackend.cpp index 019aeb2..83b1e1c 100644 --- a/src/lib/backends/opentripplannerbackend.cpp +++ b/src/lib/backends/opentripplannerbackend.cpp @@ -1,154 +1,163 @@ /* 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 #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"))); + KGraphQLRequest gqlReq(graphQLEndpoint()); if (req.hasCoordinate()) { gqlReq.setQueryFromFile(graphQLPath(QStringLiteral("stationByCoordinate.graphql"))); gqlReq.setVariable(QStringLiteral("lat"), req.latitude()); gqlReq.setVariable(QStringLiteral("lon"), req.longitude()); } else { gqlReq.setQueryFromFile(graphQLPath(QStringLiteral("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, p.parseLocationsByCoordinate(gqlReply.data())); } else { 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"))); + KGraphQLRequest gqlReq(graphQLEndpoint()); gqlReq.setQueryFromFile(graphQLPath(QStringLiteral("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 { 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"))); + KGraphQLRequest gqlReq(graphQLEndpoint()); gqlReq.setQueryFromFile(graphQLPath(QStringLiteral("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("dateTime"), req.dateTime().toString(Qt::ISODate)); 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 { OpenTripPlannerParser p(backendId()); addResult(reply, this, p.parseJourneys(gqlReply.data())); } }); return true; } +QUrl OpenTripPlannerBackend::graphQLEndpoint() const +{ + if (m_apiVersion == QLatin1String("entur")) { + return QUrl(m_endpoint); + } + return QUrl(m_endpoint + QLatin1String("index/graphql")); +} + static QString graphQLBasePath() { return QStringLiteral(":/org.kde.kpublictransport/otp/"); } QString OpenTripPlannerBackend::graphQLPath(const QString &fileName) const { if (!m_apiVersion.isEmpty()) { const QString versionedPath = graphQLBasePath() + m_apiVersion + QLatin1Char('/') + fileName; if (QFile::exists(versionedPath)) { return versionedPath; } } return graphQLBasePath() + fileName; } diff --git a/src/lib/backends/opentripplannerbackend.h b/src/lib/backends/opentripplannerbackend.h index c7ba3e6..1d6f894 100644 --- a/src/lib/backends/opentripplannerbackend.h +++ b/src/lib/backends/opentripplannerbackend.h @@ -1,51 +1,52 @@ /* 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_OPENTRIPPLANNERBACKEND_H #define KPUBLICTRANSPORT_OPENTRIPPLANNERBACKEND_H #include "abstractbackend.h" namespace KPublicTransport { /** Access to OpenTripPlanner GraphQL based backends. */ class OpenTripPlannerBackend : public AbstractBackend { Q_GADGET Q_PROPERTY(QString endpoint MEMBER m_endpoint) Q_PROPERTY(QString apiVersion MEMBER m_apiVersion) public: OpenTripPlannerBackend(); ~OpenTripPlannerBackend(); Capabilities capabilities() const override; bool needsLocationQuery(const Location &loc, AbstractBackend::QueryType type) const override; bool queryJourney(const JourneyRequest &req, JourneyReply *reply, QNetworkAccessManager *nam) const override; bool queryDeparture(const DepartureRequest &req, DepartureReply *reply, QNetworkAccessManager *nam) const override; bool queryLocation(const LocationRequest &req, LocationReply *reply, QNetworkAccessManager *nam) const override; private: + QUrl graphQLEndpoint() const; QString graphQLPath(const QString &fileName) const; QString m_endpoint; QString m_apiVersion; }; } #endif // KPUBLICTRANSPORT_OPENTRIPPLANNERBACKEND_H diff --git a/src/lib/backends/opentripplannerparser.cpp b/src/lib/backends/opentripplannerparser.cpp index fbb565e..ef0b296 100644 --- a/src/lib/backends/opentripplannerparser.cpp +++ b/src/lib/backends/opentripplannerparser.cpp @@ -1,273 +1,293 @@ /* 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 #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()); const auto tzId = obj.value(QLatin1String("timezone")).toString(); if (!tzId.isEmpty()) { loc.setTimeZone(QTimeZone(tzId.toUtf8())); } 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; } 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()); } } static QColor parseColor(const QJsonValue &value) { if (value.isNull()) { return {}; } return QColor(QLatin1Char('#') + value.toString()); } -struct { - const char *typeName; - Line::Mode mode; -} static const mode_map[] = { - { "RAIL", Line::Train }, - { "TRAM", Line::Tramway }, - { "FUNICULAR", Line::Funicular }, - { "SUBWAY", Line::Metro }, - { "BUS", Line::Bus }, -}; - Line OpenTripPlannerParser::parseLine(const QJsonObject &obj) const { parseAlerts(obj.value(QLatin1String("alerts")).toArray()); Line line; line.setName(obj.value(QLatin1String("shortName")).toString()); const auto type = obj.value(QLatin1String("type")); if (type.isString()) { - const auto typeStr = type.toString(); - for (const auto &m : mode_map) { - if (typeStr == QLatin1String(m.typeName)) { - line.setMode(m.mode); - break; - } - } + line.setMode(Gtfs::Hvt::typeToMode(type.toString())); } else { line.setMode(Gtfs::Hvt::typeToMode(type.toInt())); } - line.setColor(parseColor(obj.value(QLatin1String("color")))); - line.setTextColor(parseColor(obj.value(QLatin1String("textColor")))); + auto presentation = obj.value(QLatin1String("presentation")).toObject(); + if (presentation.isEmpty()) { + presentation = obj; + } + line.setColor(parseColor(presentation.value(QLatin1String("color")))); + line.setTextColor(parseColor(presentation.value(QLatin1String("textColor")))); return line; } Route OpenTripPlannerParser::parseRoute(const QJsonObject &obj) const { auto line = parseLine(obj.value(QLatin1String("route")).toObject()); if (line.name().isEmpty()) { line.setName(obj.value(QLatin1String("tripShortName")).toString()); } Route route; route.setLine(line); route.setDirection(obj.value(QLatin1String("tripHeadsign")).toString()); return route; } 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())); + + const auto trip = obj.value(QLatin1String("trip")).toObject(); + if (!trip.isEmpty()) { + dep.setRoute(parseRoute(trip)); + } else { + Route route; + route.setLine(parseLine(obj.value(QLatin1String("line")).toObject())); + dep.setRoute(route); + } 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; } +static QDateTime parseJourneyDateTime(const QJsonValue &val) +{ + if (val.isDouble()) { + return QDateTime::fromMSecsSinceEpoch(val.toDouble()); // ### sic! double to get 64 bit precision... + } + if (val.isString()) { + return QDateTime::fromString(val.toString(), Qt::ISODate); + } + return {}; +} + 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())); + section.setScheduledDepartureTime(parseJourneyDateTime(obj.value(QLatin1String("startTime")))); + section.setScheduledArrivalTime(parseJourneyDateTime(obj.value(QLatin1String("endTime")))); 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.setExpectedDepartureTime(parseJourneyDateTime(obj.value(QLatin1String("expectedStartTime")))); + if (!section.expectedDepartureTime().isValid()) { + section.setExpectedDepartureTime(section.scheduledDepartureTime().addSecs(obj.value(QLatin1String("departureDelay")).toInt())); + } + section.setExpectedArrivalTime(parseJourneyDateTime(obj.value(QLatin1String("expectedEndTime")))); + if (!section.expectedArrivalTime().isValid()) { + 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())); + + const auto trip = obj.value(QLatin1String("trip")).toObject(); + if (!trip.isEmpty()) { + section.setRoute(parseRoute(trip)); + } else { + Route route; + route.setLine(parseLine(obj.value(QLatin1String("line")).toObject())); + section.setRoute(route); + } } 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/otp/NOTES b/src/lib/backends/otp/NOTES index 2712b11..4106e5e 100644 --- a/src/lib/backends/otp/NOTES +++ b/src/lib/backends/otp/NOTES @@ -1 +1,3 @@ -These queries can be tested interactively on https://api.digitransit.fi/graphiql/hsl for example. +These queries can be tested interactively: +- Digitransit: https://api.digitransit.fi/graphiql/hsl +- Entur: https://api.entur.io/journey-planner/v2/ide/ diff --git a/src/lib/backends/otp/entur/journey.graphql b/src/lib/backends/otp/entur/journey.graphql new file mode 100644 index 0000000..8fbfc5b --- /dev/null +++ b/src/lib/backends/otp/entur/journey.graphql @@ -0,0 +1,71 @@ +query journeys( + $fromLat: Float!, + $fromLon: Float!, + $toLat: Float!, + $toLon: Float!, + $dateTime: DateTime!, + $arriveBy: Boolean! +) { + plan: trip ( + from: { coordinates: { latitude: $fromLat, longitude: $fromLon } } + to: { coordinates: { latitude: $toLat, longitude: $toLon } } + dateTime: $dateTime + arriveBy: $arriveBy + numTripPatterns: 3 + ) { + itineraries: tripPatterns { + legs { + startTime: aimedStartTime + endTime: aimedEndTime + realTime: realtime + expectedStartTime + expectedEndTime + distance + mode + transitLeg: ride + from: fromPlace { + name + lat: latitude + lon: longitude + stop: quay { + platformCode: publicCode + description + gtfsId: id + timezone + } + } + to: toPlace { + name + lat: latitude + lon: longitude + stop: quay { + platformCode: publicCode + description + gtfsId: id + timezone + } + } + line { + presentation { + color: colour + textColor: textColour + } + transportMode + type: transportSubmode + shortName: publicCode + name + alerts: situations { + alertHeaderTextTranslations: summary { + language + text: value + } + alertDescriptionTextTranslations: description { + language + text: value + } + } + } + } + } + } +} diff --git a/src/lib/backends/otp/otp.qrc b/src/lib/backends/otp/otp.qrc index 092517c..8ef675c 100644 --- a/src/lib/backends/otp/otp.qrc +++ b/src/lib/backends/otp/otp.qrc @@ -1,12 +1,14 @@ departure.graphql journey.graphql stationByCoordinate.graphql stationByName.graphql 20170808/departure.graphql 20170808/journey.graphql 20170808/stationByName.graphql + + entur/journey.graphql diff --git a/src/lib/gtfs/hvt.cpp b/src/lib/gtfs/hvt.cpp index d575f0a..c3b6e5f 100644 --- a/src/lib/gtfs/hvt.cpp +++ b/src/lib/gtfs/hvt.cpp @@ -1,101 +1,193 @@ /* 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 "hvt.h" #include using namespace KPublicTransport; Line::Mode Gtfs::Hvt::typeToMode(int hvt) { // individually handled cases - https://developers.google.com/transit/gtfs/reference/extended-route-types switch (hvt) { case 101: case 102: return Line::LongDistanceTrain; case 106: return Line::LocalTrain; case 109: return Line::RapidTransit; case 401: case 402: return Line::Metro; case 403: return Line::RapidTransit; case 717: return Line::Taxi; case 1502: return Line::Boat; } // coarse top-level types - https://developers.google.com/transit/gtfs/reference/#routestxt switch (hvt) { case 0: case 5: // cable car case 6: // TODO gondola return Line::Tramway; case 1: return Line::Metro; case 2: return Line::Train; case 3: return Line::Bus; case 4: return Line::Ferry; case 7: return Line::Funicular; } // type ranges - https://developers.google.com/transit/gtfs/reference/extended-route-types if (hvt >= 100 && hvt < 199) { return Line::Train; } if (hvt >= 200 && hvt < 299) { return Line::Coach; } if (hvt >= 400 && hvt < 499) { return Line::RapidTransit; } if (hvt >= 700 && hvt < 899) { return Line::Bus; } if (hvt >= 900 && hvt < 999) { return Line::Tramway; } if (hvt >= 1000 && hvt < 1099) { return Line::Boat; } if (hvt >= 1100 && hvt < 1199) { return Line::Air; } if (hvt >= 1200 && hvt < 1299) { return Line::Ferry; } if (hvt >= 1300 && hvt < 1399) { return Line::Tramway; // TODO gondola/aerial lift } if (hvt >= 1400 && hvt < 1499) { return Line::Funicular; } if (hvt >= 1500 && hvt < 1599) { return Line::Taxi; } qDebug() << "encountered unknown GTFS (extended) route type:" << hvt; return Line::Unknown; } + +// top-level types, complete list +struct { + const char *typeName; + Line::Mode mode; +} static const coarse_mode_map[] = { + { "air", Line::Air }, + { "bus", Line::Bus }, + { "cableway", Line::Tramway }, // TODO + { "coach", Line::Coach }, + { "funicular", Line::Funicular }, + { "lift", Line::Tramway }, // ??? + { "metro", Line::Metro }, + { "rail", Line::Train }, + { "subway", Line::Metro }, + { "tram", Line::Tramway }, + { "unknown", Line::Unknown }, + { "water", Line::Boat }, +}; + +// fine-grained types, special cases that can't be found by pattern matches +struct { + const char *typeName; + Line::Mode mode; +} static const fine_mode_map[] = { + { "airportlinkrail", Line::RapidTransit }, + { "airshipservice", Line::Air }, + { "blackcab", Line::Taxi }, + { "canalbarge", Line::Boat }, + { "cablecar", Line::Tramway }, // TODO + { "helicopterservice", Line::Air }, + { "international", Line::LongDistanceTrain }, + { "interregionalrail", Line::Train }, + { "local", Line::LocalTrain }, + { "longdistance", Line::LongDistanceTrain }, + { "minicab", Line::Taxi }, + { "regionalrail", Line::LocalTrain }, + { "riverbus", Line::Boat }, + { "streetcablecar", Line::Tramway }, + { "suburbanrailway", Line::RapidTransit }, + { "trainFerry", Line::Ferry }, // disambiguate as this matches multiple patterns + { "tube", Line::Metro }, + { "urbanrailway", Line::RapidTransit }, + { "watertaxi", Line::Boat }, +}; + +// patterns for groups of fine-grained types +struct { + const char *namePattern; + Line::Mode mode; +} static const mode_pattern_map[] = { + { "boat", Line::Boat }, + { "bus", Line::Bus }, + { "coach", Line::Coach }, + { "ferry", Line::Ferry }, + { "flight", Line::Air }, + { "funicular", Line::Funicular }, + { "highspeed", Line::LongDistanceTrain }, + { "lift", Line::Tramway }, // ??? + { "rail", Line::Train }, + { "taxi", Line::Taxi }, + { "telecabin", Line::Tramway }, // ??? + { "train", Line::Train }, + { "tram", Line::Tramway }, +}; + +Line::Mode Gtfs::Hvt::typeToMode(const QString &hvt) +{ + // fine-grained types, exact matches + for (const auto &m : fine_mode_map) { + if (hvt.compare(QLatin1String(m.typeName), Qt::CaseInsensitive) == 0) { + return m.mode; + } + } + + // top-level types, exact matches + for (const auto &m : coarse_mode_map) { + if (hvt.compare(QLatin1String(m.typeName), Qt::CaseInsensitive) == 0) { + return m.mode; + } + } + + // pattern matches on fine grained types + for (const auto &m : mode_pattern_map) { + if (hvt.contains(QLatin1String(m.namePattern), Qt::CaseInsensitive)) { + return m.mode; + } + } + + qDebug() << "encountered unknown GTFS (extended) route type:" << hvt; + return Line::Unknown; +} diff --git a/src/lib/gtfs/hvt.h b/src/lib/gtfs/hvt.h index 97d5325..0b08e0e 100644 --- a/src/lib/gtfs/hvt.h +++ b/src/lib/gtfs/hvt.h @@ -1,40 +1,42 @@ /* 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_GTFS_HVT_H #define KPUBLICTRANSPORT_GTFS_HVT_H #include namespace KPublicTransport { /** GTFS utilities. */ namespace Gtfs { /** Hierarchical vehicle type (HVT) mapping. * @see https://developers.google.com/transit/gtfs/reference/#routestxt * @see https://developers.google.com/transit/gtfs/reference/extended-route-types */ namespace Hvt { /** Maps the numerical HVT value to a Line::Mode mode. */ Line::Mode typeToMode(int hvt); + /** Maps HVT names to a Line::Mode mode. */ + Line::Mode typeToMode(const QString &hvt); } } } #endif // KPUBLICTRANSPORT_GTFS_HVT_H diff --git a/src/lib/networks/networks.qrc b/src/lib/networks/networks.qrc index e087472..a7f1454 100644 --- a/src/lib/networks/networks.qrc +++ b/src/lib/networks/networks.qrc @@ -1,60 +1,61 @@ navitia.json at_oebb.json be_sncb.json ch_sbb.json de_db.json de_dbwagenreihung.json eu_railteam.json dk_dsb.json fi_digitransit.json fi_waltti.json fr_sncf.json gb_traveline.json ie_tfi.json lu_mobiliteitszentral.json nl_ns.json + no_entur.json pl_pkp.json se_resrobot.json at_3_vor.json at_4_linz.json at_4_ooevv.json at_5_svv.json at_6_vvst.json at_7_vvt.json at_8_vvv.json au_nsw.json de_bb_vbb.json de_be_bvg.json de_bw_kvv.json de_bw_vvs.json de_by_bayern.json de_by_mvv.json de_by_vgn.json de_he_rmv.json de_ni_gvh.json de_nw_avv.json de_nw_muenster.json de_nw_vrr.json de_sh_sh.json de_st_insa.json fi_17_helsinki.json it_21_piemonte.json it_21_torino.json us_ca_bart.json us_ca_la_metro.json us_ga_marta.json us_il_chicago.json diff --git a/src/lib/networks/no_entur.json b/src/lib/networks/no_entur.json new file mode 100644 index 0000000..b32fd88 --- /dev/null +++ b/src/lib/networks/no_entur.json @@ -0,0 +1,20 @@ +{ + "KPlugin": { + "Description": "Public transport in Norway.", + "Name": "Entur" + }, + "filter": { + "geo": [ + [ 5.14553, 58.7711 ], + [ 8.5538, 56.8847 ], + [ 31.5632, 69.8993 ], + [ 25.3899, 85.02453 ], + [ -0.1243, 75.9905 ] + ] + }, + "options": { + "apiVersion": "entur", + "endpoint": "https://api.entur.io/journey-planner/v2/graphql/" + }, + "type": "otp" +} diff --git a/tests/TestLocationsModel.qml b/tests/TestLocationsModel.qml index d7b5086..b6c8418 100644 --- a/tests/TestLocationsModel.qml +++ b/tests/TestLocationsModel.qml @@ -1,87 +1,92 @@ /* Copyright (C) 2018-2019 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 . */ import QtQml.Models 2.10 ListModel { ListElement { label: "Paris Charles de Gaulle Airport"; lat: 49.00406; lon: 2.57110 } ListElement { label: "Paris Gare de Lyon"; lat: 48.84388; lon: 2.37708 } ListElement { label: "Zürich Flughafen"; lat: 47.45050; lon: 8.56275 } ListElement { label: "Randa"; lat: 46.09901; lon: 7.78315 } ListElement { label: "Brussels Gare du Midi"; lat: 50.83588; lon: 4.33620 } ListElement { label: "FOSDEM"; name: "ULB"; lat: 50.8139; lon: 4.38464 } ListElement { label: "Brussels - Zaventem Airport"; lat: 50.8978; lon: 4.48216 } ListElement { label: "Akademy 2008 (Mechelen)"; name: "Mechelen"; lat: 51.0175; lon: 4.48347 } ListElement { label: "Wien Flughafen"; lat: 48.12083; lon: 16.56312 } ListElement { label: "Wien Hauptbahnhof"; lat: 48.18282; lon: 16.37859 } ListElement { label: "Akademy 2018 BBQ (Vienna)"; name: "?"; lat: 48.21612; lon: 16.43191 } ListElement { label: "Almeria Airport"; lat: 36.84774; lon: -2.37251 } ListElement { label: "Akademy 2017 Accomodation (Almeria)"; name: "?"; lat: 36.83731; lon: -2.44788 } ListElement { label: "Akademy 2017 Venue (Almeria)"; name: "?"; lat: 36.82784; lon: -2.40377 } ListElement { label: "Berlin Flughafen Tegel"; lat: 52.55420; lon: 13.29281 } ListElement { label: "Berlin Flughafen Schönefeld"; lat: 52.38841; lon: 13.51870 } ListElement { label: "Akademy 2016 Venue (Berlin)"; name: "Berlin Alexanderplatz"; lat: 52.52068; lon: 13.41644 } ListElement { label: "Berlin Südkreuz"; lat: 52.47577; lon: 13.36535 } ListElement { label: "Brno central station"; lat: 49.19069; lon: 16.61287 } ListElement { label: "Akademy 2014 Venue (Brno)"; name: "?"; lat: 49.22462; lon: 16.57564 } ListElement { label: "Glasgow Airport"; lat: 55.8641; lon: -4.43157 } ListElement { label: "Akademy 2007 (Glasgow)"; name: "Glasgow Queen Street Rail Station"; lat: 55.8624; lon: -4.25082 } ListElement { label: "Akademy 2004 Accomodation (Ludwigsburg)"; name: "Schlösslesfeld, Ludwigsburg"; lat: 48.9032; lon: 9.22097 } ListElement { label: "Akademy 2004 Venue (Ludwigsburg)"; name: "Ludwigsburg, Rathaus"; lat: 48.8929; lon: 9.18721 } ListElement { label: "Copenhagen Central"; lat: 55.67238; lon: 12.56489 } ListElement { label: "Frankfurt (Main) Hauptbahnhof"; lat: 50.106944; lon: 8.6625 } ListElement { label: "Amsterdam Schiphol Airport"; lat: 52.309; lon: 4.7611 } ListElement { label: "Leipzig Hauptbahnhof"; lat: 51.3455; lon: 12.3821 } ListElement { label: "Leipzig Messe"; lat: 51.39737; lon: 12.39528 } ListElement { label: "Privacy Sprint Venue (Leipzig)"; name: "Leipzig, Lindenauer Markt"; lat: 51.3346; lon: 12.3249 } ListElement { label: "Toulouse-Blagnac Airport"; lat: 43.6301; lon: 1.37546 } ListElement { label: "Toulouse-Matabiau"; lat: 43.6114; lon: 1.45394 } ListElement { label: "PIM Sprint Venue (Toulouse)"; name: "Toulouse Jean Jaures"; lat: 43.6054; lon: 1.4489 } ListElement { label: "Avignon Centre"; lat: 43.9419; lon: 4.80552 } ListElement { label: "Hagfors flygplats"; lat: 60.0262; lon: 13.582 } ListElement { label: "Milano Centrale"; lat: 45.4863466378704; lon: 9.20452826501544 } ListElement { label: "Milano Porta Garibaldi"; lat: 45.4849263704046; lon: 9.18768320503651 } ListElement { label: "Malpensa Aeroporto"; lat: 45.6279397021696; lon: 8.71101861371495 } ListElement { label: "Milano Bicocca"; lat: 45.51466; lon: 9.20529 } ListElement { label: "Akademy 2019"; name: "Universitia Biocca Scienza"; lat: 45.51362; lon: 9.21116 } ListElement { label: "Torino Porta Nuova"; lat: 45.06079; lon: 7.67735 } ListElement { label: "Nürnberg Flughafen"; lat: 49.49400; lon: 11.07854 } ListElement { label: "Nürnberg Hauptbahnhof"; lat: 49.44553; lon: 11.08228 } ListElement { label: "SUSE office Nürnberg"; name: "Nürnberg Maxtor"; lat: 49.45960; lon: 11.08203 } ListElement { label: "Barcelona Sants"; lat: 41.37910; lon: 2.13983 } ListElement { label: "Strasbourg Gare Centrale"; lat: 48.58507; lon: 7.73368 } ListElement { label: "Tampere Station (Akademy 2010)"; name: "Tampere"; lat: 61.49859; lon: 23.77392 } ListElement { label: "Helsinki Vantaa Airport (HEL)"; name: "Lentosema"; lat: 60.31621; lon: 24.96906 } ListElement { label: "Helsinki central station"; name: "Helsinki"; lat: 60.17170; lon: 24.94094 } ListElement { label: "Leppävaara"; lat: 60.21946; lon: 24.81309 } + + ListElement { label: "Oslo Central Station"; lat: 59.91119; lon: 10.75325 } + ListElement { label: "Oslo Nydalen"; lat: 59.94957; lon: 10.76444 } + ListElement { label: "Oslo Lufthavn"; lat: 60.19318; lon: 11.09699 } + ListElement { label: "Bergen"; lat: 60.39026; lon: 5.33356 } }