diff --git a/autotests/airportdbtest.cpp b/autotests/airportdbtest.cpp index 6558d21..2d0f59d 100644 --- a/autotests/airportdbtest.cpp +++ b/autotests/airportdbtest.cpp @@ -1,173 +1,173 @@ /* 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 +#include #include #include #include #include #include using namespace KItinerary; class AirportDbTest : public QObject { Q_OBJECT private Q_SLOTS: void iataCodeTest() { const auto txl = KnowledgeDb::IataCode{"TXL"}; QVERIFY(txl.isValid()); const auto invalid = KnowledgeDb::IataCode{}; QVERIFY(!invalid.isValid()); QVERIFY(txl != invalid); QVERIFY(!(txl == invalid)); QVERIFY(txl == txl); QCOMPARE(invalid.toString(), QString()); const auto cdg = KnowledgeDb::IataCode{"CDG"}; QVERIFY(cdg.isValid()); QVERIFY(cdg != txl); QVERIFY(!(cdg == txl)); QVERIFY(cdg < txl); QVERIFY(!(txl < cdg)); QVERIFY(KnowledgeDb::IataCode{"ABC"} < KnowledgeDb::IataCode{"CBA"}); QVERIFY(!(KnowledgeDb::IataCode{"CBA"} < KnowledgeDb::IataCode{"ABC"})); } void coordinateLookupTest() { auto coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"TXL"}); QVERIFY(coord.isValid()); QCOMPARE((int)coord.longitude, 13); QCOMPARE((int)coord.latitude, 52); coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"XXX"}); QVERIFY(!coord.isValid()); QVERIFY(std::isnan(coord.latitude)); QVERIFY(std::isnan(coord.longitude)); // test coordinate parsing corner cases coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"LCY"}); QCOMPARE((int)coord.longitude, 0); QVERIFY(coord.longitude > 0.0f); coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"LHR"}); QCOMPARE((int)coord.longitude, 0); QVERIFY(coord.longitude < 0.0f); // Köln-Bonn is a hybrid civilian/military airport, so that should be included coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"CGN"}); QVERIFY(coord.isValid()); // Frankfurt-Hahn is a former military airport, should be included coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"HHN"}); QVERIFY(coord.isValid()); // Ramstein is a military airport that should not be included coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"RMS"}); QVERIFY(!coord.isValid()); // IATA codes that changed airports in various ways QVERIFY(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"DEN"}).isValid()); QVERIFY(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"MUC"}).isValid()); QVERIFY(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"GOT"}).isValid()); QVERIFY(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"OSL"}).isValid()); // IATA codes of no longer active airports QVERIFY(!KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"THF"}).isValid()); // IATA codes of civilian airports that match the primitive military filter QVERIFY(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"RAF"}).isValid()); QVERIFY(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"CFB"}).isValid()); QVERIFY(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"PAF"}).isValid()); // one airport with 3 IATA codes coord = KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"BSL"}); QVERIFY(coord.isValid()); QCOMPARE(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"BSL"}), KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"MLH"})); QCOMPARE(KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"BSL"}), KnowledgeDb::coordinateForAirport(KnowledgeDb::IataCode{"EAP"})); } void timezoneLookupTest() { auto tz = KnowledgeDb::timezoneForAirport(KnowledgeDb::IataCode{"TXL"}); QVERIFY(tz.isValid()); QCOMPARE(tz.id(), QByteArray("Europe/Berlin")); tz = KnowledgeDb::timezoneForAirport(KnowledgeDb::IataCode{"XXX"}); QVERIFY(!tz.isValid()); // tiny, make sure our lookup resolution is big enough for that tz = KnowledgeDb::timezoneForAirport(KnowledgeDb::IataCode{"LUX"}); QCOMPARE(tz.id(), QByteArray("Europe/Luxembourg")); } void iataLookupTest() { // via unique fragment lookup QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Flughafen Berlin-Tegel")), KnowledgeDb::IataCode{"TXL"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("TEGEL")), KnowledgeDb::IataCode{"TXL"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Paris Charles de Gaulle")), KnowledgeDb::IataCode{"CDG"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Zürich")), KnowledgeDb::IataCode{"ZRH"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("AMSTERDAM, NL (SCHIPHOL AIRPORT)")), KnowledgeDb::IataCode{"AMS"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("London Heathrow")), KnowledgeDb::IataCode{"LHR"}); // via non-unique fragment lookup QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("John F. Kennedy International Airport")), KnowledgeDb::IataCode{"JFK"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("San Francisco International")), KnowledgeDb::IataCode{"SFO"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Düsseldorf International")), KnowledgeDb::IataCode{"DUS"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("London City")), KnowledgeDb::IataCode{"LCY"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("DETROIT, MI (METROPOLITAN WAYNE CO)")), KnowledgeDb::IataCode{"DTW"}); // not unique QVERIFY(!KnowledgeDb::iataCodeFromName(QStringLiteral("Flughafen Berlin")).isValid()); QVERIFY(!KnowledgeDb::iataCodeFromName(QStringLiteral("Charles de Gaulle Orly")).isValid()); QVERIFY(!KnowledgeDb::iataCodeFromName(QStringLiteral("Brussels Airport, BE")).isValid()); QVERIFY(!KnowledgeDb::iataCodeFromName(QStringLiteral("Frankfurt")).isValid()); // string normalization QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Sao Paulo-Guarulhos International")), KnowledgeDb::IataCode{"GRU"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("São Paulo-Guarulhos International")), KnowledgeDb::IataCode{"GRU"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Zurich")), KnowledgeDb::IataCode{"ZRH"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Dusseldorf International")), KnowledgeDb::IataCode{"DUS"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Almeria")), KnowledgeDb::IataCode{"LEI"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("ALMERÍA")), KnowledgeDb::IataCode{"LEI"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Keflavík")), KnowledgeDb::IataCode{"KEF"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Keflavik")), KnowledgeDb::IataCode{"KEF"}); // alternative transliterations QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Duesseldorf International")), KnowledgeDb::IataCode{"DUS"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Berlin Schoenefeld")), KnowledgeDb::IataCode{"SXF"}); QCOMPARE(KnowledgeDb::iataCodeFromName(QStringLiteral("Zuerich")), KnowledgeDb::IataCode{"ZRH"}); } void countryDataTest() { auto iso = KnowledgeDb::countryForAirport(KnowledgeDb::IataCode{}); QVERIFY(!iso.isValid()); iso = KnowledgeDb::countryForAirport(KnowledgeDb::IataCode{"TXL"}); QCOMPARE(iso, KnowledgeDb::CountryId{"DE"}); } }; QTEST_APPLESS_MAIN(AirportDbTest) #include "airportdbtest.moc" diff --git a/autotests/knowledgedbtest.cpp b/autotests/knowledgedbtest.cpp index 0977cf8..cb237a7 100644 --- a/autotests/knowledgedbtest.cpp +++ b/autotests/knowledgedbtest.cpp @@ -1,160 +1,161 @@ /* 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. */ #include -#include + +#include #include #include #include #include using namespace KItinerary; using namespace KItinerary::KnowledgeDb; class KnowledgeDbTest : public QObject { Q_OBJECT private Q_SLOTS: void testIBNRLookup() { auto station = KnowledgeDb::stationForIbnr(IBNR{1234567}); QVERIFY(!station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone()); station = KnowledgeDb::stationForIbnr({}); QVERIFY(!station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone()); station = KnowledgeDb::stationForIbnr(IBNR{8011160}); QVERIFY(station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone("Europe/Berlin")); QCOMPARE(station.country, CountryId{"DE"}); station = KnowledgeDb::stationForIbnr(IBNR{8501687}); QVERIFY(station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone("Europe/Zurich")); QCOMPARE(station.country, CountryId{"CH"}); // Aachen West, very close to the NL border, should be in DE timezone station = KnowledgeDb::stationForIbnr(IBNR{8000404}); QVERIFY(station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone("Europe/Berlin")); QCOMPARE(station.country, CountryId{"DE"}); } void testGaresConnexionsIdLookup() { auto station = KnowledgeDb::stationForGaresConnexionsId({}); QVERIFY(!station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone()); station = KnowledgeDb::stationForGaresConnexionsId(GaresConnexionsId{"XXXXX"}); QVERIFY(!station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone()); station = KnowledgeDb::stationForGaresConnexionsId(GaresConnexionsId{"FRAES"}); QVERIFY(station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone("Europe/Paris")); QCOMPARE(station.country, CountryId{"FR"}); station = KnowledgeDb::stationForGaresConnexionsId(GaresConnexionsId{QStringLiteral("FRXYT")}); QVERIFY(station.coordinate.isValid()); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone("Europe/Paris")); QCOMPARE(station.country, CountryId{"FR"}); station = KnowledgeDb::stationForGaresConnexionsId(GaresConnexionsId{"CHGVA"}); QEXPECT_FAIL("", "Wikidata does not supply ids for non-French stations yet", Continue); QVERIFY(station.coordinate.isValid()); QEXPECT_FAIL("", "Wikidata does not supply ids for non-French stations yet", Continue); QCOMPARE(station.timezone.toQTimeZone(), QTimeZone("Europe/Zurich")); QEXPECT_FAIL("", "Wikidata does not supply ids for non-French stations yet", Continue); QCOMPARE(station.country, CountryId{"CH"}); } void testCountryDb() { auto country = KnowledgeDb::countryForId(CountryId{}); QCOMPARE(country.drivingSide, KnowledgeDb::DrivingSide::Unknown); QCOMPARE(country.powerPlugTypes, {Unknown}); country = KnowledgeDb::countryForId(CountryId{"DE"}); QCOMPARE(country.drivingSide, KnowledgeDb::DrivingSide::Right); QCOMPARE(country.powerPlugTypes, {TypeC|TypeF}); country = KnowledgeDb::countryForId(CountryId{"GB"}); QCOMPARE(country.drivingSide, KnowledgeDb::DrivingSide::Left); QCOMPARE(country.powerPlugTypes, {TypeG}); country = KnowledgeDb::countryForId(CountryId{"CK"}); QCOMPARE(country.drivingSide, KnowledgeDb::DrivingSide::Unknown); } void testPowerPlugCompat_data() { using namespace KnowledgeDb; QTest::addColumn("plugs"); QTest::addColumn("sockets"); QTest::addColumn("failPlugs"); QTest::addColumn("failSockets"); QTest::newRow("empty") << PowerPlugTypes{} << PowerPlugTypes{} << PowerPlugTypes{} << PowerPlugTypes{}; QTest::newRow("DE-DE") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{} << PowerPlugTypes{}; QTest::newRow("DE-CH") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeC|TypeJ} << PowerPlugTypes{TypeF} << PowerPlugTypes{TypeJ}; QTest::newRow("CH-DE") << PowerPlugTypes{TypeC|TypeJ} << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeJ} << PowerPlugTypes{TypeF}; QTest::newRow("DE-FR") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeC|TypeE} << PowerPlugTypes{} << PowerPlugTypes{}; QTest::newRow("DE-GB") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeG} << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeG}; QTest::newRow("DE-IT") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeC|TypeF|TypeL} << PowerPlugTypes{} << PowerPlugTypes{TypeL}; QTest::newRow("IT-DE") << PowerPlugTypes{TypeC|TypeF|TypeL} << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeL} << PowerPlugTypes{}; QTest::newRow("DE-IL") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeC|TypeH|TypeM} << PowerPlugTypes{TypeF} << PowerPlugTypes{TypeH|TypeM}; QTest::newRow("DE-AO") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeC} << PowerPlugTypes{TypeF} << PowerPlugTypes{}; QTest::newRow("AO-DE") << PowerPlugTypes{TypeC} << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{} << PowerPlugTypes{}; QTest::newRow("DE-DK") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeC|TypeE|TypeF|TypeK} << PowerPlugTypes{} << PowerPlugTypes{}; QTest::newRow("DK-DE") << PowerPlugTypes{TypeC|TypeF|TypeE|TypeK} << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeK} << PowerPlugTypes{}; QTest::newRow("DE-ZA") << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeC|TypeD|TypeM|TypeN} << PowerPlugTypes{TypeF} << PowerPlugTypes{TypeD|TypeM|TypeN}; QTest::newRow("ZA-CH") << PowerPlugTypes{TypeC|TypeD|TypeM|TypeN} << PowerPlugTypes{TypeC|TypeJ} << PowerPlugTypes{TypeD|TypeM|TypeN} << PowerPlugTypes{TypeJ}; QTest::newRow("ZA-DE") << PowerPlugTypes{TypeC|TypeD|TypeM|TypeN} << PowerPlugTypes{TypeC|TypeF} << PowerPlugTypes{TypeD|TypeM|TypeN} << PowerPlugTypes{TypeF}; QTest::newRow("ZA-IT") << PowerPlugTypes{TypeC|TypeD|TypeM|TypeN} << PowerPlugTypes{TypeC|TypeF|TypeL} << PowerPlugTypes{TypeD|TypeM|TypeN} << PowerPlugTypes{TypeF|TypeL}; } void testPowerPlugCompat() { using namespace KnowledgeDb; QFETCH(PowerPlugTypes, plugs); QFETCH(PowerPlugTypes, sockets); QFETCH(PowerPlugTypes, failPlugs); QFETCH(PowerPlugTypes, failSockets); QCOMPARE(KnowledgeDb::incompatiblePowerPlugs(plugs, sockets), failPlugs); QCOMPARE(KnowledgeDb::incompatiblePowerSockets(plugs, sockets), failSockets); } void testTimezoneForCountry() { using namespace KnowledgeDb; QCOMPARE(KnowledgeDb::timezoneForCountry(CountryId{"DE"}).toQTimeZone(), QTimeZone("Europe/Berlin")); QCOMPARE(KnowledgeDb::timezoneForCountry(CountryId{"FR"}).toQTimeZone(), QTimeZone("Europe/Paris")); QCOMPARE(KnowledgeDb::timezoneForCountry(CountryId{"BR"}).toQTimeZone(), QTimeZone()); } }; QTEST_APPLESS_MAIN(KnowledgeDbTest) #include "knowledgedbtest.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 38bbc83..f0ae8d7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,165 +1,163 @@ set(KDE_INSTALL_INCLUDEDIR_PIM ${KDE_INSTALL_INCLUDEDIR}/KPim) add_subdirectory(knowledgedb-generator) configure_file(config-kitinerary.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kitinerary.h) set(kitinerary_lib_srcs datatypes/action.cpp datatypes/brand.cpp datatypes/bustrip.cpp datatypes/event.cpp datatypes/flight.cpp datatypes/organization.cpp datatypes/person.cpp datatypes/place.cpp datatypes/reservation.cpp datatypes/taxi.cpp datatypes/ticket.cpp datatypes/traintrip.cpp datatypes/rentalcar.cpp datatypes/visit.cpp jsapi/barcode.cpp jsapi/context.cpp jsapi/jsonld.cpp knowledgedb/airportdb.cpp knowledgedb/countrydb.cpp knowledgedb/knowledgedb.cpp knowledgedb/timezonedb.cpp knowledgedb/trainstationdb.cpp barcodedecoder.cpp calendarhandler.cpp extractor.cpp extractorengine.cpp extractorfilter.cpp extractorpostprocessor.cpp extractorrepository.cpp genericpdfextractor.cpp htmldocument.cpp iatabcbpparser.cpp jsonlddocument.cpp jsonldimportfilter.cpp locationutil.cpp mergeutil.cpp pdfdocument.cpp qimageluminancesource.cpp qimagepurebinarizer.cpp sortutil.cpp stringutil.cpp structureddataextractor.cpp uic9183parser.cpp ) qt5_add_resources(kitinerary_lib_srcs extractors/extractors.qrc) ecm_qt_declare_logging_category(kitinerary_lib_srcs HEADER logging.h IDENTIFIER KItinerary::Log CATEGORY_NAME org.kde.kitinerary) kde_source_files_enable_exceptions(barcodedecoder.cpp) add_library(KPimItinerary ${kitinerary_lib_srcs}) add_library(KPim::Itinerary ALIAS KPimItinerary) generate_export_header(KPimItinerary BASE_NAME KItinerary) set_target_properties(KPimItinerary PROPERTIES VERSION ${KITINERARY_VERSION_STRING} SOVERSION ${KITINERARY_SOVERSION} EXPORT_NAME Itinerary ) target_include_directories(KPimItinerary INTERFACE "$") target_include_directories(KPimItinerary PUBLIC "$") target_link_libraries(KPimItinerary PUBLIC Qt5::Core KF5::Mime PRIVATE Qt5::Qml KF5::I18n KF5::Contacts KPim::PkPass ${ZLIB_LIBRARIES} ) if (HAVE_POPPLER) target_link_libraries(KPimItinerary PRIVATE Poppler::Core) endif() if (HAVE_ZXING) target_link_libraries(KPimItinerary PRIVATE ZXing::Core) elseif (HAVE_ZXING_OLD) target_link_libraries(KPimItinerary PRIVATE zxing::libzxing) endif() if (HAVE_KCAL) target_link_libraries(KPimItinerary PUBLIC KF5::CalendarCore) endif() if (HAVE_LIBXML2) target_compile_definitions(KPimItinerary PRIVATE ${LIBXML2_DEFINITIONS}) target_include_directories(KPimItinerary PRIVATE ${LIBXML2_INCLUDE_DIR}) target_link_libraries(KPimItinerary PRIVATE ${LIBXML2_LIBRARIES}) endif() if (HAVE_PHONENUMBER) target_link_libraries(KPimItinerary PRIVATE PhoneNumber::PhoneNumber) endif() ecm_generate_headers(KItinerary_FORWARDING_HEADERS HEADER_NAMES BarcodeDecoder CalendarHandler Extractor ExtractorEngine ExtractorPostprocessor HtmlDocument IataBcbpParser JsonLdDocument LocationUtil MergeUtil PdfDocument SortUtil Uic9183Parser PREFIX KItinerary REQUIRED_HEADERS KItinerary_HEADERS ) ecm_generate_headers(KItinerary_KnowledgeDb_FORWARDING_HEADERS HEADER_NAMES - AirportDb CountryDb KnowledgeDb - TrainStationDb PREFIX KItinerary REQUIRED_HEADERS KItinerary_KnowledgeDb_HEADERS RELATIVE knowledgedb ) ecm_generate_headers(KItinerary_Datatypes_FORWARDING_HEADERS HEADER_NAMES Action Brand BusTrip Datatypes Event Flight Organization Reservation RentalCar Person Place Taxi Ticket TrainTrip Visit PREFIX KItinerary REQUIRED_HEADERS KItinerary_Datatypes_HEADERS RELATIVE datatypes ) install(TARGETS KPimItinerary EXPORT KPimItineraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KItinerary_FORWARDING_HEADERS} ${KItinerary_KnowledgeDb_FORWARDING_HEADERS} ${KItinerary_Datatypes_FORWARDING_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/KItinerary ) install(FILES ${KItinerary_HEADERS} ${KItinerary_AirportDb_HEADERS} ${KItinerary_Datatypes_HEADERS} ${KItinerary_KnowledgeDb_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kitinerary_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/kitinerary ) diff --git a/src/extractorpostprocessor.cpp b/src/extractorpostprocessor.cpp index 038b406..585913a 100644 --- a/src/extractorpostprocessor.cpp +++ b/src/extractorpostprocessor.cpp @@ -1,721 +1,722 @@ /* 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 "knowledgedb/airportdb.h" +#include "knowledgedb/trainstationdb.h" + #include -#include #include #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #include #include #include #ifdef HAVE_PHONENUMBER #include #endif #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.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()); } #ifdef HAVE_PHONENUMBER // recover country from phone number, if we have that if (!place.telephone().isEmpty() && addr.addressCountry().size() != 2) { const auto phoneStr = place.telephone().toStdString(); const auto util = i18n::phonenumbers::PhoneNumberUtil::GetInstance(); i18n::phonenumbers::PhoneNumber number; if (util->ParseAndKeepRawInput(phoneStr, "ZZ", &number) == i18n::phonenumbers::PhoneNumberUtil::NO_PARSING_ERROR) { std::string isoCode; util->GetRegionCodeForNumber(number, &isoCode); if (!isoCode.empty()) { addr.setAddressCountry(QString::fromStdString(isoCode)); } } } // or complete the phone number if we know the country else if (!place.telephone().isEmpty() && addr.addressCountry().size() == 2) { auto phoneStr = place.telephone().toStdString(); const auto isoCode = addr.addressCountry().toStdString(); const auto util = i18n::phonenumbers::PhoneNumberUtil::GetInstance(); i18n::phonenumbers::PhoneNumber number; if (util->ParseAndKeepRawInput(phoneStr, isoCode, &number) == i18n::phonenumbers::PhoneNumberUtil::NO_PARSING_ERROR) { if (number.country_code_source() == i18n::phonenumbers::PhoneNumber_CountryCodeSource_FROM_DEFAULT_COUNTRY) { util->Format(number, i18n::phonenumbers::PhoneNumberUtil::INTERNATIONAL, &phoneStr); place.setTelephone(QString::fromStdString(phoneStr)); } } } #endif // normalize strings addr.setStreetAddress(addr.streetAddress().simplified()); addr.setAddressLocality(addr.addressLocality().simplified()); addr.setAddressRegion(addr.addressRegion().simplified()); 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.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(); }