calendarhandler.cpp 19.4 KB
Newer Older
Volker Krause's avatar
Volker Krause committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/*
   Copyright (c) 2017 Volker Krause <vkrause@kde.org>

   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.
*/

20
#include "config-kitinerary.h"
Volker Krause's avatar
Volker Krause committed
21 22
#include "calendarhandler.h"
#include "jsonlddocument.h"
Volker Krause's avatar
Volker Krause committed
23
#include "logging.h"
24
#include "mergeutil.h"
25
#include "sortutil.h"
Volker Krause's avatar
Volker Krause committed
26

27
#include <KItinerary/BusTrip>
28
#include <KItinerary/Event>
29 30
#include <KItinerary/Flight>
#include <KItinerary/Organization>
31
#include <KItinerary/Person>
32
#include <KItinerary/Place>
Laurent Montel's avatar
Laurent Montel committed
33
#include <KItinerary/RentalCar>
34
#include <KItinerary/Reservation>
35
#include <KItinerary/Ticket>
36
#include <KItinerary/TrainTrip>
37
#include <KItinerary/Visit>
Volker Krause's avatar
Volker Krause committed
38

39
#ifdef HAVE_KCAL
40 41 42
#include <KCalendarCore/Alarm>
#include <KCalendarCore/Calendar>
#include <KCalendarCore/Event>
43
#endif
44

45 46
#include <KContacts/Address>

Volker Krause's avatar
Volker Krause committed
47 48
#include <KLocalizedString>

49
#include <QDateTime>
50 51 52
#include <QJsonArray>
#include <QJsonDocument>

Volker Krause's avatar
Volker Krause committed
53
using namespace KItinerary;
Volker Krause's avatar
Volker Krause committed
54

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
static QString formatAddress(const PostalAddress &addr)
{
    KContacts::Address a;
    a.setStreet(addr.streetAddress());
    a.setPostalCode(addr.postalCode());
    a.setLocality(addr.addressLocality());
    a.setCountry(KContacts::Address::ISOtoCountry(addr.addressCountry()));
    return a.formattedAddress();
}

static QString formatAddressSingleLine(const PostalAddress &addr)
{
    return formatAddress(addr).replace(QLatin1String("\n\n"), QLatin1String("\n")).replace(QLatin1Char('\n'), QLatin1String(", "));
}

70
#ifdef HAVE_KCAL
71 72 73 74
using namespace KCalendarCore;
static void fillFlightReservation(const QVector<QVariant> &reservations, const KCalendarCore::Event::Ptr &event);
static void fillTrainReservation(const TrainReservation &reservation, const KCalendarCore::Event::Ptr &event);
static void fillBusReservation(const BusReservation &reservation, const KCalendarCore::Event::Ptr &event);
75
static void fillLodgingReservation(const QVector<QVariant> &reservations, const KCalendarCore::Event::Ptr &event);
76 77 78 79 80
static void fillEventReservation(const QVector<QVariant> &reservations, const KCalendarCore::Event::Ptr &event);
static void fillGeoPosition(const QVariant &place, const KCalendarCore::Event::Ptr &event);
static void fillFoodReservation(const FoodEstablishmentReservation &reservation, const KCalendarCore::Event::Ptr &event);
static void fillRentalCarReservation(const RentalCarReservation &reservation, const KCalendarCore::Event::Ptr &event);
static void fillTaxiReservation(const TaxiReservation &reservation, const KCalendarCore::Event::Ptr &event);
81
#endif
Volker Krause's avatar
Volker Krause committed
82

