File indexing completed on 2024-05-12 04:42:38

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "navitiaparser.h"
0008 #include "navitiaphysicalmode.h"
0009 #include "../geo/geojson_p.h"
0010 
0011 #include <KPublicTransport/Attribution>
0012 #include <KPublicTransport/Journey>
0013 #include <KPublicTransport/Line>
0014 #include <KPublicTransport/RentalVehicle>
0015 #include <KPublicTransport/Stopover>
0016 
0017 #include <QColor>
0018 #include <QDebug>
0019 #include <QJsonDocument>
0020 #include <QJsonObject>
0021 #include <QTimeZone>
0022 
0023 using namespace KPublicTransport;
0024 
0025 NavitiaParser::NavitiaParser() = default;
0026 NavitiaParser::~NavitiaParser() = default;
0027 
0028 static QDateTime parseDateTime(const QJsonValue &v, const QTimeZone &tz)
0029 {
0030     auto dt = QDateTime::fromString(v.toString(), QStringLiteral("yyyyMMddTHHmmss"));
0031     if (tz.isValid()) {
0032         dt.setTimeZone(tz);
0033     }
0034     return dt;
0035 }
0036 
0037 static void parseAdminRegion(Location &loc, const QJsonObject &ar)
0038 {
0039     const auto level = ar.value(QLatin1String("level")).toInt();
0040     if (level == 8) {
0041         loc.setLocality(ar.value(QLatin1String("name")).toString());
0042     }
0043     else if (level == 10) {
0044         loc.setPostalCode(ar.value(QLatin1String("zip_code")).toString());
0045     }
0046 }
0047 
0048 static Location parseLocation(const QJsonObject &obj)
0049 {
0050     Location loc;
0051     loc.setName(obj.value(QLatin1String("label")).toString());
0052     // TODO parse more fields
0053 
0054     const auto coord = obj.value(QLatin1String("coord")).toObject();
0055     loc.setCoordinate(coord.value(QLatin1String("lat")).toString().toDouble(), coord.value(QLatin1String("lon")).toString().toDouble());
0056 
0057     auto tz = obj.value(QLatin1String("timezone")).toString();
0058     if (tz.isEmpty()) {
0059         tz = obj.value(QLatin1String("stop_area")).toObject().value(QLatin1String("timezone")).toString();
0060     }
0061     if (!tz.isEmpty()) {
0062         loc.setTimeZone(QTimeZone(tz.toUtf8()));
0063     }
0064 
0065     const auto ars = obj.value(QLatin1String("administrative_regions")).toArray();
0066     for (const auto &ar : ars) {
0067         parseAdminRegion(loc, ar.toObject());
0068     }
0069 
0070     auto codes = obj.value(QLatin1String("codes")).toArray();
0071     if (codes.empty()) {
0072         codes = obj.value(QLatin1String("stop_area")).toObject().value(QLatin1String("codes")).toArray();
0073     }
0074     for (const auto &codeV : std::as_const(codes)) {
0075         const auto code = codeV.toObject();
0076         if (code.value(QLatin1String("type")).toString() == QLatin1String("UIC8")) {
0077             loc.setIdentifier(QStringLiteral("uic"), code.value(QLatin1String("value")).toString().left(7));
0078         }
0079     }
0080     const auto id = obj.value(QLatin1String("id")).toString();
0081     if (id.startsWith(QLatin1String("poi:osm:node:"))) {
0082         loc.setIdentifier(QStringLiteral("osm"), QLatin1Char('n') + QStringView(id).mid(13));
0083     }
0084 
0085     const auto poi_type = obj.value(QLatin1String("poi_type")).toObject().value(QLatin1String("id")).toString();
0086     if (poi_type == QLatin1String("poi_type:amenity:bicycle_rental")) {
0087         RentalVehicleNetwork network;
0088         network.setName(obj.value(QLatin1String("properties")).toObject().value(QLatin1String("network")).toString());
0089 
0090         RentalVehicleStation station;
0091         station.setNetwork(network);
0092         const auto standsObj = obj.value(QLatin1String("stands")).toObject();
0093         station.setAvailableVehicles(standsObj.value(QLatin1String("available_bikes")).toInt(-1));
0094         station.setCapacity(standsObj.value(QLatin1String("total_stands")).toInt(-1));
0095 
0096         loc.setType(Location::RentedVehicleStation);
0097         loc.setData(station);
0098     }
0099 
0100     return loc;
0101 }
0102 
0103 static Location parseWrappedLocation(const QJsonObject &obj)
0104 {
0105     const auto type = obj.value(QLatin1String("embedded_type")).toString();
0106     auto loc = parseLocation(obj.value(type).toObject());
0107     loc.setName(obj.value(QLatin1String("name")).toString());
0108     if (type == QLatin1String("stop_area") || type == QLatin1String("stop_point")) {
0109         loc.setType(Location::Stop);
0110     }
0111     return loc;
0112 }
0113 
0114 static void parseStopDateTime(const QJsonObject &dtObj, Stopover &departure)
0115 {
0116     departure.setScheduledDepartureTime(parseDateTime(dtObj.value(QLatin1String("base_departure_date_time")), departure.stopPoint().timeZone()));
0117     departure.setScheduledArrivalTime(parseDateTime(dtObj.value(QLatin1String("base_arrival_date_time")), departure.stopPoint().timeZone()));
0118     if (dtObj.value(QLatin1String("data_freshness")).toString() != QLatin1String("base_schedule")) {
0119         departure.setExpectedDepartureTime(parseDateTime(dtObj.value(QLatin1String("departure_date_time")), departure.stopPoint().timeZone()));
0120         departure.setExpectedArrivalTime(parseDateTime(dtObj.value(QLatin1String("arrival_date_time")), departure.stopPoint().timeZone()));
0121     }
0122 }
0123 
0124 static Path parsePathWithInstructionStartCoordinate(const QPolygonF &pathLineString, const QJsonArray &pathArray)
0125 {
0126     std::vector<PathSection> pathSections;
0127     pathSections.reserve(pathArray.size());
0128     PathSection prevPathSec;
0129     int prevPolyIdx = 0;
0130     bool isFirstSection = true;
0131     for (const auto &pathV : pathArray) {
0132         const auto pathObj = pathV.toObject();
0133         PathSection pathSec;
0134         pathSec.setDescription(pathObj.value(QLatin1String("instruction")).toString());
0135         if (pathSec.description().isEmpty()) {
0136             pathSec.setDescription(pathObj.value(QLatin1String("name")).toString());
0137         }
0138 
0139         if (!isFirstSection) {
0140             const auto coordObj = pathObj.value(QLatin1String("instruction_start_coordinate")).toObject();
0141             const QPointF coord(coordObj.value(QLatin1String("lon")).toString().toDouble(), coordObj.value(QLatin1String("lat")).toString().toDouble());
0142             const auto it = std::min_element(pathLineString.begin() + prevPolyIdx, pathLineString.end(), [coord](QPointF lhs, QPointF rhs) {
0143                 return Location::distance(lhs.y(), lhs.x(), coord.y(), coord.x()) < Location::distance(rhs.y(), rhs.x(), coord.y(), coord.x());
0144             });
0145             int polyIdx = std::distance(pathLineString.begin(), it);
0146 
0147             QPolygonF subPoly;
0148             subPoly.reserve(polyIdx - prevPolyIdx + 1);
0149             std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.begin() + polyIdx + 1, std::back_inserter(subPoly));
0150             prevPathSec.setPath(std::move(subPoly));
0151             prevPolyIdx = polyIdx;
0152             pathSections.push_back(std::move(prevPathSec));
0153         } else {
0154             isFirstSection = false;
0155         }
0156         prevPathSec = pathSec;
0157     }
0158 
0159     QPolygonF subPoly;
0160     subPoly.reserve(prevPolyIdx - pathLineString.size() + 1);
0161     std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.end(), std::back_inserter(subPoly));
0162     prevPathSec.setPath(std::move(subPoly));
0163     pathSections.push_back(std::move(prevPathSec));
0164 
0165     Path path;
0166     path.setSections(std::move(pathSections));
0167     return path;
0168 }
0169 
0170 static Path parsePathFromLength(const QPolygonF &pathLineString, const QJsonArray &pathArray)
0171 {
0172     std::vector<PathSection> pathSections;
0173     pathSections.reserve(pathArray.size());
0174     int prevPolyIdx = 0;
0175     for (const auto &pathV : pathArray) {
0176         const auto pathObj = pathV.toObject();
0177         PathSection pathSec;
0178         pathSec.setDescription(pathObj.value(QLatin1String("instruction")).toString());
0179         if (pathSec.description().isEmpty()) {
0180             pathSec.setDescription(pathObj.value(QLatin1String("name")).toString());
0181         }
0182 
0183         int polyIdx = prevPolyIdx + 1;
0184         const auto length = pathObj.value(QLatin1String("length")).toInt();
0185         for (float l = 0.0f, prevDelta = std::numeric_limits<float>::max(); polyIdx < pathLineString.size(); ++polyIdx) {
0186             l += Location::distance(pathLineString.at(polyIdx - 1).y(), pathLineString.at(polyIdx - 1).x(), pathLineString.at(polyIdx).y(), pathLineString.at(polyIdx).x());
0187             auto delta = length - l;
0188             if (delta <= 0) {
0189                 if (prevDelta < -delta) {
0190                     --polyIdx;
0191                 }
0192                 break;
0193             }
0194             prevDelta = delta;
0195         }
0196 
0197         QPolygonF subPoly;
0198         subPoly.reserve(polyIdx - prevPolyIdx + 1);
0199         std::copy(pathLineString.begin() + prevPolyIdx, pathLineString.begin() + std::min<int>(polyIdx + 1, pathLineString.size()), std::back_inserter(subPoly));
0200         pathSec.setPath(std::move(subPoly));
0201         prevPolyIdx = polyIdx;
0202         pathSections.push_back(std::move(pathSec));
0203     }
0204 
0205     // if there's a trailing polygon end, attached that to the last section
0206     if (prevPolyIdx + 1 < pathLineString.size()) {
0207         auto poly = pathSections.back().path();
0208         std::copy(pathLineString.begin() + prevPolyIdx + 1, pathLineString.end(), std::back_inserter(poly));
0209         pathSections.back().setPath(std::move(poly));
0210     }
0211 
0212     Path path;
0213     path.setSections(std::move(pathSections));
0214     return path;
0215 }
0216 
0217 JourneySection NavitiaParser::parseJourneySection(const QJsonObject &obj) const
0218 {
0219     JourneySection section;
0220     section.setFrom(parseWrappedLocation(obj.value(QLatin1String("from")).toObject()));
0221     section.setTo(parseWrappedLocation(obj.value(QLatin1String("to")).toObject()));
0222 
0223     const auto displayInfo = obj.value(QLatin1String("display_informations")).toObject();
0224     Line line;
0225     line.setName(displayInfo.value(QLatin1String("label")).toString());
0226     line.setColor(QColor(QLatin1Char('#') + displayInfo.value(QLatin1String("color")).toString()));
0227     line.setTextColor(QColor(QLatin1Char('#') + displayInfo.value(QLatin1String("text_color")).toString()));
0228     line.setModeString(displayInfo.value(QLatin1String("commercial_mode")).toString());
0229     const auto links = obj.value(QLatin1String("links")).toArray();
0230     for (const auto &v : links) {
0231         const auto link = v.toObject();
0232         const auto type = link.value(QLatin1String("type")).toString();
0233         if (type == QLatin1String("physical_mode")) {
0234             line.setMode(NavitiaPhysicalMode::parsePhysicalMode(link.value(QLatin1String("id")).toString()));
0235         }
0236         parseDisruptionLink(section, link);
0237     }
0238     const auto displayLinks = displayInfo.value(QLatin1String("links")).toArray();
0239     for (const auto &v : displayLinks) {
0240         const auto link = v.toObject();
0241         parseDisruptionLink(section, link);
0242     }
0243 
0244     Route route;
0245     route.setDirection(displayInfo.value(QLatin1String("direction")).toString());
0246     route.setLine(line);
0247     section.setRoute(route);
0248 
0249     const auto hasRealTime = obj.value(QLatin1String("data_freshness")).toString() != QLatin1String("base_schedule");
0250     section.setScheduledDepartureTime(parseDateTime(obj.value(QLatin1String("base_departure_date_time")), section.from().timeZone()));
0251     section.setScheduledArrivalTime(parseDateTime(obj.value(QLatin1String("base_arrival_date_time")), section.to().timeZone()));
0252     if (hasRealTime) {
0253         section.setScheduledArrivalTime(parseDateTime(obj.value(QLatin1String("arrival_date_time")), section.to().timeZone()));
0254         section.setScheduledDepartureTime(parseDateTime(obj.value(QLatin1String("departure_date_time")), section.from().timeZone()));
0255     }
0256 
0257     const auto typeStr = obj.value(QLatin1String("type")).toString();
0258     if (typeStr == QLatin1String("public_transport")) {
0259         section.setMode(JourneySection::PublicTransport);
0260     // TODO we have no type for parking/rent/return yet
0261     } else if (typeStr == QLatin1String("transfer") || typeStr == QLatin1String("park") ||
0262         typeStr == QLatin1String("bss_rent") || typeStr == QLatin1String("bss_put_back")) {
0263         section.setMode(JourneySection::Transfer);
0264     } else if (typeStr == QLatin1String("street_network") || typeStr == QLatin1String("walking") || typeStr == QLatin1String("crow_fly")) {
0265         const auto modeStr = obj.value(QLatin1String("mode")).toString();
0266         if (modeStr == QLatin1String("bike")) {
0267             if (section.from().type() == Location::RentedVehicleStation) {
0268                 section.setMode(JourneySection::RentedVehicle);
0269                 RentalVehicle v;
0270                 v.setType(RentalVehicle::Bicycle);
0271                 v.setNetwork(section.from().rentalVehicleStation().network());
0272                 section.setRentalVehicle(v);
0273             } else {
0274                 section.setMode(JourneySection::IndividualTransport);
0275                 section.setIndividualTransport({IndividualTransport::Bike, IndividualTransport::None});
0276             }
0277         } else if (modeStr == QLatin1String("car")) {
0278             section.setMode(JourneySection::IndividualTransport);
0279             section.setIndividualTransport({IndividualTransport::Car, IndividualTransport::None});
0280         } else {
0281             section.setMode(JourneySection::Walking);
0282         }
0283         section.setDistance(obj.value(QLatin1String("geojson")).toObject().value(QLatin1String("properties")).toArray().at(0).toObject().value(QLatin1String("length")).toInt());
0284     } else if (typeStr == QLatin1String("waiting")) {
0285         section.setMode(JourneySection::Waiting);
0286     }
0287 
0288     const auto stopsDtA = obj.value(QLatin1String("stop_date_times")).toArray();
0289     if (stopsDtA.size() > 2) { // departure/arrival are included, we don't want that
0290         std::vector<Stopover> stops;
0291         stops.reserve(stopsDtA.size() - 2);
0292         for (auto it = std::next(stopsDtA.begin()); it != std::prev(stopsDtA.end()); ++it) {
0293             const auto obj = (*it).toObject();
0294             Stopover stop;
0295             stop.setStopPoint(parseLocation(obj.value(QLatin1String("stop_point")).toObject()));
0296             parseStopDateTime(obj, stop);
0297             if (!hasRealTime) { // intermediate stops seems to miss the "data_freshness" field, so propagate that
0298                 stop.setExpectedArrivalTime({});
0299                 stop.setExpectedDepartureTime({});
0300             }
0301             stops.push_back(std::move(stop));
0302         }
0303         section.setIntermediateStops(std::move(stops));
0304     }
0305 
0306     const auto emissionObj = obj.value(QLatin1String("co2_emission")).toObject();
0307     section.setCo2Emission(emissionObj.value(QLatin1String("value")).toDouble(-1));
0308 
0309     const auto pathLineString = GeoJson::readLineString(obj.value(QLatin1String("geojson")).toObject());
0310     const auto pathArray = obj.value(QLatin1String("path")).toArray();
0311     if (!pathArray.empty()) {
0312         const auto hasInstrStartCoordinate = pathArray.at(0).toObject().contains(QLatin1String("instruction_start_coordinate"));
0313         Path path;
0314         if (hasInstrStartCoordinate) {
0315             path = parsePathWithInstructionStartCoordinate(pathLineString, pathArray);
0316         } else {
0317             path = parsePathFromLength(pathLineString, pathArray);
0318         }
0319         section.setPath(std::move(path));
0320     } else if (!pathLineString.isEmpty()) {
0321         Path path;
0322         PathSection pathSection;
0323         pathSection.setPath(pathLineString);
0324         path.setSections({pathSection});
0325         section.setPath(std::move(path));
0326     }
0327 
0328     return section;
0329 }
0330 
0331 Journey NavitiaParser::parseJourney(const QJsonObject &obj) const
0332 {
0333     Journey journey;
0334 
0335     const auto secArray = obj.value(QLatin1String("sections")).toArray();
0336     std::vector<JourneySection> sections;
0337     sections.reserve(secArray.size());
0338     for (const auto &v : secArray) {
0339         sections.push_back(parseJourneySection(v.toObject()));
0340     }
0341     journey.setSections(std::move(sections));
0342     return journey;
0343 }
0344 
0345 std::vector<Journey> NavitiaParser::parseJourneys(const QByteArray &data)
0346 {
0347     const auto topObj = QJsonDocument::fromJson(data).object();
0348     m_disruptions = topObj.value(QLatin1String("disruptions")).toArray();
0349     const auto journeys = topObj.value(QLatin1String("journeys")).toArray();
0350 
0351     std::vector<Journey> res;
0352     res.reserve(journeys.size());
0353 
0354     for (const auto &v : journeys) {
0355         res.push_back(parseJourney(v.toObject()));
0356     }
0357 
0358     parseLinks(topObj.value(QLatin1String("links")).toArray());
0359     parseAttributions(topObj.value(QLatin1String("feed_publishers")).toArray());
0360     return res;
0361 }
0362 
0363 Stopover NavitiaParser::parseDeparture(const QJsonObject &obj) const
0364 {
0365     // TODO remove code duplication with journey parsing
0366     Stopover departure;
0367     const auto displayInfo = obj.value(QLatin1String("display_informations")).toObject();
0368 
0369     Line line;
0370     line.setName(displayInfo.value(QLatin1String("label")).toString());
0371     line.setColor(QColor(QLatin1Char('#') + displayInfo.value(QLatin1String("color")).toString()));
0372     line.setTextColor(QColor(QLatin1Char('#') + displayInfo.value(QLatin1String("text_color")).toString()));
0373     line.setModeString(displayInfo.value(QLatin1String("commercial_mode")).toString());
0374     const auto links = obj.value(QLatin1String("links")).toArray();
0375     for (const auto &v : links) {
0376         const auto link = v.toObject();
0377         if (link.value(QLatin1String("type")).toString() == QLatin1String("physical_mode")) {
0378             line.setMode(NavitiaPhysicalMode::parsePhysicalMode(link.value(QLatin1String("id")).toString()));
0379         }
0380         parseDisruptionLink(departure, link);
0381     }
0382     const auto displayLinks = displayInfo.value(QLatin1String("links")).toArray();
0383     for (const auto &v : displayLinks) {
0384         const auto link = v.toObject();
0385         parseDisruptionLink(departure, link);
0386     }
0387 
0388     Route route;
0389     route.setDirection(displayInfo.value(QLatin1String("direction")).toString());
0390     const auto routeObj = obj.value(QLatin1String("route")).toObject();
0391     const auto destObj = routeObj.value(QLatin1String("direction")).toObject();
0392     route.setDestination(parseWrappedLocation(destObj));
0393     route.setLine(line);
0394 
0395     departure.setRoute(route);
0396     departure.setStopPoint(parseLocation(obj.value(QLatin1String("stop_point")).toObject()));
0397     parseStopDateTime(obj.value(QLatin1String("stop_date_time")).toObject(), departure);
0398 
0399     return departure;
0400 }
0401 
0402 std::vector<Stopover> NavitiaParser::parseDepartures(const QByteArray &data)
0403 {
0404     const auto topObj = QJsonDocument::fromJson(data).object();
0405     m_disruptions = topObj.value(QLatin1String("disruptions")).toArray();
0406     const auto departures = topObj.value(QLatin1String("departures")).toArray();
0407 
0408     std::vector<Stopover> res;
0409     res.reserve(departures.size());
0410 
0411     for (const auto &v : departures) {
0412         res.push_back(parseDeparture(v.toObject()));
0413     }
0414 
0415     parseLinks(topObj.value(QLatin1String("links")).toArray());
0416     parseAttributions(topObj.value(QLatin1String("feed_publishers")).toArray());
0417     return res;
0418 }
0419 
0420 std::vector<Location> NavitiaParser::parsePlacesNearby(const QByteArray &data)
0421 {
0422     const auto topObj = QJsonDocument::fromJson(data).object();
0423     const auto placesNearby = topObj.value(QLatin1String("places_nearby")).toArray();
0424 
0425     std::vector<Location> res;
0426     res.reserve(placesNearby.size());
0427 
0428     for (const auto &v : placesNearby) {
0429         res.push_back(parseWrappedLocation(v.toObject()));
0430     }
0431 
0432     parseAttributions(topObj.value(QLatin1String("feed_publishers")).toArray());
0433     return res;
0434 }
0435 
0436 std::vector<Location> NavitiaParser::parsePlaces(const QByteArray &data)
0437 {
0438     const auto topObj = QJsonDocument::fromJson(data).object();
0439     const auto placesNearby = topObj.value(QLatin1String("places")).toArray();
0440 
0441     std::vector<Location> res;
0442     res.reserve(placesNearby.size());
0443 
0444     for (const auto &v : placesNearby) {
0445         res.push_back(parseWrappedLocation(v.toObject()));
0446     }
0447 
0448     parseAttributions(topObj.value(QLatin1String("feed_publishers")).toArray());
0449     return res;
0450 }
0451 
0452 QString NavitiaParser::parseErrorMessage(const QByteArray &data)
0453 {
0454     const auto topObj = QJsonDocument::fromJson(data).object();
0455     const auto errorObj = topObj.value(QLatin1String("error")).toObject();
0456 
0457     // id field contains error enum, might also be useful
0458     return errorObj.value(QLatin1String("message")).toString();
0459 }
0460 
0461 void NavitiaParser::parseLinks(const QJsonArray &links)
0462 {
0463     for (const auto &v : links) {
0464         const auto link = v.toObject();
0465         const auto rel = link.value(QLatin1String("rel")).toString();
0466         if (rel == QLatin1String("next")) {
0467             nextLink = QUrl(link.value(QLatin1String("href")).toString());
0468         } else if (rel == QLatin1String("prev")) {
0469             prevLink = QUrl(link.value(QLatin1String("href")).toString());
0470         }
0471     }
0472 }
0473 
0474 void NavitiaParser::parseAttributions(const QJsonArray& feeds)
0475 {
0476     for (const auto &v : feeds) {
0477         const auto feed = v.toObject();
0478         Attribution attr;
0479         attr.setName(feed.value(QLatin1String("name")).toString());
0480         QUrl url(feed.value(QLatin1String("url")).toString());
0481         if (!url.isEmpty()) {
0482             url.setScheme(QStringLiteral("https")); // missing in a few cases
0483         }
0484         attr.setUrl(url);
0485         attr.setLicense(feed.value(QLatin1String("license")).toString());
0486         // TODO map known licenses to spdx links
0487         attributions.push_back(std::move(attr));
0488     }
0489 }
0490 
0491 QJsonObject NavitiaParser::findDisruption(const QString &id) const
0492 {
0493     for (const auto &v : m_disruptions) {
0494         const auto disruption = v.toObject();
0495         if (disruption.value(QLatin1String("uri")).toString() == id) {
0496             return disruption;
0497         }
0498     }
0499     return {};
0500 }
0501 
0502 static QString disruptionMessage(const QJsonObject &distruption)
0503 {
0504     const auto msgs = distruption.value(QLatin1String("messages")).toArray();
0505     for (const auto &msgV : msgs) {
0506         const auto msg = msgV.toObject();
0507         const auto types = msg.value(QLatin1String("channel")).toObject().value(QLatin1String("types")).toArray();
0508         for (const auto &typeV : types) {
0509             if (typeV.toString() == QLatin1String("web")) {
0510                 return msg.value(QLatin1String("text")).toString();
0511             }
0512         }
0513     }
0514     return {};
0515 }
0516 
0517 template <typename T>
0518 void NavitiaParser::parseDisruptionLink(T &element, const QJsonObject &link) const
0519 {
0520     const auto type = link.value(QLatin1String("type")).toString();
0521     if (type != QLatin1String("disruption")) {
0522         return;
0523     }
0524 
0525     const auto id = link.value(QLatin1String("id")).toString();
0526     const auto disruption = findDisruption(id);
0527     if (disruption.value(QLatin1String("severity")).toObject().value(QLatin1String("effect")).toString() == QLatin1String("NO_SERVICE")) {
0528         element.setDisruptionEffect(Disruption::NoService);
0529     }
0530     element.addNote(disruptionMessage(disruption));
0531 }