File indexing completed on 2024-12-29 04:51:01
0001 /* 0002 SPDX-FileCopyrightText: 2018-2021 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "uic9183documentprocessor.h" 0008 0009 #include <KItinerary/ExtractorResult> 0010 #include <KItinerary/ExtractorValidator> 0011 #include <KItinerary/JsonLdDocument> 0012 #include <KItinerary/Rct2Ticket> 0013 #include <KItinerary/Reservation> 0014 #include <KItinerary/Uic9183Parser> 0015 #include <KItinerary/Uic9183TicketLayout> 0016 #include <KItinerary/Ticket> 0017 #include <KItinerary/TrainTrip> 0018 0019 #include "era/fcbticket.h" 0020 #include "era/fcbutil.h" 0021 #include "uic9183/uic9183head.h" 0022 #include "uic9183/vendor0080block.h" 0023 0024 #include <KLocalizedString> 0025 0026 #include <QDateTime> 0027 #include <QJsonArray> 0028 #include <QJsonObject> 0029 0030 using namespace KItinerary; 0031 0032 Uic9183DocumentProcessor::Uic9183DocumentProcessor() 0033 { 0034 qRegisterMetaType<KItinerary::Uic9183TicketLayoutField>(); 0035 qRegisterMetaType<KItinerary::Vendor0080BLOrderBlock>(); 0036 } 0037 0038 bool Uic9183DocumentProcessor::canHandleData(const QByteArray &encodedData, [[maybe_unused]] QStringView fileName) const 0039 { 0040 return Uic9183Parser::maybeUic9183(encodedData); 0041 } 0042 0043 ExtractorDocumentNode Uic9183DocumentProcessor::createNodeFromData(const QByteArray &encodedData) const 0044 { 0045 Uic9183Parser p; 0046 p.parse(encodedData); 0047 if (!p.isValid()) { 0048 return {}; 0049 } 0050 0051 ExtractorDocumentNode node; 0052 node.setContent(p); 0053 return node; 0054 } 0055 0056 void Uic9183DocumentProcessor::expandNode(ExtractorDocumentNode &node, [[maybe_unused]] const ExtractorEngine *engine) const 0057 { 0058 // only use the U_HEAD issuing time as context if we have nothing better 0059 // while that is usually correct it cannot contain a time zone, unlike the (often) enclosing PDF document´ 0060 if (!node.contextDateTime().isValid()) { 0061 const auto p = node.content<Uic9183Parser>(); 0062 if (const auto u_flex = p.findBlock<Fcb::UicRailTicketData>(); u_flex.isValid()) { 0063 node.setContextDateTime(u_flex.issuingDetail.issueingDateTime()); 0064 } else if (const auto u_head = p.findBlock<Uic9183Head>(); u_head.isValid()) { 0065 node.setContextDateTime(u_head.issuingDateTime()); 0066 } 0067 } 0068 } 0069 0070 static ProgramMembership extractCustomerCard(const Fcb::CardReferenceType &card) 0071 { 0072 ProgramMembership p; 0073 p.setProgramName(card.cardName); 0074 if (card.cardIdNumIsSet()) { 0075 p.setMembershipNumber(QString::number(card.cardIdNum)); 0076 } else if (card.cardIdIA5IsSet()) { 0077 p.setMembershipNumber(QString::fromUtf8(card.cardIdIA5)); 0078 } 0079 return p; 0080 } 0081 0082 static ProgramMembership extractCustomerCard(const QList <Fcb::TariffType> &tariffs) 0083 { 0084 // TODO what do we do with the (so far theoretical) case of multiple discount cards in use? 0085 for (const auto &tariff : tariffs) { 0086 for (const auto &card : tariff.reductionCard) { 0087 return extractCustomerCard(card); 0088 } 0089 } 0090 0091 return {}; 0092 } 0093 0094 static void fixFcbStationCode(TrainStation &station) 0095 { 0096 // UIC codes in Germany are wildly unreliable, there seem to be different 0097 // code tables in use by different operators, so we unfortunately have to ignore 0098 // those entirely 0099 if (station.identifier().startsWith(QLatin1StringView("uic:80"))) { 0100 PostalAddress addr; 0101 addr.setAddressCountry(QStringLiteral("DE")); 0102 station.setAddress(addr); 0103 station.setIdentifier(QString()); 0104 } 0105 } 0106 0107 void Uic9183DocumentProcessor::preExtract(ExtractorDocumentNode &node, [[maybe_unused]] const ExtractorEngine *engine) const 0108 { 0109 const auto p = node.content<Uic9183Parser>(); 0110 0111 Ticket ticket; 0112 ticket.setName(p.name()); 0113 ticket.setTicketToken(QLatin1StringView("aztecbin:") + 0114 QString::fromLatin1(p.rawData().toBase64())); 0115 Seat seat; 0116 if (const auto seatingType = p.seatingType(); !seatingType.isEmpty()) { 0117 seat.setSeatingType(seatingType); 0118 } 0119 0120 TrainReservation res; 0121 res.setReservationNumber(p.pnr()); 0122 res.setUnderName(p.person()); 0123 0124 ExtractorValidator validator; 0125 validator.setAcceptedTypes<TrainTrip>(); 0126 0127 QList<QVariant> results; 0128 0129 const auto rct2 = p.rct2Ticket(); 0130 if (rct2.isValid()) { 0131 TrainTrip trip, returnTrip; 0132 trip.setProvider(p.issuer()); 0133 0134 switch (rct2.type()) { 0135 case Rct2Ticket::Unknown: 0136 case Rct2Ticket::RailPass: 0137 break; 0138 case Rct2Ticket::Reservation: 0139 case Rct2Ticket::TransportReservation: 0140 { 0141 trip.setTrainNumber(rct2.trainNumber()); 0142 seat.setSeatSection(rct2.coachNumber()); 0143 seat.setSeatNumber(rct2.seatNumber()); 0144 [[fallthrough]]; 0145 } 0146 case Rct2Ticket::Transport: 0147 case Rct2Ticket::Upgrade: 0148 { 0149 trip.setDepartureStation(p.outboundDepartureStation()); 0150 trip.setArrivalStation(p.outboundArrivalStation()); 0151 0152 if (rct2.outboundDepartureTime().isValid()) { 0153 trip.setDepartureDay(rct2.outboundDepartureTime().date()); 0154 } else { 0155 trip.setDepartureDay(rct2.firstDayOfValidity()); 0156 } 0157 0158 if (rct2.outboundDepartureTime() != rct2.outboundArrivalTime()) { 0159 trip.setDepartureTime(rct2.outboundDepartureTime()); 0160 trip.setArrivalTime(rct2.outboundArrivalTime()); 0161 } 0162 0163 if (rct2.type() == Rct2Ticket::Transport && !p.returnDepartureStation().name().isEmpty()) { 0164 returnTrip.setProvider(p.issuer()); 0165 returnTrip.setDepartureStation(p.returnDepartureStation()); 0166 returnTrip.setArrivalStation(p.returnArrivalStation()); 0167 0168 if (rct2.returnDepartureTime().isValid()) { 0169 returnTrip.setDepartureDay(rct2.returnDepartureTime().date()); 0170 } else { 0171 returnTrip.setDepartureDay(rct2.firstDayOfValidity()); 0172 } 0173 0174 if (rct2.returnDepartureTime() != rct2.returnArrivalTime()) { 0175 returnTrip.setDepartureTime(rct2.returnDepartureTime()); 0176 returnTrip.setArrivalTime(rct2.returnArrivalTime()); 0177 } 0178 } 0179 0180 break; 0181 } 0182 } 0183 0184 if (const auto currency = rct2.currency(); !currency.isEmpty()) { 0185 res.setPriceCurrency(currency); 0186 res.setTotalPrice(rct2.price()); 0187 } 0188 0189 // provide names for typically "addon" tickets, so we can distinguish them in the UI 0190 switch (rct2.type()) { 0191 case Rct2Ticket::Reservation: 0192 ticket.setName(i18n("Reservation")); 0193 break; 0194 case Rct2Ticket::Upgrade: 0195 ticket.setName(i18n("Upgrade")); 0196 break; 0197 default: 0198 break; 0199 } 0200 0201 ticket.setTicketedSeat(seat); 0202 if (validator.isValidElement(trip)) { 0203 res.setReservationFor(trip); 0204 res.setReservedTicket(ticket); 0205 results.push_back(res); 0206 } 0207 if (validator.isValidElement(returnTrip)) { 0208 res.setReservationFor(returnTrip); 0209 res.setReservedTicket(ticket); 0210 results.push_back(res); 0211 } 0212 } 0213 0214 const auto fcb = p.findBlock<Fcb::UicRailTicketData>(); 0215 if (fcb.isValid()) { 0216 res.setPriceCurrency(QString::fromUtf8(fcb.issuingDetail.currency)); 0217 const auto issueDt = fcb.issuingDetail.issueingDateTime(); 0218 for (const auto &doc : fcb.transportDocument) { 0219 if (doc.ticket.userType() == qMetaTypeId<Fcb::ReservationData>()) { 0220 const auto irt = doc.ticket.value<Fcb::ReservationData>(); 0221 TrainTrip trip; 0222 trip.setProvider(p.issuer()); 0223 0224 TrainStation dep; 0225 dep.setName(irt.fromStationNameUTF8); 0226 dep.setIdentifier(FcbUtil::fromStationIdentifier(irt)); 0227 fixFcbStationCode(dep); 0228 trip.setDepartureStation(dep); 0229 0230 TrainStation arr; 0231 arr.setName(irt.toStationNameUTF8); 0232 arr.setIdentifier(FcbUtil::toStationIdentifier(irt)); 0233 fixFcbStationCode(arr); 0234 trip.setArrivalStation(arr); 0235 0236 trip.setDepartureTime(irt.departureDateTime(issueDt)); 0237 trip.setArrivalTime(irt.arrivalDateTime(issueDt)); 0238 0239 if (irt.trainNumIsSet()) { 0240 trip.setTrainNumber(irt.serviceBrandAbrUTF8 + QLatin1Char(' ') + QString::number(irt.trainNum)); 0241 } else { 0242 trip.setTrainNumber(irt.serviceBrandAbrUTF8 + QLatin1Char(' ') + QString::fromUtf8(irt.trainIA5)); 0243 } 0244 0245 Seat s; 0246 s.setSeatingType(FcbUtil::classCodeToString(irt.classCode)); 0247 if (irt.placesIsSet()) { 0248 s.setSeatSection(QString::fromUtf8(irt.places.coach)); 0249 QStringList l; 0250 for (const auto &b : irt.places.placeIA5) 0251 l.push_back(QString::fromUtf8(b)); 0252 for (auto i : irt.places.placeNum) 0253 l.push_back(QString::number(i)); 0254 s.setSeatNumber(l.join(QLatin1StringView(", "))); 0255 // TODO other seat encoding variants 0256 } 0257 0258 Ticket t(ticket); 0259 t.setTicketedSeat(s); 0260 res.setProgramMembershipUsed(extractCustomerCard(irt.tariffs)); 0261 0262 if (irt.priceIsSet()) { 0263 res.setTotalPrice(irt.price / std::pow(10, fcb.issuingDetail.currencyFract)); 0264 } 0265 0266 if (validator.isValidElement(trip)) { 0267 res.setReservationFor(trip); 0268 res.setReservedTicket(t); 0269 results.push_back(res); 0270 } 0271 0272 } else if (doc.ticket.userType() == qMetaTypeId<Fcb::OpenTicketData>()) { 0273 const auto nrt = doc.ticket.value<Fcb::OpenTicketData>(); 0274 0275 Seat s; 0276 s.setSeatingType(FcbUtil::classCodeToString(nrt.classCode)); 0277 Ticket t(ticket); 0278 t.setTicketedSeat(s); 0279 res.setProgramMembershipUsed(extractCustomerCard(nrt.tariffs)); 0280 0281 if (nrt.priceIsSet()) { 0282 res.setTotalPrice(nrt.price / std::pow(10, fcb.issuingDetail.currencyFract)); 0283 } 0284 0285 // check for TrainLinkType regional validity constrains 0286 bool trainLinkTypeFound = false; 0287 for (const auto ®ionalValidity : nrt.validRegion) { 0288 if (regionalValidity.value.userType() != qMetaTypeId<Fcb::TrainLinkType>()) { 0289 continue; 0290 } 0291 const auto trainLink = regionalValidity.value.value<Fcb::TrainLinkType>(); 0292 TrainTrip trip; 0293 trip.setProvider(p.issuer()); 0294 0295 // TODO station identifier 0296 TrainStation dep; 0297 dep.setName(trainLink.fromStationNameUTF8); 0298 fixFcbStationCode(dep); 0299 trip.setDepartureStation(dep); 0300 0301 TrainStation arr; 0302 arr.setName(trainLink.toStationNameUTF8); 0303 fixFcbStationCode(arr); 0304 trip.setArrivalStation(arr); 0305 0306 trip.setDepartureTime(trainLink.departureDateTime(issueDt)); 0307 0308 if (trainLink.trainNumIsSet()) { 0309 trip.setTrainNumber(QString::number(trainLink.trainNum)); 0310 } else { 0311 trip.setTrainNumber(QString::fromUtf8(trainLink.trainIA5)); 0312 } 0313 0314 if (validator.isValidElement(trip)) { 0315 res.setReservationFor(trip); 0316 res.setReservedTicket(t); 0317 results.push_back(res); 0318 trainLinkTypeFound = true; 0319 } 0320 } 0321 0322 if (!trainLinkTypeFound) { 0323 TrainTrip trip; 0324 trip.setProvider(p.issuer()); 0325 trip.setDepartureStation(p.outboundDepartureStation()); 0326 trip.setArrivalStation(p.outboundArrivalStation()); 0327 trip.setDepartureDay(nrt.validFrom(issueDt).date()); 0328 if (validator.isValidElement(trip)) { 0329 res.setReservationFor(trip); 0330 res.setReservedTicket(t); 0331 results.push_back(res); 0332 } 0333 // TODO handle nrt.returnIncluded 0334 } 0335 } 0336 } 0337 } 0338 0339 if (!results.isEmpty()) { 0340 node.addResult(results); 0341 return; 0342 } 0343 0344 // only Ticket 0345 ticket.setTicketedSeat(seat); 0346 ticket.setIssuedBy(p.issuer()); 0347 ticket.setTicketNumber(p.pnr()); 0348 ticket.setUnderName(p.person()); 0349 ticket.setValidFrom(p.validFrom()); 0350 ticket.setValidUntil(p.validUntil()); 0351 node.addResult(QList<QVariant>({ticket})); 0352 } 0353