83
QSharedPointer<KCalendarCore::Event> CalendarHandler::findEvent(const QSharedPointer<KCalendarCore::Calendar> &calendar, const QVariant &reservation)
84
{
85
#ifdef HAVE_KCAL
Volker Krause's avatar
Volker Krause committed
86
    if (!JsonLd::canConvert<Reservation>(reservation) || !calendar) {
87 88
        return {};
    }
89

90
    const auto dt = SortUtil::startDateTime(reservation).date();
91
    const auto events = calendar->events(dt);
92
    for (const auto &event : events) {
Volker Krause's avatar
Volker Krause committed
93
        if (!event->uid().startsWith(QLatin1String("KIT-"))) {
94 95
            continue;
        }
96 97 98 99 100
        const auto otherRes = CalendarHandler::reservationsForEvent(event);
        for (const auto &other : otherRes) {
            if (MergeUtil::isSame(other, reservation)) {
                return event;
            }
101 102
        }
    }
103 104 105 106
#else
    Q_UNUSED(calendar);
    Q_UNUSED(reservation);
#endif
107

108 109 110
    return {};
}

111
QVector<QVariant> CalendarHandler::reservationsForEvent(const QSharedPointer<KCalendarCore::Event> &event)
112
{
113
#ifdef HAVE_KCAL
114 115
    const auto payload = event->customProperty("KITINERARY", "RESERVATION").toUtf8();
    const auto json = QJsonDocument::fromJson(payload).array();
116
    return JsonLdDocument::fromJson(json);
117 118 119 120
#else
    Q_UNUSED(event);
    return {};
#endif
121 122
}

123
void CalendarHandler::fillEvent(const QVector<QVariant> &reservations, const QSharedPointer<KCalendarCore::Event> &event)
124 125 126 127 128
{
    if (reservations.isEmpty()) {
        return;
    }

129
#ifdef HAVE_KCAL
130
    // TODO pass reservationS into all functions below for multi-traveler support
131
    const auto &reservation = reservations.at(0);
Volker Krause's avatar
Volker Krause committed
132 133
    const int typeId = reservation.userType();
    if (typeId == qMetaTypeId<FlightReservation>()) {
134
        fillFlightReservation(reservations, event);
Volker Krause's avatar
Volker Krause committed
135
    } else if (typeId == qMetaTypeId<LodgingReservation>()) {
136
        fillLodgingReservation(reservations, event);
Volker Krause's avatar
Volker Krause committed
137
    } else if (typeId == qMetaTypeId<TrainReservation>()) {
138
        fillTrainReservation(reservation.value<TrainReservation>(), event);
139
    } else if (typeId == qMetaTypeId<BusReservation>()) {
140
        fillBusReservation(reservation.value<BusReservation>(), event);
141 142
    } else if (JsonLd::isA<EventReservation>(reservation)) {
        fillEventReservation(reservations, event);
143 144
    } else if (JsonLd::isA<FoodEstablishmentReservation>(reservation)) {
        fillFoodReservation(reservation.value<FoodEstablishmentReservation>(), event);
Laurent Montel's avatar
Laurent Montel committed
145 146
    } else if (JsonLd::isA<RentalCarReservation>(reservation)) {
        fillRentalCarReservation(reservation.value<RentalCarReservation>(), event);
147 148
    } else if (JsonLd::isA<TaxiReservation>(reservation)) {
        fillTaxiReservation(reservation.value<TaxiReservation>(), event);
149 150
    } else {
        return;
151 152
    }

153 154
    if (!event->uid().startsWith(QLatin1String("KIT-"))) {
        event->setUid(QLatin1String("KIT-") + event->uid());
Volker Krause's avatar
Volker Krause committed
155
    }
156

157
    const auto payload = QJsonDocument(JsonLdDocument::toJson(reservations)).toJson(QJsonDocument::Compact);
158
    event->setCustomProperty("KITINERARY", "RESERVATION", QString::fromUtf8(payload));
159 160 161
#else
    Q_UNUSED(event);
#endif
Volker Krause's avatar
Volker Krause committed
162 163
}

164
#ifdef HAVE_KCAL
165 166 167 168 169
static QString airportDisplayCode(const Airport &airport)
{
    return airport.iataCode().isEmpty() ? airport.name() : airport.iataCode();
}

