1 /*
2    SPDX-FileCopyrightText: 2017 Volker Krause <vkrause@kde.org>
3 
4    SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "config-kitinerary.h"
8 #include "extractorpostprocessor.h"
9 #include "extractorpostprocessor_p.h"
10 #include "extractorvalidator.h"
11 #include "flightpostprocessor_p.h"
12 
13 #include "extractorutil.h"
14 #include "iata/iatabcbpparser.h"
15 #include "jsonlddocument.h"
16 #include "logging.h"
17 #include "mergeutil.h"
18 #include "sortutil.h"
19 
20 #include "knowledgedb/trainstationdb.h"
21 
22 #include <KItinerary/Action>
23 #include <KItinerary/BusTrip>
24 #include <KItinerary/Event>
25 #include <KItinerary/Flight>
26 #include <KItinerary/Organization>
27 #include <KItinerary/Person>
28 #include <KItinerary/Place>
29 #include <KItinerary/ProgramMembership>
30 #include <KItinerary/RentalCar>
31 #include <KItinerary/Reservation>
32 #include <KItinerary/Taxi>
33 #include <KItinerary/Ticket>
34 #include <KItinerary/TrainTrip>
35 #include <KItinerary/Visit>
36 
37 #if HAVE_KI18N_LOCALE_DATA
38 #include <KCountry>
39 #else
40 #include <KContacts/Address>
41 #endif
42 
43 #include <QDebug>
44 #include <QJsonArray>
45 #include <QJsonDocument>
46 #include <QTimeZone>
47 #include <QUrl>
48 
49 #ifdef HAVE_PHONENUMBER
50 #include <phonenumbers/phonenumberutil.h>
51 #endif
52 
53 #include <algorithm>
54 
55 using namespace KItinerary;
56 
ExtractorPostprocessor()57 ExtractorPostprocessor::ExtractorPostprocessor()
58     : d(new ExtractorPostprocessorPrivate)
59 {
60     // configure the default set of accepted types, for backward compatibility
61     d->m_validator.setAcceptedTypes<
62         FlightReservation,
63         TrainReservation,
64         BusReservation,
65         RentalCarReservation,
66         TaxiReservation,
67         EventReservation,
68         FoodEstablishmentReservation,
69         LodgingReservation,
70         // reservationFor types
71         Flight,
72         TrainTrip,
73         BusTrip,
74         RentalCar,
75         Taxi,
76         Event,
77         TouristAttractionVisit,
78         FoodEstablishment,
79         // PBI types
80         LocalBusiness
81     >();
82 }
83 
84 ExtractorPostprocessor::ExtractorPostprocessor(ExtractorPostprocessor &&) noexcept = default;
85 ExtractorPostprocessor::~ExtractorPostprocessor() = default;
86 
process(const QVector<QVariant> & data)87 void ExtractorPostprocessor::process(const QVector<QVariant> &data)
88 {
89     d->m_resultFinalized = false;
90     d->m_data.reserve(d->m_data.size() + data.size());
91     for (auto elem : data) {
92         // reservation types
93         if (JsonLd::isA<FlightReservation>(elem)) {
94             elem = d->processFlightReservation(elem.value<FlightReservation>());
95         } else if (JsonLd::isA<TrainReservation>(elem)) {
96             elem = d->processTrainReservation(elem.value<TrainReservation>());
97         } else if (JsonLd::isA<LodgingReservation>(elem)) {
98             elem = d->processLodgingReservation(elem.value<LodgingReservation>());
99         } else if (JsonLd::isA<FoodEstablishmentReservation>(elem)) {
100             elem = d->processFoodEstablishmentReservation(elem.value<FoodEstablishmentReservation>());
101         } else if (JsonLd::isA<TouristAttractionVisit>(elem)) {
102             elem = d->processTouristAttractionVisit(elem.value<TouristAttractionVisit>());
103         } else if (JsonLd::isA<BusReservation>(elem)) {
104             elem = d->processBusReservation(elem.value<BusReservation>());
105         } else if (JsonLd::isA<EventReservation>(elem)) {
106             elem = d->processEventReservation(elem.value<EventReservation>());
107         } else if (JsonLd::isA<RentalCarReservation>(elem)) {
108             elem = d->processRentalCarReservation(elem.value<RentalCarReservation>());
109         } else if (JsonLd::isA<TaxiReservation>(elem)) {
110             elem = d->processTaxiReservation(elem.value<TaxiReservation>());
111         }
112 
113         // "reservationFor" types
114         else if (JsonLd::isA<LodgingBusiness>(elem)) {
115             elem = d->processPlace(elem.value<LodgingBusiness>());
116         } else if (JsonLd::isA<FoodEstablishment>(elem)) {
117             elem = d->processPlace(elem.value<FoodEstablishment>());
118         } else if (JsonLd::isA<Event>(elem)) {
119             elem = d->processEvent(elem.value<Event>());
120         }
121 
122         d->mergeOrAppend(elem);
123     }
124 }
125 
result() const126 QVector<QVariant> ExtractorPostprocessor::result() const
127 {
128     if (!d->m_resultFinalized && d->m_validationEnabled) {
129         d->m_data.erase(std::remove_if(d->m_data.begin(), d->m_data.end(), [this](const auto &elem) {
130             return !d->m_validator.isValidElement(elem);
131         }), d->m_data.end());
132         d->m_resultFinalized = true;
133     }
134 
135     std::stable_sort(d->m_data.begin(), d->m_data.end(), SortUtil::isBefore);
136     return d->m_data;
137 }
138 
setContextDate(const QDateTime & dt)139 void ExtractorPostprocessor::setContextDate(const QDateTime& dt)
140 {
141     d->m_contextDate = dt;
142 }
143 
setValidationEnabled(bool validate)144 void ExtractorPostprocessor::setValidationEnabled(bool validate)
145 {
146     d->m_validationEnabled = validate;
147 }
148 
mergeOrAppend(const QVariant & elem)149 void ExtractorPostprocessorPrivate::mergeOrAppend(const QVariant &elem)
150 {
151     const auto it = std::find_if(m_data.begin(), m_data.end(), [elem](const QVariant &other) {
152         return MergeUtil::isSame(elem, other);
153     });
154 
155     if (it == m_data.end()) {
156         m_data.push_back(elem);
157     } else {
158         *it = MergeUtil::merge(*it, elem);
159     }
160 }
161 
processFlightReservation(FlightReservation res) const162 QVariant ExtractorPostprocessorPrivate::processFlightReservation(FlightReservation res) const
163 {
164     // expand ticketToken for IATA BCBP data
165     const auto bcbp = res.reservedTicket().value<Ticket>().ticketTokenData().toString();
166     if (!bcbp.isEmpty()) {
167         const auto bcbpData = IataBcbpParser::parse(bcbp, m_contextDate);
168         if (bcbpData.size() == 1) {
169             res = JsonLdDocument::apply(bcbpData.at(0), res).value<FlightReservation>();
170             // standardize on the BCBP booking reference, not some secondary one we might have in structured data for example
171             res.setReservationNumber(bcbpData.at(0).value<FlightReservation>().reservationNumber());
172         } else {
173             for (const auto &data : bcbpData) {
174                 if (MergeUtil::isSame(res, data)) {
175                     res = JsonLdDocument::apply(data, res).value<FlightReservation>();
176                     break;
177                 }
178             }
179         }
180     }
181 
182     if (res.reservationFor().isValid()) {
183         FlightPostProcessor p;
184         res.setReservationFor(p.processFlight(res.reservationFor().value<Flight>()));
185     }
186     return processReservation(res);
187 }
188 
processTrainReservation(TrainReservation res) const189 TrainReservation ExtractorPostprocessorPrivate::processTrainReservation(TrainReservation res) const
190 {
191     if (res.reservationFor().isValid()) {
192         res.setReservationFor(processTrainTrip(res.reservationFor().value<TrainTrip>()));
193     }
194     return processReservation(res);
195 }
196 
processTrainTrip(TrainTrip trip) const197 TrainTrip ExtractorPostprocessorPrivate::processTrainTrip(TrainTrip trip) const
198 {
199     trip.setArrivalPlatform(trip.arrivalPlatform().trimmed());
200     trip.setDeparturePlatform(trip.departurePlatform().trimmed());
201     trip.setDepartureStation(processTrainStation(trip.departureStation()));
202     trip.setArrivalStation(processTrainStation(trip.arrivalStation()));
203     trip.setDepartureTime(processTrainTripTime(trip.departureTime(), trip.departureDay(), trip.departureStation()));
204     trip.setArrivalTime(processTrainTripTime(trip.arrivalTime(), trip.departureDay(), trip.arrivalStation()));
205     trip.setTrainNumber(trip.trainNumber().simplified());
206     trip.setTrainName(trip.trainName().simplified());
207     return trip;
208 }
209 
applyStationData(const KnowledgeDb::TrainStation & record,TrainStation & station)210 static void applyStationData(const KnowledgeDb::TrainStation &record, TrainStation &station)
211 {
212     if (!station.geo().isValid() && record.coordinate.isValid()) {
213         GeoCoordinates geo;
214         geo.setLatitude(record.coordinate.latitude);
215         geo.setLongitude(record.coordinate.longitude);
216         station.setGeo(geo);
217     }
218     auto addr = station.address();
219     if (addr.addressCountry().isEmpty() && record.country.isValid()) {
220         addr.setAddressCountry(record.country.toString());
221         station.setAddress(addr);
222     }
223 }
224 
applyStationCountry(const QString & isoCode,TrainStation & station)225 static void applyStationCountry(const QString &isoCode, TrainStation &station)
226 {
227     auto addr = station.address();
228     if (addr.addressCountry().isEmpty()) {
229         addr.setAddressCountry(isoCode.toUpper());
230         station.setAddress(addr);
231     }
232 }
233 
processTrainStation(TrainStation station) const234 TrainStation ExtractorPostprocessorPrivate::processTrainStation(TrainStation station) const
235 {
236     const auto id = station.identifier();
237     if (id.isEmpty()) { // empty -> null cleanup, to have more compact json-ld output
238         station.setIdentifier(QString());
239     } else if (id.startsWith(QLatin1String("sncf:")) && id.size() == 10) {
240         const auto record = KnowledgeDb::stationForSncfStationId(KnowledgeDb::SncfStationId{id.mid(5)});
241         applyStationData(record, station);
242         applyStationCountry(id.mid(5, 2).toUpper(), station);
243     } else if (id.startsWith(QLatin1String("ibnr:")) && id.size() == 12) {
244         const auto record = KnowledgeDb::stationForIbnr(KnowledgeDb::IBNR{id.mid(5).toUInt()});
245         applyStationData(record, station);
246         const auto country = KnowledgeDb::countryIdForUicCode(id.midRef(5, 2).toUShort()).toString();
247         applyStationCountry(country, station);
248     } else if (id.startsWith(QLatin1String("uic:")) && id.size() == 11) {
249         const auto record = KnowledgeDb::stationForUic(KnowledgeDb::UICStation{id.mid(4).toUInt()});
250         applyStationData(record, station);
251         const auto country = KnowledgeDb::countryIdForUicCode(id.midRef(4, 2).toUShort()).toString();
252         applyStationCountry(country, station);
253     } else if (id.startsWith(QLatin1String("ir:")) && id.size() > 4) {
254         const auto record = KnowledgeDb::stationForIndianRailwaysStationCode(id.mid(3));
255         applyStationData(record, station);
256     } else if (id.startsWith(QLatin1String("benerail:")) && id.size() == 14) {
257         const auto record = KnowledgeDb::stationForBenerailId(KnowledgeDb::BenerailStationId(id.mid(9)));
258         applyStationData(record, station);
259         applyStationCountry(id.mid(9, 2).toUpper(), station);
260     } else if (id.startsWith(QLatin1String("vrfi:")) && id.size() >= 7 && id.size() <= 9) {
261         const auto record = KnowledgeDb::stationForVRStationCode(KnowledgeDb::VRStationCode(id.mid(5)));
262         applyStationData(record, station);
263     }
264 
265     return processPlace(station);
266 }
267 
processTrainTripTime(QDateTime dt,QDate departureDay,const TrainStation & station) const268 QDateTime ExtractorPostprocessorPrivate::processTrainTripTime(QDateTime dt, QDate departureDay, const TrainStation& station) const
269 {
270     if (!dt.isValid()) {
271         return dt;
272     }
273 
274     if (dt.date().year() <= 1970 && departureDay.isValid()) { // we just have the time, but not the day
275         dt.setDate(departureDay);
276     }
277 
278     if (dt.timeSpec() == Qt::TimeZone) {
279         return dt;
280     }
281 
282     const auto tz = KnowledgeDb::timezoneForLocation(station.geo().latitude(), station.geo().longitude(), station.address().addressCountry());
283     if (!tz.isValid()) {
284         return dt;
285     }
286 
287     // prefer our timezone over externally provided UTC offset, if they match
288     if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) {
289         return dt;
290     }
291 
292     if (dt.timeSpec() == Qt::OffsetFromUTC || dt.timeSpec() == Qt::LocalTime) {
293         dt.setTimeSpec(Qt::TimeZone);
294         dt.setTimeZone(tz);
295     } else if (dt.timeSpec() == Qt::UTC) {
296         dt = dt.toTimeZone(tz);
297     }
298     return dt;
299 }
300 
processBusReservation(BusReservation res) const301 BusReservation ExtractorPostprocessorPrivate::processBusReservation(BusReservation res) const
302 {
303     if (res.reservationFor().isValid()) {
304         res.setReservationFor(processBusTrip(res.reservationFor().value<BusTrip>()));
305     }
306     return processReservation(res);
307 }
308 
processBusTrip(BusTrip trip) const309 BusTrip ExtractorPostprocessorPrivate::processBusTrip(BusTrip trip) const
310 {
311     trip.setDepartureBusStop(processPlace(trip.departureBusStop()));
312     trip.setArrivalBusStop(processPlace(trip.arrivalBusStop()));
313     trip.setDepartureTime(processTimeForLocation(trip.departureTime(), trip.departureBusStop()));
314     trip.setArrivalTime(processTimeForLocation(trip.arrivalTime(), trip.arrivalBusStop()));
315     trip.setBusNumber(trip.busNumber().simplified());
316     trip.setBusName(trip.busName().simplified());
317     return trip;
318 }
319 
processLodgingReservation(LodgingReservation res) const320 LodgingReservation ExtractorPostprocessorPrivate::processLodgingReservation(LodgingReservation res) const
321 {
322     if (res.reservationFor().isValid()) {
323         res.setReservationFor(processPlace(res.reservationFor().value<LodgingBusiness>()));
324         res.setCheckinTime(processTimeForLocation(res.checkinTime(), res.reservationFor().value<LodgingBusiness>()));
325         res.setCheckoutTime(processTimeForLocation(res.checkoutTime(), res.reservationFor().value<LodgingBusiness>()));
326     }
327     return processReservation(res);
328 }
329 
processTaxiReservation(TaxiReservation res) const330 TaxiReservation ExtractorPostprocessorPrivate::processTaxiReservation(TaxiReservation res) const
331 {
332     res.setPickupLocation(processPlace(res.pickupLocation()));
333     res.setPickupTime(processTimeForLocation(res.pickupTime(), res.pickupLocation()));
334     return processReservation(res);
335 }
336 
processRentalCarReservation(RentalCarReservation res) const337 RentalCarReservation ExtractorPostprocessorPrivate::processRentalCarReservation(RentalCarReservation res) const
338 {
339     if (res.reservationFor().isValid()) {
340         res.setReservationFor(processRentalCar(res.reservationFor().value<RentalCar>()));
341     }
342     res.setPickupLocation(processPlace(res.pickupLocation()));
343     res.setDropoffLocation(processPlace(res.dropoffLocation()));
344     res.setPickupTime(processTimeForLocation(res.pickupTime(), res.pickupLocation()));
345     res.setDropoffTime(processTimeForLocation(res.dropoffTime(), res.dropoffLocation()));
346     return processReservation(res);
347 }
348 
processRentalCar(RentalCar car) const349 RentalCar ExtractorPostprocessorPrivate::processRentalCar(RentalCar car) const
350 {
351     car.setName(car.name().trimmed());
352     return car;
353 }
354 
processFoodEstablishmentReservation(FoodEstablishmentReservation res) const355 FoodEstablishmentReservation ExtractorPostprocessorPrivate::processFoodEstablishmentReservation(FoodEstablishmentReservation res) const
356 {
357     if (res.reservationFor().isValid()) {
358         res.setReservationFor(processPlace(res.reservationFor().value<FoodEstablishment>()));
359         res.setStartTime(processTimeForLocation(res.startTime(), res.reservationFor().value<FoodEstablishment>()));
360         res.setEndTime(processTimeForLocation(res.endTime(), res.reservationFor().value<FoodEstablishment>()));
361     }
362     return processReservation(res);
363 }
364 
processTouristAttractionVisit(TouristAttractionVisit visit) const365 TouristAttractionVisit ExtractorPostprocessorPrivate::processTouristAttractionVisit(TouristAttractionVisit visit) const
366 {
367     visit.setTouristAttraction(processPlace(visit.touristAttraction()));
368     visit.setArrivalTime(processTimeForLocation(visit.arrivalTime(), visit.touristAttraction()));
369     visit.setDepartureTime(processTimeForLocation(visit.departureTime(), visit.touristAttraction()));
370     return visit;
371 }
372 
processEventReservation(EventReservation res) const373 EventReservation ExtractorPostprocessorPrivate::processEventReservation(EventReservation res) const
374 {
375     if (res.reservationFor().isValid()) {
376         res.setReservationFor(processEvent(res.reservationFor().value<Event>()));
377     }
378     return processReservation(res);
379 }
380 
processEvent(Event event) const381 Event ExtractorPostprocessorPrivate::processEvent(Event event) const
382 {
383     event.setName(event.name().trimmed());
384 
385     // normalize location to be a Place
386     if (JsonLd::isA<PostalAddress>(event.location())) {
387         Place place;
388         place.setAddress(event.location().value<PostalAddress>());
389         event.setLocation(place);
390     }
391 
392     if (JsonLd::isA<Place>(event.location())) {
393         event.setLocation(processPlace(event.location().value<Place>()));
394 
395         // try to obtain timezones if we have a location
396         event.setStartDate(processTimeForLocation(event.startDate(), event.location().value<Place>()));
397         event.setEndDate(processTimeForLocation(event.endDate(), event.location().value<Place>()));
398         event.setDoorTime(processTimeForLocation(event.doorTime(), event.location().value<Place>()));
399     }
400 
401     return event;
402 }
403 
processProgramMembership(ProgramMembership program) const404 ProgramMembership ExtractorPostprocessorPrivate::processProgramMembership(ProgramMembership program) const
405 {
406     program.setProgramName(program.programName().simplified());
407      // avoid emitting spurious empty ProgramMembership objects caused by empty elements in JSON-LD/Microdata input
408     if (program.programName().isEmpty() && !program.programName().isNull()) {
409         program.setProgramName(QString());
410     }
411     program.setMember(processPerson(program.member()));
412     return program;
413 }
414 
415 template <typename T>
processReservation(T res) const416 T ExtractorPostprocessorPrivate::processReservation(T res) const
417 {
418     res.setUnderName(processPerson(res.underName().template value<Person>()));
419     res.setPotentialAction(processActions(res.potentialAction()));
420     res.setReservationNumber(res.reservationNumber().trimmed());
421     res.setProgramMembershipUsed(processProgramMembership(res.programMembershipUsed()));
422     return res;
423 }
424 
425 
processPerson(Person person) const426 Person ExtractorPostprocessorPrivate::processPerson(Person person) const
427 {
428     person.setName(person.name().simplified());
429     person.setFamilyName(person.familyName().simplified());
430     person.setGivenName(person.givenName().simplified());
431 
432     // fill name with name parts, if it's empty
433     if ((person.name().isEmpty() || person.name() == person.familyName() || person.name() == person.givenName())
434         && !person.familyName().isEmpty() && !person.givenName().isEmpty())
435     {
436         person.setName(person.givenName() + QLatin1Char(' ') + person.familyName());
437     }
438 
439     // strip prefixes, they break comparisons
440     static const char* const honorificPrefixes[] = { "MR ", "MS ", "MRS " };
441     for (auto prefix : honorificPrefixes) {
442         if (person.name().startsWith(QLatin1String(prefix), Qt::CaseInsensitive)) {
443             person.setName(person.name().mid(strlen(prefix)));
444             break;
445         }
446     }
447 
448     return person;
449 }
450 
processAddress(PostalAddress addr,const QString & phoneNumber,const GeoCoordinates & geo)451 PostalAddress ExtractorPostprocessorPrivate::processAddress(PostalAddress addr, const QString &phoneNumber, const GeoCoordinates &geo)
452 {
453     // convert to ISO 3166-1 alpha-2 country codes
454     if (addr.addressCountry().size() > 2) {
455 #if HAVE_KI18N_LOCALE_DATA
456         QString alpha2Code;
457 
458         // try ISO 3166-1 alpha-3, we get that e.g. from Flixbus
459         if (addr.addressCountry().size() == 3) {
460             alpha2Code = KCountry::fromAlpha3(addr.addressCountry()).alpha2();
461         }
462         if (alpha2Code.isEmpty()) {
463             alpha2Code = KCountry::fromName(addr.addressCountry()).alpha2();
464         }
465         if (!alpha2Code.isEmpty()) {
466             addr.setAddressCountry(alpha2Code);
467         }
468 #else
469         const auto isoCode = KContacts::Address::countryToISO(addr.addressCountry()).toUpper();
470         if (!isoCode.isEmpty()) {
471             addr.setAddressCountry(isoCode);
472 
473         // try ISO 3166-1 alpha-3, we get that e.g. from Flixbus
474         } else if (addr.addressCountry().size() == 3) {
475             const auto c = KnowledgeDb::countryIdFromIso3166_1alpha3(KnowledgeDb::CountryId3(addr.addressCountry()));
476             if (c.isValid()) {
477                 addr.setAddressCountry(c.toString());
478             }
479         }
480 #endif
481     }
482 
483     // upper case country codes
484     if (addr.addressCountry().size() == 2) {
485         addr.setAddressCountry(addr.addressCountry().toUpper());
486     }
487 
488     // normalize strings
489     addr.setStreetAddress(addr.streetAddress().simplified());
490     addr.setAddressLocality(addr.addressLocality().simplified());
491     addr.setAddressRegion(addr.addressRegion().simplified());
492 
493 #ifdef HAVE_PHONENUMBER
494     // recover country from phone number, if we have that
495     if (!phoneNumber.isEmpty() && addr.addressCountry().size() != 2) {
496         const auto phoneStr = phoneNumber.toStdString();
497         const auto util = i18n::phonenumbers::PhoneNumberUtil::GetInstance();
498         i18n::phonenumbers::PhoneNumber number;
499         if (util->ParseAndKeepRawInput(phoneStr, "ZZ", &number) == i18n::phonenumbers::PhoneNumberUtil::NO_PARSING_ERROR) {
500             std::string isoCode;
501             util->GetRegionCodeForNumber(number, &isoCode);
502             if (!isoCode.empty()) {
503                 addr.setAddressCountry(QString::fromStdString(isoCode));
504             }
505         }
506     }
507 #endif
508 
509     if (geo.isValid() && addr.addressCountry().isEmpty()) {
510         addr.setAddressCountry(KnowledgeDb::countryForCoordinate(geo.latitude(), geo.longitude()));
511     }
512 
513     addr = ExtractorUtil::extractPostalCode(addr);
514     return addr;
515 }
516 
processPhoneNumber(const QString & phoneNumber,const PostalAddress & addr)517 QString ExtractorPostprocessorPrivate::processPhoneNumber(const QString &phoneNumber, const PostalAddress &addr)
518 {
519 #ifdef HAVE_PHONENUMBER
520     // or complete the phone number if we know the country
521     if (!phoneNumber.isEmpty() && addr.addressCountry().size() == 2) {
522         auto phoneStr = phoneNumber.toStdString();
523         const auto isoCode = addr.addressCountry().toStdString();
524         const auto util = i18n::phonenumbers::PhoneNumberUtil::GetInstance();
525         i18n::phonenumbers::PhoneNumber number;
526         if (util->ParseAndKeepRawInput(phoneStr, isoCode, &number) == i18n::phonenumbers::PhoneNumberUtil::NO_PARSING_ERROR) {
527             if (number.country_code_source() == i18n::phonenumbers::PhoneNumber_CountryCodeSource_FROM_DEFAULT_COUNTRY) {
528                 util->Format(number, i18n::phonenumbers::PhoneNumberUtil::INTERNATIONAL, &phoneStr);
529                 return QString::fromStdString(phoneStr);
530             }
531         }
532     }
533 #else
534     Q_UNUSED(addr)
535 #endif
536     return phoneNumber;
537 }
538 
processActions(QVariantList actions) const539 QVariantList ExtractorPostprocessorPrivate::processActions(QVariantList actions) const
540 {
541     // remove non-actions and actions with invalid URLs
542     QUrl viewUrl;
543     for (auto it = actions.begin(); it != actions.end();) {
544         if (!JsonLd::canConvert<Action>(*it)) {
545             it = actions.erase(it);
546             continue;
547         }
548 
549         const auto action = JsonLd::convert<Action>(*it);
550         if (!action.target().isValid()) {
551             it = actions.erase(it);
552             continue;
553         }
554 
555         if (JsonLd::isA<ViewAction>(*it)) {
556             viewUrl = action.target();
557         }
558         ++it;
559     }
560 
561     // normalize the order, so JSON comparison still yields correct results
562     std::sort(actions.begin(), actions.end(), [](const QVariant &lhs, const QVariant &rhs) {
563         return strcmp(lhs.typeName(), rhs.typeName()) < 0;
564     });
565 
566     // remove actions that don't actually have their own target, or duplicates
567     QUrl prevUrl;
568     const char* prevType = nullptr;
569     for (auto it = actions.begin(); it != actions.end();) {
570         const auto action = JsonLd::convert<Action>(*it);
571         const auto isDuplicate = action.target() == prevUrl && (prevType ? strcmp(prevType, (*it).typeName()) == 0 : false);
572         if ((JsonLd::isA<ViewAction>(*it) || action.target() != viewUrl) && !isDuplicate) {
573             prevUrl = action.target();
574             prevType = (*it).typeName();
575             ++it;
576         } else {
577             it = actions.erase(it);
578         }
579     }
580 
581     return actions;
582 }
583 
584 template <typename T>
processTimeForLocation(QDateTime dt,const T & place) const585 QDateTime ExtractorPostprocessorPrivate::processTimeForLocation(QDateTime dt, const T &place) const
586 {
587     if (!dt.isValid() || dt.timeSpec() == Qt::TimeZone) {
588         return dt;
589     }
590 
591     const auto tz = KnowledgeDb::timezoneForLocation(place.geo().latitude(), place.geo().longitude(), place.address().addressCountry());
592     if (!tz.isValid()) {
593         return dt;
594     }
595 
596     // prefer our timezone over externally provided UTC offset, if they match
597     if (dt.timeSpec() == Qt::OffsetFromUTC && tz.offsetFromUtc(dt) != dt.offsetFromUtc()) {
598         qCDebug(Log) << "UTC offset clashes with expected timezone!" << dt << dt.offsetFromUtc() << tz.id() << tz.offsetFromUtc(dt);
599         return dt;
600     }
601 
602     if (dt.timeSpec() == Qt::OffsetFromUTC || dt.timeSpec() == Qt::LocalTime) {
603         dt.setTimeSpec(Qt::TimeZone);
604         dt.setTimeZone(tz);
605     } else if (dt.timeSpec() == Qt::UTC) {
606         dt = dt.toTimeZone(tz);
607     }
608     return dt;
609 }
610