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 "hafasqueryparser.h" 0008 #include "hafasjourneyresponse_p.h" 0009 #include "logging.h" 0010 0011 #include <json/jsonp_p.h> 0012 0013 #include <KPublicTransport/Journey> 0014 #include <KPublicTransport/Location> 0015 #include <KPublicTransport/Stopover> 0016 0017 #include <QDateTime> 0018 #include <QDebug> 0019 #include <QJsonArray> 0020 #include <QJsonDocument> 0021 #include <QJsonObject> 0022 #include <QRegularExpression> 0023 #include <QXmlStreamReader> 0024 0025 #include <zlib.h> 0026 0027 using namespace KPublicTransport; 0028 0029 HafasQueryParser::HafasQueryParser() = default; 0030 HafasQueryParser::~HafasQueryParser() = default; 0031 0032 std::vector<Stopover> HafasQueryParser::parseStationBoardResponse(const QByteArray &data, bool isArrival) 0033 { 0034 clearErrorState(); 0035 qDebug().noquote() << data; 0036 std::vector<Stopover> res; 0037 0038 QXmlStreamReader reader; 0039 if (data.startsWith("<Journey")) { // SBB and RT don't reply with valid XML... 0040 reader.addData("<dummyRoot>"); 0041 reader.addData(data); 0042 reader.addData("</dummyRoot>"); 0043 } else { 0044 reader.addData(data); 0045 } 0046 0047 Location stopPoint; 0048 while (!reader.atEnd()) { 0049 const auto token = reader.readNext(); 0050 switch (token) { 0051 case QXmlStreamReader::StartElement: 0052 if (reader.name() == QLatin1String("St")) { 0053 stopPoint.setName(reader.attributes().value(QLatin1String("name")).toString()); 0054 setLocationIdentifier(stopPoint, reader.attributes().value(QLatin1String("evaId")).toString()); 0055 } else if (reader.name() == QLatin1String("Journey")) { 0056 auto dt = QDateTime::fromString(reader.attributes().value(QLatin1String("fpDate")) + reader.attributes().value(QLatin1String("fpTime")), QStringLiteral("dd.MM.yyhh:mm")); 0057 if (dt.date().year() < 2000) { 0058 dt = dt.addYears(100); 0059 } 0060 const auto delayStr = reader.attributes().value(QLatin1String("e_delay")); 0061 const auto delaySecs = delayStr.toInt() * 60; 0062 Stopover dep; 0063 if (isArrival) { 0064 dep.setScheduledArrivalTime(dt); 0065 if (!delayStr.isEmpty()) { 0066 dep.setExpectedArrivalTime(dt.addSecs(delaySecs)); 0067 } 0068 } else { 0069 dep.setScheduledDepartureTime(dt); 0070 if (!delayStr.isEmpty()) { 0071 dep.setExpectedDepartureTime(dt.addSecs(delaySecs)); 0072 } 0073 } 0074 dep.setScheduledPlatform(reader.attributes().value(QLatin1String("platform")).toString()); 0075 dep.setExpectedPlatform(reader.attributes().value(QLatin1String("newpl")).toString()); 0076 0077 if (reader.attributes().value(QLatin1String("delay")) == QLatin1String("cancel")) { 0078 dep.setDisruptionEffect(Disruption::NoService); 0079 } 0080 0081 Route route; 0082 route.setDirection(reader.attributes().value(QLatin1String("targetLoc")).toString()); 0083 Line line; 0084 line.setName(reader.attributes().value(QLatin1String("hafasname")).toString()); 0085 if (line.name().isEmpty()) { 0086 const auto prod = reader.attributes().value(QLatin1String("prod")); 0087 const auto idx = prod.indexOf(QLatin1Char('#')); 0088 line.setName(prod.left(idx).toString().simplified()); 0089 } 0090 0091 line.setMode(parseLineMode(reader.attributes().value(QLatin1String("class")))); 0092 // TODO line mode from second part of prod attribute, if class not set 0093 route.setLine(line); 0094 dep.setRoute(route); 0095 0096 dep.setStopPoint(stopPoint); 0097 res.push_back(dep); 0098 } 0099 break; 0100 default: 0101 break; 0102 } 0103 } 0104 0105 return res; 0106 } 0107 0108 std::vector<Location> HafasQueryParser::parseGetStopResponse(const QByteArray &data) 0109 { 0110 clearErrorState(); 0111 0112 QJsonParseError parseError; 0113 const auto doc = QJsonDocument::fromJson(JsonP::decode(data), &parseError); 0114 if (parseError.error != QJsonParseError::NoError) { 0115 qCWarning(Log) << parseError.errorString() << data; 0116 } 0117 const auto suggestions = doc.object().value(QLatin1String("suggestions")).toArray(); 0118 std::vector<Location> res; 0119 res.reserve(suggestions.size()); 0120 for (const auto &suggestion : suggestions) { 0121 const auto obj = suggestion.toObject(); 0122 const auto extId = obj.value(QLatin1String("extId")).toString(); 0123 if (extId.isEmpty()) { 0124 continue; // not a stop/station 0125 } 0126 Location loc; 0127 setLocationIdentifier(loc, extId); 0128 loc.setName(obj.value(QLatin1String("value")).toString()); 0129 loc.setLatitude(obj.value(QLatin1String("ycoord")).toString().toInt() / 1000000.0); 0130 loc.setLongitude(obj.value(QLatin1String("xcoord")).toString().toInt() / 1000000.0); 0131 res.push_back(loc); 0132 } 0133 0134 return res; 0135 } 0136 0137 std::vector<Location> HafasQueryParser::parseQueryLocationResponse(const QByteArray &data) 0138 { 0139 clearErrorState(); 0140 0141 QJsonParseError parseError; 0142 auto doc = QJsonDocument::fromJson(data, &parseError); 0143 if (parseError.error != QJsonParseError::NoError) { 0144 qCWarning(Log) << parseError.errorString() << data; 0145 0146 // try again after attempting to fix SBB's creative JSON 0147 auto s = QString::fromUtf8(data); 0148 s.replace(QRegularExpression(QStringLiteral("([a-zI]+)\\s*:")), QStringLiteral("\"\\1\":")); 0149 doc = QJsonDocument::fromJson(s.toUtf8(), &parseError); 0150 qDebug() << parseError.errorString(); 0151 } 0152 //qDebug().noquote() << doc.toJson(); 0153 const auto stops = doc.object().value(QLatin1String("stops")).toArray(); 0154 std::vector<Location> res; 0155 res.reserve(stops.size()); 0156 for (const auto &stop : stops) { 0157 const auto obj = stop.toObject(); 0158 Location loc; 0159 setLocationIdentifier(loc, obj.value(QLatin1String("extId")).toString()); 0160 loc.setName(obj.value(QLatin1String("name")).toString()); 0161 loc.setLatitude(obj.value(QLatin1String("y")).toString().toInt() / 1000000.0); 0162 loc.setLongitude(obj.value(QLatin1String("x")).toString().toInt() / 1000000.0); 0163 res.push_back(loc); 0164 } 0165 return res; 0166 } 0167 0168 static QByteArray gzipDecompress(const QByteArray &data) 0169 { 0170 QByteArray rawData; 0171 z_stream stream; 0172 unsigned char buffer[1024]; 0173 0174 stream.zalloc = nullptr; 0175 stream.zfree = nullptr; 0176 stream.opaque = nullptr; 0177 stream.avail_in = data.size(); 0178 stream.next_in = reinterpret_cast<unsigned char*>(const_cast<char*>(data.data())); 0179 0180 auto ret = inflateInit2(&stream, 15 + 32); // see docs, the magic numbers enable gzip decoding 0181 if (ret != Z_OK) { 0182 qCWarning(Log) << "Failed to initialize zlib stream."; 0183 return {}; 0184 } 0185 0186 do { 0187 stream.avail_out = sizeof(buffer); 0188 stream.next_out = buffer; 0189 0190 ret = inflate(&stream, Z_NO_FLUSH); 0191 if (ret != Z_OK && ret != Z_STREAM_END) { 0192 qCWarning(Log) << "Zlib decoding failed!" << ret; 0193 break; 0194 } 0195 0196 rawData.append(reinterpret_cast<char*>(buffer), sizeof(buffer) - stream.avail_out); 0197 } while (stream.avail_out == 0); 0198 inflateEnd(&stream); 0199 0200 return rawData; 0201 } 0202 0203 static QDateTime parseDateTime(const QDate &baseDate, uint16_t time) 0204 { 0205 if (time == 0xffff) { // value is unset 0206 return {}; 0207 } 0208 0209 const auto days = time / 2400; 0210 const auto hours = (time / 100) % 24; 0211 const auto mins = time % 100; 0212 0213 auto dt = QDateTime(baseDate, QTime(hours, mins)); 0214 return dt.addDays(days); 0215 } 0216 0217 std::vector<Journey> HafasQueryParser::parseQueryJourneyResponse(const QByteArray &data) 0218 { 0219 #if Q_BYTE_ORDER == Q_BIG_ENDIAN 0220 #warning Hafas binary response parsing not implemented on big endian architectures! 0221 Q_UNUSED(data); 0222 return {}; 0223 #endif 0224 clearErrorState(); 0225 0226 // yes, this is gzip compressed rather than using the HTTP compression transparently... 0227 const auto rawData = gzipDecompress(data); 0228 0229 if (rawData.size() < (int)sizeof(HafasJourneyResponseHeader)) { 0230 qCWarning(Log) << "Response too small for header structure."; 0231 return {}; 0232 } 0233 const auto header = reinterpret_cast<const HafasJourneyResponseHeader*>(rawData.constData()); 0234 qDebug() << header->version << header->numJourneys; 0235 0236 if (rawData.size() < (int)(sizeof(HafasJourneyResponseExtendedHeader) + header->extendedHeaderOffset)) { 0237 qCWarning(Log) << "Response too short for extended header structure."; 0238 return {}; 0239 } 0240 const auto extHeader = reinterpret_cast<const HafasJourneyResponseExtendedHeader*>(rawData.constData() + header->extendedHeaderOffset); 0241 if (extHeader->length < (int)sizeof(HafasJourneyResponseExtendedHeader)) { 0242 qCWarning(Log) << "Extended header is shorter than expected" << extHeader->length; 0243 return {}; 0244 } 0245 if (extHeader->errorCode != 0) { 0246 qCDebug(Log) << "Journey query returned error" << extHeader->errorCode; 0247 // TODO we could distinguish between not found and service errors here 0248 m_error = Reply::NotFoundError; 0249 return {}; 0250 } 0251 0252 const auto detailsHeader = reinterpret_cast<const HafasJourneyResponseDetailsHeader*>(rawData.constData() + extHeader->detailsOffset); 0253 qDebug() << "details header:" << detailsHeader->version; 0254 0255 HafasJourneyResponseStringTable stringTable(rawData, header->stringTableOffset, extHeader->encodingStr); 0256 QDate baseDate(1979, 12, 31); 0257 baseDate = baseDate.addDays(header->date); 0258 0259 std::vector<Journey> journeys; 0260 journeys.reserve(header->numJourneys); 0261 for (int journeyIdx = 0; journeyIdx < header->numJourneys; ++journeyIdx) { 0262 const auto journeyInfo = reinterpret_cast<const HafasJourneyResponseJourney*>(rawData.constData() + sizeof(HafasJourneyResponseHeader) + journeyIdx * sizeof(HafasJourneyResponseJourney)); 0263 qDebug() << "section count: " << journeyInfo->numSections; 0264 0265 const auto journeyDetailsOffset = *reinterpret_cast<const uint16_t*>(rawData.constData() 0266 + extHeader->detailsOffset 0267 + detailsHeader->journeyDetailsIndexOffset 0268 + journeyIdx * sizeof(uint16_t)); 0269 0270 std::vector<JourneySection> sections; 0271 sections.reserve(journeyInfo->numSections); 0272 for (int sectionIdx = 0; sectionIdx < journeyInfo->numSections; ++sectionIdx) { 0273 const auto sectionInfo = reinterpret_cast<const HafasJourneyResponseSection*>(rawData.constData() 0274 + sizeof(HafasJourneyResponseHeader) 0275 + journeyInfo->sectionsOffset 0276 + sectionIdx * sizeof(HafasJourneyResponseSection)); 0277 qDebug() << stringTable.lookup(sectionInfo->lineNameStr) << sectionInfo->type; 0278 0279 Location from; 0280 const auto fromInfo = reinterpret_cast<const HafasJourneyResponseStation*>(rawData.constData() 0281 + header->stationTableOffset 0282 + sectionInfo->departureStationIdx * sizeof(HafasJourneyResponseStation)); 0283 from.setName(stringTable.lookup(fromInfo->nameStr)); 0284 from.setLatitude(fromInfo->latitude / 1000000.0); 0285 from.setLongitude(fromInfo->longitude / 1000000.0); 0286 setLocationIdentifier(from, QString::number(fromInfo->id)); 0287 0288 Location to; 0289 const auto toInfo = reinterpret_cast<const HafasJourneyResponseStation*>(rawData.constData() 0290 + header->stationTableOffset 0291 + sectionInfo->arrivalStationIdx * sizeof(HafasJourneyResponseStation)); 0292 to.setName(stringTable.lookup(toInfo->nameStr)); 0293 to.setLatitude(toInfo->latitude / 1000000.0); 0294 to.setLongitude(toInfo->longitude / 1000000.0); 0295 setLocationIdentifier(to, QString::number(toInfo->id)); 0296 0297 JourneySection section; 0298 section.setFrom(from); 0299 section.setTo(to); 0300 0301 section.setScheduledDepartureTime(parseDateTime(baseDate, sectionInfo->scheduledDepartureTime)); 0302 section.setScheduledArrivalTime(parseDateTime(baseDate, sectionInfo->scheduledArrivalTime)); 0303 0304 const auto journeyAttrIndex = *reinterpret_cast<const uint16_t*>(rawData.constData() 0305 + extHeader->journeyAttributesIndexOffset + journeyIdx * sizeof(uint16_t)); 0306 auto attr = HafasJourneyResponse::attribute(rawData.constData(), extHeader, journeyAttrIndex); 0307 while (!attr->atEnd()) { 0308 qDebug() << "journey attr" << stringTable.lookup(attr->keyStr) << stringTable.lookup(attr->valueStr); 0309 ++attr; 0310 } 0311 0312 if (sectionInfo->type == HafasJourneyResponseSectionMode::PublicTransport) { 0313 Route route; 0314 Line line; 0315 line.setName(stringTable.lookup(sectionInfo->lineNameStr).trimmed()); 0316 0317 auto attr = HafasJourneyResponse::attribute(rawData.constData(), extHeader, sectionInfo->sectionAttributeIndex); 0318 while (!attr->atEnd()) { 0319 qDebug() << "section attr" << stringTable.lookup(attr->keyStr) << stringTable.lookup(attr->valueStr); 0320 const auto key = stringTable.lookup(attr->keyStr); 0321 if (key == QLatin1String("Direction")) { 0322 const auto dir = stringTable.lookup(attr->valueStr); 0323 if (dir != QLatin1String("---")) { 0324 route.setDirection(dir.trimmed()); 0325 } 0326 } else if (key == QLatin1String("Class")) { 0327 line.setMode(parseLineMode(stringTable.lookup(attr->valueStr))); 0328 } 0329 ++attr; 0330 } 0331 0332 const auto *commentPtr = reinterpret_cast<const uint16_t*>(rawData.constBegin() + header->commentTableOffset + sectionInfo->commentIdx); 0333 const auto commentCount = *commentPtr; 0334 for (int i = 0; i < commentCount; ++i) { 0335 ++commentPtr; 0336 // format: XX - <human readable comment>, where XX is two character code for the comment 0337 const auto note = stringTable.lookup(*commentPtr); 0338 if (note.size() > 5 && QStringView(note).mid(2, 3) == QLatin1String(" - ")) { 0339 section.addNote(note.mid(5)); 0340 } else { 0341 section.addNote(note); 0342 } 0343 } 0344 0345 route.setLine(line); 0346 section.setRoute(route); 0347 section.setMode(JourneySection::PublicTransport); 0348 section.setScheduledDeparturePlatform(stringTable.lookup(sectionInfo->scheduledDeparturePlatformStr)); 0349 section.setScheduledArrivalPlatform(stringTable.lookup(sectionInfo->scheduledArrivalPlatformStr)); 0350 0351 const auto sectionDetail = reinterpret_cast<const HafasJourneyResponseSectionDetail*>(rawData.constData() 0352 + extHeader->detailsOffset 0353 + journeyDetailsOffset 0354 + detailsHeader->sectionDetailsOffset 0355 + detailsHeader->sectionDetailsSize * sectionIdx); 0356 qDebug() << "section detail" << sectionDetail->expectedArrivalPlatformStr << sectionDetail->expectedDeparturePlatformStr; 0357 section.setExpectedDeparturePlatform(stringTable.lookup(sectionDetail->expectedDeparturePlatformStr)); 0358 section.setExpectedArrivalPlatform(stringTable.lookup(sectionDetail->expectedArrivalPlatformStr)); 0359 0360 section.setExpectedDepartureTime(parseDateTime(baseDate, sectionDetail->expectedDepartureTime)); 0361 section.setExpectedArrivalTime(parseDateTime(baseDate, sectionDetail->expectedArrivalTime)); 0362 0363 std::vector<Stopover> stops; 0364 stops.reserve(sectionDetail->numStops); 0365 for (int i = 0; i < sectionDetail->numStops; ++i) { 0366 const auto stopInfo = reinterpret_cast<const HafasJourneyResponseStop*>(rawData.constData() + extHeader->detailsOffset + detailsHeader->stopsOffset + i * detailsHeader->stopsSize); 0367 0368 Location loc; 0369 const auto locInfo = reinterpret_cast<const HafasJourneyResponseStation*>(rawData.constData() 0370 + header->stationTableOffset 0371 + stopInfo->stationIdx * sizeof(HafasJourneyResponseStation)); 0372 loc.setName(stringTable.lookup(locInfo->nameStr)); 0373 loc.setLatitude(locInfo->latitude / 1000000.0); 0374 loc.setLongitude(locInfo->longitude / 1000000.0); 0375 0376 Stopover stop; 0377 stop.setStopPoint(loc); 0378 stop.setScheduledArrivalTime(parseDateTime(baseDate, stopInfo->scheduledArrivalTime)); 0379 stop.setScheduledDepartureTime(parseDateTime(baseDate, stopInfo->scheduledDepartureTime)); 0380 stop.setExpectedArrivalTime(parseDateTime(baseDate, stopInfo->expectedArrivalTime)); 0381 stop.setExpectedDepartureTime(parseDateTime(baseDate, stopInfo->expectedDepartureTime)); 0382 stop.setScheduledPlatform(stringTable.lookup(stopInfo->scheduledDeparturePlatformStr)); 0383 stop.setExpectedPlatform(stringTable.lookup(stopInfo->expectedDeparturePlatformStr)); 0384 0385 stops.push_back(stop); 0386 } 0387 section.setIntermediateStops(std::move(stops)); 0388 0389 } else if (sectionInfo->type == HafasJourneyResponseSectionMode::Walk) { 0390 section.setMode(JourneySection::Walking); 0391 } else if (sectionInfo->type == HafasJourneyResponseSectionMode::Transfer1 || sectionInfo->type == HafasJourneyResponseSectionMode::Transfer2) { 0392 section.setMode(JourneySection::Transfer); 0393 } else { 0394 qCWarning(Log) << "Unknown section mode:" << sectionInfo->type; 0395 } 0396 0397 sections.push_back(section); 0398 } 0399 0400 const auto disruptionTable = HafasJourneyResponse::disruptionTable(rawData.constData(), extHeader); 0401 auto disruption = disruptionTable->firstDisruptionForJourney(journeyIdx); 0402 while (disruption) { 0403 qDebug() << "disruption" << stringTable.lookup(disruption->idStr) << stringTable.lookup(disruption->titleStr) << stringTable.lookup(disruption->messageStr) 0404 << stringTable.lookup(disruption->startStr) << stringTable.lookup(disruption->endStr) << disruption->bitmask << disruption->section; 0405 sections[disruption->section].addNote(stringTable.lookup(disruption->titleStr)); 0406 sections[disruption->section].addNote(stringTable.lookup(disruption->messageStr)); 0407 0408 auto attr = HafasJourneyResponse::attribute(rawData.constData(), extHeader, disruption->disruptionAttributeIndex); 0409 while (!attr->atEnd()) { 0410 qDebug() << "disruption attr" << stringTable.lookup(attr->keyStr) << stringTable.lookup(attr->valueStr); 0411 ++attr; 0412 } 0413 0414 disruption = disruption->next(disruptionTable); 0415 } 0416 0417 Journey journey; 0418 journey.setSections(std::move(sections)); 0419 journeys.push_back(journey); 0420 } 0421 0422 return journeys; 0423 }