170
static void fillFlightReservation(const QVector<QVariant> &reservations, const KCalendarCore::Event::Ptr &event)
Volker Krause's avatar
Volker Krause committed
171
{
172
    const auto flight = reservations.at(0).value<FlightReservation>().reservationFor().value<Flight>();
173 174 175
    const auto airline = flight.airline();
    const auto depPort = flight.departureAirport();
    const auto arrPort = flight.arrivalAirport();
Volker Krause's avatar
Volker Krause committed
176

177
    const QString flightNumber = airline.iataCode() + QLatin1Char(' ') + flight.flightNumber();
178

179
    event->setSummary(i18n("Flight %1 from %2 to %3", flightNumber, airportDisplayCode(depPort), airportDisplayCode(arrPort)));
180
    event->setLocation(depPort.name());
181
    fillGeoPosition(depPort, event);
182 183
    event->setDtStart(flight.departureTime());
    event->setDtEnd(flight.arrivalTime());
Volker Krause's avatar
Volker Krause committed
184
    event->setAllDay(false);
185

186 187
    const auto boardingTime = flight.boardingTime();
    const auto departureGate = flight.departureGate();
188
    if (boardingTime.isValid()) {
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
        const auto startOffset = Duration(event->dtStart(), boardingTime);
        const auto existinAlarms = event->alarms();
        const auto it = std::find_if(existinAlarms.begin(), existinAlarms.end(), [startOffset](const Alarm::Ptr &other) {
            return other->startOffset() == startOffset;
        });
        if (it == existinAlarms.end()) {
            Alarm::Ptr alarm(new Alarm(event.data()));
            alarm->setStartOffset(Duration(event->dtStart(), boardingTime));
            if (departureGate.isEmpty()) {
                alarm->setDisplayAlarm(i18n("Boarding for flight %1", flightNumber));
            } else {
                alarm->setDisplayAlarm(i18n("Boarding for flight %1 at gate %2", flightNumber, departureGate));
            }
            alarm->setEnabled(true);
            event->addAlarm(alarm);
Laurent Montel's avatar
Laurent Montel committed
204
        }
205
    }
206 207 208 209 210 211 212 213

    QStringList desc;
    if (boardingTime.isValid()) {
        desc.push_back(i18n("Boarding time: %1", QLocale().toString(boardingTime.time(), QLocale::ShortFormat)));
    }
    if (!departureGate.isEmpty()) {
        desc.push_back(i18n("Departure gate: %1", departureGate));
    }
214

Laurent Montel's avatar
Laurent Montel committed
215
    for (const auto &r : reservations) {
216 217 218 219 220 221 222 223 224 225 226 227 228 229
        const auto reservation = r.value<FlightReservation>();
        const auto person = reservation.underName().value<KItinerary::Person>();
        if (!person.name().isEmpty()) {
            desc.push_back(person.name());
        }
        if (!reservation.boardingGroup().isEmpty()) {
            desc.push_back(i18n("Boarding group: %1", reservation.boardingGroup()));
        }
        if (!reservation.airplaneSeat().isEmpty()) {
            desc.push_back(i18n("Seat: %1", reservation.airplaneSeat()));
        }
        if (!reservation.reservationNumber().isEmpty()) {
            desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber()));
        }
230
    }
231
    event->setDescription(desc.join(QLatin1Char('\n')));
Volker Krause's avatar
Volker Krause committed
232 233
}

