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 &regionalValidity : 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