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

KDE's Doxygen guidelines are available online.