234
static void fillTrainReservation(const TrainReservation &reservation, const KCalendarCore::Event::Ptr &event)
Volker Krause's avatar
Volker Krause committed
235
{
236
    const auto trip = reservation.reservationFor().value<TrainTrip>();
237
    const auto depStation = trip.departureStation();
238
    const auto arrStation = trip.arrivalStation();
Volker Krause's avatar
Volker Krause committed
239

240
    event->setSummary(i18n("Train %1 from %2 to %3", trip.trainNumber(), depStation.name(), arrStation.name()));
241
    event->setLocation(depStation.name());
242
    fillGeoPosition(depStation, event);
243 244
    event->setDtStart(trip.departureTime());
    event->setDtEnd(trip.arrivalTime());
Volker Krause's avatar
Volker Krause committed
245 246 247
    event->setAllDay(false);

    QStringList desc;
248 249
    if (!trip.departurePlatform().isEmpty()) {
        desc.push_back(i18n("Departure platform: %1", trip.departurePlatform()));
Volker Krause's avatar
Volker Krause committed
250
    }
251
    const auto ticket = reservation.reservedTicket().value<Ticket>();
252 253 254
    const auto seat = ticket.ticketedSeat();
    if (!seat.seatSection().isEmpty()) {
        desc.push_back(i18n("Coach: %1", seat.seatSection()));
Volker Krause's avatar
Volker Krause committed
255
    }
256 257
    if (!seat.seatNumber().isEmpty()) {
        desc.push_back(i18n("Seat: %1", seat.seatNumber()));
Volker Krause's avatar
Volker Krause committed
258
    }
259 260
    if (!trip.arrivalPlatform().isEmpty()) {
        desc.push_back(i18n("Arrival platform: %1", trip.arrivalPlatform()));
Volker Krause's avatar
Volker Krause committed
261
    }
262 263
    if (!reservation.reservationNumber().isEmpty()) {
        desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber()));
Volker Krause's avatar
Volker Krause committed
264 265 266 267
    }
    event->setDescription(desc.join(QLatin1Char('\n')));
}

268
static void fillBusReservation(const BusReservation &reservation, const KCalendarCore::Event::Ptr &event)
269
{
270
    const auto trip = reservation.reservationFor().value<BusTrip>();
271 272
    const auto depStation = trip.departureBusStop();
    const auto arrStation = trip.arrivalBusStop();
273

274
    event->setSummary(i18n("Bus %1 from %2 to %3", trip.busNumber(), depStation.name(), arrStation.name()));
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
    event->setLocation(depStation.name());
    fillGeoPosition(depStation, event);
    event->setDtStart(trip.departureTime());
    event->setDtEnd(trip.arrivalTime());
    event->setAllDay(false);

    QStringList desc;
    const auto ticket = reservation.reservedTicket().value<Ticket>();
    const auto seat = ticket.ticketedSeat();
    if (!seat.seatNumber().isEmpty()) {
        desc.push_back(i18n("Seat: %1", seat.seatNumber()));
    }
    if (!reservation.reservationNumber().isEmpty()) {
        desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber()));
    }
    event->setDescription(desc.join(QLatin1Char('\n')));
291 292
}

293
static void fillLodgingReservation(const QVector<QVariant> &reservations, const KCalendarCore::Event::Ptr &event)
Volker Krause's avatar
Volker Krause committed
294
{
295
    const auto reservation = reservations.at(0).value<LodgingReservation>();
296
    const auto lodgingBusiness = reservation.reservationFor().value<LodgingBusiness>();
Volker Krause's avatar
Volker Krause committed
297

298
    event->setSummary(i18n("Hotel reservation: %1", lodgingBusiness.name()));
299
    event->setLocation(formatAddressSingleLine(lodgingBusiness.address()));
300
    fillGeoPosition(lodgingBusiness, event);
301

302 303
    event->setDtStart(QDateTime(reservation.checkinTime().date(), QTime()));
    event->setDtEnd(QDateTime(reservation.checkoutTime().date(), QTime()));
Volker Krause's avatar
Volker Krause committed
304
    event->setAllDay(true);
305
    event->setTransparency(KCalendarCore::Event::Transparent);
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334

    QStringList desc;
    if (reservation.checkinTime().isValid()) {
        desc.push_back(i18n("Check-in: %1", QLocale().toString(reservation.checkinTime().time(), QLocale::ShortFormat)));
    }
    if (reservation.checkoutTime().isValid()) {
        desc.push_back(i18n("Check-out: %1", QLocale().toString(reservation.checkoutTime().time(), QLocale::ShortFormat)));
    }
    if (!lodgingBusiness.telephone().isEmpty()) {
        desc.push_back(i18n("Phone: %1", lodgingBusiness.telephone()));
    }
    if (!lodgingBusiness.email().isEmpty()) {
        desc.push_back(i18n("Email: %1", lodgingBusiness.email()));
    }
    if (!lodgingBusiness.url().isEmpty()) {
        desc.push_back(i18n("Website: %1", lodgingBusiness.url().toString()));
    }

    for (const auto &r : reservations) {
        const auto reservation = r.value<LodgingReservation>();
        const auto person = reservation.underName().value<KItinerary::Person>();
        if (!person.name().isEmpty()) {
            desc.push_back(person.name());
        }
        if (!reservation.reservationNumber().isEmpty()) {
            desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber()));
        }
    }
    event->setDescription(desc.join(QLatin1Char('\n')));
