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