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, [§ion, 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 }