335 336
}

337
static void fillEventReservation(const QVector<QVariant> &reservations, const KCalendarCore::Event::Ptr &event)
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
{
    const auto ev = reservations.at(0).value<EventReservation>().reservationFor().value<KItinerary::Event>();
    Place location;
    if (JsonLd::canConvert<Place>(ev.location())) {
        location = JsonLd::convert<Place>(ev.location());
    }

    event->setSummary(ev.name());
    event->setLocation(location.name());
    fillGeoPosition(location, event);
    event->setDtStart(ev.startDate());
    event->setDtEnd(ev.endDate());
    event->setAllDay(false);

    if (ev.doorTime().isValid()) {
        const auto startOffset = Duration(event->dtStart(), ev.doorTime());
        const auto existinAlarms = event->alarms();
        const auto it = std::find_if(existinAlarms.begin(), existinAlarms.end(), [startOffset](const Alarm::Ptr &other) {
            return other->startOffset() == startOffset;
        });
        if (it == existinAlarms.end()) {
            Alarm::Ptr alarm(new Alarm(event.data()));
            alarm->setStartOffset(Duration(event->dtStart(), ev.doorTime()));
            alarm->setDisplayAlarm(i18n("Entrance for %1", ev.name()));
            alarm->setEnabled(true);
            event->addAlarm(alarm);
        }
    }

    QStringList desc;
Laurent Montel's avatar
Laurent Montel committed
368
    for (const auto &r : reservations) {
369 370 371 372 373 374 375 376 377 378 379
        const auto reservation = r.value<EventReservation>();
        const auto person = reservation.underName().value<KItinerary::Person>();
        if (!person.name().isEmpty()) {
            desc.push_back(person.name());
        }
        // TODO: add seat information if present
        if (!reservation.reservationNumber().isEmpty()) {
            desc.push_back(i18n("Booking reference: %1", reservation.reservationNumber()));
        }
    }
    event->setDescription(desc.join(QLatin1Char('\n')));
Volker Krause's avatar
Volker Krause committed
380
}
381

382
static void fillGeoPosition(const QVariant &place, const KCalendarCore::Event::Ptr &event)
383
{
384 385 386 387
    if (!JsonLd::canConvert<Place>(place)) {
        return;
    }
    const auto geo = JsonLd::convert<Place>(place).geo();
Volker Krause's avatar
Volker Krause committed
388
    if (!geo.isValid()) {
389 390 391 392
        return;
    }

    event->setHasGeo(true);
Volker Krause's avatar
Volker Krause committed
393 394
    event->setGeoLatitude(geo.latitude());
    event->setGeoLongitude(geo.longitude());
395
}
396

397
static void fillFoodReservation(const FoodEstablishmentReservation &reservation, const KCalendarCore::Event::Ptr &event)
398 399 400 401
{
    const auto foodEstablishment = reservation.reservationFor().value<FoodEstablishment>();

    event->setSummary(i18n("Restaurant reservation: %1", foodEstablishment.name()));
402
    event->setLocation(formatAddressSingleLine(foodEstablishment.address()));
403 404 405 406 407
    fillGeoPosition(foodEstablishment, event);

    event->setDtStart(reservation.startTime());
    auto endTime = reservation.endTime();
    if (!endTime.isValid()) {
408
        endTime = reservation.startTime().addSecs(7200); // if we have no end time, let's assume 2h
409 410 411 412
    }
    event->setDtEnd(endTime);
    event->setAllDay(false);

413 414 415 416 417 418 419 420 421 422 423 424
    QStringList desc;
    if (reservation.partySize() > 0) {
        desc.push_back(i18n("Number Of People: %1", reservation.partySize()));
    }
    if (!reservation.reservationNumber().isEmpty()) {
        desc.push_back(i18n("Reservation reference: %1", reservation.reservationNumber()));
    }
    const auto person = reservation.underName().value<KItinerary::Person>();
    if (!person.name().isEmpty()) {
        desc.push_back(i18n("Under name: %1", person.name()));
    }
    event->setDescription(desc.join(QLatin1Char('\n')));
425
}
Laurent Montel's avatar
Laurent Montel committed
426

