1 /*
2     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "navitiaparser.h"
8 #include "../geo/geojson_p.h"
9 
10 #include <KPublicTransport/Attribution>
11 #include <KPublicTransport/Journey>
12 #include <KPublicTransport/Line>
13 #include <KPublicTransport/RentalVehicle>
14 #include <KPublicTransport/Stopover>
15 
16 #include <QColor>
17 #include <QDebug>
18 #include <QJsonDocument>
19 #include <QJsonObject>
20 #include <QTimeZone>
21 
22 using namespace KPublicTransport;
23 
24 NavitiaParser::NavitiaParser() = default;
25 NavitiaParser::~NavitiaParser() = default;
26 
parseDateTime(const QJsonValue & v,const QTimeZone & tz)27 static QDateTime parseDateTime(const QJsonValue &v, const QTimeZone &tz)
28 {
29     auto dt = QDateTime::fromString(v.toString(), QStringLiteral("yyyyMMddTHHmmss"));
30     if (tz.isValid()) {
31         dt.setTimeZone(tz);
32     }
33     return dt;
34 }
35 
36 struct {
37     const char *name;
38     Line::Mode mode;
39 } static const navitia_physical_modes[] = {
40     { "Air", Line::Air },
41     { "Boat", Line::Boat },
42     { "Bus", Line::Bus },
43     { "BusRapidTransit", Line::BusRapidTransit },
44     { "Coach", Line::Coach },
45     { "Ferry", Line::Ferry },
46     { "Funicular", Line::Funicular },
47     { "LocalTrain", Line::LocalTrain },
48     { "LongDistanceTrain", Line::LongDistanceTrain },
49     { "Metro", Line::Metro },
50     { "RailShuttle", Line::RailShuttle },
51     { "RapidTransit", Line::RapidTransit },
52     { "Shuttle", Line::Shuttle },
53     { "Taxi", Line::Taxi },
54     { "Train", Line::Train },
55     { "Tramway", Line::Tramway }
56 };
57 
parsePhysicalMode(const QString & mode)58 static Line::Mode parsePhysicalMode(const QString &mode)
59 {
60     const auto modeStr = mode.toLatin1();
61     if (!modeStr.startsWith("physical_mode:")) {
62         return Line::Unknown;
63     }
64     for (auto it = std::begin(navitia_physical_modes); it != std::end(navitia_physical_modes); ++it) {
65         if (strcmp(modeStr.constData() + 14, it->name) == 0) {
66             return it->mode;
67         }
68     }
69 
70     return Line::Unknown;
71 }
72 
parseAdminRegion(Location & loc,const QJsonObject & ar)73 static void parseAdminRegion(Location &loc, const QJsonObject &ar)
74 {
75     const auto level = ar.value(QLatin1String("level")).toInt();
76     if (level == 8) {
77         loc.setLocality(ar.value(QLatin1String("name")).toString());
78     }
79     else if (level == 10) {
80         loc.setPostalCode(ar.value(QLatin1String("zip_code")).toString());
81     }
82 }
83 
parseLocation(const QJsonObject & obj)84 static Location parseLocation(const QJsonObject &obj)
85 {
86     Location loc;
87     loc.setName(obj.value(QLatin1String("label")).toString());
88     // TODO parse more fields
89 
90     const auto coord = obj.value(QLatin1String("coord")).toObject();
91     loc.setCoordinate(coord.value(QLatin1String("lat")).toString().toDouble(), coord.value(QLatin1String("lon")).toString().toDouble());
92 
93     auto tz = obj.value(QLatin1String("timezone")).toString();
94     if (tz.isEmpty()) {
95         tz = obj.value(QLatin1String("stop_area")).toObject().value(QLatin1String("timezone")).toString();
96     }
97     if (!tz.isEmpty()) {
98         loc.setTimeZone(QTimeZone(tz.toUtf8()));
99     }
100 
101     const auto ars = obj.value(QLatin1String("administrative_regions")).toArray();
102     for (const auto &ar : ars) {
103         parseAdminRegion(loc, ar.toObject());
104     }
105 
106     auto codes = obj.value(QLatin1String("codes")).toArray();
107     if (codes.empty()) {
108         codes = obj.value(QLatin1String("stop_area")).toObject().value(QLatin1String("codes")).toArray();
109     }
110     for (const auto &codeV : qAsConst(codes)) {
111         const auto code = codeV.toObject();
112         if (code.value(QLatin1String("type")).toString() == QLatin1String("UIC8")) {
113             loc.setIdentifier(QStringLiteral("uic"), code.value(QLatin1String("value")).toString().left(7));
114         }
115     }
116     const auto id = obj.value(QLatin1String("id")).toString();
117     if (id.startsWith(QLatin1String("poi:osm:node:"))) {
118         loc.setIdentifier(QStringLiteral("osm"), QLatin1Char('n') + QStringView(id).mid(13));
119     }
120 
121     const auto poi_type = obj.value(QLatin1String("poi_type")).toObject().value(QLatin1String("id")).toString();
122     if (poi_type == QLatin1String("poi_type:amenity:bicycle_rental")) {
123         RentalVehicleNetwork network;
124         network.setName(obj.value(QLatin1String("properties")).toObject().value(QLatin1String("network")).toString());
125 
126         RentalVehicleStation station;
127         station.setNetwork(network);
128         const auto standsObj = obj.value(QLatin1String("stands")).toObject();
129         station.setAvailableVehicles(standsObj.value(QLatin1String("available_bikes")).toInt(-1));
130         station.setCapacity(standsObj.value(QLatin1String("total_stands")).toInt(-1));
131 
132         loc.setType(Location::RentedVehicleStation);
133         loc.setData(station);
134     }
135 
136     return loc;
137 }
138 
parseWrappedLocation(const QJsonObject & obj)139 static Location parseWrappedLocation(const QJsonObject &obj)
140 {
141     const auto type = obj.value(QLatin1String("embedded_type")).toString();
142     auto loc = parseLocation(obj.value(type).toObject());
143     loc.setName(obj.value(QLatin1String("name")).toString());
144     if (type == QLatin1String("stop_area") || type == QLatin1String("stop_point")) {
145         loc.setType(Location::Stop);
146     }
147     return loc;
148 }
149 
parseStopDateTime(const QJsonObject & dtObj,Stopover & departure)150 static void parseStopDateTime(const QJsonObject &dtObj, Stopover &departure)
151 {
152     departure.setScheduledDepartureTime(parseDateTime(dtObj.value(QLatin1String("base_departure_date_time")), departure.stopPoint().timeZone()));
153     departure.setScheduledArrivalTime(parseDateTime(dtObj.value(QLatin1String("base_arrival_date_time")), departure.stopPoint().timeZone()));
154     if (dtObj.value(QLatin1String("data_freshness")).toString() != QLatin1String("base_schedule")) {
155         departure.setExpectedDepartureTime(parseDateTime(dtObj.value(QLatin1String("departure_date_time")), departure.stopPoint().timeZone()));
156         departure.setExpectedArrivalTime(parseDateTime(dtObj.value(QLatin1String("arrival_date_time")), departure.stopPoint().timeZone()));
157     }
158 }
159 
parsePathWithInstructionStartCoordinate(const QPolygonF & pathLineString,const QJsonArray & pathArray)160 static Path parsePathWithInstructionStartCoordinate(const QPolygonF &pathLineString, const QJsonArray &pathArray)
161 {
162     std::vector<PathSection> pathSections;
163     pathSections.reserve(pathArray.size());
164     PathSection prevPathSec;
165     int prevPolyIdx = 0;
166     bool isFirstSection = true;
167     for (const auto &pathV : pathArray) {
168         const auto pathObj = pathV.toObject();
169         PathSection pathSec;
170         pathSec.setDescription(pathObj.value(QLatin1String("instruction")).toString());
171         if (pathSec.description().isEmpty()) {
172             pathSec.setDescription(pathObj.value(QLatin1String("name")).toString());
173         }
174 
175         if (!isFirstSection) {
176             const auto coordObj = pathObj.value(QLatin1String("instruction_start_coordinate")).toObject();
177             const QPointF coord(coordObj.value(QLatin1String("lon")).toString().toDouble(), coordObj.value(QLatin1String("lat")).toString().toDouble());
178             const auto it = std::min_element(pathLineString.begin() + prevPolyIdx, pathLineString.end(), [coord](QPointF lhs, QPointF rhs) {
179                 return Location::distance(lhs.y(), lhs.x(), coord.y(), coord.x()) < Location::distance(rhs.y(), rhs.x(), coord.y(), coord.x());
180             });
181             int polyIdx = std::distance(pathLineString.begin(), it);
182 
183             QPolygonF subPoly;
184             subPoly.reserve(polyIdx - prevPolyIdx + 1);
185             std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.begin() + polyIdx + 1, std::back_inserter(subPoly));
186             prevPathSec.setPath(std::move(subPoly));
187             prevPolyIdx = polyIdx;
188             pathSections.push_back(std::move(prevPathSec));
189         } else {
190             isFirstSection = false;
191         }
192         prevPathSec = pathSec;
193     }
194 
195     QPolygonF subPoly;
196     subPoly.reserve(prevPolyIdx - pathLineString.size() + 1);
197     std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.end(), std::back_inserter(subPoly));
198     prevPathSec.setPath(std::move(subPoly));
199     pathSections.push_back(std::move(prevPathSec));
200 
201     Path path;
202     path.setSections(std::move(pathSections));
203     return path;
204 }
205 
parsePathFromLength(const QPolygonF & pathLineString,const QJsonArray & pathArray)206 static Path parsePathFromLength(const QPolygonF &pathLineString, const QJsonArray &pathArray)
207 {
208     std::vector<PathSection> pathSections;
209     pathSections.reserve(pathArray.size());
210     int prevPolyIdx = 0;
211     for (const auto &pathV : pathArray) {
212         const auto pathObj = pathV.toObject();
213         PathSection pathSec;
214         pathSec.setDescription(pathObj.value(QLatin1String("instruction")).toString());
215         if (pathSec.description().isEmpty()) {
216             pathSec.setDescription(pathObj.value(QLatin1String("name")).toString());
217         }
218 
219         int polyIdx = prevPolyIdx + 1;
220         const auto length = pathObj.value(QLatin1String("length")).toInt();
221         for (float l = 0.0f, prevDelta = std::numeric_limits<float>::max(); polyIdx < pathLineString.size(); ++polyIdx) {
222             l += Location::distance(pathLineString.at(polyIdx - 1).y(), pathLineString.at(polyIdx - 1).x(), pathLineString.at(polyIdx).y(), pathLineString.at(polyIdx).x());
223             auto delta = length - l;
224             if (delta <= 0) {
225                 if (prevDelta < -delta) {
226                     --polyIdx;
227                 }
228                 break;
229             }
230             prevDelta = delta;
231         }
232 
233         QPolygonF subPoly;
234         subPoly.reserve(polyIdx - prevPolyIdx + 1);
235         std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.begin() + std::min(polyIdx + 1, pathLineString.size()), std::back_inserter(subPoly));
236         pathSec.setPath(std::move(subPoly));
237         prevPolyIdx = polyIdx;
238         pathSections.push_back(std::move(pathSec));
239     }
240 
241     // if there's a trailing polygon end, attached that to the last section
242     if (prevPolyIdx + 1 < pathLineString.size()) {
243         auto poly = pathSections.back().path();
244         std::copy(pathLineString.begin() + prevPolyIdx + 1, pathLineString.end(), std::back_inserter(poly));
245         pathSections.back().setPath(std::move(poly));
246     }
247 
248     Path path;
249     path.setSections(std::move(pathSections));
250     return path;
251 }
252 
parseJourneySection(const QJsonObject & obj) const253 JourneySection NavitiaParser::parseJourneySection(const QJsonObject &obj) const
254 {
255     JourneySection section;
256     section.setFrom(parseWrappedLocation(obj.value(QLatin1String("from")).toObject()));
257     section.setTo(parseWrappedLocation(obj.value(QLatin1String("to")).toObject()));
258 
259     const auto displayInfo = obj.value(QLatin1String("display_informations")).toObject();
260     Line line;
261     line.setName(displayInfo.value(QLatin1String("label")).toString());
262     line.setColor(QColor(QLatin1Char('#') + displayInfo.value(QLatin1String("color")).toString()));
263     line.setTextColor(QColor(QLatin1Char('#') + displayInfo.value(QLatin1String("text_color")).toString()));
264     line.setModeString(displayInfo.value(QLatin1String("commercial_mode")).toString());
265     const auto links = obj.value(QLatin1String("links")).toArray();
266     for (const auto &v : links) {
267         const auto link = v.toObject();
268         const auto type = link.value(QLatin1String("type")).toString();
269         if (type == QLatin1String("physical_mode")) {
270             line.setMode(parsePhysicalMode(link.value(QLatin1String("id")).toString()));
271         }
272         parseDisruptionLink(section, link);
273     }
274     const auto displayLinks = displayInfo.value(QLatin1String("links")).toArray();
275     for (const auto &v : displayLinks) {
276         const auto link = v.toObject();
277         parseDisruptionLink(section, link);
278     }
279 
280     Route route;
281     route.setDirection(displayInfo.value(QLatin1String("direction")).toString());
282     route.setLine(line);
283     section.setRoute(route);
284 
285     const auto hasRealTime = obj.value(QLatin1String("data_freshness")).toString() != QLatin1String("base_schedule");
286     section.setScheduledDepartureTime(parseDateTime(obj.value(QLatin1String("base_departure_date_time")), section.from().timeZone()));
287     section.setScheduledArrivalTime(parseDateTime(obj.value(QLatin1String("base_arrival_date_time")), section.to().timeZone()));
288     if (hasRealTime) {
289         section.setScheduledArrivalTime(parseDateTime(obj.value(QLatin1String("arrival_date_time")), section.to().timeZone()));
290         section.setScheduledDepartureTime(parseDateTime(obj.value(QLatin1String("departure_date_time")), section.from().timeZone()));
291     }
292 
293     const auto typeStr = obj.value(QLatin1String("type")).toString();
294     if (typeStr == QLatin1String("public_transport")) {
295         section.setMode(JourneySection::PublicTransport);
296     // TODO we have no type for parking/rent/return yet
297     } else if (typeStr == QLatin1String("transfer") || typeStr == QLatin1String("park") ||
298         typeStr == QLatin1String("bss_rent") || typeStr == QLatin1String("bss_put_back")) {
299         section.setMode(JourneySection::Transfer);
300     } else if (typeStr == QLatin1String("street_network") || typeStr == QLatin1String("walking") || typeStr == QLatin1String("crow_fly")) {
301         const auto modeStr = obj.value(QLatin1String("mode")).toString();
302         if (modeStr == QLatin1String("bike")) {
303             if (section.from().type() == Location::RentedVehicleStation) {
304                 section.setMode(JourneySection::RentedVehicle);
305                 RentalVehicle v;
306                 v.setType(RentalVehicle::Bicycle);
307                 v.setNetwork(section.from().rentalVehicleStation().network());
308                 section.setRentalVehicle(v);
309             } else {
310                 section.setMode(JourneySection::IndividualTransport);
311                 section.setIndividualTransport({IndividualTransport::Bike, IndividualTransport::None});
312             }
313         } else if (modeStr == QLatin1String("car")) {
314             section.setMode(JourneySection::IndividualTransport);
315             section.setIndividualTransport({IndividualTransport::Car, IndividualTransport::None});
316         } else {
317             section.setMode(JourneySection::Walking);
318         }
319         section.setDistance(obj.value(QLatin1String("geojson")).toObject().value(QLatin1String("properties")).toArray().at(0).toObject().value(QLatin1String("length")).toInt());
320     } else if (typeStr == QLatin1String("waiting")) {
321         section.setMode(JourneySection::Waiting);
322     }
323 
324     const auto stopsDtA = obj.value(QLatin1String("stop_date_times")).toArray();
325     if (stopsDtA.size() > 2) { // departure/arrival are included, we don't want that
326         std::vector<Stopover> stops;
327         stops.reserve(stopsDtA.size() - 2);
328         for (auto it = std::next(stopsDtA.begin()); it != std::prev(stopsDtA.end()); ++it) {
329             const auto obj = (*it).toObject();
330             Stopover stop;
331             stop.setStopPoint(parseLocation(obj.value(QLatin1String("stop_point")).toObject()));
332             parseStopDateTime(obj, stop);
333             if (!hasRealTime) { // intermediate stops seems to miss the "data_freshness" field, so propagate that
334                 stop.setExpectedArrivalTime({});
335                 stop.setExpectedDepartureTime({});
336             }
337             stops.push_back(std::move(stop));
338         }
339         section.setIntermediateStops(std::move(stops));
340     }
341 
342     const auto emissionObj = obj.value(QLatin1String("co2_emission")).toObject();
343     section.setCo2Emission(emissionObj.value(QLatin1String("value")).toDouble(-1));
344 
345     const auto pathLineString = GeoJson::readLineString(obj.value(QLatin1String("geojson")).toObject());
346     const auto pathArray = obj.value(QLatin1String("path")).toArray();
347     if (!pathArray.empty()) {
348         const auto hasInstrStartCoordinate = pathArray.at(0).toObject().contains(QLatin1String("instruction_start_coordinate"));
349         Path path;
350         if (hasInstrStartCoordinate) {
351             path = parsePathWithInstructionStartCoordinate(pathLineString, pathArray);
352         } else {
353             path = parsePathFromLength(pathLineString, pathArray);
354         }
355         section.setPath(std::move(path));
356     } else if (!pathLineString.isEmpty()) {
357         Path path;
358         PathSection pathSection;
359         pathSection.setPath(pathLineString);
360         path.setSections({pathSection});
361         section.setPath(std::move(path));
362     }
363 
364     return section;
365 }
366 
parseJourney(const QJsonObject & obj) const367 Journey NavitiaParser::parseJourney(const QJsonObject &obj) const
368 {
369     Journey journey;
370 
371     const auto secArray = obj.value(QLatin1String("sections")).toArray();
372     std::vector<JourneySection> sections;
373     sections.reserve(secArray.size());
374     for (const auto &v : secArray) {
375         sections.push_back(parseJourneySection(v.toObject()));
376     }
377     journey.setSections(std::move(sections));
378     return journey;
379 }
380 
parseJourneys(const QByteArray & data)381 std::vector<Journey> NavitiaParser::parseJourneys(const QByteArray &data)
382 {
383     const auto topObj = QJsonDocument::fromJson(data).object();
384     m_disruptions = topObj.value(QLatin1String("disruptions")).toArray();
385     const auto journeys = topObj.value(QLatin1String("journeys")).toArray();
386 
387     std::vector<Journey> res;
388     res.reserve(journeys.size());
389 
390     for (const auto &v : journeys) {
391         res.push_back(parseJourney(v.toObject()));
392     }
393 
394     parseLinks(topObj.value(QLatin1String("links")).toArray());
395     parseAttributions(topObj.value(QLatin1String("feed_publishers")).toArray());
396     return res;
397 }
398 
parseDeparture(const QJsonObject & obj) const399 Stopover NavitiaParser::parseDeparture(const QJsonObject &obj) const
400 {
401     // TODO remove code duplication with journey parsing
402     Stopover departure;
403     const auto displayInfo = obj.value(QLatin1String("display_informations")).toObject();
404 
405     Line line;
406     line.setName(displayInfo.value(QLatin1String("label")).toString());
407     line.setColor(QColor(QLatin1Char('#') + displayInfo.value(QLatin1String("color")).toString()));
408     line.setTextColor(QColor(QLatin1Char('#') + displayInfo.value(QLatin1String("text_color")).toString()));
409     line.setModeString(displayInfo.value(QLatin1String("commercial_mode")).toString());
410     const auto links = obj.value(QLatin1String("links")).toArray();
411     for (const auto &v : links) {
412         const auto link = v.toObject();
413         if (link.value(QLatin1String("type")).toString() == QLatin1String("physical_mode")) {
414             line.setMode(parsePhysicalMode(link.value(QLatin1String("id")).toString()));
415         }
416         parseDisruptionLink(departure, link);
417     }
418     const auto displayLinks = displayInfo.value(QLatin1String("links")).toArray();
419     for (const auto &v : displayLinks) {
420         const auto link = v.toObject();
421         parseDisruptionLink(departure, link);
422     }
423 
424     Route route;
425     route.setDirection(displayInfo.value(QLatin1String("direction")).toString());
426     const auto routeObj = obj.value(QLatin1String("route")).toObject();
427     const auto destObj = routeObj.value(QLatin1String("direction")).toObject();
428     route.setDestination(parseWrappedLocation(destObj));
429     route.setLine(line);
430 
431     departure.setRoute(route);
432     departure.setStopPoint(parseLocation(obj.value(QLatin1String("stop_point")).toObject()));
433     parseStopDateTime(obj.value(QLatin1String("stop_date_time")).toObject(), departure);
434 
435     return departure;
436 }
437 
parseDepartures(const QByteArray & data)438 std::vector<Stopover> NavitiaParser::parseDepartures(const QByteArray &data)
439 {
440     const auto topObj = QJsonDocument::fromJson(data).object();
441     m_disruptions = topObj.value(QLatin1String("disruptions")).toArray();
442     const auto departures = topObj.value(QLatin1String("departures")).toArray();
443 
444     std::vector<Stopover> res;
445     res.reserve(departures.size());
446 
447     for (const auto &v : departures) {
448         res.push_back(parseDeparture(v.toObject()));
449     }
450 
451     parseLinks(topObj.value(QLatin1String("links")).toArray());
452     parseAttributions(topObj.value(QLatin1String("feed_publishers")).toArray());
453     return res;
454 }
455 
parsePlacesNearby(const QByteArray & data)456 std::vector<Location> NavitiaParser::parsePlacesNearby(const QByteArray &data)
457 {
458     const auto topObj = QJsonDocument::fromJson(data).object();
459     const auto placesNearby = topObj.value(QLatin1String("places_nearby")).toArray();
460 
461     std::vector<Location> res;
462     res.reserve(placesNearby.size());
463 
464     for (const auto &v : placesNearby) {
465         res.push_back(parseWrappedLocation(v.toObject()));
466     }
467 
468     parseAttributions(topObj.value(QLatin1String("feed_publishers")).toArray());
469     return res;
470 }
471 
parsePlaces(const QByteArray & data)472 std::vector<Location> NavitiaParser::parsePlaces(const QByteArray &data)
473 {
474     const auto topObj = QJsonDocument::fromJson(data).object();
475     const auto placesNearby = topObj.value(QLatin1String("places")).toArray();
476 
477     std::vector<Location> res;
478     res.reserve(placesNearby.size());
479 
480     for (const auto &v : placesNearby) {
481         res.push_back(parseWrappedLocation(v.toObject()));
482     }
483 
484     parseAttributions(topObj.value(QLatin1String("feed_publishers")).toArray());
485     return res;
486 }
487 
parseErrorMessage(const QByteArray & data)488 QString NavitiaParser::parseErrorMessage(const QByteArray &data)
489 {
490     const auto topObj = QJsonDocument::fromJson(data).object();
491     const auto errorObj = topObj.value(QLatin1String("error")).toObject();
492 
493     // id field contains error enum, might also be useful
494     return errorObj.value(QLatin1String("message")).toString();
495 }
496 
parseLinks(const QJsonArray & links)497 void NavitiaParser::parseLinks(const QJsonArray &links)
498 {
499     for (const auto &v : links) {
500         const auto link = v.toObject();
501         const auto rel = link.value(QLatin1String("rel")).toString();
502         if (rel == QLatin1String("next")) {
503             nextLink = QUrl(link.value(QLatin1String("href")).toString());
504         } else if (rel == QLatin1String("prev")) {
505             prevLink = QUrl(link.value(QLatin1String("href")).toString());
506         }
507     }
508 }
509 
parseAttributions(const QJsonArray & feeds)510 void NavitiaParser::parseAttributions(const QJsonArray& feeds)
511 {
512     for (const auto &v : feeds) {
513         const auto feed = v.toObject();
514         Attribution attr;
515         attr.setName(feed.value(QLatin1String("name")).toString());
516         QUrl url(feed.value(QLatin1String("url")).toString());
517         if (!url.isEmpty()) {
518             url.setScheme(QStringLiteral("https")); // missing in a few cases
519         }
520         attr.setUrl(url);
521         attr.setLicense(feed.value(QLatin1String("license")).toString());
522         // TODO map known licenses to spdx links
523         attributions.push_back(std::move(attr));
524     }
525 }
526 
findDisruption(const QString & id) const527 QJsonObject NavitiaParser::findDisruption(const QString &id) const
528 {
529     for (const auto &v : m_disruptions) {
530         const auto disruption = v.toObject();
531         if (disruption.value(QLatin1String("uri")).toString() == id) {
532             return disruption;
533         }
534     }
535     return {};
536 }
537 
disruptionMessage(const QJsonObject & distruption)538 static QString disruptionMessage(const QJsonObject &distruption)
539 {
540     const auto msgs = distruption.value(QLatin1String("messages")).toArray();
541     for (const auto &msgV : msgs) {
542         const auto msg = msgV.toObject();
543         const auto types = msg.value(QLatin1String("channel")).toObject().value(QLatin1String("types")).toArray();
544         for (const auto &typeV : types) {
545             if (typeV.toString() == QLatin1String("web")) {
546                 return msg.value(QLatin1String("text")).toString();
547             }
548         }
549     }
550     return {};
551 }
552 
553 template <typename T>
parseDisruptionLink(T & element,const QJsonObject & link) const554 void NavitiaParser::parseDisruptionLink(T &element, const QJsonObject &link) const
555 {
556     const auto type = link.value(QLatin1String("type")).toString();
557     if (type != QLatin1String("disruption")) {
558         return;
559     }
560 
561     const auto id = link.value(QLatin1String("id")).toString();
562     const auto disruption = findDisruption(id);
563     if (disruption.value(QLatin1String("severity")).toObject().value(QLatin1String("effect")).toString() == QLatin1String("NO_SERVICE")) {
564         element.setDisruptionEffect(Disruption::NoService);
565     }
566     element.addNote(disruptionMessage(disruption));
567 }
568