Commit 4ccb9ee3 authored by Volker Krause's avatar Volker Krause
Browse files

Implement distance-based path splitting for Navitia

We don't seem to always get the instruction_start_coordinate value, which
the previous approach relied on. The distance-based approach isn't as
precise though, so we only use that as a fallback.
parent cae91562
Pipeline #90321 passed with stage
in 1 minute and 29 seconds
[
{
"license": "ODbL",
"name": "openstreetmap",
"url": "https://www.openstreetmap.org/copyright"
},
{
"license": "Private (unspecified)",
"name": "Haute-Garonne - Arc-en-Ciel (open)"
},
{
"license": "ODbL",
"name": "Occitanie - Ariège"
},
{
"license": "Private (unspecified)",
"name": "Occitanie - Gers"
},
{
"license": "Private (unspecified)",
"name": "Occitanie Tarn-et-Garonne"
},
{
"license": "Private (unspecified)",
"name": "Toulouse - Tisseo (open)"
},
{
"license": "SNCF",
"name": "SNCF Intercités Open Data",
"url": "https://ressources.data.sncf.com/"
},
{
"license": "SNCF",
"name": "SNCF TER Open Data",
"url": "https://ressources.data.sncf.com/"
},
{
"license": "SNCF",
"name": "SNCF TGV Open Data",
"url": "https://ressources.data.sncf.com/"
},
{
"license": "Licence Ouverte / Open License",
"name": "jcdecaux",
"url": "https://developer.jcdecaux.com/#/opendata/license"
}
]
[
{
"license": "ODbL",
"name": "openstreetmap",
"url": "https://www.openstreetmap.org/copyright"
},
{
"license": "ODbL",
"name": "Île de France Mobilités",
"url": "https://www.vianavigo.com"
}
]
This diff is collapsed.
......@@ -4,159 +4,75 @@
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "testhelpers.h"
#include <KPublicTransport/Attribution>
#include <KPublicTransport/Journey>
#include <KPublicTransport/Line>
#include <KPublicTransport/NavitiaParser>
#include <KPublicTransport/RentalVehicle>
#include <KPublicTransport/Stopover>
#include <QFile>
#include <QJsonDocument>
#include <QTest>
#include <QTimeZone>
#define s(x) QStringLiteral(x)
using namespace KPublicTransport;
class NavitiaParserTest : public QObject
{
Q_OBJECT
private:
QByteArray readFile(const char *fn)
{
QFile f(QString::fromUtf8(fn));
f.open(QFile::ReadOnly);
return f.readAll();
}
private Q_SLOTS:
void initTestCase()
{
qputenv("TZ", "UTC");
qRegisterMetaType<KPublicTransport::RentalVehicle>();
}
void testParseJourneys()
void testParseJourney_data()
{
KPublicTransport::NavitiaParser parser;
const auto res = parser.parseJourneys(readFile(SOURCE_DIR "/data/navitia/journey-response.json"));
QCOMPARE(res.size(), 4);
{
const auto journey = res[0];
QCOMPARE(journey.sections().size(), 6);
QCOMPARE(journey.scheduledDepartureTime(), QDateTime({2018, 12, 2}, {22, 4, 51}));
QEXPECT_FAIL("", "tz propagation not implemented yet", Continue);
QCOMPARE(journey.scheduledDepartureTime().timeZone().id(), "Europe/Paris");
QCOMPARE(journey.scheduledArrivalTime(), QDateTime({2018, 12, 2}, {23, 0, 15}));
QEXPECT_FAIL("", "tz propagation not implemented yet", Continue);
QCOMPARE(journey.scheduledArrivalTime().timeZone().id(), "Europe/Paris");
QCOMPARE(journey.duration(), 3324);
auto sec = journey.sections()[0];
QCOMPARE(sec.mode(), KPublicTransport::JourneySection::Walking);
QCOMPARE(sec.distance(), 77);
QEXPECT_FAIL("", "tz propagation not implemented yet", Continue);
QCOMPARE(sec.from().timeZone().id(), "Europe/Paris");
sec = journey.sections()[1];
QCOMPARE(sec.mode(), KPublicTransport::JourneySection::PublicTransport);
QCOMPARE(sec.scheduledDepartureTime(), QDateTime({2018, 12, 2}, {22, 6}, QTimeZone("Europe/Paris")));
QCOMPARE(sec.scheduledDepartureTime().timeZone().id(), "Europe/Paris");
QCOMPARE(sec.hasExpectedDepartureTime(), false);
QCOMPARE(sec.scheduledArrivalTime(), QDateTime({2018, 12, 2}, {22, 41}, QTimeZone("Europe/Paris")));
QCOMPARE(sec.scheduledArrivalTime().timeZone().id(), "Europe/Paris");
QCOMPARE(sec.hasExpectedArrivalTime(), false);
QCOMPARE(sec.duration(), 2100);
QCOMPARE(sec.from().name(), QStringLiteral("Aéroport CDG 2 TGV (Le Mesnil-Amelot)"));
QCOMPARE(sec.from().latitude(), 49.0047f);
QCOMPARE(sec.from().timeZone().id(), "Europe/Paris");
QCOMPARE(sec.from().locality(), QStringLiteral("Le Mesnil-Amelot"));
QCOMPARE(sec.to().name(), QStringLiteral("Châtelet les Halles (Paris)"));
QCOMPARE(sec.to().longitude(), 2.34701f);
QCOMPARE(sec.to().timeZone().id(), "Europe/Paris");
QCOMPARE(sec.to().locality(), QStringLiteral("Paris"));
QCOMPARE(sec.to().postalCode(), QStringLiteral("75001"));
QCOMPARE(sec.route().line().name(), QStringLiteral("B"));
QCOMPARE(sec.route().line().mode(), KPublicTransport::Line::RapidTransit);
QCOMPARE(sec.route().line().modeString(), QStringLiteral("RER"));
QCOMPARE(sec.route().line().color(), QColor(123, 163, 220));
QCOMPARE(sec.route().line().textColor(), QColor(255, 255, 255));
QCOMPARE(sec.intermediateStops().size(), 3);
for (const auto &stop : sec.intermediateStops()) {
QVERIFY(!stop.stopPoint().name().isEmpty());
QVERIFY(stop.stopPoint().hasCoordinate());
QVERIFY(stop.scheduledArrivalTime().isValid());
QVERIFY(stop.scheduledDepartureTime().isValid());
QVERIFY(!stop.expectedArrivalTime().isValid());
QVERIFY(!stop.expectedDepartureTime().isValid());
}
QCOMPARE(sec.co2Emission(), 147);
QCOMPARE(sec.path().isEmpty(), false);
QCOMPARE(sec.path().sections()[0].path().size(), 5);
sec = journey.sections()[2];
QCOMPARE(sec.mode(), KPublicTransport::JourneySection::Transfer);
sec = journey.sections()[3];
QCOMPARE(sec.mode(), KPublicTransport::JourneySection::Waiting);
sec = journey.sections()[4];
QCOMPARE(sec.scheduledDepartureTime(), QDateTime({2018, 12, 2}, {22, 49}, QTimeZone("Europe/Paris")));
QCOMPARE(sec.scheduledArrivalTime(), QDateTime({2018, 12, 2}, {22, 51}, QTimeZone("Europe/Paris")));
QCOMPARE(sec.duration(), 120);
QCOMPARE(sec.route().line().name(), QStringLiteral("A"));
QCOMPARE(sec.route().line().color(), QColor(QStringLiteral("#D1302F")));
QCOMPARE(sec.route().line().textColor(), QColor(255, 255, 255));
QCOMPARE(sec.from().name(), QStringLiteral("Châtelet les Halles (Paris)"));
QCOMPARE(sec.to().name(), QStringLiteral("Gare de Lyon RER A (Paris)"));
QCOMPARE(sec.intermediateStops().size(), 0);
}
{
const auto journey = res[1];
QCOMPARE(journey.sections().size(), 6);
auto sec = journey.sections()[1];
QCOMPARE(sec.route().line().name(), QStringLiteral("B"));
QCOMPARE(sec.route().line().mode(), KPublicTransport::Line::RapidTransit);
sec = journey.sections()[4];
QCOMPARE(sec.route().line().name(), QStringLiteral("65"));
}
QTest::addColumn<QString>("inFileName");
QTest::addColumn<QString>("refFileName");
QTest::addColumn<QString>("attrRefFileName");
QTest::newRow("journey")
<< s(SOURCE_DIR "/data/navitia/journey-response.json")
<< s(SOURCE_DIR "/data/navitia/journey-response.out.json")
<< s(SOURCE_DIR "/data/navitia/journey-response.attr.json");
QTest::newRow("journey-bss-path-no-instr-start-coord")
<< s(SOURCE_DIR "/data/navitia/journey-bss-path-no-instr-start-coord.json")
<< s(SOURCE_DIR "/data/navitia/journey-bss-path-no-instr-start-coord.out.json")
<< s(SOURCE_DIR "/data/navitia/journey-bss-path-no-instr-start-coord.attr.json");
}
{
const auto journey = res[2];
QCOMPARE(journey.sections().size(), 6);
auto sec = journey.sections()[1];
QCOMPARE(sec.route().line().name(), QStringLiteral("B"));
sec = journey.sections()[4];
QCOMPARE(sec.route().line().name(), QStringLiteral("91"));
QCOMPARE(sec.route().line().modeString(), QStringLiteral("Bus"));
QCOMPARE(sec.route().line().mode(), KPublicTransport::Line::Bus);
}
void testParseJourney()
{
QFETCH(QString, inFileName);
QFETCH(QString, refFileName);
QFETCH(QString, attrRefFileName);
{
const auto journey = res[3];
QCOMPARE(journey.sections().size(), 3);
NavitiaParser parser;
const auto res = parser.parseJourneys(Test::readFile(inFileName));
const auto jsonRes = Journey::toJson(res);
auto sec = journey.sections()[1];
QCOMPARE(sec.route().line().name(), QStringLiteral("DIRECT 4"));
QCOMPARE(sec.route().line().mode(), KPublicTransport::Line::Bus);
QCOMPARE(sec.route().line().modeString(), QStringLiteral("Bus"));
}
const auto ref = QJsonDocument::fromJson(Test::readFile(refFileName)).array();
QVERIFY(Test::compareJson(refFileName, jsonRes, ref));
QVERIFY(parser.nextLink.isValid());
QVERIFY(parser.prevLink.isValid());
QCOMPARE(parser.attributions.size(), 2);
const auto &attr = parser.attributions.at(0);
QCOMPARE(attr.name(), QStringLiteral("openstreetmap"));
QCOMPARE(attr.license(), QStringLiteral("ODbL"));
QEXPECT_FAIL("", "not implemented yet", Continue);
QCOMPARE(attr.licenseUrl().host(), QStringLiteral("spdx.org"));
QCOMPARE(attr.url().host(), QStringLiteral("www.openstreetmap.org"));
const auto attrRes = Attribution::toJson(parser.attributions);
const auto attrRef = QJsonDocument::fromJson(Test::readFile(attrRefFileName)).array();
QVERIFY(Test::compareJson(attrRefFileName, attrRes, attrRef));
}
void testParseDepartures()
{
KPublicTransport::NavitiaParser parser;
const auto res = parser.parseDepartures(readFile(SOURCE_DIR "/data/navitia/departure-response.json"));
const auto res = parser.parseDepartures(Test::readFile(s(SOURCE_DIR "/data/navitia/departure-response.json")));
QCOMPARE(res.size(), 10);
{
......
......@@ -151,7 +151,92 @@ static void parseStopDateTime(const QJsonObject &dtObj, Stopover &departure)
departure.setExpectedDepartureTime(parseDateTime(dtObj.value(QLatin1String("departure_date_time")), departure.stopPoint().timeZone()));
departure.setExpectedArrivalTime(parseDateTime(dtObj.value(QLatin1String("arrival_date_time")), departure.stopPoint().timeZone()));
}
}
static Path parsePathWithInstructionStartCoordinate(const QPolygonF &pathLineString, const QJsonArray &pathArray)
{
std::vector<PathSection> pathSections;
pathSections.reserve(pathArray.size());
PathSection prevPathSec;
int prevPolyIdx = 0;
bool isFirstSection = true;
for (const auto &pathV : pathArray) {
const auto pathObj = pathV.toObject();
PathSection pathSec;
pathSec.setDescription(pathObj.value(QLatin1String("instruction")).toString());
if (pathSec.description().isEmpty()) {
pathSec.setDescription(pathObj.value(QLatin1String("name")).toString());
}
if (!isFirstSection) {
const auto coordObj = pathObj.value(QLatin1String("instruction_start_coordinate")).toObject();
const QPointF coord(coordObj.value(QLatin1String("lon")).toString().toDouble(), coordObj.value(QLatin1String("lat")).toString().toDouble());
const auto it = std::min_element(pathLineString.begin() + prevPolyIdx, pathLineString.end(), [coord](QPointF lhs, QPointF rhs) {
return Location::distance(lhs.y(), lhs.x(), coord.y(), coord.x()) < Location::distance(rhs.y(), rhs.x(), coord.y(), coord.x());
});
int polyIdx = std::distance(pathLineString.begin(), it);
QPolygonF subPoly;
subPoly.reserve(polyIdx - prevPolyIdx + 1);
std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.begin() + polyIdx + 1, std::back_inserter(subPoly));
prevPathSec.setPath(std::move(subPoly));
prevPolyIdx = polyIdx;
pathSections.push_back(std::move(prevPathSec));
} else {
isFirstSection = false;
}
prevPathSec = pathSec;
}
QPolygonF subPoly;
subPoly.reserve(prevPolyIdx - pathLineString.size() + 1);
std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.end(), std::back_inserter(subPoly));
prevPathSec.setPath(std::move(subPoly));
pathSections.push_back(std::move(prevPathSec));
Path path;
path.setSections(std::move(pathSections));
return path;
}
static Path parsePathFromLength(const QPolygonF &pathLineString, const QJsonArray &pathArray)
{
std::vector<PathSection> pathSections;
pathSections.reserve(pathArray.size());
int prevPolyIdx = 0;
for (const auto &pathV : pathArray) {
const auto pathObj = pathV.toObject();
PathSection pathSec;
pathSec.setDescription(pathObj.value(QLatin1String("instruction")).toString());
if (pathSec.description().isEmpty()) {
pathSec.setDescription(pathObj.value(QLatin1String("name")).toString());
}
int polyIdx = prevPolyIdx + 1;
const auto length = pathObj.value(QLatin1String("length")).toInt();
for (float l = 0.0f, prevDelta = std::numeric_limits<float>::max(); polyIdx < pathLineString.size(); ++polyIdx) {
l += Location::distance(pathLineString.at(polyIdx - 1).y(), pathLineString.at(polyIdx - 1).x(), pathLineString.at(polyIdx).y(), pathLineString.at(polyIdx).x());
auto delta = length - l;
if (delta <= 0) {
if (prevDelta < -delta) {
--polyIdx;
}
break;
}
prevDelta = delta;
}
QPolygonF subPoly;
subPoly.reserve(polyIdx - prevPolyIdx + 1);
std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.begin() + std::min(polyIdx + 1, pathLineString.size()), std::back_inserter(subPoly));
pathSec.setPath(std::move(subPoly));
prevPolyIdx = polyIdx;
pathSections.push_back(std::move(pathSec));
}
Path path;
path.setSections(std::move(pathSections));
return path;
}
JourneySection NavitiaParser::parseJourneySection(const QJsonObject &obj) const
......@@ -249,47 +334,13 @@ JourneySection NavitiaParser::parseJourneySection(const QJsonObject &obj) const
const auto pathLineString = GeoJson::readLineString(obj.value(QLatin1String("geojson")).toObject());
const auto pathArray = obj.value(QLatin1String("path")).toArray();
if (!pathArray.empty()) {
std::vector<PathSection> pathSections;
pathSections.reserve(pathArray.size());
PathSection prevPathSec;
int prevPolyIdx = 0;
bool isFirstSection = true;
for (const auto &pathV : pathArray) {
const auto pathObj = pathV.toObject();
PathSection pathSec;
pathSec.setDescription(pathObj.value(QLatin1String("instruction")).toString());
if (pathSec.description().isEmpty()) {
pathSec.setDescription(pathObj.value(QLatin1String("name")).toString());
}
if (!isFirstSection) {
const auto coordObj = pathObj.value(QLatin1String("instruction_start_coordinate")).toObject();
const QPointF coord(coordObj.value(QLatin1String("lon")).toString().toDouble(), coordObj.value(QLatin1String("lat")).toString().toDouble());
const auto it = std::min_element(pathLineString.begin() + prevPolyIdx, pathLineString.end(), [coord](QPointF lhs, QPointF rhs) {
return Location::distance(lhs.y(), lhs.x(), coord.y(), coord.x()) < Location::distance(rhs.y(), rhs.x(), coord.y(), coord.x());
});
int polyIdx = std::distance(pathLineString.begin(), it);
QPolygonF subPoly;
subPoly.reserve(polyIdx - prevPolyIdx + 1);
std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.begin() + polyIdx + 1, std::back_inserter(subPoly));
prevPathSec.setPath(std::move(subPoly));
prevPolyIdx = polyIdx;
pathSections.push_back(std::move(prevPathSec));
} else {
isFirstSection = false;
}
prevPathSec = pathSec;
}
QPolygonF subPoly;
subPoly.reserve(prevPolyIdx - pathLineString.size() + 1);
std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.end(), std::back_inserter(subPoly));
prevPathSec.setPath(std::move(subPoly));
pathSections.push_back(std::move(prevPathSec));
const auto hasInstrStartCoordinate = pathArray.at(0).toObject().contains(QLatin1String("instruction_start_coordinate"));
Path path;
path.setSections(std::move(pathSections));
if (hasInstrStartCoordinate) {
path = parsePathWithInstructionStartCoordinate(pathLineString, pathArray);
} else {
path = parsePathFromLength(pathLineString, pathArray);
}
section.setPath(std::move(path));
} else if (!pathLineString.isEmpty()) {
Path path;
......
......@@ -34,7 +34,7 @@ for journey in inJson:
feature = {}
feature['type'] = 'Feature'
feature['properties'] = properties
feature['geometry'] = path['path']
feature['geometry'] = path.get('path', {})
output['features'].append(feature)
print(json.dumps(output))
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment