timelinemodel.cpp 12.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/*
    Copyright (C) 2018 Volker Krause <vkrause@kde.org>

    This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Library General Public License as published by
    the Free Software Foundation; either version 2 of the License, or (at your
    option) any later version.

    This program is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
    License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "timelinemodel.h"
19
#include "countryinformation.h"
20
#include "pkpassmanager.h"
21 22
#include "reservationmanager.h"

Volker Krause's avatar
Volker Krause committed
23
#include <KItinerary/BusTrip>
24
#include <KItinerary/CountryDb>
25 26
#include <KItinerary/Flight>
#include <KItinerary/JsonLdDocument>
27
#include <KItinerary/Organization>
28
#include <KItinerary/Reservation>
29
#include <KItinerary/SortUtil>
Volker Krause's avatar
Volker Krause committed
30
#include <KItinerary/TrainTrip>
31
#include <KItinerary/Visit>
32

Volker Krause's avatar
Volker Krause committed
33
#include <KPkPass/Pass>
34

35 36
#include <KLocalizedString>

37
#include <QDateTime>
38
#include <QDebug>
39
#include <QLocale>
40

41 42
#include <cassert>

43 44
using namespace KItinerary;

45 46 47 48 49
static bool needsSplitting(const QVariant &res)
{
    return res.userType() == qMetaTypeId<LodgingReservation>();
}

50
static QDateTime relevantDateTime(const QVariant &res, TimelineModel::RangeType range)
51
{
52 53
    if (range == TimelineModel::RangeBegin || range == TimelineModel::SelfContained) {
        return SortUtil::startDateTime(res);
54
    }
55 56
    if (range == TimelineModel::RangeEnd) {
        return SortUtil::endtDateTime(res);
57
    }
58 59 60 61 62 63 64 65

    return {};
}

static QString passId(const QVariant &res)
{
    const auto passTypeId = JsonLdDocument::readProperty(res, "pkpassPassTypeIdentifier").toString();
    const auto serialNum = JsonLdDocument::readProperty(res, "pkpassSerialNumber").toString();
66
    if (passTypeId.isEmpty() || serialNum.isEmpty()) {
67
        return {};
68
    }
69 70 71
    return passTypeId + QLatin1Char('/') + QString::fromUtf8(serialNum.toUtf8().toBase64(QByteArray::Base64UrlEncoding));
}

72 73 74 75 76 77 78
static TimelineModel::ElementType elementType(const QVariant &res)
{
    if (JsonLd::isA<FlightReservation>(res)) { return TimelineModel::Flight; }
    if (JsonLd::isA<LodgingReservation>(res)) { return TimelineModel::Hotel; }
    if (JsonLd::isA<TrainReservation>(res)) { return TimelineModel::TrainTrip; }
    if (JsonLd::isA<BusReservation>(res)) { return TimelineModel::BusTrip; }
    if (JsonLd::isA<FoodEstablishmentReservation>(res)) { return TimelineModel::Restaurant; }
79
    if (JsonLd::isA<TouristAttractionVisit>(res)) { return TimelineModel::TouristAttraction; }
80 81 82
    return {};
}

83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
static QString destinationCountry(const QVariant &res)
{
    if (JsonLd::isA<FlightReservation>(res)) {
        return res.value<FlightReservation>().reservationFor().value<Flight>().arrivalAirport().address().addressCountry();
    }
    if (JsonLd::isA<TrainReservation>(res)) {
        return res.value<TrainReservation>().reservationFor().value<TrainTrip>().arrivalStation().address().addressCountry();
    }
    if (JsonLd::isA<LodgingReservation>(res)) {
        return res.value<LodgingReservation>().reservationFor().value<LodgingBusiness>().address().addressCountry();
    }
    if (JsonLd::isA<BusReservation>(res)) {
        return res.value<BusReservation>().reservationFor().value<BusTrip>().arrivalStation().address().addressCountry();
    }
    if (JsonLd::isA<FoodEstablishmentReservation>(res)) {
        return res.value<FoodEstablishmentReservation>().reservationFor().value<FoodEstablishment>().address().addressCountry();
    }
100 101 102
    if (JsonLd::isA<TouristAttractionVisit>(res)) {
        return res.value<TouristAttractionVisit>().touristAttraction().address().addressCountry();
    }
103 104 105
    return {};
}

106 107 108 109 110 111 112
TimelineModel::TimelineModel(QObject *parent)
    : QAbstractListModel(parent)
{
}

TimelineModel::~TimelineModel() = default;

Volker Krause's avatar
Volker Krause committed
113 114
void TimelineModel::setReservationManager(ReservationManager* mgr)
{
115
    beginResetModel();
Volker Krause's avatar
Volker Krause committed
116
    m_resMgr = mgr;
117
    for (const auto &resId : mgr->reservations()) {
118 119
        const auto res = m_resMgr->reservation(resId);
        if (needsSplitting(res)) {
120 121
            m_elements.push_back(Element{resId, {}, relevantDateTime(res, RangeBegin), elementType(res), RangeBegin});
            m_elements.push_back(Element{resId, {}, relevantDateTime(res, RangeEnd), elementType(res), RangeEnd});
122
        } else {
123
            m_elements.push_back(Element{resId, {}, relevantDateTime(res, SelfContained), elementType(res), SelfContained});
124
        }
125
    }
126
    m_elements.push_back(Element{{}, {}, QDateTime(QDate::currentDate(), QTime(0, 0)), TodayMarker, SelfContained});
127
    std::sort(m_elements.begin(), m_elements.end(), [](const Element &lhs, const Element &rhs) {
128
        return lhs.dt < rhs.dt;
129 130 131 132 133
    });
    connect(mgr, &ReservationManager::reservationAdded, this, &TimelineModel::reservationAdded);
    connect(mgr, &ReservationManager::reservationUpdated, this, &TimelineModel::reservationUpdated);
    connect(mgr, &ReservationManager::reservationRemoved, this, &TimelineModel::reservationRemoved);
    endResetModel();
134 135

    updateInformationElements();
136
    emit todayRowChanged();
Volker Krause's avatar
Volker Krause committed
137 138
}

139 140
int TimelineModel::rowCount(const QModelIndex& parent) const
{
141
    if (parent.isValid() || !m_resMgr) {
142
        return 0;
143
    }
144
    return m_elements.size();
145 146 147 148
}

QVariant TimelineModel::data(const QModelIndex& index, int role) const
{
149
    if (!index.isValid() || !m_resMgr) {
150
        return {};
151
    }
152

153 154
    const auto &elem = m_elements.at(index.row());
    const auto res = m_resMgr->reservation(elem.id);
155 156
    switch (role) {
        case PassIdRole:
157
            return passId(res);
158
        case SectionHeader:
159
        {
160
            if (elem.dt.isNull()) {
161
                return {};
162 163
            }
            if (elem.dt.date() == QDate::currentDate()) {
164
                return i18n("Today");
165
            }
166
            return i18nc("weekday, date", "%1, %2", QLocale().dayName(elem.dt.date().dayOfWeek(), QLocale::LongFormat), QLocale().toString(elem.dt.date(), QLocale::ShortFormat));
167
        }
168 169
        case ReservationRole:
            return res;
170 171
        case ReservationIdRole:
            return elem.id;
172
        case ElementTypeRole:
173
            return elem.elementType;
174
        case TodayEmptyRole:
175
            if (elem.elementType == TodayMarker) {
176
                return index.row() == (int)(m_elements.size() - 1) || m_elements.at(index.row() + 1).dt.date() > QDate::currentDate();
177 178 179
            }
            return {};
        case IsTodayRole:
180 181 182
            return elem.dt.date() == QDate::currentDate();
        case ElementRangeRole:
            return elem.rangeType;
183 184
        case CountryInformationRole:
            return elem.content;
185 186 187 188 189 190 191 192
    }
    return {};
}

QHash<int, QByteArray> TimelineModel::roleNames() const
{
    auto names = QAbstractListModel::roleNames();
    names.insert(PassIdRole, "passId");
193
    names.insert(SectionHeader, "sectionHeader");
194
    names.insert(ReservationRole, "reservation");
195
    names.insert(ReservationIdRole, "reservationId");
196 197 198
    names.insert(ElementTypeRole, "type");
    names.insert(TodayEmptyRole, "isTodayEmpty");
    names.insert(IsTodayRole, "isToday");
199
    names.insert(ElementRangeRole, "rangeType");
200
    names.insert(CountryInformationRole, "countryInformation");
201 202 203
    return names;
}

204 205
int TimelineModel::todayRow() const
{
206
    const auto it = std::find_if(m_elements.begin(), m_elements.end(), [](const Element &e) { return e.elementType == TodayMarker; });
207
    return std::distance(m_elements.begin(), it);
208 209
}

210
void TimelineModel::reservationAdded(const QString &resId)
211
{
212 213
    const auto res = m_resMgr->reservation(resId);
    if (needsSplitting(res)) {
214 215
        insertElement(Element{resId, {}, relevantDateTime(res, RangeBegin), elementType(res), RangeBegin});
        insertElement(Element{resId, {}, relevantDateTime(res, RangeEnd), elementType(res), RangeEnd});
216
    } else {
217
        insertElement(Element{resId, {}, relevantDateTime(res, SelfContained), elementType(res), SelfContained});
218 219
    }

220
    updateInformationElements();
221 222 223 224 225 226 227
    emit todayRowChanged();
}

void TimelineModel::insertElement(Element &&elem)
{
    auto it = std::lower_bound(m_elements.begin(), m_elements.end(), elem.dt, [](const Element &lhs, const QDateTime &rhs) {
        return lhs.dt < rhs;
Volker Krause's avatar
Volker Krause committed
228
    });
229
    auto index = std::distance(m_elements.begin(), it);
Volker Krause's avatar
Volker Krause committed
230
    beginInsertRows({}, index, index);
231
    m_elements.insert(it, std::move(elem));
232 233
    endInsertRows();
}
234

235
void TimelineModel::reservationUpdated(const QString &resId)
236
{
237 238 239 240 241 242 243
    const auto res = m_resMgr->reservation(resId);
    if (needsSplitting(res)) {
        updateElement(resId, res, RangeBegin);
        updateElement(resId, res, RangeEnd);
    } else {
        updateElement(resId, res, SelfContained);
    }
244 245

    updateInformationElements();
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
}

void TimelineModel::updateElement(const QString &resId, const QVariant &res, TimelineModel::RangeType rangeType)
{
    const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId, rangeType](const Element &e) { return e.id == resId && e.rangeType == rangeType; });
    if (it == m_elements.end()) {
        return;
    }
    const auto row = std::distance(m_elements.begin(), it);
    const auto newDt = relevantDateTime(res, rangeType);

    if ((*it).dt != newDt) {
        // element moved
        beginRemoveRows({}, row, row);
        m_elements.erase(it);
        endRemoveRows();
262
        insertElement(Element{resId, {}, newDt, elementType(res), rangeType});
263 264 265
    } else {
        emit dataChanged(index(row, 0), index(row, 0));
    }
266
}
267

268
void TimelineModel::reservationRemoved(const QString &resId)
269
{
270 271
    const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId](const Element &e) { return e.id == resId; });
    if (it == m_elements.end()) {
272 273
        return;
    }
274
    const auto isSplit = (*it).rangeType == RangeBegin;
275 276 277
    const auto row = std::distance(m_elements.begin(), it);
    beginRemoveRows({}, row, row);
    m_elements.erase(it);
278
    endRemoveRows();
279
    emit todayRowChanged();
280 281 282 283

    if (isSplit) {
        reservationRemoved(resId);
    }
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300

    updateInformationElements();
}

void TimelineModel::updateInformationElements()
{
    // the country information is shown before transitioning into a country that
    // differs in one or more properties from the home country and we where that
    // differences is introduced by the transition

    CountryInformation homeCountry;
    homeCountry.setIsoCode(QLatin1String("DE")); // TODO configurable home country

    auto previousCountry = homeCountry;
    for (auto it = m_elements.begin(); it != m_elements.end(); ++it) {
        switch ((*it).elementType) {
            case TodayMarker:
301
                it = erasePreviousCountyInfo(it);
302 303 304
                continue;
            case CountryInfo:
                previousCountry = (*it).content.value<CountryInformation>();
305
                it = erasePreviousCountyInfo(it); // purge multiple consecutive country info elements
306 307 308 309 310
                continue;
            default:
                break;
        }

311
        auto newCountry = homeCountry;
312 313 314 315 316 317 318
        newCountry.setIsoCode(destinationCountry(m_resMgr->reservation((*it).id)));
        if (newCountry == previousCountry) {
            continue;
        }
        if (newCountry == homeCountry) {
            assert(it != m_elements.begin()); // previousCountry == homeCountry in this case
            // purge outdated country info element
319 320
            it = erasePreviousCountyInfo(it);
            previousCountry = newCountry;
321 322 323 324 325 326 327 328 329 330 331
            continue;
        }

        // add new country info element
        auto row = std::distance(m_elements.begin(), it);
        beginInsertRows({}, row, row);
        it = m_elements.insert(it, Element{{}, QVariant::fromValue(newCountry), (*it).dt, CountryInfo, SelfContained});
        endInsertRows();

        previousCountry = newCountry;
    }
332
}
333 334 335 336 337 338 339 340 341 342 343 344 345

std::vector<TimelineModel::Element>::iterator TimelineModel::erasePreviousCountyInfo(std::vector<Element>::iterator it)
{
    auto it2 = it;
    --it2;
    if ((*it2).elementType == CountryInfo) {
        auto row = std::distance(m_elements.begin(), it2);
        beginRemoveRows({}, row, row);
        it = m_elements.erase(it2);
        endRemoveRows();
    }
    return it;
}