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 }