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

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "hafasmgateparser.h"
0008 #include "hafasconfiguration.h"
0009 #include "hafasvehiclelayoutparser.h"
0010 #include "logging.h"
0011 #include "datatypes/loadutil_p.h"
0012 #include "geo/polylinedecoder_p.h"
0013 #include "ifopt/ifoptutil.h"
0014 #include "json/jsonpointer_p.h"
0015 
0016 #include <KPublicTransport/Journey>
0017 #include <KPublicTransport/Platform>
0018 #include <KPublicTransport/RentalVehicle>
0019 #include <KPublicTransport/Stopover>
0020 #include <KPublicTransport/Vehicle>
0021 
0022 #include <QDateTime>
0023 #include <QDebug>
0024 #include <QJsonArray>
0025 #include <QJsonDocument>
0026 #include <QJsonObject>
0027 #include <QRegularExpression>
0028 
0029 using namespace KPublicTransport;
0030 
0031 // REM or HIM elements
0032 struct Message {
0033     QVariant content;
0034     Disruption::Effect effect = Disruption::NormalService;
0035     LoadInfo loadInfo;
0036 };
0037 
0038 HafasMgateParser::HafasMgateParser() = default;
0039 HafasMgateParser::~HafasMgateParser() = default;
0040 
0041 static std::vector<Ico> parseIcos(const QJsonArray &icoL)
0042 {
0043     std::vector<Ico> icos;
0044     icos.reserve(icoL.size());
0045     for (const auto &icoV : icoL) {
0046         const auto icoObj = icoV.toObject();
0047         Ico ico;
0048         const auto fg = icoObj.value(QLatin1String("fg")).toObject();
0049         if (!fg.isEmpty()) {
0050             ico.fg = QColor(fg.value(QLatin1String("r")).toInt(), fg.value(QLatin1String("g")).toInt(), fg.value(QLatin1String("b")).toInt());
0051         }
0052         const auto bg = icoObj.value(QLatin1String("bg")).toObject();
0053         if (!bg.isEmpty()) {
0054             ico.bg = QColor(bg.value(QLatin1String("r")).toInt(), bg.value(QLatin1String("g")).toInt(), bg.value(QLatin1String("b")).toInt());
0055         }
0056         icos.push_back(ico);
0057     }
0058     return icos;
0059 }
0060 
0061 static constexpr const Load::Category load_value_map[] = {
0062     Load::Unknown,
0063     Load::Low, // 1
0064     Load::Medium, // 2
0065     Load::High, // 3
0066     Load::Full // 4
0067 };
0068 
0069 static const struct {
0070     const char *type;
0071     const char *code;
0072 } ignored_remarks[] = {
0073     { "A", "1" }, // different name formats for the line, used by SBB
0074     { "A", "2" },
0075     { "A", "3" },
0076     { "A", "4" }, // same as above, containing product, line number and journey number
0077     { "A", "OPERATOR" }, // operator information should be a dedicated field if we ever need it
0078     { "A", "moreAttr" }, // ZVV: pointless note about checking intermediate stops for more details
0079     { "H", "wagenstand_v2" }, // contains a pointless note about checking trip details
0080     { "I", "FD" }, // SBB line number?
0081     { "I", "RN" }, // SBB: some unknown number for buses
0082     { "I", "TC" }, // SBB: some unknown number for buses
0083     { "I", "XC" }, // SBB: some XML structure of unknown content, related to train/platform layouts
0084     { "I", "XG" }, // SBB: some XML structure of unknown content, related to train/platform layouts
0085     { "I", "XT" }, // SBB: some XML structure of unknown content, related to train/platform layouts
0086 };
0087 
0088 static std::vector<Message> parseRemarks(const QJsonArray &remL)
0089 {
0090     std::vector<Message> rems;
0091     rems.reserve(remL.size());
0092     for (const auto &remV : remL) {
0093         const auto remObj = remV.toObject();
0094 
0095         const auto type = remObj.value(QLatin1String("type")).toString();
0096         const auto code = remObj.value(QLatin1String("code")).toString();
0097         bool skip = false;
0098         for (const auto &ignored_remark : ignored_remarks) {
0099             if (type == QLatin1String(ignored_remark.type) && code == QLatin1String(ignored_remark.code)) {
0100                 skip = true;
0101                 break;
0102             }
0103         }
0104         if (skip) {
0105             rems.push_back({}); // make sure the indices still match!
0106             continue;
0107         }
0108 
0109         Message m;
0110         if (type == QLatin1Char('I') && code == QLatin1String("JF")) {
0111             m.content = HafasVehicleLayoutParser::parseTrainFormation(remObj.value(QLatin1String("txtN")).toString().toUtf8());
0112         } else if (type == QLatin1Char('I') && code == QLatin1String("XP")) {
0113             m.content = HafasVehicleLayoutParser::parsePlatformSectors(remObj.value(QLatin1String("txtN")).toString().toUtf8());
0114         } else if (type == QLatin1Char('A') && (code.startsWith(QLatin1String("text.occup.loc.")) || code.startsWith(QLatin1String("text.occup.jny.")))) {
0115             static QRegularExpression rx(QStringLiteral("\\.(max|1st|2nd)\\.1([1-4])$"));
0116             const auto match = rx.match(code);
0117             if (match.hasMatch()) {
0118                 const auto r = match.captured(2).toInt();
0119                 if (r >= 0 && r <= 4) {
0120                     m.loadInfo.setLoad(load_value_map[r]);
0121                 }
0122                 if (match.captured(1) != QLatin1String("max")) {
0123                     m.loadInfo.setSeatingClass(match.captured(1).left(1));
0124                 }
0125             } else {
0126                 m.content = remObj.value(QLatin1String("txtN")).toString();
0127             }
0128         } else {
0129             // generic text
0130             m.content = remObj.value(QLatin1String("txtN")).toString();
0131             if (code == QLatin1String("text.realtime.stop.cancelled") || code == QLatin1String("text.realtime.stop.entry.exit.disabled")) {
0132                 m.effect = Disruption::NoService;
0133             }
0134         }
0135         rems.push_back(std::move(m));
0136     }
0137     return rems;
0138 }
0139 
0140 static std::vector<Message> parseWarnings(const QJsonArray &himL)
0141 {
0142     std::vector<Message> hims;
0143     hims.reserve(himL.size());
0144     for (const auto &himV : himL) {
0145         const auto himObj = himV.toObject();
0146         Message m;
0147         m.content = QString(himObj.value(QLatin1String("head")).toString() + QLatin1Char('\n')
0148                + himObj.value(QLatin1String("lead")).toString() + QLatin1Char('\n')
0149                + himObj.value(QLatin1String("text")).toString());
0150         hims.push_back(m);
0151     }
0152     return hims;
0153 }
0154 
0155 template <typename Func>
0156 static void processMessageList(const QJsonObject &obj, const std::vector<Message> &remarks, const std::vector<Message> &warnings, Func func)
0157 {
0158     const auto msgL = obj.value(QLatin1String("msgL")).toArray();
0159     QStringList notes;
0160     for (const auto &msgV : msgL) {
0161         const auto msgObj = msgV.toObject();
0162         const auto msgType = msgObj.value(QLatin1String("type")).toString();
0163 
0164         const std::vector<Message> *source = nullptr;
0165         if (msgType == QLatin1String("REM")) {
0166             source = &remarks;
0167         } else if (msgType == QLatin1String("HIM")) {
0168             source = &warnings;
0169         } else {
0170             qDebug() << "unsupported message type:" << msgType;
0171             continue;
0172         }
0173 
0174         const auto remX = msgObj.value(QLatin1String("remX")).toInt();
0175         if (static_cast<size_t>(remX) >= source->size()) {
0176             qCDebug(Log) << "Invalid message index:" << remX << msgType;
0177             continue;
0178         }
0179         const auto &msg = (*source)[remX];
0180         func(msg, msgObj);
0181     }
0182 }
0183 
0184 template <typename T>
0185 static void applyMessage(T &elem, const Message &msg)
0186 {
0187     if (msg.content.userType() == QMetaType::QString) {
0188         elem.addNote(msg.content.toString());
0189     }
0190     if (msg.effect == Disruption::NoService) {
0191         elem.setDisruptionEffect(msg.effect);
0192     }
0193     if (msg.loadInfo.load() != Load::Unknown) {
0194         elem.setLoadInformation(LoadUtil::merge(elem.loadInformation(), {msg.loadInfo}));
0195     }
0196 }
0197 
0198 template <typename T>
0199 static void parseMessageList(T &elem, const QJsonObject &obj, const std::vector<Message> &remarks, const std::vector<Message> &warnings)
0200 {
0201     processMessageList(obj, remarks, warnings, [&elem](const Message &msg, const QJsonObject&) {
0202         applyMessage(elem, msg);
0203     });
0204 }
0205 
0206 static std::vector<LoadInfo> parseLoadInformation(const QJsonArray &tcocL)
0207 {
0208     std::vector<LoadInfo> loadInfos;
0209     loadInfos.reserve(tcocL.size());
0210     for (const auto &tcocV : tcocL) {
0211         const auto tcocObj = tcocV.toObject();
0212         const auto r = tcocObj.value(QLatin1String("r")).toInt(-1);
0213         if (r < 0 || r > 4) {
0214             continue;
0215         }
0216         LoadInfo loadInfo;
0217         loadInfo.setLoad(load_value_map[r]);
0218         const auto c = tcocObj.value(QLatin1String("c")).toString();
0219         loadInfo.setSeatingClass(c == QLatin1String("FIRST") ? QStringLiteral("1") : QStringLiteral("2"));
0220         loadInfos.push_back(std::move(loadInfo));
0221     }
0222     return loadInfos;
0223 }
0224 
0225 std::vector<Location> HafasMgateParser::parseLocations(const QJsonArray &locL) const
0226 {
0227     std::vector<Location> locs;
0228     locs.reserve(locL.size());
0229     for (const auto &locV : locL) {
0230         const auto locObj = locV.toObject();
0231 
0232         // resolve references to the master location
0233         const auto masterIdx = locObj.value(QLatin1String("mMastLocX")).toInt(-1);
0234         if (masterIdx >= 0 && masterIdx < (int)locs.size()) {
0235             locs.push_back(locs[masterIdx]);
0236             continue;
0237         }
0238 
0239         Location loc;
0240         loc.setName(locObj.value(QLatin1String("name")).toString());
0241         loc.setType(locObj.value(QLatin1String("type")).toString() == QLatin1Char('S') ? Location::Stop : Location::Place);
0242         setLocationIdentifier(loc, locObj.value(QLatin1String("extId")).toString());
0243         const auto coordObj = locObj.value(QLatin1String("crd")).toObject();
0244         loc.setCoordinate(coordObj.value(QLatin1String("y")).toDouble() / 1000000.0, coordObj.value(QLatin1String("x")).toDouble() / 1000000.0);
0245 
0246         const auto gidL = locObj.value(QLatin1String("gidL")).toArray();
0247         for (const auto &gidV : gidL) {
0248             const auto gid = gidV.toString() ;
0249             // ### is this A× prefix actually standard or do we need to configure that per provider?
0250             if (gid.startsWith(QStringLiteral("A×")) && IfoptUtil::isValid(QStringView(gid).mid(2))) {
0251                 loc.setIdentifier(IfoptUtil::identifierType(), gid.mid(2));
0252             }
0253         }
0254 
0255         locs.push_back(loc);
0256     }
0257     return locs;
0258 }
0259 
0260 std::vector<Route> HafasMgateParser::parseProducts(const QJsonArray &prodL, const std::vector<Ico> &icos) const
0261 {
0262     std::vector<Route> routes;
0263     routes.reserve(prodL.size());
0264     for (const auto &prodV : prodL) {
0265         const auto prodObj = prodV.toObject();
0266         const auto prodCls = prodObj.value(QLatin1String("cls")).toInt();
0267 
0268         Route route;
0269         Line line;
0270         line.setMode(parseLineMode(prodCls));
0271 
0272         const auto it = std::find(m_productNameMappings.begin(), m_productNameMappings.end(), prodCls);
0273         if (it != m_productNameMappings.end()) {
0274             for (const auto &lineName : (*it).lineName) {
0275                 line.setName(JsonPointer::evaluate(prodObj, lineName).toString());
0276                 if (!line.name().isEmpty()) {
0277                     break;
0278                 }
0279             }
0280             for (const auto &routeName : (*it).routeName) {
0281                 route.setName(JsonPointer::evaluate(prodObj, routeName).toString());
0282                 if (!route.name().isEmpty()) {
0283                     break;
0284                 }
0285             }
0286         } else {
0287             line.setName(prodObj.value(QLatin1String("name")).toString());
0288         }
0289 
0290         const auto icoIdx = prodObj.value(QLatin1String("icoX")).toInt();
0291         if ((unsigned int)icoIdx < icos.size()) {
0292             line.setColor(icos[icoIdx].bg);
0293             line.setTextColor(icos[icoIdx].fg);
0294         }
0295 
0296         route.setLine(std::move(line));
0297         routes.push_back(std::move(route));
0298     }
0299 
0300     return routes;
0301 }
0302 
0303 static QString parsePlatform(const QJsonObject &obj, char ad, char rs)
0304 {
0305     const auto p = obj.value(QLatin1Char(ad) + QLatin1String("Platf") + QLatin1Char(rs)).toString();
0306     if (!p.isEmpty()) {
0307         return p;
0308     }
0309 
0310     const auto pObj = obj.value(QLatin1Char(ad) + QLatin1String("Pltf") + QLatin1Char(rs)).toObject();
0311     return pObj.value(QLatin1String("txt")).toString();
0312 }
0313 
0314 std::vector<Stopover> HafasMgateParser::parseStationBoardResponse(const QJsonObject &obj) const
0315 {
0316     const auto commonObj = obj.value(QLatin1String("common")).toObject();
0317     const auto icos = parseIcos(commonObj.value(QLatin1String("icoL")).toArray());
0318     const auto locs = parseLocations(commonObj.value(QLatin1String("locL")).toArray());
0319     const auto products = parseProducts(commonObj.value(QLatin1String("prodL")).toArray(), icos);
0320     const auto remarks = parseRemarks(commonObj.value(QLatin1String("remL")).toArray());
0321     const auto warnings = parseWarnings(commonObj.value(QLatin1String("himL")).toArray());
0322 
0323     std::vector<Stopover> res;
0324     const auto jnyL = obj.value(QLatin1String("jnyL")).toArray();
0325     res.reserve(jnyL.size());
0326 
0327     for (const auto &jny : jnyL) {
0328         const auto jnyObj = jny.toObject();
0329         const auto stbStop = jnyObj.value(QLatin1String("stbStop")).toObject();
0330 
0331         Stopover dep;
0332         Route route;
0333         const auto prodIdx = jnyObj.value(QLatin1String("prodX")).toInt(-1);
0334         if (prodIdx >= 0 && (unsigned int)prodIdx < products.size()) {
0335             route = products[prodIdx];
0336         }
0337         route.setDirection(jnyObj.value(QLatin1String("dirTxt")).toString());
0338 
0339         const auto dateStr = jnyObj.value(QLatin1String("date")).toString();
0340         dep.setScheduledDepartureTime(parseDateTime(dateStr, stbStop.value(QLatin1String("dTimeS")), stbStop.value(QLatin1String("dTZOffset"))));
0341         dep.setExpectedDepartureTime(parseDateTime(dateStr, stbStop.value(QLatin1String("dTimeR")), stbStop.value(QLatin1String("dTZOffset"))));
0342         dep.setScheduledArrivalTime(parseDateTime(dateStr, stbStop.value(QLatin1String("aTimeS")), stbStop.value(QLatin1String("aTZOffset"))));
0343         dep.setExpectedArrivalTime(parseDateTime(dateStr, stbStop.value(QLatin1String("aTimeR")),  stbStop.value(QLatin1String("aTZOffset"))));
0344 
0345         dep.setScheduledPlatform(parsePlatform(stbStop, 'd', 'S'));
0346         dep.setExpectedPlatform(parsePlatform(stbStop, 'd', 'R'));
0347         if (dep.scheduledPlatform().isEmpty()) {
0348             dep.setScheduledPlatform(parsePlatform(stbStop, 'a', 'S'));
0349         }
0350         if (dep.expectedPlatform().isEmpty()) {
0351             dep.setExpectedPlatform(parsePlatform(stbStop, 'a', 'R'));
0352         }
0353         if (stbStop.value(QLatin1String("dCncl")).toBool()) {
0354             dep.setDisruptionEffect(Disruption::NoService);
0355         }
0356 
0357         const auto startLocIdx = stbStop.value(QLatin1String("locX")).toInt(-1);
0358         if (startLocIdx >= 0 && (unsigned int)startLocIdx < locs.size()) {
0359             dep.setStopPoint(locs[startLocIdx]);
0360         }
0361 
0362         const auto stopL = jnyObj.value(QLatin1String("stopL")).toArray();
0363         bool foundLoop = false; // check for loops, circular lines have no destination
0364         for (int i = 1; i < stopL.size() && !foundLoop; ++i) {
0365             const auto locX = stopL.at(i).toObject().value(QLatin1String("locX")).toInt(-1);
0366             if (locX == startLocIdx) {
0367                 foundLoop = true;
0368             }
0369         }
0370         const auto destLocX = stopL.last().toObject().value(QLatin1String("locX")).toInt(-1);
0371         if (!foundLoop && destLocX >= 0 && (unsigned int)destLocX < locs.size() && startLocIdx != destLocX) {
0372             route.setDestination(locs[destLocX]);
0373         }
0374 
0375         parseMessageList(dep, jnyObj, remarks, warnings);
0376         parseMessageList(dep, stbStop, remarks, warnings);
0377         dep.setRoute(route);
0378         res.push_back(dep);
0379     }
0380 
0381     return res;
0382 }
0383 
0384 bool HafasMgateParser::parseError(const QJsonObject& obj) const
0385 {
0386     const auto err = obj.value(QLatin1String("err")).toString();
0387     if (!err.isEmpty() && err != QLatin1String("OK")) {
0388         m_error = err == QLatin1String("LOCATION") ? Reply::NotFoundError : Reply::UnknownError;
0389         m_errorMsg = obj.value(QLatin1String("errTxt")).toString();
0390         if (m_errorMsg.isEmpty()) {
0391             m_errorMsg = err;
0392         }
0393         return false;
0394     }
0395 
0396     m_error = Reply::NoError;
0397     m_errorMsg.clear();
0398     return true;
0399 }
0400 
0401 
0402 std::vector<Stopover> HafasMgateParser::parseDepartures(const QByteArray &data) const
0403 {
0404     const auto topObj = QJsonDocument::fromJson(data).object();
0405     if (!parseError(topObj)) {
0406         return {};
0407     }
0408 
0409     const auto svcResL = topObj.value(QLatin1String("svcResL")).toArray();
0410     for (const auto &v : svcResL) {
0411         const auto obj = v.toObject();
0412         if (obj.value(QLatin1String("meth")).toString() == QLatin1String("StationBoard")) {
0413             if (parseError(obj)) {
0414                 return parseStationBoardResponse(obj.value(QLatin1String("res")).toObject());
0415             }
0416             return {};
0417         }
0418     }
0419 
0420     return {};
0421 }
0422 
0423 std::vector<Location> HafasMgateParser::parseLocations(const QByteArray &data) const
0424 {
0425     const auto topObj = QJsonDocument::fromJson(data).object();
0426     if (!parseError(topObj)) {
0427         return {};
0428     }
0429 
0430     const auto svcResL = topObj.value(QLatin1String("svcResL")).toArray();
0431     for (const auto &v : svcResL) {
0432         const auto obj = v.toObject();
0433         const auto meth = obj.value(QLatin1String("meth")).toString();
0434         if (meth == QLatin1String("LocMatch") || meth == QLatin1String("LocGeoPos")) {
0435             if (parseError(obj)) {
0436                 const auto resObj = obj.value(QLatin1String("res")).toObject();
0437                 if (resObj.contains(QLatin1String("locL"))) {
0438                     return parseLocations(resObj.value(QLatin1String("locL")).toArray());
0439                 }
0440                 if (resObj.contains(QLatin1String("match"))) {
0441                     return parseLocations(resObj.value(QLatin1String("match")).toObject().value(QLatin1String("locL")).toArray());
0442                 }
0443                 qCDebug(Log).noquote() << "Failed to parse location query response:" << QJsonDocument(obj).toJson();
0444                 return {};
0445             }
0446             return {};
0447         }
0448     }
0449 
0450     return {};
0451 }
0452 
0453 std::vector<Journey> HafasMgateParser::parseJourneys(const QByteArray &data)
0454 {
0455     m_nextJourneyContext.clear();
0456     m_previousJourneyContext.clear();
0457 
0458     const auto topObj = QJsonDocument::fromJson(data).object();
0459     if (!parseError(topObj)) {
0460         return {};
0461     }
0462 
0463     const auto svcResL = topObj.value(QLatin1String("svcResL")).toArray();
0464     for (const auto &v : svcResL) {
0465         const auto obj = v.toObject();
0466         if (obj.value(QLatin1String("meth")).toString() == QLatin1String("TripSearch")) {
0467             if (parseError(obj)) {
0468                 return parseTripSearch(obj.value(QLatin1String("res")).toObject());
0469             }
0470             return {};
0471         }
0472     }
0473 
0474     return {};
0475 }
0476 
0477 static void setPlatformLayout(Stopover &stop, const Platform &platform) { stop.setPlatformLayout(platform); }
0478 static void setPlatformLayout(JourneySection &jny, const Platform &platform) { jny.setDeparturePlatformLayout(platform); }
0479 static void setVehicleLayout(Stopover &stop, const Vehicle &vehicle) { stop.setVehicleLayout(vehicle); }
0480 static void setVehicleLayout(JourneySection &jny, const Vehicle &vehicle) { jny.setDepartureVehicleLayout(vehicle); }
0481 
0482 template <typename T>
0483 static void parseTrainComposition(const QJsonObject &obj, T &result,
0484                                   const std::vector<LoadInfo> &loadInfos,
0485                                   const std::vector<Platform> &platforms,
0486                                   const std::vector<Vehicle> &vehicles)
0487 {
0488     const auto dTrnCmpSX = obj.value(QLatin1String("dTrnCmpSX")).toObject();
0489 
0490     // load
0491     const auto tcocX = dTrnCmpSX.value(QLatin1String("tcocX")).toArray();
0492     std::vector<LoadInfo> load;
0493     load.reserve(tcocX.size());
0494     for (const auto &v : tcocX) {
0495         const auto i = v.toInt();
0496         if (i >= 0 && i < (int)loadInfos.size()) {
0497             load.push_back(loadInfos[i]);
0498         }
0499     }
0500     result.setLoadInformation(LoadUtil::merge(std::move(load), result.loadInformation()));
0501 
0502     // platform
0503     const auto tcpdX = dTrnCmpSX.value(QLatin1String("tcpdX")).toInt(-1);
0504     if (tcpdX >= 0 && tcpdX < (int)platforms.size()) {
0505         setPlatformLayout(result, platforms[tcpdX]);
0506     }
0507 
0508     // vehicle
0509     const auto stcGX = dTrnCmpSX.value(QLatin1String("stcGX")).toArray();
0510     const auto vehicleIdx = stcGX.empty() ? -1 : stcGX.at(0).toInt(-1);
0511     if (vehicleIdx >= 0 && vehicleIdx < (int)vehicles.size()) {
0512         setVehicleLayout(result, vehicles[vehicleIdx]);
0513     }
0514 }
0515 
0516 static std::vector<Path> parsePaths(const QJsonArray &polyL, const std::vector<Location> &locs)
0517 {
0518     std::vector<Path> paths;
0519     paths.reserve(polyL.size());
0520     for (const auto &polyV : polyL) {
0521         const auto polyObj = polyV.toObject();
0522 
0523         // path coordinate index to location mapping
0524         const auto ppLocRefL = polyObj.value(QLatin1String("ppLocRefL")).toArray();
0525         // 2-dimensional differential encoded coordinates
0526         const auto crdEncYX = polyObj.value(QLatin1String("crdEncYX")).toString().toUtf8();
0527         PolylineDecoder<2> crdEncYXDecoder(crdEncYX.constData());
0528         // crdEncDist: 1-dimensional integer values with differential encoding containing distances in meters
0529         // crdEncZ: 1-dimensional, always 0?
0530         // crdEncS: 1-dimensional, unknown meaning, but very low-entropy data
0531         // crdEncF: 1-dimensional, always 0?
0532 
0533         std::vector<PathSection> sections;
0534         sections.reserve(std::max<int>(0, ppLocRefL.size() - 1));
0535         int prevPpIdx = 0;
0536         QPointF prevCoord;
0537         for (const auto &ppLocRefV : ppLocRefL) {
0538             const auto ppLocRef = ppLocRefV.toObject();
0539             const auto ppIdx = ppLocRef.value(QLatin1String("ppIdx")).toInt();
0540             if (ppIdx == 0 || ppIdx < prevPpIdx) {
0541                 continue;
0542             }
0543 
0544             QPolygonF poly;
0545             poly.reserve(prevPpIdx - ppIdx + 2);
0546             if (!prevCoord.isNull()) {
0547                 poly.push_back(prevCoord);
0548             }
0549             crdEncYXDecoder.readPolygon(poly, ppIdx - prevPpIdx + 1);
0550             if (!poly.empty()) {
0551                 prevCoord = poly.back();
0552             }
0553 
0554             PathSection section;
0555             section.setPath(std::move(poly));
0556             prevPpIdx = ppIdx;
0557 
0558             const auto locX = ppLocRef.value(QLatin1String("locX")).toInt();
0559             if (locX >= 0 && locX < (int)locs.size()) {
0560                 section.setDescription(locs[locX].name());
0561             }
0562 
0563             sections.push_back(std::move(section));
0564         }
0565 
0566         Path path;
0567         if (!sections.empty()) {
0568             path.setSections(std::move(sections));
0569         }
0570         paths.push_back(std::move(path));
0571     }
0572     return paths;
0573 }
0574 
0575 static Path parsePolyG(const QJsonObject &obj, const std::vector<Path> &paths)
0576 {
0577     const auto polyG = obj.value(QLatin1String("polyG")).toObject();
0578     const auto polyXL = polyG.value(QLatin1String("polyXL")).toArray();
0579     if (polyXL.size() != 1) {
0580         return {};
0581     }
0582     const auto polyX = polyXL.at(0).toInt();
0583     if (polyX < 0 || polyX >= (int)paths.size()) {
0584         return {};
0585     }
0586 
0587     const auto segL = obj.value(QLatin1String("segL")).toArray();
0588     auto path = paths[polyX];
0589     if (segL.isEmpty() || path.sections().size() != 1) {
0590         return path;
0591     }
0592 
0593     const auto poly = path.sections()[0].path();
0594     std::vector<PathSection> pathSections;
0595     pathSections.reserve(segL.size());
0596     for (const auto &segV : segL) {
0597         const auto segObj = segV.toObject();
0598         PathSection sec;
0599         sec.setDescription(segObj.value(QLatin1String("manTx")).toString());
0600         const auto polyS = segObj.value(QLatin1String("polyS")).toInt();
0601         const auto polyE = segObj.value(QLatin1String("polyE")).toInt();
0602         if (polyS >= 0 && polyS < poly.size() && polyE >= polyS && polyE < poly.size()) {
0603             QPolygonF subPoly;
0604             subPoly.reserve(polyE - polyS + 1);
0605             std::copy(poly.begin() + polyS, poly.begin() + polyE + 1, std::back_inserter(subPoly));
0606             sec.setPath(std::move(subPoly));
0607         }
0608         pathSections.push_back(std::move(sec));
0609     }
0610     path.setSections(std::move(pathSections));
0611     return path;
0612 }
0613 
0614 static void parseMcpData(const QJsonObject &obj, Location &loc)
0615 {
0616     const auto mcp = obj.value(QLatin1String("mcp")).toObject();
0617     if (mcp.isEmpty()) {
0618         return;
0619     }
0620     const auto mcpData = mcp.value(QLatin1String("mcpData")).toObject();
0621     // TODO mcpData.provider to vehicle type lookup from meta data
0622     const auto providerName = mcpData.value(QLatin1String("providerName")).toString();
0623     qDebug() << providerName << mcpData;
0624     if (providerName.isEmpty()) {
0625         return;
0626     }
0627 
0628     // ### are we even sure this is always a station? how does this distinguish free floating vehicles?
0629     RentalVehicleNetwork network;
0630     network.setName(providerName);
0631     RentalVehicleStation station;
0632     station.setNetwork(network);
0633     loc.setData(station);
0634     loc.setType(Location::RentedVehicleStation);
0635 }
0636 
0637 std::vector<Journey> HafasMgateParser::parseTripSearch(const QJsonObject &obj)
0638 {
0639     const auto commonObj = obj.value(QLatin1String("common")).toObject();
0640     const auto icos = parseIcos(commonObj.value(QLatin1String("icoL")).toArray());
0641     const auto locs = parseLocations(commonObj.value(QLatin1String("locL")).toArray());
0642     const auto products = parseProducts(commonObj.value(QLatin1String("prodL")).toArray(), icos);
0643     const auto remarks = parseRemarks(commonObj.value(QLatin1String("remL")).toArray());
0644     const auto warnings = parseWarnings(commonObj.value(QLatin1String("himL")).toArray());
0645     const auto loadInfos = parseLoadInformation(commonObj.value(QLatin1String("tcocL")).toArray());
0646     const auto paths = parsePaths(commonObj.value(QLatin1String("polyL")).toArray(), locs);
0647     const auto platforms = HafasVehicleLayoutParser::parsePlatforms(commonObj);
0648     const auto vehicles = HafasVehicleLayoutParser::parseVehicleLayouts(commonObj);
0649 
0650     std::vector<Journey> res;
0651     const auto outConL = obj.value(QLatin1String("outConL")).toArray();
0652     res.reserve(outConL.size());
0653 
0654     for (const auto &outConV: outConL) {
0655         const auto outCon = outConV.toObject();
0656 
0657         const auto dateStr = outCon.value(QLatin1String("date")).toString();
0658 
0659         const auto secL = outCon.value(QLatin1String("secL")).toArray();
0660         std::vector<JourneySection> sections;
0661         sections.reserve(secL.size());
0662 
0663 
0664         for (const auto &secV : secL) {
0665             const auto secObj = secV.toObject();
0666             JourneySection section;
0667 
0668             const auto dep = secObj.value(QLatin1String("dep")).toObject();
0669             section.setScheduledDepartureTime(parseDateTime(dateStr, dep.value(QLatin1String("dTimeS")), dep.value(QLatin1String("dTZOffset"))));
0670             section.setExpectedDepartureTime(parseDateTime(dateStr, dep.value(QLatin1String("dTimeR")), dep.value(QLatin1String("dTZOffset"))));
0671             const auto fromIdx = dep.value(QLatin1String("locX")).toInt(-1);
0672             if ((unsigned int)fromIdx < locs.size()) {
0673                 auto loc = locs[fromIdx];
0674                 parseMcpData(dep, loc);
0675                 section.setFrom(std::move(loc));
0676             }
0677             section.setScheduledDeparturePlatform(parsePlatform(dep, 'd', 'S'));
0678             section.setExpectedDeparturePlatform(parsePlatform(dep, 'd', 'R'));
0679             if (dep.value(QLatin1String("dCncl")).toBool()) {
0680                 section.setDisruptionEffect(Disruption::NoService);
0681             }
0682 
0683             const auto arr = secObj.value(QLatin1String("arr")).toObject();
0684             section.setScheduledArrivalTime(parseDateTime(dateStr, arr.value(QLatin1String("aTimeS")), arr.value(QLatin1String("aTZOffset"))));
0685             section.setExpectedArrivalTime(parseDateTime(dateStr, arr.value(QLatin1String("aTimeR")), arr.value(QLatin1String("aTZOffset"))));
0686             const auto toIdx = arr.value(QLatin1String("locX")).toInt(-1);
0687             if ((unsigned int)toIdx < locs.size()) {
0688                 auto loc = locs[toIdx];
0689                 parseMcpData(arr, loc);
0690                 section.setTo(loc);
0691             }
0692             section.setScheduledArrivalPlatform(parsePlatform(arr, 'a', 'S'));
0693             section.setExpectedArrivalPlatform(parsePlatform(arr, 'a', 'R'));
0694             if (arr.value(QLatin1String("aCncl")).toBool()) {
0695                 section.setDisruptionEffect(Disruption::NoService);
0696             }
0697 
0698             const auto typeStr = secObj.value(QLatin1String("type")).toString();
0699             if (typeStr == QLatin1String("JNY")) {
0700                 section.setMode(JourneySection::PublicTransport);
0701 
0702                 const auto jnyObj = secObj.value(QLatin1String("jny")).toObject();
0703                 Route route;
0704                 const auto prodIdx = jnyObj.value(QLatin1String("prodX")).toInt(-1);
0705                 if (prodIdx >= 0 && (unsigned int)prodIdx < products.size()) {
0706                     route = products[prodIdx];
0707                 }
0708                 route.setDirection(jnyObj.value(QLatin1String("dirTxt")).toString());
0709                 section.setRoute(route);
0710 
0711                 if (jnyObj.value(QLatin1String("isCncl")).toBool()) {
0712                     section.setDisruptionEffect(Disruption::NoService);
0713                 }
0714 
0715                 const auto stopL = jnyObj.value(QLatin1String("stopL")).toArray();
0716                 if (stopL.size() > 2) { // we don't want departure/arrival stops in here
0717                     std::vector<Stopover> stops;
0718                     stops.reserve(stopL.size() - 2);
0719                     for (auto it = std::next(stopL.begin()); it != std::prev(stopL.end()); ++it) {
0720                         const auto stopObj = (*it).toObject();
0721                         Stopover stop;
0722                         const auto locIdx = stopObj.value(QLatin1String("locX")).toInt();
0723                         if ((unsigned int)locIdx < locs.size()) {
0724                             stop.setStopPoint(locs[locIdx]);
0725                         }
0726                         stop.setScheduledDepartureTime(parseDateTime(dateStr, stopObj.value(QLatin1String("dTimeS")), stopObj.value(QLatin1String("dTZOffset"))));
0727                         stop.setExpectedDepartureTime(parseDateTime(dateStr, stopObj.value(QLatin1String("dTimeR")), stopObj.value(QLatin1String("dTZOffset"))));
0728                         stop.setScheduledArrivalTime(parseDateTime(dateStr, stopObj.value(QLatin1String("aTimeS")), stopObj.value(QLatin1String("aTZOffset"))));
0729                         stop.setExpectedArrivalTime(parseDateTime(dateStr, stopObj.value(QLatin1String("aTimeR")), stopObj.value(QLatin1String("aTZOffset"))));
0730                         stop.setScheduledPlatform(parsePlatform(stopObj, 'd', 'S'));
0731                         stop.setExpectedPlatform(parsePlatform(stopObj, 'd', 'R'));
0732                         if (stopObj.value(QLatin1String("aCncl")).toBool() || stopObj.value(QLatin1String("dCncl")).toBool()) {
0733                             stop.setDisruptionEffect(Disruption::NoService);
0734                         }
0735                         parseMessageList(stop, stopObj, remarks, warnings);
0736                         processMessageList(jnyObj, remarks, warnings, [&stop, locIdx](const Message &msg, const QJsonObject &msgObj) {
0737                             const auto fromIdx = msgObj.value(QLatin1String("fLocX")).toInt(-1);
0738                             const auto toIdx = msgObj.value(QLatin1String("tLocX")).toInt(-1);
0739                             if (fromIdx != toIdx || fromIdx != locIdx) {
0740                                 return;
0741                             }
0742                             if (msg.content.userType() == qMetaTypeId<Platform>()) {
0743                                 stop.setPlatformLayout(Platform::merge(stop.platformLayout(), msg.content.value<Platform>()));
0744                             } else if (msg.content.userType() == qMetaTypeId<Vehicle>()) {
0745                                 stop.setVehicleLayout(Vehicle::merge(stop.vehicleLayout(), msg.content.value<Vehicle>()));
0746                             }
0747                             applyMessage(stop, msg);
0748                         });
0749                         parseTrainComposition(stopObj, stop, loadInfos, platforms, vehicles);
0750                         stops.push_back(stop);
0751                     }
0752                     section.setIntermediateStops(std::move(stops));
0753                 }
0754 
0755                 processMessageList(jnyObj, remarks, warnings, [&section, fromIdx, toIdx](const Message &msg, const QJsonObject &msgObj) {
0756                     const auto from = msgObj.value(QLatin1String("fLocX")).toInt(-1);
0757                     const auto to = msgObj.value(QLatin1String("tLocX")).toInt(-1);
0758                     if (from >= 0 && to >= 0 && from != fromIdx && toIdx != to && from == to) {
0759                         return;
0760                     }
0761                     if (msg.content.userType() == qMetaTypeId<Platform>()) {
0762                         if (from == to && to == toIdx) {
0763                             section.setArrivalPlatformLayout(Platform::merge(section.arrivalPlatformLayout(), msg.content.value<Platform>()));
0764                         } else {
0765                             section.setDeparturePlatformLayout(Platform::merge(section.departurePlatformLayout(), msg.content.value<Platform>()));
0766                         }
0767                     } else if (msg.content.userType() == qMetaTypeId<Vehicle>()) {
0768                         if (from == to && to == toIdx) {
0769                             section.setArrivalVehicleLayout(Vehicle::merge(section.arrivalVehicleLayout(), msg.content.value<Vehicle>()));
0770                         } else {
0771                             section.setDepartureVehicleLayout(Vehicle::merge(section.departureVehicleLayout(), msg.content.value<Vehicle>()));
0772                         }
0773                     }
0774                     applyMessage(section, msg);
0775                 });
0776                 parseTrainComposition(dep, section, loadInfos, platforms, vehicles);
0777                 section.setPath(parsePolyG(jnyObj, paths));
0778             } else {
0779                 const auto gis = secObj.value(QLatin1String("gis")).toObject();
0780                 section.setDistance(gis.value(QLatin1String("dist")).toInt());
0781                 section.setPath(parsePolyG(gis, paths));
0782                 if (typeStr == QLatin1String("WALK")) {
0783                     section.setMode(JourneySection::Walking);
0784                 } else if (typeStr == QLatin1String("TRSF")) {
0785                     section.setMode(JourneySection::Transfer);
0786                 } else if (typeStr == QLatin1String("BIKE")) {
0787                     if (section.from().type() == Location::RentedVehicleStation) {
0788                         section.setMode(JourneySection::RentedVehicle);
0789                         RentalVehicle v;
0790                         v.setNetwork(section.from().rentalVehicleStation().network());
0791                         v.setType(RentalVehicle::Bicycle); // TODO we also get here for kick scooters?
0792                         section.setRentalVehicle(v);
0793                     } else {
0794                         section.setMode(JourneySection::IndividualTransport);
0795                         section.setIndividualTransport({ IndividualTransport::Bike });
0796                     }
0797                 } else if (typeStr == QLatin1String("PARK")) { // this means "drive to parking space", not "park the car"...
0798                     section.setMode(JourneySection::IndividualTransport);
0799                     section.setIndividualTransport({ IndividualTransport::Car, IndividualTransport::Park });
0800                 } else if (typeStr == QLatin1String("CHKO")) { // ... while this means "park the car"
0801                     section.setMode(JourneySection::Transfer); // ### we don't have any metter mode for this atm
0802                 } else if (typeStr == QLatin1String("KISS")) {
0803                     section.setMode(JourneySection::RentedVehicle);
0804                     RentalVehicle v;
0805                     v.setType(RentalVehicle::Car);
0806                     section.setRentalVehicle(v);
0807                 } else {
0808                     qCWarning(Log) << "Unhandled section mode:" << typeStr;
0809                 }
0810             }
0811 
0812             sections.push_back(section);
0813         }
0814 
0815         Journey journey;
0816         journey.setSections(std::move(sections));
0817         res.push_back(journey);
0818     }
0819 
0820     m_previousJourneyContext = obj.value(QLatin1String("outCtxScrB")).toString();
0821     m_nextJourneyContext =  obj.value(QLatin1String("outCtxScrF")).toString();
0822 
0823     return res;
0824 }
0825 
0826 QDateTime HafasMgateParser::parseDateTime(const QString &date, const QJsonValue &time, const QJsonValue &tzOffset)
0827 {
0828     const auto timeStr = time.toString();
0829     if (date.isEmpty() || timeStr.isEmpty()) {
0830         return {};
0831     }
0832 
0833     int dayOffset = 0;
0834     if (timeStr.size() > 6) {
0835         dayOffset = QStringView(timeStr).left(timeStr.size() - 6).toInt();
0836     }
0837 
0838     auto dt = QDateTime::fromString(date + QStringView(timeStr).right(6), QStringLiteral("yyyyMMddhhmmss"));
0839     dt = dt.addDays(dayOffset);
0840     if (!tzOffset.isNull() && !tzOffset.isUndefined()) {
0841         dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(tzOffset.toInt() * 60));
0842     }
0843 
0844     return dt;
0845 }
0846 
0847 void HafasMgateParser::setProductNameMappings(std::vector<HafasMgateProductNameMapping> &&productNameMappings)
0848 {
0849     m_productNameMappings = std::move(productNameMappings);
0850 }