KItinerary

uic9183documentprocessor.cpp
1/*
2 SPDX-FileCopyrightText: 2018-2021 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "uic9183documentprocessor.h"
8
9#include <KItinerary/ExtractorResult>
10#include <KItinerary/ExtractorValidator>
11#include <KItinerary/JsonLdDocument>
12#include <KItinerary/Rct2Ticket>
13#include <KItinerary/Reservation>
14#include <KItinerary/Uic9183Parser>
15#include <KItinerary/Uic9183TicketLayout>
16#include <KItinerary/Ticket>
17#include <KItinerary/TrainTrip>
18
19#include "era/fcbticket.h"
20#include "era/fcbutil.h"
21#include "uic9183/uic9183head.h"
22#include "uic9183/vendor0080block.h"
23
24#include <KLocalizedString>
25
26#include <QDateTime>
27#include <QJsonArray>
28#include <QJsonObject>
29
30using namespace KItinerary;
31
32Uic9183DocumentProcessor::Uic9183DocumentProcessor()
33{
34 qRegisterMetaType<KItinerary::Uic9183TicketLayoutField>();
35 qRegisterMetaType<KItinerary::Vendor0080BLOrderBlock>();
36}
37
38bool Uic9183DocumentProcessor::canHandleData(const QByteArray &encodedData, [[maybe_unused]] QStringView fileName) const
39{
40 return Uic9183Parser::maybeUic9183(encodedData);
41}
42
44{
46 p.parse(encodedData);
47 if (!p.isValid()) {
48 return {};
49 }
50
52 node.setContent(p);
53 return node;
54}
55
56void Uic9183DocumentProcessor::expandNode(ExtractorDocumentNode &node, [[maybe_unused]] const ExtractorEngine *engine) const
57{
58 // only use the U_HEAD issuing time as context if we have nothing better
59 // while that is usually correct it cannot contain a time zone, unlike the (often) enclosing PDF document´
60 if (!node.contextDateTime().isValid()) {
61 const auto p = node.content<Uic9183Parser>();
62 if (const auto u_flex = p.findBlock<Fcb::UicRailTicketData>(); u_flex.isValid()) {
63 node.setContextDateTime(u_flex.issuingDetail.issueingDateTime());
64 } else if (const auto u_head = p.findBlock<Uic9183Head>(); u_head.isValid()) {
65 node.setContextDateTime(u_head.issuingDateTime());
66 }
67 }
68}
69
70static ProgramMembership extractCustomerCard(const Fcb::CardReferenceType &card)
71{
73 p.setProgramName(card.cardName);
74 if (card.cardIdNumIsSet()) {
75 p.setMembershipNumber(QString::number(card.cardIdNum));
76 } else if (card.cardIdIA5IsSet()) {
77 p.setMembershipNumber(QString::fromUtf8(card.cardIdIA5));
78 }
79 return p;
80}
81
82static ProgramMembership extractCustomerCard(const QList <Fcb::TariffType> &tariffs)
83{
84 // TODO what do we do with the (so far theoretical) case of multiple discount cards in use?
85 for (const auto &tariff : tariffs) {
86 for (const auto &card : tariff.reductionCard) {
87 return extractCustomerCard(card);
88 }
89 }
90
91 return {};
92}
93
94static void fixFcbStationCode(TrainStation &station)
95{
96 // UIC codes in Germany are wildly unreliable, there seem to be different
97 // code tables in use by different operators, so we unfortunately have to ignore
98 // those entirely
99 if (station.identifier().startsWith(QLatin1StringView("uic:80"))) {
100 PostalAddress addr;
101 addr.setAddressCountry(QStringLiteral("DE"));
102 station.setAddress(addr);
103 station.setIdentifier(QString());
104 }
105}
106
107void Uic9183DocumentProcessor::preExtract(ExtractorDocumentNode &node, [[maybe_unused]] const ExtractorEngine *engine) const
108{
109 const auto p = node.content<Uic9183Parser>();
110
111 Ticket ticket;
112 ticket.setName(p.name());
113 ticket.setTicketToken(QLatin1StringView("aztecbin:") +
114 QString::fromLatin1(p.rawData().toBase64()));
115 Seat seat;
116 if (const auto seatingType = p.seatingType(); !seatingType.isEmpty()) {
117 seat.setSeatingType(seatingType);
118 }
119
121 res.setReservationNumber(p.pnr());
122 res.setUnderName(p.person());
123
124 ExtractorValidator validator;
125 validator.setAcceptedTypes<TrainTrip>();
126
127 QList<QVariant> results;
128
129 const auto rct2 = p.rct2Ticket();
130 if (rct2.isValid()) {
131 TrainTrip trip, returnTrip;
132 trip.setProvider(p.issuer());
133
134 switch (rct2.type()) {
137 break;
140 {
141 trip.setTrainNumber(rct2.trainNumber());
142 seat.setSeatSection(rct2.coachNumber());
143 seat.setSeatNumber(rct2.seatNumber());
144 [[fallthrough]];
145 }
148 {
149 trip.setDepartureStation(p.outboundDepartureStation());
150 trip.setArrivalStation(p.outboundArrivalStation());
151
152 if (rct2.outboundDepartureTime().isValid()) {
153 trip.setDepartureDay(rct2.outboundDepartureTime().date());
154 } else {
155 trip.setDepartureDay(rct2.firstDayOfValidity());
156 }
157
158 if (rct2.outboundDepartureTime() != rct2.outboundArrivalTime()) {
159 trip.setDepartureTime(rct2.outboundDepartureTime());
160 trip.setArrivalTime(rct2.outboundArrivalTime());
161 }
162
163 if (rct2.type() == Rct2Ticket::Transport && !p.returnDepartureStation().name().isEmpty()) {
164 returnTrip.setProvider(p.issuer());
165 returnTrip.setDepartureStation(p.returnDepartureStation());
166 returnTrip.setArrivalStation(p.returnArrivalStation());
167
168 if (rct2.returnDepartureTime().isValid()) {
169 returnTrip.setDepartureDay(rct2.returnDepartureTime().date());
170 } else {
171 returnTrip.setDepartureDay(rct2.firstDayOfValidity());
172 }
173
174 if (rct2.returnDepartureTime() != rct2.returnArrivalTime()) {
175 returnTrip.setDepartureTime(rct2.returnDepartureTime());
176 returnTrip.setArrivalTime(rct2.returnArrivalTime());
177 }
178 }
179
180 break;
181 }
182 }
183
184 if (const auto currency = rct2.currency(); !currency.isEmpty()) {
185 res.setPriceCurrency(currency);
186 res.setTotalPrice(rct2.price());
187 }
188
189 // provide names for typically "addon" tickets, so we can distinguish them in the UI
190 switch (rct2.type()) {
192 ticket.setName(i18n("Reservation"));
193 break;
195 ticket.setName(i18n("Upgrade"));
196 break;
197 default:
198 break;
199 }
200
201 ticket.setTicketedSeat(seat);
202 if (validator.isValidElement(trip)) {
203 res.setReservationFor(trip);
204 res.setReservedTicket(ticket);
205 results.push_back(res);
206 }
207 if (validator.isValidElement(returnTrip)) {
208 res.setReservationFor(returnTrip);
209 res.setReservedTicket(ticket);
210 results.push_back(res);
211 }
212 }
213
214 const auto fcb = p.findBlock<Fcb::UicRailTicketData>();
215 if (fcb.isValid()) {
216 res.setPriceCurrency(QString::fromUtf8(fcb.issuingDetail.currency));
217 const auto issueDt = fcb.issuingDetail.issueingDateTime();
218 for (const auto &doc : fcb.transportDocument) {
219 if (doc.ticket.userType() == qMetaTypeId<Fcb::ReservationData>()) {
220 const auto irt = doc.ticket.value<Fcb::ReservationData>();
221 TrainTrip trip;
222 trip.setProvider(p.issuer());
223
224 TrainStation dep;
225 dep.setName(irt.fromStationNameUTF8);
226 dep.setIdentifier(FcbUtil::fromStationIdentifier(irt));
227 fixFcbStationCode(dep);
228 trip.setDepartureStation(dep);
229
230 TrainStation arr;
231 arr.setName(irt.toStationNameUTF8);
232 arr.setIdentifier(FcbUtil::toStationIdentifier(irt));
233 fixFcbStationCode(arr);
234 trip.setArrivalStation(arr);
235
236 trip.setDepartureTime(irt.departureDateTime(issueDt));
237 trip.setArrivalTime(irt.arrivalDateTime(issueDt));
238
239 if (irt.trainNumIsSet()) {
240 trip.setTrainNumber(irt.serviceBrandAbrUTF8 + QLatin1Char(' ') + QString::number(irt.trainNum));
241 } else {
242 trip.setTrainNumber(irt.serviceBrandAbrUTF8 + QLatin1Char(' ') + QString::fromUtf8(irt.trainIA5));
243 }
244
245 Seat s;
246 s.setSeatingType(FcbUtil::classCodeToString(irt.classCode));
247 if (irt.placesIsSet()) {
248 s.setSeatSection(QString::fromUtf8(irt.places.coach));
249 QStringList l;
250 for (const auto &b : irt.places.placeIA5)
252 for (auto i : irt.places.placeNum)
254 s.setSeatNumber(l.join(QLatin1StringView(", ")));
255 // TODO other seat encoding variants
256 }
257
258 Ticket t(ticket);
259 t.setTicketedSeat(s);
260 res.setProgramMembershipUsed(extractCustomerCard(irt.tariffs));
261
262 if (irt.priceIsSet()) {
263 res.setTotalPrice(irt.price / std::pow(10, fcb.issuingDetail.currencyFract));
264 }
265
266 if (validator.isValidElement(trip)) {
267 res.setReservationFor(trip);
268 res.setReservedTicket(t);
269 results.push_back(res);
270 }
271
272 } else if (doc.ticket.userType() == qMetaTypeId<Fcb::OpenTicketData>()) {
273 const auto nrt = doc.ticket.value<Fcb::OpenTicketData>();
274
275 Seat s;
276 s.setSeatingType(FcbUtil::classCodeToString(nrt.classCode));
277 Ticket t(ticket);
278 t.setTicketedSeat(s);
279 res.setProgramMembershipUsed(extractCustomerCard(nrt.tariffs));
280
281 if (nrt.priceIsSet()) {
282 res.setTotalPrice(nrt.price / std::pow(10, fcb.issuingDetail.currencyFract));
283 }
284
285 // check for TrainLinkType regional validity constrains
286 bool trainLinkTypeFound = false;
287 for (const auto &regionalValidity : nrt.validRegion) {
288 if (regionalValidity.value.userType() != qMetaTypeId<Fcb::TrainLinkType>()) {
289 continue;
290 }
291 const auto trainLink = regionalValidity.value.value<Fcb::TrainLinkType>();
292 TrainTrip trip;
293 trip.setProvider(p.issuer());
294
295 // TODO station identifier
296 TrainStation dep;
297 dep.setName(trainLink.fromStationNameUTF8);
298 fixFcbStationCode(dep);
299 trip.setDepartureStation(dep);
300
301 TrainStation arr;
302 arr.setName(trainLink.toStationNameUTF8);
303 fixFcbStationCode(arr);
304 trip.setArrivalStation(arr);
305
306 trip.setDepartureTime(trainLink.departureDateTime(issueDt));
307
308 if (trainLink.trainNumIsSet()) {
309 trip.setTrainNumber(QString::number(trainLink.trainNum));
310 } else {
311 trip.setTrainNumber(QString::fromUtf8(trainLink.trainIA5));
312 }
313
314 if (validator.isValidElement(trip)) {
315 res.setReservationFor(trip);
316 res.setReservedTicket(t);
317 results.push_back(res);
318 trainLinkTypeFound = true;
319 }
320 }
321
322 if (!trainLinkTypeFound) {
323 TrainTrip trip;
324 trip.setProvider(p.issuer());
325 trip.setDepartureStation(p.outboundDepartureStation());
326 trip.setArrivalStation(p.outboundArrivalStation());
327 trip.setDepartureDay(nrt.validFrom(issueDt).date());
328 if (validator.isValidElement(trip)) {
329 res.setReservationFor(trip);
330 res.setReservedTicket(t);
331 results.push_back(res);
332 }
333 // TODO handle nrt.returnIncluded
334 }
335 } else if (doc.ticket.userType() == qMetaTypeId<Fcb::CustomerCardData>()) {
336 const auto ccd = doc.ticket.value<Fcb::CustomerCardData>();
338 if (ccd.cardIdNumIsSet()) {
339 pm.setMembershipNumber(QString::number(ccd.cardIdNum));
340 } else {
341 pm.setMembershipNumber(QString::fromUtf8(ccd.cardIdIA5));
342 }
343 pm.setProgramName(ccd.cardTypeDescr);
344 pm.setMember(p.person());
345 pm.setValidFrom(ccd.validFrom().startOfDay());
346 pm.setValidUntil(ccd.validUntil().startOfDay());
347 pm.setToken(ticket.ticketToken());
348 results.push_back(pm);
349 }
350 }
351 }
352
353 if (!results.isEmpty()) {
354 node.addResult(results);
355 return;
356 }
357
358 // only Ticket
359 ticket.setTicketedSeat(seat);
360 ticket.setIssuedBy(p.issuer());
361 ticket.setTicketNumber(p.pnr());
362 ticket.setUnderName(p.person());
363 ticket.setValidFrom(p.validFrom());
364 ticket.setValidUntil(p.validUntil());
365 node.addResult(QList<QVariant>({ticket}));
366}
367
A node in the extracted document object tree.
void setContextDateTime(const QDateTime &contextDateTime)
Set the context date/time.
QJSValue content
The decoded content of this node.
void addResult(ExtractorResult &&result)
Add additional results from an extraction step.
QDateTime contextDateTime
The best known context date/time at this point in the document tree.
void setContent(const QVariant &content)
Set decoded content.
Semantic data extraction engine.
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 fromStationIdentifier(Fcb::CodeTableType stationCodeTable, const T &doc)
Departure station identifier for a travel document, in the format needed for output with our JSON-LD ...
Definition fcbutil.h:24
static QString classCodeToString(Fcb::TravelClassType classCode)
Convert a class code enum value to a string for human representation.
Definition fcbutil.cpp:30
static QString toStationIdentifier(Fcb::CodeTableType stationCodeTable, const T &doc)
Arrival station identifier for a travel document, in the format needed for output with our JSON-LD fo...
Definition fcbutil.h:41
Customer card information.
Definition fcbticket.h:324
Customer card document.
Definition fcbticket.h:755
Open ticket document (NRT).
Definition fcbticket.h:615
Reservation document (IRT, RES).
Definition fcbticket.h:490
Reference to a specific train journey.
Definition fcbticket.h:188
Top-level type for the ERA FCB ticket structure.
Definition fcbticket.h:1005
QString identifier
Identifier.
Definition place.h:85
Postal address.
Definition place.h:46
A frequent traveler, bonus points or discount scheme program membership.
QDateTime validFrom
Non-standard extension for ticket validity time ranges.
@ Transport
Non-integrated Reservation Ticket (NRT)
Definition rct2ticket.h:70
@ RailPass
Rail Pass Ticket (RPT)
Definition rct2ticket.h:74
@ Upgrade
Update Document (UPG)
Definition rct2ticket.h:73
@ TransportReservation
Integration Reservation Ticket (IRT)
Definition rct2ticket.h:71
@ Unknown
ticket type could not be detected, or ticket type not supported yet
Definition rct2ticket.h:75
@ Reservation
Reservation Only Document (RES)
Definition rct2ticket.h:72
A reserved seat.
Definition ticket.h:23
A booked ticket.
Definition ticket.h:41
A train reservation.
Train station.
Definition place.h:126
A train trip.
Definition traintrip.h:24
void expandNode(ExtractorDocumentNode &node, const ExtractorEngine *engine) const override
Create child nodes for node, as far as that's necessary for this document type.
ExtractorDocumentNode createNodeFromData(const QByteArray &encodedData) const override
Create a document node from raw data.
void preExtract(ExtractorDocumentNode &node, const ExtractorEngine *engine) const override
Called before extractors are applied to node.
bool canHandleData(const QByteArray &encodedData, QStringView fileName) const override
Fast check whether the given encoded data can possibly be processed by this instance.
U_HEAD block of a UIC 918.3 ticket container.
Definition uic9183head.h:21
bool isValid() const
Returns true if this is a valid U_HEAD block.
Parser for UIC 918.3 and 918.3* train tickets.
static bool maybeUic9183(const QByteArray &data)
Quickly checks if might be UIC 918.3 content.
QString i18n(const char *text, const TYPE &arg...)
Classes for reservation/travel data models, data extraction and data augmentation.
Definition berelement.h:17
bool isValid() const const
void push_back(parameter_type value)
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
QString number(double n, char format, int precision)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString join(QChar separator) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:28:48 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.