KItinerary

fcbextractor.cpp
1/*
2 SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "fcbextractor_p.h"
7
8#include "variantvisitor_p.h"
9
10#include <KItinerary/ExtractorValidator>
11#include <KItinerary/Organization>
12#include <KItinerary/Person>
13#include <KItinerary/ProgramMembership>
14#include <KItinerary/Reservation>
15#include <KItinerary/Ticket>
16#include <KItinerary/TrainTrip>
17
18#include <type_traits>
19
20using namespace Qt::Literals;
21using namespace KItinerary;
22
23[[nodiscard]] static QString ticketNameForDocument(const QVariant &doc)
24{
25 return VariantVisitor([](auto &&doc) {
26 auto n = doc.tariffs.isEmpty() ? QString() : doc.tariffs.at(0).tariffDesc;
27 if (!n.isEmpty()) {
28 return n;
29 }
30 if constexpr (std::is_same_v<std::decay_t<decltype(doc)>, Fcb::v13::PassData> || std::is_same_v<std::decay_t<decltype(doc)>, Fcb::v2::PassData> || std::is_same_v<std::decay_t<decltype(doc)>, Fcb::v3::PassData>) {
31 if (!doc.passDescription.isEmpty()) {
32 return doc.passDescription;
33 }
34 }
35
36 return doc.infoText;
37 }).visit<FCB_VERSIONED(ReservationData), FCB_VERSIONED(OpenTicketData), FCB_VERSIONED(PassData)>(doc);
38}
39
40QString FcbExtractor::ticketName(const Fcb::UicRailTicketData &fcb)
41{
42 return std::visit([](auto &&fcb) {
43 for (const auto &doc : fcb.transportDocument) {
44 if (auto n = ticketNameForDocument(doc.ticket); !n.isEmpty()) {
45 return n;
46 }
47 }
48 return QString();
49 }, fcb);
50}
51
52template <typename T>
53[[nodiscard]] static QString fcbReference(const T &data)
54{
55 if (!data.referenceIA5.isEmpty()) {
56 return QString::fromLatin1(data.referenceIA5);
57 }
58 if (data.referenceNumIsSet()) {
59 return QString::number(data.referenceNum);
60 }
61 return {};
62}
63
64QString FcbExtractor::pnr(const Fcb::UicRailTicketData &fcb)
65{
66 return std::visit([](auto &&fcb) {
67 if (!fcb.issuingDetail.issuerPNR.isEmpty()) {
68 return QString::fromLatin1(fcb.issuingDetail.issuerPNR);
69 }
70
71 for (const auto &doc : fcb.transportDocument) {
72 auto pnr = VariantVisitor([](auto &&doc) {
73 return fcbReference(doc);
74 }).template visit<FCB_VERSIONED(ReservationData), FCB_VERSIONED(OpenTicketData), FCB_VERSIONED(PassData)>(doc.ticket);
75 if (!pnr.isEmpty()) {
76 return pnr;
77 }
78 }
79
80 return QString();
81 }, fcb);
82}
83
84QString FcbExtractor::seatingType(const Fcb::UicRailTicketData &fcb)
85{
86 return std::visit([](auto &&fcb) {
87 for (const auto &doc : fcb.transportDocument) {
88 auto s = VariantVisitor([](auto &&doc) {
89 return FcbUtil::classCodeToString(doc.classCode);
90 }).template visit<FCB_VERSIONED(ReservationData), FCB_VERSIONED(OpenTicketData), FCB_VERSIONED(PassData)>(doc.ticket);
91 if (!s.isEmpty()) {
92 return s;
93 }
94 }
95 return QString();
96 }, fcb);
97}
98
99[[nodiscard]] static QString formatIssuerId(int num)
100{
101 auto id = QString::number(num);
102 if (id.size() < 4) {
103 id.insert(0, QString(4 - id.size(), '0'_L1));
104 }
105 return id;
106}
107
108QString FcbExtractor::issuerId(const Fcb::UicRailTicketData &fcb)
109{
110 return std::visit([](auto &&fcb) {
111 if (fcb.issuingDetail.issuerNumIsSet()) {
112 return formatIssuerId(fcb.issuingDetail.issuerNum);
113 }
114 if (fcb.issuingDetail.issuerIA5IsSet()) {
115 return QString::fromLatin1(fcb.issuingDetail.issuerIA5);
116 }
117 if (fcb.issuingDetail.securityProviderNumIsSet()) {
118 return formatIssuerId(fcb.issuingDetail.securityProviderNum);
119 }
120 if (fcb.issuingDetail.securityProviderIA5IsSet()) {
121 return QString::fromLatin1(fcb.issuingDetail.securityProviderIA5);
122 }
123 return QString();
124 }, fcb);
125}
126
127Organization FcbExtractor::issuer(const Fcb::UicRailTicketData &fcb)
128{
129 Organization issuer;
130 if (auto id = issuerId(fcb); !id.isEmpty()) {
131 issuer.setIdentifier("uic:"_L1 + id);
132 }
133 std::visit([&issuer](auto &&fcb) {
134 if (fcb.issuingDetail.issuerNameIsSet()) {
135 issuer.setName(fcb.issuingDetail.issuerName);
136 }
137 }, fcb);
138 return issuer;
139}
140
141Person FcbExtractor::person(const Fcb::UicRailTicketData &fcb)
142{
143 return std::visit([](auto &&fcb) {
144 Person p;
145 if (!fcb.travelerDetailIsSet() || fcb.travelerDetail.traveler.size() != 1) {
146 return p;
147 }
148 const auto traveler = fcb.travelerDetail.traveler.at(0);
149 if (traveler.firstNameIsSet() || traveler.secondNameIsSet()) {
150 p.setGivenName(QString(traveler.firstName + ' '_L1 + traveler.secondName).trimmed());
151 }
152 p.setFamilyName(traveler.lastName);
153 return p;
154 }, fcb);
155}
156
157QDateTime FcbExtractor::issuingDateTime(const Fcb::UicRailTicketData &fcb)
158{
159 return std::visit([](auto &&data) { return data.issuingDetail.issueingDateTime(); }, fcb);
160}
161
162QDateTime FcbExtractor::validFrom(const Fcb::UicRailTicketData &fcb)
163{
164 return std::visit([](auto &&fcb) {
165 for (const auto &doc : fcb.transportDocument) {
166 auto dt = VariantVisitor([&fcb](auto &&doc) {
167 return doc.departureDateTime(fcb.issuingDetail.issueingDateTime());
168 }).template visit<FCB_VERSIONED(ReservationData)>(doc.ticket);
169 if (dt.isValid()) {
170 return dt;
171 }
172 dt = VariantVisitor([&fcb](auto &&doc) {
173 return doc.validFrom(fcb.issuingDetail.issueingDateTime());
174 }).template visit<FCB_VERSIONED(OpenTicketData), FCB_VERSIONED(PassData)>(doc.ticket);
175 if (dt.isValid()) {
176 return dt;
177 }
178 }
179 return QDateTime();
180 }, fcb);
181}
182
183QDateTime FcbExtractor::validUntil(const Fcb::UicRailTicketData &fcb)
184{
185 return std::visit([](auto &&fcb) {
186 for (const auto &doc : fcb.transportDocument) {
187 auto dt = VariantVisitor([&fcb](auto &&doc) {
188 return doc.arrivalDateTime(fcb.issuingDetail.issueingDateTime());
189 }).template visit<FCB_VERSIONED(ReservationData)>(doc.ticket);
190 if (dt.isValid()) {
191 return dt;
192 }
193 dt = VariantVisitor([&fcb](auto &&doc) {
194 return doc.validUntil(fcb.issuingDetail.issueingDateTime());
195 }).template visit<FCB_VERSIONED(OpenTicketData), FCB_VERSIONED(PassData)>(doc.ticket);
196 if (dt.isValid()) {
197 return dt;
198 }
199 }
200 return QDateTime();
201 }, fcb);
202}
203
204FcbExtractor::PriceData FcbExtractor::price(const Fcb::UicRailTicketData &fcb)
205{
206 return std::visit([](auto &&fcb) {
207 PriceData p;
208 p.currency = QString::fromUtf8(fcb.issuingDetail.currency);
209 const auto fract = std::pow(10, fcb.issuingDetail.currencyFract);
210 for (const auto &doc : fcb.transportDocument) {
211 p.price = VariantVisitor([fract](auto &&doc) {
212 return doc.priceIsSet() ? doc.price / fract : NAN;
213 }).template visit<FCB_VERSIONED(ReservationData), FCB_VERSIONED(OpenTicketData), FCB_VERSIONED(PassData)>(doc.ticket);
214 if (!std::isnan(p.price)) {
215 continue;
216 }
217 }
218 return p;
219 }, fcb);
220}
221
222template <typename CardReferenceTypeT>
223static ProgramMembership extractCustomerCard(const CardReferenceTypeT &card)
224{
226 p.setProgramName(card.cardName);
227 if (card.cardIdNumIsSet()) {
228 p.setMembershipNumber(QString::number(card.cardIdNum));
229 } else if (card.cardIdIA5IsSet()) {
230 p.setMembershipNumber(QString::fromUtf8(card.cardIdIA5));
231 }
232 return p;
233}
234
235template <typename TariffTypeT>
236static ProgramMembership extractCustomerCard(const QList <TariffTypeT> &tariffs)
237{
238 // TODO what do we do with the (so far theoretical) case of multiple discount cards in use?
239 for (const auto &tariff : tariffs) {
240 for (const auto &card : tariff.reductionCard) {
241 return extractCustomerCard(card);
242 }
243 }
244
245 return {};
246}
247
248void FcbExtractor::extractReservation(const QVariant &res, const Fcb::UicRailTicketData &fcb, const Ticket &ticket, QList<QVariant> &result)
249{
250 const auto issuingDateTime = FcbExtractor::issuingDateTime(fcb);
251 VariantVisitor([&fcb, &result, ticket, issuingDateTime](auto &&irt) {
252 Ticket t(ticket);
253
254 TrainTrip trip;
255 trip.setProvider(FcbExtractor::issuer(fcb));
256 if (trip.provider().identifier().isEmpty() && trip.provider().name().isEmpty()) {
257 trip.setProvider(ticket.issuedBy());
258 }
259 t.setIssuedBy({});
260
261 TrainStation dep;
262 FcbExtractor::readDepartureStation(irt, dep);
263 trip.setDepartureStation(dep);
264
265 TrainStation arr;
266 FcbExtractor::readArrivalStation(irt, arr);
267 trip.setArrivalStation(arr);
268
269 trip.setDepartureTime(irt.departureDateTime(issuingDateTime));
270 trip.setArrivalTime(irt.arrivalDateTime(issuingDateTime));
271
272 if (irt.trainNumIsSet()) {
273 trip.setTrainNumber(irt.serviceBrandAbrUTF8 + ' '_L1 + QString::number(irt.trainNum));
274 } else {
275 trip.setTrainNumber(irt.serviceBrandAbrUTF8 + ' '_L1 + QString::fromUtf8(irt.trainIA5));
276 }
277
278 Seat s;
279 s.setSeatingType(FcbUtil::classCodeToString(irt.classCode));
280 if (irt.placesIsSet()) {
281 s.setSeatSection(QString::fromUtf8(irt.places.coach));
282 QStringList l;
283 for (const auto &b : irt.places.placeIA5) {
285 }
286 for (auto i : irt.places.placeNum) {
288 }
289 s.setSeatNumber(l.join(", "_L1));
290 // TODO other seat encoding variants
291 }
292 t.setTicketedSeat(s);
293
295 res.setReservationNumber(FcbExtractor::pnr(fcb));
296 if (res.reservationNumber().isEmpty()) {
297 res.setReservationNumber(ticket.ticketNumber());
298 }
299 t.setTicketNumber(fcbReference(irt));
300 res.setUnderName(FcbExtractor::person(fcb));
301 res.setProgramMembershipUsed(::extractCustomerCard(irt.tariffs));
302
303 if (irt.priceIsSet()) {
304 res.setTotalPrice(irt.price / std::pow(10, std::visit([](auto &&fcb) { return fcb.issuingDetail.currencyFract; }, fcb)));
305 }
306 res.setPriceCurrency(QString::fromUtf8(std::visit([](auto &&fcb) { return fcb.issuingDetail.currency; }, fcb)));
307
308 ExtractorValidator validator;
309 validator.setAcceptedTypes<TrainTrip>();
310 if (validator.isValidElement(trip)) {
311 res.setReservationFor(trip);
312 res.setReservedTicket(t);
313 result.push_back(res);
314 }
315 }).visit<FCB_VERSIONED(ReservationData)>(res);
316}
317
318[[nodiscard]] static bool extractValidRegion(const QVariant &regionalValidity, const QDateTime &issuingDateTime, const TrainReservation &baseRes, const TrainTrip &baseTrip, QList<QVariant> &result)
319{
320 return VariantVisitor([&baseTrip, issuingDateTime, &baseRes, &result](auto &&trainLink) {
321 TrainTrip trip(baseTrip);
322
323 // TODO station identifier, use FcbExtractor::read[Arrival|Departure]Station
324 if (trainLink.fromStationNameUTF8IsSet()) {
325 TrainStation dep;
326 dep.setName(trainLink.fromStationNameUTF8);
327 FcbExtractor::fixStationCode(dep);
328 trip.setDepartureStation(dep);
329 }
330
331 if (trainLink.toStationNameUTF8IsSet()) {
332 TrainStation arr;
333 arr.setName(trainLink.toStationNameUTF8);
334 FcbExtractor::fixStationCode(arr);
335 trip.setArrivalStation(arr);
336 }
337
338 trip.setDepartureDay({}); // reset explicit value in case of departure after midnight
339 trip.setDepartureTime(trainLink.departureDateTime(issuingDateTime));
340
341 if (trainLink.trainNumIsSet()) {
342 trip.setTrainNumber(QString::number(trainLink.trainNum));
343 } else {
344 trip.setTrainNumber(QString::fromUtf8(trainLink.trainIA5));
345 }
346
347 ExtractorValidator validator;
348 validator.setAcceptedTypes<TrainTrip>();
349 if (validator.isValidElement(trip)) {
350 TrainReservation res(baseRes);
351 res.setReservationFor(trip);
352 result.push_back(res);
353 return true;
354 }
355
356 return false;
357 }).visit<FCB_VERSIONED(TrainLinkType)>(regionalValidity);
358}
359
360void FcbExtractor::extractOpenTicket(const QVariant &res, const Fcb::UicRailTicketData &fcb, const Ticket &ticket, QList<QVariant> &result)
361{
362 const auto issuingDateTime = FcbExtractor::issuingDateTime(fcb);
363 VariantVisitor([&fcb, ticket, &result, issuingDateTime] (auto &&nrt) {
364 Seat s;
365 s.setSeatingType(FcbUtil::classCodeToString(nrt.classCode));
366 Ticket t(ticket);
367 t.setTicketedSeat(s);
368
370 res.setReservationNumber(FcbExtractor::pnr(fcb));
371 if (res.reservationNumber().isEmpty()) {
372 res.setReservationNumber(ticket.ticketNumber());
373 }
374 t.setTicketNumber(fcbReference(nrt));
375 t.setIssuedBy({});
376 res.setReservedTicket(t);
377
378 res.setUnderName(FcbExtractor::person(fcb));
379 res.setProgramMembershipUsed(::extractCustomerCard(nrt.tariffs));
380
381 if (nrt.priceIsSet()) {
382 res.setTotalPrice(nrt.price / std::pow(10, std::visit([](auto &&fcb) { return fcb.issuingDetail.currencyFract; }, fcb)));
383 }
384 res.setPriceCurrency(QString::fromUtf8(std::visit([](auto &&fcb) { return fcb.issuingDetail.currency; }, fcb)));
385
386 TrainTrip baseTrip;
387 baseTrip.setProvider(FcbExtractor::issuer(fcb));
388 if (baseTrip.provider().name().isEmpty() && baseTrip.provider().identifier().isEmpty()) {
389 baseTrip.setProvider(ticket.issuedBy());
390 }
391 TrainStation dep;
392 FcbExtractor::readDepartureStation(nrt, dep);
393 baseTrip.setDepartureStation(dep);
394 TrainStation arr;
395 FcbExtractor::readArrivalStation(nrt, arr);
396 baseTrip.setArrivalStation(arr);
397 baseTrip.setDepartureDay(nrt.validFrom(issuingDateTime).date());
398
399 ExtractorValidator validator;
400 validator.setAcceptedTypes<TrainTrip>();
401
402 // check for TrainLinkType regional validity constrains
403 bool trainLinkTypeFound = false;
404 for (const auto &regionalValidity : nrt.validRegion) {
405 trainLinkTypeFound |= extractValidRegion(regionalValidity.value, issuingDateTime, res, baseTrip, result);
406 }
407
408 if (!trainLinkTypeFound) {
409 if (validator.isValidElement(baseTrip)) {
410 res.setReservationFor(baseTrip);
411 result.push_back(res);
412 }
413 }
414
415 // same for return trips
416 if (nrt.returnIncluded) {
417 TrainStation retDep;
418 FcbExtractor::readDepartureStation(nrt.returnDescription, nrt.stationCodeTable, retDep);
419 TrainStation retArr;
420 FcbExtractor::readArrivalStation(nrt.returnDescription, nrt.stationCodeTable, retArr);
421
422 TrainTrip retBaseTrip;
423 retBaseTrip.setProvider(baseTrip.provider());
424 retBaseTrip.setDepartureStation(retDep);
425 retBaseTrip.setArrivalStation(retArr);
426
427 bool retTrainLinkTypeFound = false;
428 for (const auto &regionalValidity : nrt.returnDescription.validReturnRegion) {
429 retTrainLinkTypeFound |= extractValidRegion(regionalValidity.value, issuingDateTime, res, retBaseTrip, result);
430 }
431
432 if (!retTrainLinkTypeFound && validator.isValidElement(retBaseTrip)) {
433 res.setReservationFor(retBaseTrip);
434 result.push_back(retBaseTrip);
435 }
436 }
437 }).visit<FCB_VERSIONED(OpenTicketData)>(res);
438}
439
440void FcbExtractor::extractCustomerCard(const QVariant &ccd, const Fcb::UicRailTicketData &fcb, const Ticket &ticket, QList<QVariant> &result)
441{
442 VariantVisitor([&fcb, &result, ticket](auto &&ccd) {
444 if (ccd.cardIdNumIsSet()) {
445 pm.setMembershipNumber(QString::number(ccd.cardIdNum));
446 } else {
447 pm.setMembershipNumber(QString::fromUtf8(ccd.cardIdIA5));
448 }
449 pm.setProgramName(ccd.cardTypeDescr);
450 pm.setMember(FcbExtractor::person(fcb));
451 pm.setValidFrom(ccd.validFrom().startOfDay());
452 pm.setValidUntil(ccd.validUntil().startOfDay());
453 pm.setToken(ticket.ticketToken());
454 result.push_back(pm);
455 }).visit<FCB_VERSIONED(CustomerCardData)>(ccd);
456}
457
458void FcbExtractor::readDepartureStation(const QVariant &doc, TrainStation &station)
459{
460 VariantVisitor([&station](auto &&data) {
461 FcbExtractor::readDepartureStation(data, station);
462 }).visit<FCB_VERSIONED(ReservationData), FCB_VERSIONED(OpenTicketData)>(doc);
463}
464
465void FcbExtractor::readArrivalStation(const QVariant &doc, TrainStation &station)
466{
467 VariantVisitor([&station](auto &&data) {
468 FcbExtractor::readArrivalStation(data, station);
469 }).visit<FCB_VERSIONED(ReservationData), FCB_VERSIONED(OpenTicketData)>(doc);
470}
471
472void FcbExtractor::fixStationCode(TrainStation &station)
473{
474 // UIC codes in Germany are wildly unreliable, there seem to be different
475 // code tables in use by different operators, so we unfortunately have to ignore
476 // those entirely
477 if (station.identifier().startsWith("uic:80"_L1)) {
478 PostalAddress addr;
479 addr.setAddressCountry(u"DE"_s);
480 station.setAddress(addr);
481 station.setIdentifier(QString());
482 }
483}
Validates extractor results.
bool isValidElement(const QVariant &elem) const
Checks if the given element is valid.
void setAcceptedTypes(std::vector< const QMetaObject * > &&accptedTypes)
Sets the list of supported top-level types that should be accepted.
static QString classCodeToString(Fcb::v13::TravelClassType classCode)
Convert a class code enum value to a string for human representation.
Definition fcbutil.cpp:30
Rail pass document (RPT).
Definition fcbticket1.h:694
Rail pass document (RPT).
Definition fcbticket2.h:702
Rail pass document (RPT).
Definition fcbticket3.h:727
QString identifier
Identifier.
Definition place.h:85
Postal address.
Definition place.h:46
A frequent traveler, bonus points or discount scheme program membership.
A reserved seat.
Definition ticket.h:23
A booked ticket.
Definition ticket.h:41
QString ticketToken
The raw ticket token string.
Definition ticket.h:50
A train reservation.
Train station.
Definition place.h:126
A train trip.
Definition traintrip.h:24
Classes for reservation/travel data models, data extraction and data augmentation.
Definition berelement.h:17
void push_back(parameter_type value)
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString trimmed() const const
QString join(QChar separator) const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 11 2025 11:58:38 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.