KItinerary

pkpassdocumentprocessor.cpp
1/*
2 SPDX-FileCopyrightText: 2019-2021 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "pkpassdocumentprocessor.h"
8
9#include <KItinerary/Event>
10#include <KItinerary/ExtractorDocumentNodeFactory>
11#include <KItinerary/ExtractorEngine>
12#include <KItinerary/ExtractorFilter>
13#include <KItinerary/ExtractorResult>
14#include <KItinerary/Flight>
15#include <KItinerary/JsonLdDocument>
16#include <KItinerary/Reservation>
17#include <KItinerary/Ticket>
18
19#include "knowledgedb/airportdb.h"
20#include "text/nameoptimizer_p.h"
21#include "text/pricefinder_p.h"
22#include "text/timefinder_p.h"
23
24#include <KPkPass/Barcode>
25#include <KPkPass/BoardingPass>
26#include <KPkPass/Location>
27#include <KPkPass/Field>
28#include <KPkPass/Pass>
29
30#include <QJsonObject>
31#include <QJSEngine>
32#include <QTime>
33#include <QTimeZone>
34#include <QVariant>
35
36using namespace KItinerary;
37
38Q_DECLARE_METATYPE(KItinerary::Internal::OwnedPtr<KPkPass::Pass>)
39
40bool PkPassDocumentProcessor::canHandleData(const QByteArray &encodedData, QStringView fileName) const
41{
42 return encodedData.startsWith("PK\x03\x04") ||
43 fileName.endsWith(QLatin1StringView(".pkpass"), Qt::CaseInsensitive);
44}
45
47{
48 auto pass = KPkPass::Pass::fromData(encodedData);
49 if (!pass) {
50 return {};
51 }
52
54 node.setContent<Internal::OwnedPtr<KPkPass::Pass>>(pass);
55 if (pass->relevantDate().isValid()) {
56 node.setContextDateTime(pass->relevantDate().addDays(-1)); // go a bit back, to compensate for unknown departure timezone at this point
57 }
58 return node;
59}
60
62{
63 auto pass = decodedData.value<KPkPass::Pass*>();
64 if (!pass) {
65 return {};
66 }
67
69 node.setContent(pass);
70 if (pass->relevantDate().isValid()) {
71 node.setContextDateTime(pass->relevantDate().addDays(-1)); // go a bit back, to compensate for unknown departure timezone at this point
72 }
73 return node;
74}
75
77{
78 const auto pass = node.content<KPkPass::Pass*>();
79 const auto barcodes = pass->barcodes();
80 if (barcodes.empty()) {
81 return;
82 }
83
84 auto child = engine->documentNodeFactory()->createNode(barcodes[0].message().toUtf8());
85 node.appendChild(child);
86}
87
89{
90 destroyIfOwned<KPkPass::Pass>(node);
91}
92
97
98static QList<KPkPass::Field> frontFieldsForPass(KPkPass::Pass *pass) {
100 fields += pass->headerFields();
101 fields += pass->primaryFields();
102 fields += pass->secondaryFields();
103 fields += pass->auxiliaryFields();
104 return fields;
105}
106
107static bool isAirportName(const QString &name, KnowledgeDb::IataCode iataCode)
108{
109 if (name.size() <= 3) {
110 return false;
111 }
112
113 const auto codes = KnowledgeDb::iataCodesFromName(name);
114 return std::find(codes.begin(), codes.end(), iataCode) != codes.end();
115}
116
117static bool isPlausibleGate(const QString &s)
118{
119 if (s.isEmpty() || s.size() > 10 || s.count(QLatin1Char('-')) > 1 || s.count(QLatin1Char(' ')) > 2) {
120 return false;
121 }
122 for (const auto &c : s) {
123 if (!c.isLetter() && !c.isDigit() && c != QLatin1Char(' ') && c != QLatin1Char(' ')) {
124 return false;
125 }
126 }
127 return true;
128}
129
130static Flight extractBoardingPass(KPkPass::Pass *pass, Flight flight)
131{
132 // search for missing information by field key
133 QString departureTerminal;
134 TimeFinder timeFinder;
135 const auto fields = pass->fields();
136 for (const auto &field : fields) {
137 // boarding time
138 if (!flight.boardingTime().isValid() &&
139 field.key().contains(QLatin1StringView("boarding"),
141 const auto time =
142 timeFinder.findSingularTime(field.value().toString());
143 if (time.isValid()) {
144 // this misses date, but the postprocessor will fill that in
145 flight.setBoardingTime(QDateTime(QDate(1, 1, 1), time));
146 continue;
147 }
148 }
149 // departure gate
150 if (flight.departureGate().isEmpty() &&
151 field.key().contains(QLatin1StringView("gate"),
153 const auto gateStr = field.value().toString();
154 if (isPlausibleGate(gateStr)) {
155 flight.setDepartureGate(gateStr);
156 continue;
157 }
158 }
159 // departure time
160 if (!flight.departureTime().isValid() &&
161 field.key().contains(QLatin1StringView("departure"),
163 const auto time =
164 timeFinder.findSingularTime(field.value().toString());
165 if (time.isValid()) {
166 // this misses date, but the postprocessor will fill that in
167 flight.setDepartureTime(QDateTime(QDate(1, 1, 1), time));
168 continue;
169 }
170 }
171
172 if (field.key().contains(QLatin1StringView("terminal"),
174 if (departureTerminal.isNull()) {
175 departureTerminal = field.value().toString();
176 } else {
177 departureTerminal = QStringLiteral(
178 ""); // empty but not null, marking multiple terminal candidates
179 }
180 }
181 }
182
183 if (flight.departureTerminal().isEmpty() && !departureTerminal.isEmpty()) {
184 flight.setDepartureTerminal(departureTerminal);
185 }
186
187 // "relevantDate" is the best guess for the boarding time if we didn't find an explicit field for it
188 if (pass->relevantDate().isValid() && !flight.boardingTime().isValid()) {
189 const auto tz = KnowledgeDb::timezoneForAirport(KnowledgeDb::IataCode{flight.departureAirport().iataCode()});
190 if (tz.isValid()) {
191 flight.setBoardingTime(pass->relevantDate().toTimeZone(tz));
192 } else {
193 flight.setBoardingTime(pass->relevantDate());
194 }
195 }
196
197 // search for missing information in field content
198 const auto depIata = KnowledgeDb::IataCode(flight.departureAirport().iataCode());
199 const auto arrIata = KnowledgeDb::IataCode(flight.arrivalAirport().iataCode());
200 const auto frontFields = frontFieldsForPass(pass);
201 for (const auto &field : frontFields) {
202 // full airport names
203 if (flight.departureAirport().name().isEmpty()) {
204 if (isAirportName(field.value().toString(), depIata)) {
205 auto airport = flight.departureAirport();
206 airport.setName(field.value().toString());
207 flight.setDepartureAirport(airport);
208 } else if (isAirportName(field.label(), depIata)) {
209 auto airport = flight.departureAirport();
210 airport.setName(field.label());
211 flight.setDepartureAirport(airport);
212 }
213 }
214 if (flight.arrivalAirport().name().isEmpty()) {
215 if (isAirportName(field.value().toString(), arrIata)) {
216 auto airport = flight.arrivalAirport();
217 airport.setName(field.value().toString());
218 flight.setArrivalAirport(airport);
219 } else if (isAirportName(field.label(), arrIata)) {
220 auto airport = flight.arrivalAirport();
221 airport.setName(field.label());
222 flight.setArrivalAirport(airport);
223 }
224 }
225 }
226
227 // location is the best guess for the departure airport geo coordinates
228 auto depAirport = flight.departureAirport();
229 auto depGeo = depAirport.geo();
230 if (pass->locations().size() == 1 && !depGeo.isValid()) {
231 const auto loc = pass->locations().at(0);
232 depGeo.setLatitude(loc.latitude());
233 depGeo.setLongitude(loc.longitude());
234 depAirport.setGeo(depGeo);
235 flight.setDepartureAirport(depAirport);
236 }
237
238 // organizationName is the best guess for airline name
239 auto airline = flight.airline();
240 if (airline.name().isEmpty()) {
241 airline.setName(pass->organizationName());
242 flight.setAirline(airline);
243 }
244
245 return flight;
246}
247
248static void extractEventTicketPass(KPkPass::Pass *pass, EventReservation &eventRes)
249{
250 auto event = eventRes.reservationFor().value<Event>();
251
252 if (event.name().isEmpty()) {
253 event.setName(pass->description());
254 }
255
256 // "relevantDate" is the best guess for the start time
257 if (pass->relevantDate().isValid() && !event.startDate().isValid()) {
258 event.setStartDate(pass->relevantDate());
259
260 // "expirationDate" is the best guess for the end time
261 if (pass->expirationDate().isValid() && pass->relevantDate().date() == pass->expirationDate().date() &&
262 pass->expirationDate() > pass->relevantDate() && !event.endDate().isValid()) {
263 event.setEndDate(pass->expirationDate());
264 }
265 }
266
267 // location is the best guess for the venue
268 auto venue = event.location().value<Place>();
269 auto geo = venue.geo();
270 if (!pass->locations().isEmpty() && !geo.isValid()) {
271 const auto loc = pass->locations().at(0);
272 geo.setLatitude(loc.latitude());
273 geo.setLongitude(loc.longitude());
274 venue.setGeo(geo);
275 if (venue.name().isEmpty()) {
276 venue.setName(loc.relevantText());
277 }
278 event.setLocation(venue);
279 }
280
281 // search for prices
282 PriceFinder priceFinder;
283 std::vector<PriceFinder::Result> prices;
284 const auto fields = pass->fields();
285 for (const auto &field : fields) {
286 priceFinder.findAll(field.value().toString(), prices);
287 }
288 if (const auto price = priceFinder.highest(prices); price.hasResult()) {
289 eventRes.setTotalPrice(price.value);
290 eventRes.setPriceCurrency(price.currency);
291 }
292
293 eventRes.setReservationFor(event);
294}
295
296static Person extractPerson(const KPkPass::Pass *pass, Person person)
297{
298 const auto fields = pass->fields();
299 for (const auto &field : fields) {
300 person = NameOptimizer::optimizeName(field.valueDisplayString(), person);
301 }
302 return person;
303}
304
305void PkPassDocumentProcessor::preExtract(ExtractorDocumentNode &node, [[maybe_unused]] const ExtractorEngine *engine) const
306{
307 const auto pass = node.content<KPkPass::Pass*>();
308 QJsonObject result;
309 if (auto boardingPass = qobject_cast<KPkPass::BoardingPass*>(pass)) {
310 switch (boardingPass->transitType()) {
311 case KPkPass::BoardingPass::Air:
312 result.insert(QStringLiteral("@type"),
313 QLatin1StringView("FlightReservation"));
314 break;
315 case KPkPass::BoardingPass::Train:
316 result.insert(QStringLiteral("@type"),
317 QLatin1StringView("TrainReservation"));
318 break;
319 case KPkPass::BoardingPass::Bus:
320 result.insert(QStringLiteral("@type"),
321 QLatin1StringView("BusReservation"));
322 break;
323 // TODO expand once we have test files for other types
324 default:
325 break;
326 }
327 } else {
328 switch (pass->type()) {
329 case KPkPass::Pass::EventTicket:
330 result.insert(QStringLiteral("@type"),
331 QLatin1StringView("EventReservation"));
332 break;
333 default:
334 return;
335 }
336 }
337
338 // barcode contains the ticket token
339 if (!pass->barcodes().isEmpty()) {
340 const auto barcode = pass->barcodes().at(0);
341 QString token;
342 switch (barcode.format()) {
343 case KPkPass::Barcode::QR:
344 token += QLatin1StringView("qrCode:");
345 break;
346 case KPkPass::Barcode::Aztec:
347 token += QLatin1StringView("aztecCode:");
348 break;
349 default:
350 break;
351 }
352 token += barcode.message();
353 QJsonObject ticket =
354 result.value(QLatin1StringView("reservedTicket")).toObject();
355 ticket.insert(QStringLiteral("@type"), QLatin1StringView("Ticket"));
356 ticket.insert(QStringLiteral("ticketToken"), token);
357 result.insert(QStringLiteral("reservedTicket"), ticket);
358 }
359
360 // explicitly merge with the decoded barcode data, as this would other wise not match
361 auto res = JsonLdDocument::fromJsonSingular(result);
363 // if this doesn't contain a single IATA BCBP we wont be able to get sufficient information out of this
364 if (node.childNodes().size() != 1 || node.childNodes()[0].result().size() != 1) {
365 return;
366 }
367 res = JsonLdDocument::apply(node.childNodes()[0].result().result().at(0), res).value<FlightReservation>();
368 }
369
370 // extract structured data from a pkpass, if the extractor script hasn't done so already
371 switch (pass->type()) {
372 case KPkPass::Pass::BoardingPass:
373 {
374 if (auto boardingPass = qobject_cast<KPkPass::BoardingPass*>(pass)) {
375 switch (boardingPass->transitType()) {
376 case KPkPass::BoardingPass::Air:
377 {
378 auto flightRes = res.value<FlightReservation>();
379 flightRes.setReservationFor(extractBoardingPass(pass, flightRes.reservationFor().value<Flight>()));
380 flightRes.setUnderName(extractPerson(pass, flightRes.underName().value<Person>()));
381 res = flightRes;
382 break;
383 }
384 default:
385 break;
386 }
387 }
388 break;
389 }
390 case KPkPass::Pass::EventTicket:
391 {
392 auto evRes = res.value<EventReservation>();
393 extractEventTicketPass(pass, evRes);
394 res = evRes;
395 break;
396 }
397 default:
398 break;
399 }
400 node.setResult(QList<QVariant>({res}));
401}
402
403void PkPassDocumentProcessor::postExtract(ExtractorDocumentNode &node, [[maybe_unused]] const ExtractorEngine *engine) const
404{
405 const auto pass = node.content<KPkPass::Pass*>();
406 if (pass->passTypeIdentifier().isEmpty() || pass->serialNumber().isEmpty()) {
407 return;
408 }
409
410 // associate the pass with the result, so we can find the pass again for display
411 auto result = node.result().jsonLdResult();
412 for (auto resV : result) {
413 auto res = resV.toObject();
414 res.insert(QLatin1StringView("pkpassPassTypeIdentifier"),
415 pass->passTypeIdentifier());
416 res.insert(QLatin1StringView("pkpassSerialNumber"),
417 pass->serialNumber());
418 // pass->relevantDate() as modification time is inherently unreliable (it wont change most of the time)
419 // so if we have something from an enclosing document, that's probably better
420 if (node.parent().contextDateTime().isValid()) {
421 res.insert(QLatin1StringView("modifiedTime"),
423 }
424 resV = res;
425 }
426 node.setResult(result);
427}
An event reservation.
An event.
Definition event.h:21
ExtractorDocumentNode createNode(const QByteArray &data, QStringView fileName={}, QStringView mimeType={}) const
Create a new document node from data.
A node in the extracted document object tree.
QJsonArray result
Result access for QJSEngine.
void setResult(ExtractorResult &&result)
Replace the existing results by result.
void appendChild(ExtractorDocumentNode &child)
Add another child node.
void setContextDateTime(const QDateTime &contextDateTime)
Set the context date/time.
QJSValue content
The decoded content of this node.
QVariantList childNodes
Child nodes, for QJSEngine access.
QDateTime contextDateTime
The best known context date/time at this point in the document tree.
void setContent(const QVariant &content)
Set decoded content.
KItinerary::ExtractorDocumentNode parent
The parent node, or a null node if this is the root node.
Semantic data extraction engine.
const ExtractorDocumentNodeFactory * documentNodeFactory() const
Factory for creating new document nodes.
A flight reservation.
Definition reservation.h:90
A flight.
Definition flight.h:25
static QVariant apply(const QVariant &lhs, const QVariant &rhs)
Apply all properties of rhs on to lhs.
static QVariant fromJsonSingular(const QJsonObject &obj)
Convert a single JSON-LD object into an instantiated data type.
A person.
Definition person.h:20
Processor for Apple Wallet pass files.
void preExtract(ExtractorDocumentNode &node, const ExtractorEngine *engine) const override
Called before extractors are applied to node.
ExtractorDocumentNode createNodeFromContent(const QVariant &decodedData) const override
Create a document node from an already decoded data type.
QJSValue contentToScriptValue(const ExtractorDocumentNode &node, QJSEngine *engine) const override
Create a QJSValue for the node content.
ExtractorDocumentNode createNodeFromData(const QByteArray &encodedData) const override
Create a document node from raw data.
void postExtract(ExtractorDocumentNode &node, const ExtractorEngine *engine) const override
Called after extractors have been applied to node.
void destroyNode(ExtractorDocumentNode &node) const override
Destroys type-specific data in node.
void expandNode(ExtractorDocumentNode &node, const ExtractorEngine *engine) const override
Create child nodes for node, as far as that's necessary for this document type.
Base class for places.
Definition place.h:69
static Pass * fromData(const QByteArray &data, QObject *parent=nullptr)
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
bool canConvert(const QVariant &value)
Checks if the given value can be up-cast to T.
Definition datatypes.h:31
QTimeZone timezoneForAirport(IataCode iataCode)
Returns the timezone the airport with IATA code iataCode is in.
Definition airportdb.cpp:40
AlphaId< uint16_t, 3 > IataCode
IATA airport code.
Definition iatacode.h:17
std::vector< IataCode > iataCodesFromName(QStringView name)
Returns all possible IATA code candidates for the given airport name.
GeoCoordinates geo(const QVariant &location)
Returns the geo coordinates of a given location.
Classes for reservation/travel data models, data extraction and data augmentation.
Definition berelement.h:17
bool isValid() const const
QString toString(QStringView format, QCalendar cal) const const
QJSValue toScriptValue(const T &value)
iterator insert(iterator before, const QJsonValue &value)
iterator insert(QLatin1StringView key, const QJsonValue &value)
QJsonValue value(QLatin1StringView key) const const
QJsonObject toObject() const const
qsizetype count() const const
bool isEmpty() const const
bool isNull() const const
qsizetype size() const const
CaseInsensitive
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:49 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.