diff --git a/autotests/pkpassdata/airbaltic.json b/autotests/pkpassdata/airbaltic.json index 34ba3ae..0669055 100644 --- a/autotests/pkpassdata/airbaltic.json +++ b/autotests/pkpassdata/airbaltic.json @@ -1,60 +1,62 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "14E", "passengerSequenceNumber": "63", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "BT", "name": "airBaltic" }, "arrivalAirport": { "@type": "Airport", "address": { "@type": "PostalAddress", "addressCountry": "LV" }, "geo": { "@type": "GeoCoordinates", "latitude": 56.920799255371094, "longitude": 23.970800399780273 }, - "iataCode": "RIX" + "iataCode": "RIX", + "name": "Riga" }, "boardingTime": { "@type": "QDateTime", "@value": "2017-11-05T08:25:00+01:00", "timezone": "Europe/Berlin" }, "departureAirport": { "@type": "Airport", "address": { "@type": "PostalAddress", "addressCountry": "DE" }, "geo": { "@type": "GeoCoordinates", "latitude": 52.55970001220703, "longitude": 13.287799835205078 }, - "iataCode": "TXL" + "iataCode": "TXL", + "name": "Berlin" }, "departureDay": "2017-11-05", "flightNumber": "212" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 TXLRIXBT 0212 309Y014E0063 100" }, "underName": { "@type": "Person", "familyName": "KRAUSE", "givenName": "VOLKER", "name": "VOLKER KRAUSE" } } ] diff --git a/autotests/pkpassdata/airberlin.json b/autotests/pkpassdata/airberlin.json index bf88816..1892820 100644 --- a/autotests/pkpassdata/airberlin.json +++ b/autotests/pkpassdata/airberlin.json @@ -1,60 +1,63 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "19F", "passengerSequenceNumber": "60", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "AB", "name": "airberlin" }, "arrivalAirport": { "@type": "Airport", "address": { "@type": "PostalAddress", "addressCountry": "DE" }, "geo": { "@type": "GeoCoordinates", "latitude": 48.35390090942383, "longitude": 11.786100387573242 }, - "iataCode": "MUC" + "iataCode": "MUC", + "name": "Munich" }, "boardingTime": { "@type": "QDateTime", "@value": "2017-10-24T15:55:00+02:00", "timezone": "Europe/Berlin" }, "departureAirport": { "@type": "Airport", "address": { "@type": "PostalAddress", "addressCountry": "DE" }, "geo": { "@type": "GeoCoordinates", "latitude": 52.559688568115234, "longitude": 13.287711143493652 }, - "iataCode": "TXL" + "iataCode": "TXL", + "name": "Berlin - Tegel" }, "departureDay": "2017-10-24", + "departureGate": "C62", "flightNumber": "6203" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 TXLMUCAB 6203 297Y019F0060 33C>5080 B2A N00 " }, "underName": { "@type": "Person", "familyName": "KRAUSE", "givenName": "VOLKER", "name": "VOLKER KRAUSE" } } ] diff --git a/autotests/pkpassdata/eurowings.json b/autotests/pkpassdata/eurowings.json index e23be85..2f7af58 100644 --- a/autotests/pkpassdata/eurowings.json +++ b/autotests/pkpassdata/eurowings.json @@ -1,63 +1,62 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "17C", "passengerSequenceNumber": "40", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "4U", "name": "Germanwings" }, "arrivalAirport": { "@type": "Airport", "address": { "@type": "PostalAddress", "addressCountry": "DE" }, "geo": { "@type": "GeoCoordinates", "latitude": 52.55970001220703, "longitude": 13.287799835205078 }, "iataCode": "TXL", "name": "Berlin-Tegel" }, "boardingTime": { "@type": "QDateTime", "@value": "2017-06-18T18:40:00+01:00", "timezone": "Europe/London" }, "departureAirport": { "@type": "Airport", "address": { "@type": "PostalAddress", "addressCountry": "GB" }, "geo": { "@type": "GeoCoordinates", "latitude": 51.477500915527344, "longitude": -0.4613890051841736 }, "iataCode": "LHR", "name": "London Heathrow" }, "departureDay": "2017-06-18", - "departureGate": " - ", "flightNumber": "8465" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1KRAUSE/VOLKER EXXX007 LHRTXL4U 8465 169Y017C0040 147>1181 7168B4U 0000000000000291040PASSPORTID2 LH 123412341234012 " }, "underName": { "@type": "Person", "familyName": "KRAUSE", "givenName": "VOLKER", "name": "VOLKER KRAUSE" } } ] diff --git a/autotests/pkpassdata/lufthansa-with-timezone.json b/autotests/pkpassdata/lufthansa-with-timezone.json index 3f6a1a1..2ae7b4a 100644 --- a/autotests/pkpassdata/lufthansa-with-timezone.json +++ b/autotests/pkpassdata/lufthansa-with-timezone.json @@ -1,63 +1,62 @@ [ { "@context": "http://schema.org", "@type": "FlightReservation", "airplaneSeat": "1A", "passengerSequenceNumber": "1", "reservationFor": { "@type": "Flight", "airline": { "@type": "Airline", "iataCode": "LH", "name": "Lufthansa" }, "arrivalAirport": { "@type": "Airport", "address": { "@type": "PostalAddress", "addressCountry": "DE" }, "geo": { "@type": "GeoCoordinates", "latitude": 50.03329849243164, "longitude": 8.570560455322266 }, "iataCode": "FRA", "name": "FRANKFURT" }, "boardingTime": { "@type": "QDateTime", "@value": "2019-05-21T18:20:00-04:00", "timezone": "America/Toronto" }, "departureAirport": { "@type": "Airport", "address": { "@type": "PostalAddress", "addressCountry": "CA" }, "geo": { "@type": "GeoCoordinates", "latitude": 45.47060012817383, "longitude": -73.74079895019531 }, "iataCode": "YUL", "name": "MONTREAL" }, "departureDay": "2019-05-21", - "departureGate": "....", "flightNumber": "489" }, "reservationNumber": "XXX007", "reservedTicket": { "@type": "Ticket", "ticketToken": "aztecCode:M1JOHN/DOE EXXX007 YULFRALH 0489 141M001A0001 300" }, "underName": { "@type": "Person", "familyName": "JOHN", "givenName": "DOE", "name": "DOE JOHN" } } ] diff --git a/src/extractors/eurowings-pkpass.js b/src/extractors/eurowings-pkpass.js index b31f8b6..5589a22 100644 --- a/src/extractors/eurowings-pkpass.js +++ b/src/extractors/eurowings-pkpass.js @@ -1,31 +1,29 @@ /* Copyright (c) 2018 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ function main(pass) { var res = JsonLd.newFlightReservation(); - res.reservationFor.departureGate = pass.field["gate"].value; res.reservationFor.departureAirport.name = pass.field["origin"].label; res.reservationFor.arrivalAirport.name = pass.field["destination"].label; - res.reservationFor.boardingTime = JsonLd.toDateTime(pass.field["departureDate"].value + ' ' + pass.field["boarding"].value, "M/d/yyyy hh:mm", "en"); if (pass.field["operatingcarrier"]) res.reservationFor.airline.name = pass.field["operatingcarrier"].value; return res; } diff --git a/src/extractors/extractors.qrc b/src/extractors/extractors.qrc index a97a989..4f8b8f4 100644 --- a/src/extractors/extractors.qrc +++ b/src/extractors/extractors.qrc @@ -1,84 +1,83 @@ acprail.json acprail.js aerlingus.json aerlingus.js airbaltic.json airbaltic.js aircoach-ie.json aircoach-ie.js amadeus.json amadeus.js americanairlines.json americanairlines.js aohostels.json aohostels.js availpro.json availpro.js booking.json booking.js brusselsairlines.json brusselsairlines.js brusselsairlines-receipt.js czechrailways.json czechrailways.js deutschebahn.json deutschebahn.js dinnerbooking.json dinnerbooking.js easyairportparking.json easyairportparking-pkpass.js easyjet.json easyjet.js eurowings.json eurowings.js eurowings-pkpass.js fcmtravel.json fcmtravel.js hertz.js hertz.json iberia.json iberia.js irctc.json irctc.js klm.json klm.js koleje-malopolskie.json koleje-malopolskie.js korail.json korail.js lufthansa.json lufthansa-pkpass.js nationalexpress.json nationalexpress.js nh-hotels.json nh-hotels.js np4.json np4.js regiojet.json regiojet.js renfe.json renfe.js sas.json sas-boardingpass.js sas-receipt.js simplebooking.json simplebooking.js sncf.json sncf.js stansted-express.json stansted-express.js swiss.json swiss.js - swiss-pkpass.js travelport-galileo.json travelport-galileo.js trenitalia.json trenitalia.js vgn.json vgn.js vueling.json vueling.js diff --git a/src/extractors/lufthansa-pkpass.js b/src/extractors/lufthansa-pkpass.js index 8e9487d..f5c8b6e 100644 --- a/src/extractors/lufthansa-pkpass.js +++ b/src/extractors/lufthansa-pkpass.js @@ -1,32 +1,31 @@ /* Copyright (c) 2018 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ function main(pass) { // if (pass.transitType != KPkPass.BoardinPass.Air) { // TODO this needs to be registered in the engine // return null; // } var res = JsonLd.newFlightReservation(); - res.reservationFor.departureGate = pass.field["gate"].value; res.reservationFor.departureAirport.name = pass.field["origin"].label; res.reservationFor.arrivalAirport.name = pass.field["destination"].label; return res; } diff --git a/src/extractors/swiss-pkpass.js b/src/extractors/swiss-pkpass.js deleted file mode 100644 index 67d874a..0000000 --- a/src/extractors/swiss-pkpass.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - Copyright (c) 2018 Volker Krause - - This library is free software; you can redistribute it and/or modify it - under the terms of the GNU Library General Public License as published by - the Free Software Foundation; either version 2 of the License, or (at your - option) any later version. - - This library is distributed in the hope that it will be useful, but WITHOUT - ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public - License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to the - Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301, USA. -*/ - -function main(pass) -{ - var res = JsonLd.newFlightReservation(); - res.reservationFor.departureGate = pass.field["gate"].value; - - return res; -} - diff --git a/src/extractors/swiss.json b/src/extractors/swiss.json index 97aac54..7381356 100644 --- a/src/extractors/swiss.json +++ b/src/extractors/swiss.json @@ -1,12 +1,7 @@ [ { "type": "text", "filter": [ { "header": "From", "match": "noreply@swiss.com" } ], "script": "swiss.js" - }, - { - "type": "pkpass", - "filter": [ { "field": "passTypeIdentifier", "match": "pass.booking.swiss.com" } ], - "script": "swiss-pkpass.js" } ] diff --git a/src/generic/genericpkpassextractor.cpp b/src/generic/genericpkpassextractor.cpp index db0f525..488d619 100644 --- a/src/generic/genericpkpassextractor.cpp +++ b/src/generic/genericpkpassextractor.cpp @@ -1,222 +1,289 @@ /* Copyright (c) 2019 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "genericpkpassextractor_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; +static QVector frontFieldsForPass(KPkPass::Pass *pass) +{ + QVector fields; + fields += pass->headerFields(); + fields += pass->primaryFields(); + fields += pass->secondaryFields(); + fields += pass->auxiliaryFields(); + return fields; +} + +static bool isAirportName(const QString &name, KnowledgeDb::IataCode iataCode) +{ + if (name.size() <= 3) { + return false; + } + + const auto codes = KnowledgeDb::iataCodesFromName(name); + return std::find(codes.begin(), codes.end(), iataCode) != codes.end(); +} + +static bool isPlausibeGate(const QString &s) +{ + for (const auto &c : s) { + if (c.isLetter() || c.isDigit()) { + return true; + } + } + return false; +} + static Flight extractBoardingPass(KPkPass::Pass *pass, Flight flight) { // "relevantDate" is the best guess for the boarding time if (pass->relevantDate().isValid() && !flight.boardingTime().isValid()) { const auto tz = KnowledgeDb::timezoneForAirport(KnowledgeDb::IataCode{flight.departureAirport().iataCode()}); if (tz.isValid()) { flight.setBoardingTime(pass->relevantDate().toTimeZone(tz)); } else { flight.setBoardingTime(pass->relevantDate()); } } - // look for common field names containing the boarding time, if we still have no idea - if (!flight.boardingTime().isValid()) { - const auto fields = pass->fields(); - for (const auto &field : fields) { - if (!field.key().contains(QLatin1String("boarding"), Qt::CaseInsensitive)) { - continue; - } + + // search for missing information by field key + const auto fields = pass->fields(); + for (const auto &field : fields) { + // boarding time + if (!flight.boardingTime().isValid() && field.key().contains(QLatin1String("boarding"), Qt::CaseInsensitive)) { const auto time = QTime::fromString(field.value().toString()); if (time.isValid()) { // this misses date, but the postprocessor will fill that in flight.setBoardingTime(QDateTime(QDate(1, 1, 1), time)); - break; + continue; + } + } + // departure gate + if (flight.departureGate().isEmpty() && field.key().contains(QLatin1String("gate"), Qt::CaseInsensitive)) { + const auto gateStr = field.value().toString(); + if (isPlausibeGate(gateStr)) { + flight.setDepartureGate(gateStr); + continue; + } + } + } + + // search for missing information in field content + const auto depIata = KnowledgeDb::IataCode(flight.departureAirport().iataCode()); + const auto arrIata = KnowledgeDb::IataCode(flight.arrivalAirport().iataCode()); + const auto frontFields = frontFieldsForPass(pass); + for (const auto &field : frontFields) { + // full airport names + if (flight.departureAirport().name().isEmpty()) { + if (isAirportName(field.value().toString(), depIata)) { + auto airport = flight.departureAirport(); + airport.setName(field.value().toString()); + flight.setDepartureAirport(airport); + } else if (isAirportName(field.label(), depIata)) { + auto airport = flight.departureAirport(); + airport.setName(field.label()); + flight.setDepartureAirport(airport); + } + } + if (flight.arrivalAirport().name().isEmpty()) { + if (isAirportName(field.value().toString(), arrIata)) { + auto airport = flight.arrivalAirport(); + airport.setName(field.value().toString()); + flight.setArrivalAirport(airport); + } else if (isAirportName(field.label(), arrIata)) { + auto airport = flight.arrivalAirport(); + airport.setName(field.label()); + flight.setArrivalAirport(airport); } } } // location is the best guess for the departure airport geo coordinates auto depAirport = flight.departureAirport(); auto depGeo = depAirport.geo(); if (pass->locations().size() == 1 && !depGeo.isValid()) { const auto loc = pass->locations().at(0); depGeo.setLatitude(loc.latitude()); depGeo.setLongitude(loc.longitude()); depAirport.setGeo(depGeo); flight.setDepartureAirport(depAirport); } // organizationName is the best guess for airline name auto airline = flight.airline(); if (airline.name().isEmpty()) { airline.setName(pass->organizationName()); flight.setAirline(airline); } return flight; } static Event extractEventTicketPass(KPkPass::Pass *pass, Event event) { if (event.name().isEmpty()) { event.setName(pass->description()); } // "relevantDate" is the best guess for the start time if (pass->relevantDate().isValid() && !event.startDate().isValid()) { event.setStartDate(pass->relevantDate()); } // location is the best guess for the venue auto venue = event.location().value(); auto geo = venue.geo(); if (!pass->locations().isEmpty() && !geo.isValid()) { const auto loc = pass->locations().at(0); geo.setLatitude(loc.latitude()); geo.setLongitude(loc.longitude()); venue.setGeo(geo); if (venue.name().isEmpty()) { venue.setName(loc.relevantText()); } event.setLocation(venue); } return event; } static QDateTime iataContextDate(KPkPass::Pass *pass, const QDateTime &context) { if (!pass->relevantDate().isValid()) { return context; } return pass->relevantDate().addDays(-1); // go a bit back, to compensate for unknown departure timezone at this point } QJsonObject GenericPkPassExtractor::extract(KPkPass::Pass *pass, const QJsonObject &extracted, const QDateTime &contextDate) { auto result = extracted; if (result.isEmpty()) { // no previous extractor ran, so we need to create the top-level element ourselves if (auto boardingPass = qobject_cast(pass)) { switch (boardingPass->transitType()) { case KPkPass::BoardingPass::Air: result.insert(QStringLiteral("@type"), QLatin1String("FlightReservation")); break; // TODO expand once we have test files for train tickets default: break; } } else { switch (pass->type()) { case KPkPass::Pass::EventTicket: result.insert(QStringLiteral("@type"), QLatin1String("EventReservation")); break; default: return result; } } } // barcode contains the ticket token if (!pass->barcodes().isEmpty()) { const auto barcode = pass->barcodes().at(0); QString token; switch (barcode.format()) { case KPkPass::Barcode::QR: token += QLatin1String("qrCode:"); break; case KPkPass::Barcode::Aztec: token += QLatin1String("aztecCode:"); break; default: break; } token += barcode.message(); QJsonObject ticket = result.value(QLatin1String("reservedTicket")).toObject(); ticket.insert(QStringLiteral("@type"), QLatin1String("Ticket")); if (!ticket.contains(QLatin1String("ticketToken"))) { ticket.insert(QStringLiteral("ticketToken"), token); } result.insert(QStringLiteral("reservedTicket"), ticket); } // decode the barcode here already, so we have more information available for the following steps // also, we have additional context time information here auto res = JsonLdDocument::fromJson(result); if (JsonLd::isA(res)) { const auto bcbp = res.value().reservedTicket().value().ticketTokenData(); const auto bcbpData = IataBcbpParser::parse(bcbp, iataContextDate(pass, contextDate).date()); if (bcbpData.size() == 1) { res = JsonLdDocument::apply(bcbpData.at(0), res).value(); } } // extract structured data from a pkpass, if the extractor script hasn't done so already switch (pass->type()) { case KPkPass::Pass::BoardingPass: { if (auto boardingPass = qobject_cast(pass)) { switch (boardingPass->transitType()) { case KPkPass::BoardingPass::Air: { auto flightRes = res.value(); flightRes.setReservationFor(extractBoardingPass(pass, flightRes.reservationFor().value())); res = flightRes; break; } default: break; } } break; } case KPkPass::Pass::EventTicket: { auto evRes = res.value(); evRes.setReservationFor(extractEventTicketPass(pass, evRes.reservationFor().value())); res = evRes; break; } default: break; } // associate the pass with the result, so we can find the pass again for display result = JsonLdDocument::toJson(res); if (!pass->passTypeIdentifier().isEmpty() && !pass->serialNumber().isEmpty()) { result.insert(QStringLiteral("pkpassPassTypeIdentifier"), pass->passTypeIdentifier()); result.insert(QStringLiteral("pkpassSerialNumber"), pass->serialNumber()); } return result; }