File indexing completed on 2024-05-12 04:42:37
0001 /* 0002 SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "ivvassparser.h" 0008 #include "ivvassproductmap.h" 0009 #include "ifopt/ifoptutil.h" 0010 0011 #include <KPublicTransport/Equipment> 0012 #include <KPublicTransport/Journey> 0013 #include <KPublicTransport/Stopover> 0014 0015 #include <QByteArray> 0016 #include <QDebug> 0017 #include <QJsonArray> 0018 #include <QJsonDocument> 0019 #include <QJsonObject> 0020 #include <QRegularExpression> 0021 0022 #include <cmath> 0023 0024 using namespace KPublicTransport; 0025 0026 IvvAssParser::IvvAssParser(const QTimeZone &tz, const QString &locationIdentifier) 0027 : m_timezone(tz) 0028 , m_locationIdentifier(locationIdentifier) 0029 { 0030 } 0031 0032 IvvAssParser::LocationData IvvAssParser::parseLocation(const QJsonObject &stopObj) const 0033 { 0034 LocationData r; 0035 r.loc.setLatitude(stopObj.value(QLatin1String("lat")).toDouble(NAN)); 0036 r.loc.setLongitude(stopObj.value(QLatin1String("lon")).toDouble(NAN)); 0037 if (!r.loc.hasCoordinate()) { 0038 // sic, x/y are swapped here! 0039 r.loc.setLatitude(stopObj.value(QLatin1String("x")).toDouble(NAN)); 0040 r.loc.setLongitude(stopObj.value(QLatin1String("y")).toDouble(NAN)); 0041 } 0042 0043 r.loc.setName(stopObj.value(QLatin1String("name")).toString()); 0044 if (r.loc.name().isEmpty()) { 0045 r.loc.setName(stopObj.value(QLatin1String("description")).toString()); 0046 } 0047 r.loc.setLocality(stopObj.value(QLatin1String("city")).toString()); 0048 if (r.loc.locality().isEmpty()) { 0049 r.loc.setLocality(stopObj.value(QLatin1String("municipality")).toString()); 0050 } 0051 0052 if (stopObj.value(QLatin1String("type")).toString() == QLatin1String("coordinate")) { 0053 return r; 0054 } 0055 0056 const auto id = stopObj.value(IfoptUtil::identifierType()).toString(); 0057 if (IfoptUtil::isValid(id)) { 0058 r.loc.setIdentifier(IfoptUtil::identifierType(), id); 0059 } else { 0060 r.loc.setIdentifier(m_locationIdentifier, id); 0061 } 0062 0063 // location is a platform: split out platform name 0064 if (stopObj.value(QLatin1String("subtype")).toString() == QLatin1String("Post")) { 0065 static QRegularExpression rx(QStringLiteral(R"((.*) \(((:?Gleis|Bahnsteig) .*)\)$)")); 0066 const auto match = rx.match(r.loc.name()); 0067 if (match.hasMatch()) { 0068 r.loc.setName(match.captured(1)); 0069 r.platform = match.captured(2); 0070 } 0071 } 0072 0073 r.loc.setType(Location::Stop); 0074 return r; 0075 } 0076 0077 std::vector<Location> IvvAssParser::parseLocations(const QByteArray &data) 0078 { 0079 const auto top = QJsonDocument::fromJson(data).object(); 0080 if (!parseError(top)) { 0081 return {}; 0082 } 0083 std::vector<Location> locs; 0084 0085 const auto stops = top.value(QLatin1String("stops")).toArray(); 0086 locs.reserve(stops.size()); 0087 for (const auto &stopV : stops) { 0088 const auto stopObj = stopV.toObject(); 0089 locs.push_back(parseLocation(stopObj).loc); 0090 } 0091 const auto objects = top.value(QLatin1String("objects")).toArray(); 0092 locs.reserve(locs.size() + objects.size()); 0093 for (const auto &objectV : objects) { 0094 const auto obj = objectV.toObject(); 0095 const auto type = obj.value(QLatin1String("type")).toString(); 0096 if (type != QLatin1String("stop")) { // poi, parking, etc 0097 continue; 0098 } 0099 locs.push_back(parseLocation(obj).loc); 0100 0101 const auto elevators = obj.value(QLatin1String("elevators")).toArray(); 0102 const auto escalators = obj.value(QLatin1String("escalators")).toArray(); 0103 locs.reserve(locs.size() + elevators.size() + escalators.size()); 0104 for (const auto &eleV : elevators) { 0105 const auto eleObj = eleV.toObject(); 0106 auto ele = parseLocation(eleObj).loc; 0107 Equipment eq; 0108 eq.setType(Equipment::Elevator); 0109 eq.setDisruptionEffect(eleObj.value(QLatin1String("status")).toString() == QLatin1String("active") ? Disruption::NormalService : Disruption::NoService); 0110 ele.setData(std::move(eq)); 0111 ele.setType(Location::Equipment); 0112 locs.push_back(std::move(ele)); 0113 } 0114 for (const auto &escV : escalators) { 0115 const auto escObj = escV.toObject(); 0116 auto esc = parseLocation(escObj).loc; 0117 Equipment eq; 0118 eq.setType(Equipment::Escalator); 0119 eq.setDisruptionEffect(escObj.value(QLatin1String("status")).toString() == QLatin1String("active") ? Disruption::NormalService : Disruption::NoService); 0120 esc.setData(std::move(eq)); 0121 esc.setType(Location::Equipment); 0122 locs.push_back(std::move(esc)); 0123 } 0124 } 0125 0126 const auto vehicles = top.value(QLatin1String("vehicles")).toArray(); 0127 locs.reserve(locs.size() + vehicles.size()); 0128 for (const auto &vehicleV : vehicles) { 0129 auto v = parseLocation(vehicleV.toObject()).loc; 0130 v.setType(Location::RentedVehicle); // TODO can we get vehicle types and networks from somewhere? 0131 locs.push_back(std::move(v)); 0132 } 0133 0134 return locs; 0135 } 0136 0137 static Route parseRoute(const QJsonObject &lineObj) 0138 { 0139 Line line; 0140 line.setName(lineObj.value(QLatin1String("number")).toString()); 0141 line.setMode(IvvAssProductMap::parseProduct(lineObj.value(QLatin1String("product")).toString())); 0142 0143 Route route; 0144 route.setLine(std::move(line)); 0145 route.setDirection(lineObj.value(QLatin1String("direction")).toString()); 0146 return route; 0147 } 0148 0149 static void applyTimeZone(QDateTime &dt, const QTimeZone &tz) 0150 { 0151 if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) == dt.offsetFromUtc()) { 0152 dt.setTimeZone(tz); 0153 } 0154 } 0155 0156 IvvAssParser::EventTime IvvAssParser::parseTime(const QJsonObject &obj, const char *baseKey, const char *scheduledKey) const 0157 { 0158 EventTime t; 0159 t.scheduled = QDateTime::fromString(obj.value(QLatin1String(scheduledKey)).toString(), Qt::ISODate); 0160 t.expected = QDateTime::fromString(obj.value(QLatin1String(baseKey)).toString(), Qt::ISODate); 0161 if (!t.scheduled.isValid() && t.expected.isValid()) { 0162 std::swap(t.scheduled, t.expected); 0163 } 0164 applyTimeZone(t.scheduled, m_timezone); 0165 applyTimeZone(t.expected, m_timezone); 0166 return t; 0167 } 0168 0169 std::vector<Stopover> IvvAssParser::parseStopovers(const QByteArray &data) 0170 { 0171 const auto top = QJsonDocument::fromJson(data).object(); 0172 if (!parseError(top)) { 0173 return {}; 0174 } 0175 const auto timetable = top.value(QLatin1String("timetable")).toArray(); 0176 0177 std::vector<Stopover> stopovers; 0178 for (const auto &timetableV : timetable) { 0179 const auto timetableObj = timetableV.toObject(); 0180 const auto stopObj = timetableObj.value(QLatin1String("stop")).toObject(); 0181 const auto stop = parseLocation(stopObj); 0182 0183 const auto events = timetableObj.value(QLatin1String("events")).toArray(); 0184 stopovers.reserve(stopovers.size() + events.size()); 0185 for (const auto &eventV : events) { 0186 const auto eventObj = eventV.toObject(); 0187 0188 Stopover s; 0189 s.setStopPoint(stop.loc); 0190 s.setScheduledPlatform(stop.platform); 0191 const auto t = parseTime(eventObj, "departure", "departureScheduled"); 0192 s.setScheduledDepartureTime(t.scheduled); 0193 s.setExpectedDepartureTime(t.expected); 0194 0195 const auto lineObj = eventObj.value(QLatin1String("line")).toObject(); 0196 s.setRoute(parseRoute(lineObj)); 0197 0198 const auto postObj = eventObj.value(QLatin1String("post")).toObject(); 0199 const auto postName = postObj.value(QLatin1String("name")).toString(); 0200 if (postName.startsWith(QLatin1Char('(')) && postName.endsWith(QLatin1Char(')'))) { 0201 s.setScheduledPlatform(postName.mid(1, postName.size() - 2)); 0202 } else { 0203 s.setScheduledPlatform(postName); 0204 } 0205 0206 stopovers.push_back(std::move(s)); 0207 } 0208 } 0209 0210 return stopovers; 0211 } 0212 0213 static std::vector<LoadInfo> parseDemand(const QJsonValue &demand) 0214 { 0215 if (!demand.isDouble()) { 0216 return {}; 0217 } 0218 0219 const auto d = demand.toInt(); 0220 if (d < 0 || d > 2) { 0221 return {}; 0222 } 0223 0224 static constexpr const Load::Category load_category_map[] = { Load::Low, Load::Medium, Load::High }; 0225 0226 LoadInfo l; 0227 l.setLoad(load_category_map[d]); 0228 return {l}; 0229 } 0230 0231 static Path parseDirections(Path &&fullPath, const QJsonArray &directions) 0232 { 0233 if (directions.isEmpty() || fullPath.isEmpty()) { 0234 return std::move(fullPath); 0235 } 0236 0237 const auto poly = fullPath.sections()[0].path(); 0238 0239 Path p; 0240 std::vector<PathSection> secs; 0241 0242 int prevSecStart = 0; 0243 PathSection prevSec; 0244 0245 for (const auto &dirV : directions) { 0246 const auto dirObj = dirV.toObject(); 0247 const auto lat = dirObj.value(QLatin1String("x")).toDouble(); 0248 const auto lon = dirObj.value(QLatin1String("y")).toDouble(); 0249 0250 int nextSecStart = prevSecStart; 0251 for (; nextSecStart < poly.size(); ++nextSecStart) { 0252 if (Location::distance(lat, lon, poly[nextSecStart].y(), poly[nextSecStart].x()) < 1.0f) { 0253 break; 0254 } 0255 } 0256 0257 if (nextSecStart > prevSecStart) { 0258 QPolygonF subPoly; 0259 subPoly.reserve(nextSecStart - prevSecStart + 1); 0260 std::copy(poly.begin() + prevSecStart, poly.begin() + nextSecStart, std::back_inserter(subPoly)); 0261 subPoly.push_back({lon, lat}); 0262 prevSec.setPath(std::move(subPoly)); 0263 secs.push_back(prevSec); 0264 } 0265 prevSecStart = nextSecStart; 0266 prevSec.setDescription(dirObj.value(QLatin1String("street")).toString()); 0267 } 0268 if (poly.size() > prevSecStart) { 0269 QPolygonF subPoly; 0270 subPoly.reserve(poly.size() - prevSecStart); 0271 std::copy(poly.begin() + prevSecStart, poly.end(), std::back_inserter(subPoly)); 0272 prevSec.setPath(std::move(subPoly)); 0273 secs.push_back(prevSec); 0274 } 0275 0276 p.setSections(std::move(secs)); 0277 return p; 0278 } 0279 0280 std::vector<Journey> IvvAssParser::parseJourneys(const QByteArray &data) 0281 { 0282 const auto top = QJsonDocument::fromJson(data).object(); 0283 if (!parseError(top)) { 0284 return {}; 0285 } 0286 const auto routes = top.value(QLatin1String("routes")).toArray(); 0287 0288 std::vector<Journey> journeys; 0289 journeys.reserve(routes.size()); 0290 for (const auto &routeV : routes) { 0291 const auto routeObj = routeV.toObject(); 0292 Journey journey; 0293 0294 const auto segments = routeObj.value(QLatin1String("segments")).toArray(); 0295 std::vector<JourneySection> sections; 0296 sections.reserve(segments.size()); 0297 for (const auto &segmentV : segments) { 0298 const auto segmentObj = segmentV.toObject(); 0299 JourneySection s; 0300 0301 const auto origin = parseLocation(segmentObj.value(QLatin1String("origin")).toObject()); 0302 s.setFrom(origin.loc); 0303 s.setScheduledDeparturePlatform(origin.platform); 0304 const auto dest = parseLocation(segmentObj.value(QLatin1String("destination")).toObject()); 0305 s.setTo(dest.loc); 0306 s.setScheduledArrivalPlatform(dest.platform); 0307 0308 const auto dt = parseTime(segmentObj, "departure", "departureScheduled"); 0309 s.setScheduledDepartureTime(dt.scheduled); 0310 s.setExpectedDepartureTime(dt.expected); 0311 const auto at = parseTime(segmentObj, "arrival", "arrivalScheduled"); 0312 s.setScheduledArrivalTime(at.scheduled); 0313 s.setExpectedArrivalTime(at.expected); 0314 0315 const auto type = segmentObj.value(QLatin1String("type")).toString(); 0316 // TODO "publicTransport" 0317 s.setMode(type == QLatin1String("walk") ? JourneySection::Walking : JourneySection::PublicTransport); 0318 0319 if (s.mode() == JourneySection::PublicTransport) { 0320 const auto lineObj = segmentObj.value(QLatin1String("line")).toObject(); 0321 s.setRoute(parseRoute(lineObj)); 0322 } 0323 0324 const auto vias = segmentObj.value(QLatin1String("vias")).toArray(); 0325 std::vector<Stopover> intermediateStops; 0326 intermediateStops.reserve(vias.size()); 0327 for (const auto &viaV : vias) { 0328 const auto viaObj = viaV.toObject(); 0329 Stopover stop; 0330 const auto viaLoc = parseLocation(viaObj); 0331 stop.setStopPoint(viaLoc.loc); 0332 stop.setScheduledPlatform(viaLoc.platform); 0333 0334 const auto dt = parseTime(viaObj, "departure", "departureScheduled"); 0335 stop.setScheduledDepartureTime(dt.scheduled); 0336 stop.setExpectedDepartureTime(dt.expected); 0337 const auto at = parseTime(viaObj, "arrival", "arrivalScheduled"); 0338 stop.setScheduledArrivalTime(at.scheduled); 0339 stop.setExpectedArrivalTime(at.expected); 0340 0341 const auto demand = viaObj.value(QLatin1String("demandEstimated")); 0342 stop.setLoadInformation(parseDemand(demand)); 0343 0344 intermediateStops.push_back(std::move(stop)); 0345 } 0346 s.setIntermediateStops(std::move(intermediateStops)); 0347 0348 s.setDistance(segmentObj.value(QLatin1String("distance")).toInt()); 0349 const auto polygon = segmentObj.value(QLatin1String("polygon")).toString(); 0350 QPolygonF poly; 0351 const auto coords = QStringView(polygon).split(QLatin1Char(' '), Qt::SkipEmptyParts); 0352 poly.reserve(coords.size()); 0353 for (const auto &coord : coords) { 0354 const auto p = coord.split(QLatin1Char(',')); 0355 if (p.size() == 2) { 0356 poly.push_back({p[1].toDouble(), p[0].toDouble()}); 0357 } 0358 } 0359 PathSection section; 0360 section.setPath(poly); 0361 Path path; 0362 path.setSections({section}); 0363 s.setPath(parseDirections(std::move(path), segmentObj.value(QLatin1String("directions")).toArray())); 0364 0365 const auto highestDemand = segmentObj.value(QLatin1String("highestDemandEstimated")); 0366 s.setLoadInformation(parseDemand(highestDemand)); 0367 0368 const auto infos = segmentObj.value(QLatin1String("infos")).toArray(); 0369 for (const auto &infoV : infos) { 0370 const auto infoObj = infoV.toObject(); 0371 s.addNote(infoObj.value(QLatin1String("text")).toString()); 0372 } 0373 0374 sections.push_back(std::move(s)); 0375 } 0376 0377 journey.setSections(std::move(sections)); 0378 journeys.push_back(std::move(journey)); 0379 } 0380 0381 return journeys; 0382 } 0383 0384 bool IvvAssParser::parseError(const QJsonObject &top) 0385 { 0386 errorMessage = top.value(QLatin1String("error")).toString(); 0387 return errorMessage.isEmpty(); 0388 }