427
static void fillRentalCarReservation(const RentalCarReservation &reservation, const KCalendarCore::Event::Ptr &event)
Laurent Montel's avatar
Laurent Montel committed
428
{
429
    const auto rentalCalPickup = reservation.pickupLocation();
Laurent Montel's avatar
Laurent Montel committed
430 431 432
    const auto addressPickUp = rentalCalPickup.address();
    const auto rentalCar = reservation.reservationFor().value<RentalCar>();
    event->setSummary(i18n("Rental Car reservation: %1", rentalCar.name()));
433
    event->setLocation(formatAddressSingleLine(addressPickUp));
Laurent Montel's avatar
Laurent Montel committed
434 435 436 437
    fillGeoPosition(rentalCalPickup, event);

    event->setDtStart(reservation.pickupTime());
    event->setDtEnd(reservation.dropoffTime());
Laurent Montel's avatar
Laurent Montel committed
438
    event->setAllDay(false);
439
    event->setTransparency(KCalendarCore::Event::Transparent);
Laurent Montel's avatar
Laurent Montel committed
440 441
    event->setSummary(i18n("Rent car reservation: %1", rentalCar.name()));

442
    const QString pickUpAddress = formatAddress(addressPickUp);
443
    const auto rentalCalDropOff = reservation.dropoffLocation();
Laurent Montel's avatar
Laurent Montel committed
444
    const auto addressDropOff = rentalCalDropOff.address();
445
    const QString dropAddress = formatAddress(addressDropOff);
Laurent Montel's avatar
Laurent Montel committed
446

Laurent Montel's avatar
Laurent Montel committed
447
    const QString description = i18n("Reservation reference: %1\nUnder name: %2\n\nPickUp location: %3\n\nDropoff Location: %4",
Laurent Montel's avatar
Laurent Montel committed
448 449 450 451 452 453 454 455
                                     reservation.reservationNumber(),
                                     reservation.underName().value<KItinerary::Person>().name(),
                                     pickUpAddress,
                                     dropAddress);

    event->setDescription(description);
}

456
static void fillTaxiReservation(const TaxiReservation &reservation, const KCalendarCore::Event::Ptr &event)
457
{
458
    const auto taxiPickup = reservation.pickupLocation();
459 460 461
    const auto addressPickUp = taxiPickup.address();
    //TODO const auto rentalCar = reservation.reservationFor().value<RentalCar>();
    //TODO event->setSummary(i18n("Rental Car reservation: %1", rentalCar.name()));
462
    event->setLocation(formatAddressSingleLine(addressPickUp));
463 464 465 466 467
    fillGeoPosition(taxiPickup, event);

    event->setDtStart(reservation.pickupTime());
    //TODO event->setDtEnd(reservation.dropoffTime());
    event->setAllDay(false);
468
    event->setTransparency(KCalendarCore::Event::Transparent);
469
    //TODO event->setSummary(i18n("Rent car reservation: %1", rentalCar.name()));
470
    const QString pickUpAddress = formatAddress(addressPickUp);
471 472 473 474 475 476 477 478 479

    const QString description = i18n("Reservation reference: %1\nUnder name: %2\nPickUp location: %3",
                                     reservation.reservationNumber(),
                                     reservation.underName().value<KItinerary::Person>().name(),
                                     pickUpAddress);

    event->setDescription(description);
}

480
#endif