diff --git a/autotests/extractordata/regiojet/regiojet_bus_cs-one-leg-return.txt.json b/autotests/extractordata/regiojet/regiojet_bus_cs-one-leg-return.txt.json index 63d8fe4..3763349 100644 --- a/autotests/extractordata/regiojet/regiojet_bus_cs-one-leg-return.txt.json +++ b/autotests/extractordata/regiojet/regiojet_bus_cs-one-leg-return.txt.json @@ -1,68 +1,68 @@ [ { "@context": "http://schema.org", "@type": "BusReservation", "potentialAction": [ { "@type": "UpdateAction", "target": "https://jizdenky.studentagency.cz/OnlineTicket?pam1=9876543210&pam2=0123456789" } ], "reservationFor": { "@type": "BusTrip", - "arrivalStation": { + "arrivalBusStop": { "@type": "BusStation", "name": "Wien, Lassallestr./Praterstern" }, "arrivalTime": "2012-08-11T11:20:00", "busName": "Praha → Vídeň", "busNumber": "SA", - "departureStation": { + "departureBusStop": { "@type": "BusStation", "name": "Brno, AN u hotelu Grand" }, "departureTime": "2012-08-11T09:30:00" }, "reservationNumber": "0123456789", "reservedTicket": { "@type": "Ticket", "ticketedSeat": { "@type": "Seat", "seatNumber": "51, 52, 55, 56" } } }, { "@context": "http://schema.org", "@type": "BusReservation", "potentialAction": [ { "@type": "UpdateAction", "target": "https://jizdenky.studentagency.cz/OnlineTicket?pam1=9876543210&pam2=0123456789" } ], "reservationFor": { "@type": "BusTrip", - "arrivalStation": { + "arrivalBusStop": { "@type": "BusStation", "name": "Brno, AN u hotelu Grand" }, "arrivalTime": "2012-08-12T00:30:00", "busName": "Vídeň → Praha", "busNumber": "SA", - "departureStation": { + "departureBusStop": { "@type": "BusStation", "name": "Wien, Lassallestr./Praterstern" }, "departureTime": "2012-08-11T22:40:00" }, "reservationNumber": "0123456789", "reservedTicket": { "@type": "Ticket", "ticketedSeat": { "@type": "Seat", "seatNumber": "35, 36, 39, 40" } } } ] diff --git a/autotests/extractordata/regiojet/regiojet_bus_cs-one-leg-single.txt.json b/autotests/extractordata/regiojet/regiojet_bus_cs-one-leg-single.txt.json index d5179d0..d014839 100644 --- a/autotests/extractordata/regiojet/regiojet_bus_cs-one-leg-single.txt.json +++ b/autotests/extractordata/regiojet/regiojet_bus_cs-one-leg-single.txt.json @@ -1,35 +1,35 @@ [ { "@context": "http://schema.org", "@type": "BusReservation", "potentialAction": [ { "@type": "UpdateAction", "target": "http://jizdenky.studentagency.cz/OnlineTicket?pam1=0987654321&pam2=0123456789" } ], "reservationFor": { "@type": "BusTrip", - "arrivalStation": { + "arrivalBusStop": { "@type": "BusStation", "name": "Wien, SchwechatAirport Bbf." }, "arrivalTime": "2012-06-29T05:35:00", "busName": "Praha → Vídeň", "busNumber": "SA", - "departureStation": { + "departureBusStop": { "@type": "BusStation", "name": "Brno, AN u hotelu Grand" }, "departureTime": "2012-06-29T03:20:00" }, "reservationNumber": "0123456789", "reservedTicket": { "@type": "Ticket", "ticketedSeat": { "@type": "Seat", "seatNumber": "43" } } } ] diff --git a/autotests/extractordata/regiojet/regiojet_bus_en-one-leg-single.txt.json b/autotests/extractordata/regiojet/regiojet_bus_en-one-leg-single.txt.json index 8194c97..ac1fded 100644 --- a/autotests/extractordata/regiojet/regiojet_bus_en-one-leg-single.txt.json +++ b/autotests/extractordata/regiojet/regiojet_bus_en-one-leg-single.txt.json @@ -1,36 +1,36 @@ [ { "@context": "http://schema.org", "@type": "BusReservation", "potentialAction": [ { "@type": "UpdateAction", "target": "https://jizdenky.studentagency.cz/OnlineTicket?pam1=0987654321&pam2=0123456789" } ], "reservationFor": { "@type": "BusTrip", - "arrivalStation": { + "arrivalBusStop": { "@type": "BusStation", "name": "Brno, AN u hotelu Grand" }, "arrivalTime": "2015-08-01T00:30:00", "busName": "Vienna → Prague", "busNumber": "SA", - "departurePlatform": "2", - "departureStation": { + "departureBusStop": { "@type": "BusStation", "name": "Wien, Schwechat Airport Bbf." }, + "departurePlatform": "2", "departureTime": "2015-07-31T22:10:00" }, "reservationNumber": "0123456789", "reservedTicket": { "@type": "Ticket", "ticketedSeat": { "@type": "Seat", "seatNumber": "48" } } } ] diff --git a/src/calendarhandler.cpp b/src/calendarhandler.cpp index 9ca2001..8153743 100644 --- a/src/calendarhandler.cpp +++ b/src/calendarhandler.cpp @@ -1,443 +1,443 @@ /* Copyright (c) 2017 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 "config-kitinerary.h" #include "calendarhandler.h" #include "jsonlddocument.h" #include "logging.h" #include "mergeutil.h" #include "sortutil.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_KCAL #include #include #include #endif #include #include #include #include #include using namespace KItinerary; static QString formatAddress(const PostalAddress &addr) { KContacts::Address a; a.setStreet(addr.streetAddress()); a.setPostalCode(addr.postalCode()); a.setLocality(addr.addressLocality()); a.setCountry(KContacts::Address::ISOtoCountry(addr.addressCountry())); return a.formattedAddress(); } static QString formatAddressSingleLine(const PostalAddress &addr) { return formatAddress(addr).replace(QLatin1String("\n\n"), QLatin1String("\n")).replace(QLatin1Char('\n'), QLatin1String(", ")); } #ifdef HAVE_KCAL using namespace KCalCore; static void fillFlightReservation(const QVector &reservations, const KCalCore::Event::Ptr &event); static void fillTrainReservation(const TrainReservation &reservation, const KCalCore::Event::Ptr &event); static void fillBusReservation(const BusReservation &reservation, const KCalCore::Event::Ptr &event); static void fillLodgingReservation(const LodgingReservation &reservation, const KCalCore::Event::Ptr &event); static void fillEventReservation(const QVector &reservations, const KCalCore::Event::Ptr &event); static void fillGeoPosition(const QVariant &place, const KCalCore::Event::Ptr &event); static void fillFoodReservation(const FoodEstablishmentReservation &reservation, const KCalCore::Event::Ptr &event); static void fillRentalCarReservation(const RentalCarReservation &reservation, const KCalCore::Event::Ptr &event); static void fillTaxiReservation(const TaxiReservation &reservation, const KCalCore::Event::Ptr &event); #endif QSharedPointer CalendarHandler::findEvent(const QSharedPointer &calendar, const QVariant &reservation) { #ifdef HAVE_KCAL if (!JsonLd::canConvert(reservation)) { return {}; } const auto dt = SortUtil::startDateTime(reservation).date(); const auto events = calendar->events(dt); for (const auto &event : events) { if (!event->uid().startsWith(QLatin1String("KIT-"))) { continue; } const auto otherRes = CalendarHandler::reservationsForEvent(event); for (const auto &other : otherRes) { if (MergeUtil::isSame(other, reservation)) { return event; } } } #else Q_UNUSED(calendar); Q_UNUSED(reservation); #endif return {}; } QVector CalendarHandler::reservationsForEvent(const QSharedPointer &event) { #ifdef HAVE_KCAL const auto payload = event->customProperty("KITINERARY", "RESERVATION").toUtf8(); const auto json = QJsonDocument::fromJson(payload).array(); return JsonLdDocument::fromJson(json); #else Q_UNUSED(event); return {}; #endif } void CalendarHandler::fillEvent(const QVector &reservations, const QSharedPointer &event) { if (reservations.isEmpty()) { return; } #ifdef HAVE_KCAL // TODO pass reservationS into all functions below for multi-traveler support const auto reservation = reservations.at(0); const int typeId = reservation.userType(); if (typeId == qMetaTypeId()) { fillFlightReservation(reservations, event); } else if (typeId == qMetaTypeId()) { fillLodgingReservation(reservation.value(), event); } else if (typeId == qMetaTypeId()) { fillTrainReservation(reservation.value(), event); } else if (typeId == qMetaTypeId()) { fillBusReservation(reservation.value(), event); } else if (JsonLd::isA(reservation)) { fillEventReservation(reservations, event); } else if (JsonLd::isA(reservation)) { fillFoodReservation(reservation.value(), event); } else if (JsonLd::isA(reservation)) { fillRentalCarReservation(reservation.value(), event); } else if (JsonLd::isA(reservation)) { fillTaxiReservation(reservation.value(), event); } else { return; } if (!event->uid().startsWith(QLatin1String("KIT-"))) { event->setUid(QLatin1String("KIT-") + event->uid()); } const auto payload = QJsonDocument(JsonLdDocument::toJson(reservations)).toJson(QJsonDocument::Compact); event->setCustomProperty("KITINERARY", "RESERVATION", QString::fromUtf8(payload)); #else Q_UNUSED(event); #endif } #ifdef HAVE_KCAL static void fillFlightReservation(const QVector &reservations, const KCalCore::Event::Ptr &event) { const auto flight = reservations.at(0).value().reservationFor().value(); const auto airline = flight.airline(); const auto depPort = flight.departureAirport(); const auto arrPort = flight.arrivalAirport(); const QString flightNumber = airline.iataCode() + QLatin1Char(' ') + flight.flightNumber(); event->setSummary(i18n("Flight %1 from %2 to %3", flightNumber, depPort.iataCode(), arrPort.iataCode())); event->setLocation(depPort.name()); fillGeoPosition(depPort, event); event->setDtStart(flight.departureTime()); event->setDtEnd(flight.arrivalTime()); event->setAllDay(false); const auto boardingTime = flight.boardingTime(); const auto departureGate = flight.departureGate(); if (boardingTime.isValid()) { const auto startOffset = Duration(event->dtStart(), boardingTime); const auto existinAlarms = event->alarms(); const auto it = std::find_if(existinAlarms.begin(), existinAlarms.end(), [startOffset](const Alarm::Ptr &other) { return other->startOffset() == startOffset; }); if (it == existinAlarms.end()) { Alarm::Ptr alarm(new Alarm(event.data())); alarm->setStartOffset(Duration(event->dtStart(), boardingTime)); if (departureGate.isEmpty()) { alarm->setDisplayAlarm(i18n("Boarding for flight %1", flightNumber)); } else { alarm->setDisplayAlarm(i18n("Boarding for flight %1 at gate %2", flightNumber, departureGate)); } alarm->setEnabled(true); event->addAlarm(alarm); } } QStringList desc; if (boardingTime.isValid()) { desc.push_back(i18n("Boarding time: %1", QLocale().toString(boardingTime.time(), QLocale::ShortFormat))); } if (!departureGate.isEmpty()) { desc.push_back(i18n("Departure gate: %1", departureGate)); } for (const auto &r : reservations) { const auto reservation = r.value(); const auto person = reservation.underName().value(); if (!person.name().isEmpty()) { desc.push_back(person.name()); } if (!reservation.boardingGroup().isEmpty()) { desc.push_back(i18n("Boarding group: %1", reservation.boardingGroup())); } if (!reservation.airplaneSeat().isEmpty()) { desc.push_back(i18n("Seat: %1", reservation.airplaneSeat())); } if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillTrainReservation(const TrainReservation &reservation, const KCalCore::Event::Ptr &event) { const auto trip = reservation.reservationFor().value(); const auto depStation = trip.departureStation(); const auto arrStation = trip.arrivalStation(); event->setSummary(i18n("Train %1 from %2 to %3", trip.trainNumber(), depStation.name(), arrStation.name())); event->setLocation(depStation.name()); fillGeoPosition(depStation, event); event->setDtStart(trip.departureTime()); event->setDtEnd(trip.arrivalTime()); event->setAllDay(false); QStringList desc; if (!trip.departurePlatform().isEmpty()) { desc.push_back(i18n("Departure platform: %1", trip.departurePlatform())); } const auto ticket = reservation.reservedTicket().value(); const auto seat = ticket.ticketedSeat(); if (!seat.seatSection().isEmpty()) { desc.push_back(i18n("Coach: %1", seat.seatSection())); } if (!seat.seatNumber().isEmpty()) { desc.push_back(i18n("Seat: %1", seat.seatNumber())); } if (!trip.arrivalPlatform().isEmpty()) { desc.push_back(i18n("Arrival platform: %1", trip.arrivalPlatform())); } if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillBusReservation(const BusReservation &reservation, const KCalCore::Event::Ptr &event) { const auto trip = reservation.reservationFor().value(); - const auto depStation = trip.departureStation(); - const auto arrStation = trip.arrivalStation(); + const auto depStation = trip.departureBusStop(); + const auto arrStation = trip.arrivalBusStop(); event->setSummary(i18n("Bus %1 from %2 to %3", trip.busNumber(), depStation.name(), arrStation.name())); event->setLocation(depStation.name()); fillGeoPosition(depStation, event); event->setDtStart(trip.departureTime()); event->setDtEnd(trip.arrivalTime()); event->setAllDay(false); QStringList desc; const auto ticket = reservation.reservedTicket().value(); const auto seat = ticket.ticketedSeat(); if (!seat.seatNumber().isEmpty()) { desc.push_back(i18n("Seat: %1", seat.seatNumber())); } if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillLodgingReservation(const LodgingReservation &reservation, const KCalCore::Event::Ptr &event) { const auto lodgingBusiness = reservation.reservationFor().value(); event->setSummary(i18n("Hotel reservation: %1", lodgingBusiness.name())); event->setLocation(formatAddressSingleLine(lodgingBusiness.address())); fillGeoPosition(lodgingBusiness, event); event->setDtStart(QDateTime(reservation.checkinTime().date(), QTime())); event->setDtEnd(QDateTime(reservation.checkoutTime().date(), QTime())); event->setAllDay(true); event->setDescription(i18n("Check-in: %1\nCheck-out: %2\nBooking reference: %3", QLocale().toString(reservation.checkinTime().time(), QLocale::ShortFormat), QLocale().toString(reservation.checkoutTime().time(), QLocale::ShortFormat), reservation.reservationNumber())); event->setTransparency(KCalCore::Event::Transparent); } static void fillEventReservation(const QVector &reservations, const KCalCore::Event::Ptr &event) { const auto ev = reservations.at(0).value().reservationFor().value(); Place location; if (JsonLd::canConvert(ev.location())) { location = JsonLd::convert(ev.location()); } event->setSummary(ev.name()); event->setLocation(location.name()); fillGeoPosition(location, event); event->setDtStart(ev.startDate()); event->setDtEnd(ev.endDate()); event->setAllDay(false); if (ev.doorTime().isValid()) { const auto startOffset = Duration(event->dtStart(), ev.doorTime()); const auto existinAlarms = event->alarms(); const auto it = std::find_if(existinAlarms.begin(), existinAlarms.end(), [startOffset](const Alarm::Ptr &other) { return other->startOffset() == startOffset; }); if (it == existinAlarms.end()) { Alarm::Ptr alarm(new Alarm(event.data())); alarm->setStartOffset(Duration(event->dtStart(), ev.doorTime())); alarm->setDisplayAlarm(i18n("Entrance for %1", ev.name())); alarm->setEnabled(true); event->addAlarm(alarm); } } QStringList desc; for (const auto &r : reservations) { const auto reservation = r.value(); const auto person = reservation.underName().value(); if (!person.name().isEmpty()) { desc.push_back(person.name()); } // TODO: add seat information if present if (!reservation.reservationNumber().isEmpty()) { desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber())); } } event->setDescription(desc.join(QLatin1Char('\n'))); } static void fillGeoPosition(const QVariant &place, const KCalCore::Event::Ptr &event) { if (!JsonLd::canConvert(place)) { return; } const auto geo = JsonLd::convert(place).geo(); if (!geo.isValid()) { return; } event->setHasGeo(true); event->setGeoLatitude(geo.latitude()); event->setGeoLongitude(geo.longitude()); } static void fillFoodReservation(const FoodEstablishmentReservation &reservation, const KCalCore::Event::Ptr &event) { const auto foodEstablishment = reservation.reservationFor().value(); event->setSummary(i18n("Restaurant reservation: %1", foodEstablishment.name())); event->setLocation(formatAddressSingleLine(foodEstablishment.address())); fillGeoPosition(foodEstablishment, event); event->setDtStart(reservation.startTime()); auto endTime = reservation.endTime(); if (!endTime.isValid()) { endTime = QDateTime(reservation.startTime().date(), QTime(23, 59, 59)); } event->setDtEnd(endTime); event->setAllDay(false); event->setTransparency(KCalCore::Event::Transparent); event->setSummary(i18n("Restaurant reservation: %1", foodEstablishment.name())); event->setDescription(i18n("Number Of People: %1\nReservation reference: %2\nUnder name: %3", reservation.partySize(), reservation.reservationNumber(), reservation.underName().value().name())); } static void fillRentalCarReservation(const RentalCarReservation &reservation, const KCalCore::Event::Ptr &event) { const auto rentalCalPickup = reservation.pickupLocation(); const auto addressPickUp = rentalCalPickup.address(); const auto rentalCar = reservation.reservationFor().value(); event->setSummary(i18n("Rental Car reservation: %1", rentalCar.name())); event->setLocation(formatAddressSingleLine(addressPickUp)); fillGeoPosition(rentalCalPickup, event); event->setDtStart(reservation.pickupTime()); event->setDtEnd(reservation.dropoffTime()); event->setAllDay(false); event->setTransparency(KCalCore::Event::Transparent); event->setSummary(i18n("Rent car reservation: %1", rentalCar.name())); const QString pickUpAddress = formatAddress(addressPickUp); const auto rentalCalDropOff = reservation.dropoffLocation(); const auto addressDropOff = rentalCalDropOff.address(); const QString dropAddress = formatAddress(addressDropOff); const QString description = i18n("Reservation reference: %1\nUnder name: %2\n\nPickUp location: %3\n\nDropoff Location: %4", reservation.reservationNumber(), reservation.underName().value().name(), pickUpAddress, dropAddress); event->setDescription(description); } static void fillTaxiReservation(const TaxiReservation &reservation, const KCalCore::Event::Ptr &event) { const auto taxiPickup = reservation.pickupLocation(); const auto addressPickUp = taxiPickup.address(); //TODO const auto rentalCar = reservation.reservationFor().value(); //TODO event->setSummary(i18n("Rental Car reservation: %1", rentalCar.name())); event->setLocation(formatAddressSingleLine(addressPickUp)); fillGeoPosition(taxiPickup, event); event->setDtStart(reservation.pickupTime()); //TODO event->setDtEnd(reservation.dropoffTime()); event->setAllDay(false); event->setTransparency(KCalCore::Event::Transparent); //TODO event->setSummary(i18n("Rent car reservation: %1", rentalCar.name())); const QString pickUpAddress = formatAddress(addressPickUp); const QString description = i18n("Reservation reference: %1\nUnder name: %2\nPickUp location: %3", reservation.reservationNumber(), reservation.underName().value().name(), pickUpAddress); event->setDescription(description); } #endif diff --git a/src/datatypes/bustrip.cpp b/src/datatypes/bustrip.cpp index a0569c1..3cf21c2 100644 --- a/src/datatypes/bustrip.cpp +++ b/src/datatypes/bustrip.cpp @@ -1,54 +1,54 @@ /* Copyright (C) 2018 Volker Krause This program 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 program 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 General Public License along with this program. If not, see . */ #include "bustrip.h" #include "datatypes_p.h" #include using namespace KItinerary; namespace KItinerary { class BusTripPrivate : public QSharedData { public: QString arrivalPlatform; - BusStation arrivalStation; + BusStation arrivalBusStop; QDateTime arrivalTime; QString departurePlatform; - BusStation departureStation; + BusStation departureBusStop; QDateTime departureTime; QString busName; QString busNumber; Organization provider; }; KITINERARY_MAKE_SIMPLE_CLASS(BusTrip) KITINERARY_MAKE_PROPERTY(BusTrip, QString, arrivalPlatform, setArrivalPlatform) -KITINERARY_MAKE_PROPERTY(BusTrip, BusStation, arrivalStation, setArrivalStation) +KITINERARY_MAKE_PROPERTY(BusTrip, BusStation, arrivalBusStop, setArrivalBusStop) KITINERARY_MAKE_PROPERTY(BusTrip, QDateTime, arrivalTime, setArrivalTime) KITINERARY_MAKE_PROPERTY(BusTrip, QString, departurePlatform, setDeparturePlatform) -KITINERARY_MAKE_PROPERTY(BusTrip, BusStation, departureStation, setDepartureStation) +KITINERARY_MAKE_PROPERTY(BusTrip, BusStation, departureBusStop, setDepartureBusStop) KITINERARY_MAKE_PROPERTY(BusTrip, QDateTime, departureTime, setDepartureTime) KITINERARY_MAKE_PROPERTY(BusTrip, QString, busName, setBusName) KITINERARY_MAKE_PROPERTY(BusTrip, QString, busNumber, setBusNumber) KITINERARY_MAKE_PROPERTY(BusTrip, Organization, provider, setProvider) } #include "moc_bustrip.cpp" diff --git a/src/datatypes/bustrip.h b/src/datatypes/bustrip.h index 096fe0e..82971a3 100644 --- a/src/datatypes/bustrip.h +++ b/src/datatypes/bustrip.h @@ -1,54 +1,58 @@ /* Copyright (C) 2018 Volker Krause This program 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 program 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 General Public License along with this program. If not, see . */ #ifndef KITINERARY_BUSTRIP_H #define KITINERARY_BUSTRIP_H #include "kitinerary_export.h" #include "datatypes.h" #include "organization.h" #include "place.h" namespace KItinerary { class BusTripPrivate; /** A bus trip. * @see https://schema.org/BusTrip */ class KITINERARY_EXPORT BusTrip { KITINERARY_GADGET(BusTrip) KITINERARY_PROPERTY(QString, arrivalPlatform, setArrivalPlatform) // ### is this used? it's not in the schema - KITINERARY_PROPERTY(KItinerary::BusStation, arrivalStation, setArrivalStation) // ### the schema calls this arrivalBusStop? + KITINERARY_PROPERTY(KItinerary::BusStation, arrivalBusStop, setArrivalBusStop) KITINERARY_PROPERTY(QDateTime, arrivalTime, setArrivalTime) KITINERARY_PROPERTY(QString, departurePlatform, setDeparturePlatform) // ### not in the schema - KITINERARY_PROPERTY(KItinerary::BusStation, departureStation, setDepartureStation) // ### see above + KITINERARY_PROPERTY(KItinerary::BusStation, departureBusStop, setDepartureBusStop) KITINERARY_PROPERTY(QDateTime, departureTime, setDepartureTime) KITINERARY_PROPERTY(QString, busName, setBusName) KITINERARY_PROPERTY(QString, busNumber, setBusNumber) KITINERARY_PROPERTY(KItinerary::Organization, provider, setProvider) + [[deprecated]] inline KItinerary::BusStation arrivalStation() const { return arrivalBusStop(); } + [[deprecated]] inline KItinerary::BusStation departureStation() const { return departureBusStop(); } + Q_PROPERTY(KItinerary::BusStation arrivalStation READ arrivalStation STORED false) + Q_PROPERTY(KItinerary::BusStation departureStation READ departureStation STORED false) private: QExplicitlySharedDataPointer d; }; } Q_DECLARE_METATYPE(KItinerary::BusTrip) #endif // KITINERARY_BUSTRIP_H diff --git a/src/extractorpostprocessor.cpp b/src/extractorpostprocessor.cpp index 4e1897e..00c234a 100644 --- a/src/extractorpostprocessor.cpp +++ b/src/extractorpostprocessor.cpp @@ -1,682 +1,682 @@ /* Copyright (c) 2017 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 "config-kitinerary.h" #include "extractorpostprocessor.h" #include "iatabcbpparser.h" #include "jsonlddocument.h" #include "logging.h" #include "mergeutil.h" #include "sortutil.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; namespace KItinerary { class ExtractorPostprocessorPrivate { public: void mergeOrAppend(const QVariant &elem); QVariant processFlightReservation(FlightReservation res) const; Flight processFlight(Flight flight) const; Airport processAirport(Airport airport) const; Airline processAirline(Airline airline) const; QDateTime processFlightTime(QDateTime dt, const Flight &flight, const Airport &airport) const; TrainReservation processTrainReservation(TrainReservation res) const; TrainTrip processTrainTrip(TrainTrip trip) const; TrainStation processTrainStation(TrainStation station) const; QDateTime processTrainTripTime(QDateTime dt, const TrainStation &station) const; BusReservation processBusReservation(BusReservation res) const; BusTrip processBusTrip(BusTrip trip) const; LodgingReservation processLodgingReservation(LodgingReservation res) const; FoodEstablishmentReservation processFoodEstablishmentReservation(FoodEstablishmentReservation res) const; TouristAttractionVisit processTouristAttractionVisit(TouristAttractionVisit visit) const; EventReservation processEventReservation(EventReservation res) const; RentalCarReservation processRentalCarReservation(RentalCarReservation res) const; RentalCar processRentalCar(RentalCar car) const; TaxiReservation processTaxiReservation(TaxiReservation res) const; Event processEvent(Event event) const; template T processReservation(T res) const; Person processPerson(Person person) const; template T processPlace(T place) const; QVariantList processActions(QVariantList actions) const; template QDateTime processTimeForLocation(QDateTime dt, const T &place) const; bool filterReservation(const QVariant &res) const; bool filterLodgingReservation(const LodgingReservation &res) const; bool filterFlight(const Flight &flight) const; bool filterAirport(const Airport &airport) const; bool filterTrainTrip(const TrainTrip &trip) const; bool filterBusTrip(const BusTrip &trip) const; template bool filterTrainOrBusStation(const T &station) const; bool filterEventReservation(const EventReservation &res) const; bool filterFoodReservation(const FoodEstablishmentReservation &res) const; QVector m_data; QDateTime m_contextDate; bool m_resultFinalized = false; }; } ExtractorPostprocessor::ExtractorPostprocessor() : d(new ExtractorPostprocessorPrivate) { } ExtractorPostprocessor::ExtractorPostprocessor(ExtractorPostprocessor &&) noexcept = default; ExtractorPostprocessor::~ExtractorPostprocessor() = default; void ExtractorPostprocessor::process(const QVector &data) { d->m_resultFinalized = false; d->m_data.reserve(d->m_data.size() + data.size()); for (auto elem : data) { if (JsonLd::isA(elem)) { elem = d->processFlightReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processTrainReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processLodgingReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processFoodEstablishmentReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processTouristAttractionVisit(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processBusReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processEventReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processRentalCarReservation(elem.value()); } else if (JsonLd::isA(elem)) { elem = d->processTaxiReservation(elem.value()); } d->mergeOrAppend(elem); } } QVector ExtractorPostprocessor::result() const { if (!d->m_resultFinalized) { for (auto it = d->m_data.begin(); it != d->m_data.end();) { if (d->filterReservation(*it)) { ++it; } else { //qCDebug(Log).noquote() << "Discarding element:" << QJsonDocument(JsonLdDocument::toJson({*it})).toJson(); it = d->m_data.erase(it); } } d->m_resultFinalized = true; } std::stable_sort(d->m_data.begin(), d->m_data.end(), SortUtil::isBefore); return d->m_data; } void ExtractorPostprocessor::setContextDate(const QDateTime& dt) { d->m_contextDate = dt; } void ExtractorPostprocessorPrivate::mergeOrAppend(const QVariant &elem) { const auto it = std::find_if(m_data.begin(), m_data.end(), [elem](const QVariant &other) { return MergeUtil::isSame(elem, other); }); if (it == m_data.end()) { m_data.push_back(elem); } else { *it = MergeUtil::merge(*it, elem); } } QVariant ExtractorPostprocessorPrivate::processFlightReservation(FlightReservation res) const { // expand ticketToken for IATA BCBP data const auto bcbp = res.reservedTicket().value().ticketTokenData(); if (!bcbp.isEmpty()) { const auto bcbpData = IataBcbpParser::parse(bcbp, m_contextDate.date()); if (bcbpData.size() == 1) { res = JsonLdDocument::apply(bcbpData.at(0), res).value(); } else { for (const auto &data : bcbpData) { if (MergeUtil::isSame(res, data)) { res = JsonLdDocument::apply(data, res).value(); break; } } } } res.setReservationFor(processFlight(res.reservationFor().value())); return processReservation(res); } Flight ExtractorPostprocessorPrivate::processFlight(Flight flight) const { flight.setDepartureAirport(processAirport(flight.departureAirport())); flight.setArrivalAirport(processAirport(flight.arrivalAirport())); flight.setAirline(processAirline(flight.airline())); flight.setBoardingTime(processFlightTime(flight.boardingTime(), flight, flight.departureAirport())); flight.setDepartureTime(processFlightTime(flight.departureTime(), flight, flight.departureAirport())); flight.setArrivalTime(processFlightTime(flight.arrivalTime(), flight, flight.arrivalAirport())); return flight; } Airport ExtractorPostprocessorPrivate::processAirport(Airport airport) const { // clean up name airport.setName(airport.name().simplified()); // complete missing IATA codes auto iataCode = airport.iataCode(); if (iataCode.isEmpty()) { iataCode = KnowledgeDb::iataCodeFromName(airport.name()).toString(); if (!iataCode.isEmpty()) { airport.setIataCode(iataCode); } } // complete missing geo coordinates auto geo = airport.geo(); if (!geo.isValid()) { const auto coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{iataCode}); if (coord.isValid()) { geo.setLatitude(coord.latitude); geo.setLongitude(coord.longitude); airport.setGeo(geo); } } // add country auto addr = airport.address(); if (addr.addressCountry().isEmpty()) { const auto isoCode = KnowledgeDb::countryForAirport(KnowledgeDb::IataCode{iataCode}); if (isoCode.isValid()) { addr.setAddressCountry(isoCode.toString()); airport.setAddress(addr); } } return processPlace(airport); } Airline ExtractorPostprocessorPrivate::processAirline(Airline airline) const { airline.setName(airline.name().trimmed()); return airline; } QDateTime ExtractorPostprocessorPrivate::processFlightTime(QDateTime dt, const Flight &flight, const Airport &airport) const { if (!dt.isValid()) { return dt; } if (dt.date().year() <= 1970 && flight.departureDay().isValid()) { // we just have the time, but not the day dt.setDate(flight.departureDay()); } if (dt.timeSpec() == Qt::TimeZone || airport.iataCode().isEmpty()) { return dt; } const auto tz = KnowledgeDb::timezoneForAirport(KnowledgeDb::IataCode{airport.iataCode()}); if (!tz.isValid()) { return dt; } // prefer our timezone over externally provided UTC offset, if they match if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) { return dt; } if (dt.timeSpec() == Qt::OffsetFromUTC || dt.timeSpec() == Qt::LocalTime) { dt.setTimeSpec(Qt::TimeZone); dt.setTimeZone(tz); } else if (dt.timeSpec() == Qt::UTC) { dt = dt.toTimeZone(tz); } return dt; } TrainReservation ExtractorPostprocessorPrivate::processTrainReservation(TrainReservation res) const { res.setReservationFor(processTrainTrip(res.reservationFor().value())); return processReservation(res); } TrainTrip ExtractorPostprocessorPrivate::processTrainTrip(TrainTrip trip) const { trip.setArrivalPlatform(trip.arrivalPlatform().trimmed()); trip.setDeparturePlatform(trip.departurePlatform().trimmed()); trip.setDeparatureStation(processTrainStation(trip.departureStation())); trip.setArrivalStation(processTrainStation(trip.arrivalStation())); trip.setDepartureTime(processTrainTripTime(trip.departureTime(), trip.departureStation())); trip.setArrivalTime(processTrainTripTime(trip.arrivalTime(), trip.arrivalStation())); return trip; } TrainStation ExtractorPostprocessorPrivate::processTrainStation(TrainStation station) const { const auto id = station.identifier(); if (id.isEmpty()) { // empty -> null cleanup, to have more compact json-ld output station.setIdentifier(QString()); } else if (id.startsWith(QLatin1String("sncf:")) && id.size() == 10) { // Gare & Connexion ids start with a country code, propagate that to the station address field auto addr = station.address(); if (addr.addressCountry().isEmpty()) { addr.setAddressCountry(id.mid(5, 2).toUpper()); station.setAddress(addr); } const auto record = KnowledgeDb::stationForGaresConnexionsId(KnowledgeDb::GaresConnexionsId{id.mid(5)}); if (!station.geo().isValid() && record.coordinate.isValid()) { GeoCoordinates geo; geo.setLatitude(record.coordinate.latitude); geo.setLongitude(record.coordinate.longitude); station.setGeo(geo); } if (addr.addressCountry().isEmpty() && record.country.isValid()) { addr.setAddressCountry(record.country.toString()); station.setAddress(addr); } } else if (id.startsWith(QLatin1String("ibnr:")) && id.size() == 12) { const auto record = KnowledgeDb::stationForIbnr(KnowledgeDb::IBNR{id.mid(5).toUInt()}); if (!station.geo().isValid() && record.coordinate.isValid()) { GeoCoordinates geo; geo.setLatitude(record.coordinate.latitude); geo.setLongitude(record.coordinate.longitude); station.setGeo(geo); } auto addr = station.address(); if (addr.addressCountry().isEmpty() && record.country.isValid()) { addr.setAddressCountry(record.country.toString()); station.setAddress(addr); } } return processPlace(station); } QDateTime ExtractorPostprocessorPrivate::processTrainTripTime(QDateTime dt, const TrainStation& station) const { if (!dt.isValid()) { return dt; } if (dt.timeSpec() == Qt::TimeZone) { return dt; } QTimeZone tz; if (station.identifier().startsWith(QLatin1String("sncf:"))) { const auto record = KnowledgeDb::stationForGaresConnexionsId(KnowledgeDb::GaresConnexionsId{station.identifier().mid(5)}); tz = record.timezone.toQTimeZone(); } else if (station.identifier().startsWith(QLatin1String("ibnr:"))) { const auto record = KnowledgeDb::stationForIbnr(KnowledgeDb::IBNR{station.identifier().mid(5).toUInt()}); tz = record.timezone.toQTimeZone(); } else if (!station.address().addressCountry().isEmpty()) { tz = KnowledgeDb::timezoneForCountry(KnowledgeDb::CountryId{station.address().addressCountry()}).toQTimeZone(); } if (!tz.isValid()) { return dt; } // prefer our timezone over externally provided UTC offset, if they match if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) { return dt; } if (dt.timeSpec() == Qt::OffsetFromUTC || dt.timeSpec() == Qt::LocalTime) { dt.setTimeSpec(Qt::TimeZone); dt.setTimeZone(tz); } else if (dt.timeSpec() == Qt::UTC) { dt = dt.toTimeZone(tz); } return dt; } BusReservation ExtractorPostprocessorPrivate::processBusReservation(BusReservation res) const { res.setReservationFor(processBusTrip(res.reservationFor().value())); return processReservation(res); } BusTrip ExtractorPostprocessorPrivate::processBusTrip(BusTrip trip) const { - trip.setDepartureStation(processPlace(trip.departureStation())); - trip.setArrivalStation(processPlace(trip.arrivalStation())); - trip.setDepartureTime(processTimeForLocation(trip.departureTime(), trip.departureStation())); - trip.setArrivalTime(processTimeForLocation(trip.arrivalTime(), trip.arrivalStation())); + trip.setDepartureBusStop(processPlace(trip.departureBusStop())); + trip.setArrivalBusStop(processPlace(trip.arrivalBusStop())); + trip.setDepartureTime(processTimeForLocation(trip.departureTime(), trip.departureBusStop())); + trip.setArrivalTime(processTimeForLocation(trip.arrivalTime(), trip.arrivalBusStop())); return trip; } LodgingReservation ExtractorPostprocessorPrivate::processLodgingReservation(LodgingReservation res) const { res.setReservationFor(processPlace(res.reservationFor().value())); res.setCheckinTime(processTimeForLocation(res.checkinTime(), res.reservationFor().value())); res.setCheckoutTime(processTimeForLocation(res.checkoutTime(), res.reservationFor().value())); return processReservation(res); } TaxiReservation ExtractorPostprocessorPrivate::processTaxiReservation(TaxiReservation res) const { res.setPickupLocation(processPlace(res.pickupLocation())); res.setPickupTime(processTimeForLocation(res.pickupTime(), res.pickupLocation())); return processReservation(res); } RentalCarReservation ExtractorPostprocessorPrivate::processRentalCarReservation(RentalCarReservation res) const { res.setReservationFor(processRentalCar(res.reservationFor().value())); res.setPickupLocation(processPlace(res.pickupLocation())); res.setDropoffLocation(processPlace(res.dropoffLocation())); res.setPickupTime(processTimeForLocation(res.pickupTime(), res.pickupLocation())); res.setDropoffTime(processTimeForLocation(res.dropoffTime(), res.dropoffLocation())); return processReservation(res); } RentalCar ExtractorPostprocessorPrivate::processRentalCar(RentalCar car) const { car.setName(car.name().trimmed()); return car; } FoodEstablishmentReservation ExtractorPostprocessorPrivate::processFoodEstablishmentReservation(FoodEstablishmentReservation res) const { res.setReservationFor(processPlace(res.reservationFor().value())); res.setStartTime(processTimeForLocation(res.startTime(), res.reservationFor().value())); res.setEndTime(processTimeForLocation(res.endTime(), res.reservationFor().value())); return processReservation(res); } TouristAttractionVisit ExtractorPostprocessorPrivate::processTouristAttractionVisit(TouristAttractionVisit visit) const { visit.setTouristAttraction(processPlace(visit.touristAttraction())); visit.setArrivalTime(processTimeForLocation(visit.arrivalTime(), visit.touristAttraction())); visit.setDepartureTime(processTimeForLocation(visit.departureTime(), visit.touristAttraction())); return visit; } EventReservation ExtractorPostprocessorPrivate::processEventReservation(EventReservation res) const { res.setReservationFor(processEvent(res.reservationFor().value())); return processReservation(res); } Event ExtractorPostprocessorPrivate::processEvent(Event event) const { // normalize location to be a Place if (JsonLd::isA(event.location())) { Place place; place.setAddress(event.location().value()); event.setLocation(place); } if (JsonLd::isA(event.location())) { event.setLocation(processPlace(event.location().value())); // try to obtain timezones if we have a location event.setStartDate(processTimeForLocation(event.startDate(), event.location().value())); event.setEndDate(processTimeForLocation(event.endDate(), event.location().value())); event.setDoorTime(processTimeForLocation(event.doorTime(), event.location().value())); } return event; } template T ExtractorPostprocessorPrivate::processReservation(T res) const { res.setUnderName(processPerson(res.underName().template value())); res.setPotentialAction(processActions(res.potentialAction())); return res; } Person ExtractorPostprocessorPrivate::processPerson(Person person) const { person.setName(person.name().simplified()); if (person.name().isEmpty() && !person.familyName().isEmpty() && !person.givenName().isEmpty()) { person.setName(person.givenName() + QLatin1Char(' ') + person.familyName()); } // strip prefixes, they break comparisons static const char* honorificPrefixes[] = { "MR ", "MS ", "MRS " }; for (auto prefix : honorificPrefixes) { if (person.name().startsWith(QLatin1String(prefix), Qt::CaseInsensitive)) { person.setName(person.name().mid(strlen(prefix))); break; } } return person; } template T ExtractorPostprocessorPrivate::processPlace(T place) const { auto addr = place.address(); // convert to ISO 3166-1 alpha-2 country codes if (addr.addressCountry().size() > 2) { const auto isoCode = KContacts::Address::countryToISO(addr.addressCountry()).toUpper(); if (!isoCode.isEmpty()) { addr.setAddressCountry(isoCode); } } // upper case country codes if (addr.addressCountry().size() == 2) { addr.setAddressCountry(addr.addressCountry().toUpper()); } place.setAddress(addr); return place; } QVariantList ExtractorPostprocessorPrivate::processActions(QVariantList actions) const { // remove non-actions and actions with invalid URLs QUrl viewUrl; for (auto it = actions.begin(); it != actions.end();) { if (!JsonLd::canConvert(*it)) { it = actions.erase(it); continue; } const auto action = JsonLd::convert(*it); if (!action.target().isValid()) { it = actions.erase(it); continue; } if (JsonLd::isA(*it)) { viewUrl = action.target(); } ++it; } // normalize the order, so JSON comparisson still yields correct results std::sort(actions.begin(), actions.end(), [](const QVariant &lhs, const QVariant &rhs) { return strcmp(lhs.typeName(), rhs.typeName()) < 0; }); // remove actions that don't actually have their own target, or duplicates QUrl prevUrl; const char* prevType = nullptr; for (auto it = actions.begin(); it != actions.end();) { const auto action = JsonLd::convert(*it); const auto isDuplicate = action.target() == prevUrl && (prevType ? strcmp(prevType, (*it).typeName()) == 0 : false); if ((JsonLd::isA(*it) || action.target() != viewUrl) && !isDuplicate) { prevUrl = action.target(); prevType = (*it).typeName(); ++it; } else { it = actions.erase(it); } } return actions; } template QDateTime ExtractorPostprocessorPrivate::processTimeForLocation(QDateTime dt, const T &place) const { if (!dt.isValid() || dt.timeSpec() == Qt::TimeZone) { return dt; } QTimeZone tz; if (!place.address().addressCountry().isEmpty()) { tz = KnowledgeDb::timezoneForCountry(KnowledgeDb::CountryId{place.address().addressCountry()}).toQTimeZone(); } if (!tz.isValid()) { return dt; } // prefer our timezone over externally provided UTC offset, if they match if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) { qCDebug(Log) << "UTC offset clashes with expected timezone!" << dt << dt.offsetFromUtc() << tz.id() << tz.offsetFromUtc(dt); return dt; } if (dt.timeSpec() == Qt::OffsetFromUTC || dt.timeSpec() == Qt::LocalTime) { dt.setTimeSpec(Qt::TimeZone); dt.setTimeZone(tz); } else if (dt.timeSpec() == Qt::UTC) { dt = dt.toTimeZone(tz); } return dt; } bool ExtractorPostprocessorPrivate::filterReservation(const QVariant &res) const { if (JsonLd::isA(res)) { return filterFlight(res.value().reservationFor().value()); } if (JsonLd::isA(res)) { return filterTrainTrip(res.value().reservationFor().value()); } if (JsonLd::isA(res)) { return filterBusTrip(res.value().reservationFor().value()); } if (JsonLd::isA(res)) { return filterLodgingReservation(res.value()); } if (JsonLd::isA(res)) { return filterEventReservation(res.value()); } if (JsonLd::isA(res)) { return filterFoodReservation(res.value()); } // types without specific filters yet if (JsonLd::isA(res) || JsonLd::isA(res) || JsonLd::isA(res)) { return true; } // unknown top-level type return false; } bool ExtractorPostprocessorPrivate::filterLodgingReservation(const LodgingReservation &res) const { return res.checkinTime().isValid() && res.checkoutTime().isValid(); } bool ExtractorPostprocessorPrivate::filterFlight(const Flight &flight) const { // this will be valid if either boarding time, departure time or departure day is set const auto validDate = flight.departureDay().isValid(); return filterAirport(flight.departureAirport()) && filterAirport(flight.arrivalAirport()) && validDate; } bool ExtractorPostprocessorPrivate::filterAirport(const Airport &airport) const { return !airport.iataCode().isEmpty() || !airport.name().isEmpty(); } bool ExtractorPostprocessorPrivate::filterTrainTrip(const TrainTrip &trip) const { return filterTrainOrBusStation(trip.departureStation()) && filterTrainOrBusStation(trip.arrivalStation()) && trip.departureTime().isValid() && trip.arrivalTime().isValid(); } bool ExtractorPostprocessorPrivate::filterBusTrip(const BusTrip &trip) const { - return filterTrainOrBusStation(trip.departureStation()) - && filterTrainOrBusStation(trip.arrivalStation()) + return filterTrainOrBusStation(trip.departureBusStop()) + && filterTrainOrBusStation(trip.arrivalBusStop()) && trip.departureTime().isValid() && trip.arrivalTime().isValid(); } template bool ExtractorPostprocessorPrivate::filterTrainOrBusStation(const T &station) const { return !station.name().isEmpty(); } bool ExtractorPostprocessorPrivate::filterEventReservation(const EventReservation &res) const { const auto event = res.reservationFor().value(); return !event.name().isEmpty() && event.startDate().isValid(); } bool ExtractorPostprocessorPrivate::filterFoodReservation(const FoodEstablishmentReservation &res) const { return res.startTime().isValid(); } diff --git a/src/jsonldimportfilter.cpp b/src/jsonldimportfilter.cpp index 98c510d..e7e724d 100644 --- a/src/jsonldimportfilter.cpp +++ b/src/jsonldimportfilter.cpp @@ -1,204 +1,233 @@ /* Copyright (C) 2018 Volker Krause This program 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 program 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 General Public License along with this program. If not, see . */ #include "jsonldimportfilter.h" #include #include #include #include using namespace KItinerary; static void renameProperty(QJsonObject &obj, const char *oldName, const char *newName) { const auto value = obj.value(QLatin1String(oldName)); if (!value.isNull() && !obj.contains(QLatin1String(newName))) { obj.insert(QLatin1String(newName), value); obj.remove(QLatin1String(oldName)); } } static void renameType(QJsonObject &obj, const char *oldType, const char *newType) { if (obj.value(QLatin1String("@type")) == QLatin1String(oldType)) { obj.insert(QLatin1String("@type"), QLatin1String(newType)); } } static void migrateToAction(QJsonObject &obj, const char *propName, const char *typeName, bool remove) { const auto value = obj.value(QLatin1String(propName)); if (value.isNull() || value.isUndefined()) { return; } auto actions = obj.value(QLatin1String("potentialAction")).toArray(); for (const auto &act : actions) { if (act.toObject().value(QLatin1String("@type")).toString() == QLatin1String(typeName)) { return; } } QJsonObject action; action.insert(QStringLiteral("@type"), QLatin1String(typeName)); action.insert(QStringLiteral("target"), value); actions.push_back(action); obj.insert(QLatin1String("potentialAction"), actions); if (remove) { obj.remove(QLatin1String("propName")); } } static void filterTrainTrip(QJsonObject &trip) { if (trip.value(QLatin1String("@type")).toString() != QLatin1String("TrainTrip")) { return; } // move TrainTrip::trainCompany to TrainTrip::provider (as defined by schema.org) renameProperty(trip, "trainCompany", "provider"); } static void filterLodgingBusiness(QJsonObject &hotel) { // convert LodgingBusiness sub-types we don't handle renameType(hotel, "Hotel", "LodgingBusiness"); } static void filterLodgingReservation(QJsonObject &res) { // check[in|out]Date -> check[in|out]Time (legacy Google format) renameProperty(res, "checkinDate", "checkinTime"); renameProperty(res, "checkoutDate", "checkoutTime"); QJsonObject hotel = res.value(QLatin1String("reservationFor")).toObject(); filterLodgingBusiness(hotel); res.insert(QLatin1String("reservationFor"), hotel); } static void filterTaxiReservation(QJsonObject &res) { renameProperty(res, "reservationId", "reservationNumber"); } static void filterFlight(QJsonObject &res) { // move incomplete departureTime (ie. just ISO date, no time) to departureDay if (res.value(QLatin1String("departureTime")).toString().size() == 10) { renameProperty(res, "departureTime", "departureDay"); } } static void filterReservation(QJsonObject &res) { // move ticketToken to Ticket (Google vs. schema.org difference) const auto token = res.value(QLatin1String("ticketToken")).toString(); if (!token.isEmpty()) { auto ticket = res.value(QLatin1String("reservedTicket")).toObject(); if (ticket.isEmpty()) { ticket.insert(QLatin1String("@type"), QLatin1String("Ticket")); } if (!ticket.contains(QLatin1String("ticketToken"))) { ticket.insert(QLatin1String("ticketToken"), token); res.insert(QLatin1String("reservedTicket"), ticket); res.remove(QLatin1String("ticketToken")); } } // legacy potentialAction property renameProperty(res, "action", "potentialAction"); // move Google xxxUrl properties to Action instances migrateToAction(res, "cancelReservationUrl", "CancelAction", true); migrateToAction(res, "checkinUrl", "CheckInAction", true); migrateToAction(res, "modifyReservationUrl", "UpdateAction", true); migrateToAction(res, "ticketDownloadUrl", "DownloadAction", true); migrateToAction(res, "url", "ViewAction", false); // "typos" renameProperty(res, "Url", "url"); } static void filterEvent(QJsonObject &hotel) { // convert Event sub-types we don't handle renameType(hotel, "MusicEvent", "Event"); } static void filterEventReservation(QJsonObject &res) { QJsonObject event = res.value(QLatin1String("reservationFor")).toObject(); filterEvent(event); res.insert(QLatin1String("reservationFor"), event); } +static void filterBusStop(QJsonObject &station) +{ + renameType(station, "BusStop", "BusStation"); +} + +static void filterBusTrip(QJsonObject &trip) +{ + renameProperty(trip, "arrivalStation", "arrivalBusStop"); + renameProperty(trip, "departureStation", "departureBusStop"); + renameProperty(trip, "busCompany", "provider"); + + auto station = trip.value(QLatin1String("arrivalBusStop")).toObject(); + filterBusStop(station); + trip.insert(QLatin1String("arrivalBusStop"), station); + + station = trip.value(QLatin1String("departureBusStop")).toObject(); + filterBusStop(station); + trip.insert(QLatin1String("departureBusStop"), station); +} + +static void filterBusReservation(QJsonObject &res) +{ + QJsonObject trip = res.value(QLatin1String("reservationFor")).toObject(); + filterBusTrip(trip); + res.insert(QLatin1String("reservationFor"), trip); +} + static QJsonArray filterActions(const QJsonValue &v) { QJsonArray actions; if (v.isArray()) { actions = v.toArray(); } else { actions.push_back(v); } for (auto it = actions.begin(); it != actions.end(); ++it) { auto action = (*it).toObject(); renameType(action, "EditAction", "UpdateAction"); renameProperty(action, "url", "target"); *it = action; } return actions; } QJsonObject JsonLdImportFilter::filterObject(const QJsonObject& obj) { QJsonObject res(obj); const auto type = obj.value(QLatin1String("@type")).toString(); if (type.endsWith(QLatin1String("Reservation"))) { filterReservation(res); } if (type == QLatin1String("TrainReservation")) { auto train = obj.value(QLatin1String("reservationFor")).toObject(); filterTrainTrip(train); if (!train.isEmpty()) { res.insert(QLatin1String("reservationFor"), train); } } else if (type == QLatin1String("LodgingReservation")) { filterLodgingReservation(res); } else if (type == QLatin1String("FlightReservation")) { auto flight = obj.value(QLatin1String("reservationFor")).toObject(); filterFlight(flight); if (!flight.isEmpty()) { res.insert(QLatin1String("reservationFor"), flight); } } else if (type == QLatin1String("TaxiReservation")) { filterTaxiReservation(res); } else if (type == QLatin1String("EventReservation")) { filterEventReservation(res); + } else if (type == QLatin1String("BusReservation")) { + filterBusReservation(res); } auto actions = res.value(QLatin1String("potentialAction")); if (!actions.isUndefined()) { res.insert(QLatin1String("potentialAction"), filterActions(actions)); } return res; }