File indexing completed on 2024-05-12 04:42:38
0001 /* 0002 SPDX-FileCopyrightText: 2024 Volker Krause <vkrause@kde.org> 0003 SPDX-License-Identifier: LGPL-2.0-or-later 0004 */ 0005 0006 #include "motisparser.h" 0007 0008 #include <KPublicTransport/Journey> 0009 #include <KPublicTransport/Location> 0010 #include <KPublicTransport/Stopover> 0011 0012 #include <QDebug> 0013 #include <QJsonArray> 0014 #include <QJsonDocument> 0015 #include <QJsonObject> 0016 #include <QUrl> 0017 0018 using namespace KPublicTransport; 0019 using namespace Qt::Literals::StringLiterals; 0020 0021 MotisParser::MotisParser(QString locIdentifierType) 0022 : m_locIdentifierType(std::move(locIdentifierType)) 0023 { 0024 } 0025 0026 bool MotisParser::hasError() const 0027 { 0028 return !m_errorMsg.isEmpty(); 0029 } 0030 0031 QString MotisParser::errorMessage() const 0032 { 0033 return m_errorMsg; 0034 } 0035 0036 std::vector<Journey> MotisParser::parseConnections(const QByteArray &data) 0037 { 0038 const auto content = parseContent(data); 0039 if (hasError()) { 0040 return {}; 0041 } 0042 const auto connections = content.value("connections"_L1).toArray(); 0043 0044 std::vector<Journey> journeys; 0045 journeys.reserve(connections.size()); 0046 0047 for (const auto &conV :connections) { 0048 journeys.push_back(parseConnection(conV.toObject())); 0049 } 0050 0051 return journeys; 0052 } 0053 0054 [[nodiscard]] static QDateTime scheduledTime(const QJsonObject &eventInfo) 0055 { 0056 const auto ts = eventInfo.value("schedule_time"_L1).toInteger(); 0057 return ts ? QDateTime::fromSecsSinceEpoch(ts) : QDateTime(); 0058 } 0059 0060 [[nodiscard]] static QDateTime expectedTime(const QJsonObject &eventInfo) 0061 { 0062 const auto reason = eventInfo.value("reason"_L1).toString(); 0063 if (reason == "SCHEDULE"_L1 || reason == "REPAIR"_L1) { 0064 return {}; 0065 } 0066 const auto ts = eventInfo.value("time"_L1).toInteger(); 0067 return ts ? QDateTime::fromSecsSinceEpoch(ts) : QDateTime(); 0068 } 0069 0070 [[nodiscard]] static QString expectedPlatform(const QJsonObject &eventInfo) 0071 { 0072 if (eventInfo.value("reason"_L1).toString() == "SCHEDULE"_L1) { 0073 return {}; 0074 } 0075 return eventInfo.value("track"_L1).toString(); 0076 } 0077 0078 // see https://motis-project.de/docs/api/connection.html#transport 0079 constexpr inline Line::Mode clasz_map[] = { 0080 Line::Air, // 0 flights 0081 Line::LongDistanceTrain, // 1 long distance high speed trains (e.g. TGV) 0082 Line::LongDistanceTrain, // 2 long distance inter city trains 0083 Line::Coach, // 3 long distance buses 0084 Line::LongDistanceTrain, // 4 long distance night trains 0085 Line::LocalTrain, // 5 regional express trains 0086 Line::LocalTrain, // 6 regional trains 0087 Line::RapidTransit, // 7 metro trains 0088 Line::Metro, // 8 subway trains 0089 Line::Tramway, // 9 trams 0090 Line::Bus, // 10 buses 0091 Line::Ferry, // 11 ships/ferries 0092 Line::Taxi, // 12 other (taxis, etc.) 0093 }; 0094 0095 // TODO do we generally need this, or can this eventually be fixed/normalized server-side? 0096 static void postprocessRoute(Route &route) 0097 { 0098 auto l = route.line(); 0099 if (l.mode() == Line::Metro && l.name().endsWith(" 0"_L1)) { 0100 l.setName(l.name().left(l.name().size() - 2)); 0101 } 0102 if (l.mode() == Line::Tramway && l.name().startsWith("Tram "_L1)) { 0103 l.setName(l.name().mid(5)); 0104 } 0105 while (l.mode() == Line::LongDistanceTrain && l.name().startsWith("ICE 0"_L1)) { 0106 l.setName(l.name().remove(4, 1)); 0107 } 0108 while (l.mode() == Line::LongDistanceTrain && l.name().startsWith("IC 0"_L1)) { 0109 l.setName(l.name().remove(3, 1)); 0110 } 0111 route.setLine(l); 0112 } 0113 0114 [[nodiscard]] static Route parseRoute(const QJsonObject &obj) 0115 { 0116 Line line; 0117 line.setName(obj.value("name"_L1).toString()); // TODO use category_name and line_id instead? 0118 const auto clasz = obj.value("clasz"_L1).toInt(); 0119 if (clasz >= 0 && clasz < (int)std::size(clasz_map)) { 0120 line.setMode(clasz_map[clasz]); 0121 } 0122 Route route; 0123 route.setDirection(obj.value("direction"_L1).toString()); 0124 const auto trainNr = obj.value("train_nr"_L1).toInt(); 0125 if (trainNr) { // TODO skip is already in line name? 0126 route.setName(QString::number(trainNr)); 0127 } 0128 route.setLine(line); 0129 postprocessRoute(route); 0130 return route; 0131 } 0132 0133 Journey MotisParser::parseConnection(const QJsonObject &con) const 0134 { 0135 const auto stopsA = con.value("stops"_L1).toArray(); 0136 std::vector<Stopover> stops; 0137 stops.reserve(stopsA.size()); 0138 for (const auto &stopV : stopsA) { 0139 const auto stopObj = stopV.toObject(); 0140 Stopover stop; 0141 stop.setStopPoint(parseStation(stopObj.value("station"_L1).toObject())); 0142 if (const auto dep = stopObj.value("departure"_L1).toObject(); dep.value("valid"_L1).toBool()) { 0143 stop.setScheduledDepartureTime(scheduledTime(dep)); 0144 stop.setExpectedDepartureTime(expectedTime(dep)); 0145 if (const auto p = dep.value("schedule_track"_L1).toString(); !p.isEmpty()) { 0146 stop.setScheduledPlatform(p); 0147 } 0148 if (const auto p = expectedPlatform(dep); !p.isEmpty()) { 0149 stop.setExpectedPlatform(p); 0150 } 0151 } 0152 if (const auto arr = stopObj.value("arrival"_L1).toObject(); arr.value("valid"_L1).toBool()) { 0153 stop.setScheduledArrivalTime(scheduledTime(arr)); 0154 stop.setExpectedArrivalTime(expectedTime(arr)); 0155 if (const auto p = arr.value("schedule_track"_L1).toString(); !p.isEmpty() && stop.scheduledPlatform().isEmpty()) { 0156 stop.setScheduledPlatform(p); 0157 } 0158 if (const auto p = expectedPlatform(arr); !p.isEmpty() && stop.expectedPlatform().isEmpty()) { 0159 stop.setExpectedPlatform(p); 0160 } 0161 } 0162 stops.push_back(std::move(stop)); 0163 } 0164 0165 // TODO parse notes and attributes 0166 0167 const auto transports = con.value("transports"_L1).toArray(); 0168 std::vector<JourneySection> sections; 0169 sections.reserve(transports.size()); 0170 for (const auto &transportV :transports) { 0171 const auto transport = transportV.toObject(); 0172 const auto move = transport.value("move"_L1).toObject(); 0173 const auto range = move.value("range"_L1).toObject(); 0174 const auto from = range.value("from"_L1).toInteger(); 0175 const auto to = range.value("to"_L1).toInteger(); 0176 if (from < 0 || from >= (qint64)stops.size() || to < 0 || to >= (qint64)stops.size() || to <= from) { 0177 qWarning() << "invalid range indices!"; 0178 return {}; 0179 } 0180 0181 JourneySection sec; 0182 sec.setDeparture(stops[from]); 0183 sec.setArrival(stops[to]); 0184 if (to - from > 1) { 0185 std::vector<Stopover> intermediateStops; 0186 intermediateStops.reserve(to - from); 0187 std::copy(stops.begin() + from + 1, stops.begin() + to, std::back_inserter(intermediateStops)); 0188 sec.setIntermediateStops(std::move(intermediateStops)); 0189 } 0190 0191 const auto moveType = transport.value("move_type"_L1).toString(); 0192 if (moveType == "Transport"_L1) { 0193 sec.setMode(JourneySection::PublicTransport); 0194 } else if (moveType == "Walk"_L1) { 0195 const auto mumoType = move.value("mumo_type"_L1).toString(); 0196 if (mumoType == "bike"_L1) { 0197 sec.setMode(JourneySection::IndividualTransport); 0198 sec.setIndividualTransport(IndividualTransport(IndividualTransport::Bike)); 0199 } else if (mumoType == "car"_L1) { 0200 sec.setMode(JourneySection::IndividualTransport); 0201 sec.setIndividualTransport(IndividualTransport(IndividualTransport::Car)); 0202 } else { 0203 sec.setMode(JourneySection::Walking); 0204 } 0205 } else { 0206 qWarning() << "unknown move_type" << moveType; 0207 } 0208 0209 if (sec.mode() == JourneySection::PublicTransport) { 0210 sec.setRoute(parseRoute(move)); 0211 } 0212 0213 sections.push_back(std::move(sec)); 0214 } 0215 0216 Journey jny; 0217 jny.setSections(std::move(sections)); 0218 return jny; 0219 } 0220 0221 std::vector<Stopover> MotisParser::parseEvents(const QByteArray &data) 0222 { 0223 const auto content = parseContent(data); 0224 if (hasError()) { 0225 return {}; 0226 } 0227 0228 const auto stopPoint = parseStation(content.value("station"_L1).toObject()); 0229 0230 const auto events = content.value("events"_L1).toArray(); 0231 std::vector<Stopover> result; 0232 std::vector<QString> ids; 0233 for (const auto &eventV : events) { 0234 const auto event = eventV.toObject(); 0235 0236 Stopover stop; 0237 stop.setStopPoint(stopPoint); 0238 const auto ev = event.value("event"_L1).toObject(); 0239 if (!ev.value("valid"_L1).toBool()) { 0240 continue; 0241 } 0242 const auto type = event.value("type"_L1).toString(); 0243 if (type == "DEP"_L1) { 0244 stop.setScheduledDepartureTime(scheduledTime(ev)); 0245 stop.setExpectedDepartureTime(expectedTime(ev)); 0246 } else if (type == "ARR"_L1) { 0247 stop.setScheduledArrivalTime(scheduledTime(ev)); 0248 stop.setExpectedArrivalTime(expectedTime(ev)); 0249 } else { 0250 continue; 0251 } 0252 if (const auto p = ev.value("schedule_track"_L1).toString(); !p.isEmpty()) { 0253 stop.setScheduledPlatform(p); 0254 } 0255 if (const auto p = expectedPlatform(ev); !p.isEmpty()) { 0256 stop.setExpectedPlatform(p); 0257 } 0258 0259 const auto trips = event.value("trips"_L1).toArray(); 0260 if (trips.empty()) { 0261 continue; 0262 } 0263 const auto trip = trips.at(0).toObject(); 0264 stop.setRoute(parseRoute(trip.value("transport"_L1).toObject())); 0265 // TODO what's in the id block next to transport? train_nr and destination stop ids seem relevant? 0266 0267 // merge disjoint arrival/departure events 0268 const auto id = trip.value("id"_L1).toObject().value("id"_L1).toString(); 0269 if (auto it = std::find(ids.begin(), ids.end(), id); it != ids.end()) { 0270 result[std::distance(ids.begin(), it)] = Stopover::merge(result[std::distance(ids.begin(), it)], stop); 0271 } else { 0272 result.push_back(std::move(stop)); 0273 ids.push_back(id); 0274 } 0275 } 0276 0277 return result; 0278 } 0279 0280 std::vector<Location> MotisParser::parseStations(const QByteArray &data) 0281 { 0282 const auto content = parseContent(data); 0283 if (hasError()) { 0284 return {}; 0285 } 0286 0287 QJsonArray stations; 0288 if (content.contains("guesses"_L1)) { 0289 stations = content.value("guesses"_L1).toArray(); 0290 } else { 0291 stations = content.value("stations"_L1).toArray(); 0292 } 0293 0294 std::vector<Location> result; 0295 result.reserve(stations.size()); 0296 for (const auto &stationV : stations) { 0297 result.push_back(parseStation(stationV.toObject())); 0298 } 0299 0300 return result; 0301 } 0302 0303 Location MotisParser::parseStation(const QJsonObject &station) const 0304 { 0305 Location loc; 0306 loc.setType(Location::Stop); 0307 loc.setIdentifier(m_locIdentifierType, station.value("id"_L1).toString()); 0308 loc.setName(station.value("name"_L1).toString()); 0309 const auto pos = station.value("pos"_L1).toObject(); 0310 loc.setLatitude((float)pos.value("lat"_L1).toDouble()); 0311 loc.setLongitude((float)pos.value("lng"_L1).toDouble()); 0312 return loc; 0313 } 0314 0315 QJsonObject MotisParser::parseContent(const QByteArray &data) 0316 { 0317 const auto top = QJsonDocument::fromJson(data).object(); 0318 auto content = top.value("content"_L1).toObject(); 0319 0320 if (top.value("content_type"_L1).toString() == "MotisError"_L1) { 0321 m_errorMsg = content.value("reason"_L1).toString(); 0322 } 0323 0324 return content; 0325 }