File indexing completed on 2025-02-02 05:02:37

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "timelinemodel.h"
0008 #include "constants.h"
0009 #include "locationhelper.h"
0010 #include "locationinformation.h"
0011 #include "pkpassmanager.h"
0012 #include "reservationmanager.h"
0013 #include "tripgroup.h"
0014 #include "tripgroupmanager.h"
0015 #include "transfermanager.h"
0016 #include "weatherinformation.h"
0017 
0018 #include "weatherforecast.h"
0019 #include "weatherforecastmanager.h"
0020 
0021 #include <KItinerary/BusTrip>
0022 #include <KItinerary/CountryDb>
0023 #include <KItinerary/Event>
0024 #include <KItinerary/Flight>
0025 #include <KItinerary/JsonLdDocument>
0026 #include <KItinerary/LocationUtil>
0027 #include <KItinerary/MergeUtil>
0028 #include <KItinerary/Organization>
0029 #include <KItinerary/Reservation>
0030 #include <KItinerary/SortUtil>
0031 #include <KItinerary/TrainTrip>
0032 #include <KItinerary/Visit>
0033 
0034 #include <KPkPass/Pass>
0035 
0036 #include <KLocalizedString>
0037 
0038 #include <QDebug>
0039 #include <QLocale>
0040 
0041 #include <cassert>
0042 
0043 using namespace KItinerary;
0044 
0045 static bool needsSplitting(const QVariant &res)
0046 {
0047     // multi-day event?
0048     if (JsonLd::isA<EventReservation>(res)) {
0049         const auto ev = res.value<EventReservation>().reservationFor().value<Event>();
0050         if (!ev.endDate().isValid() || ev.startDate().date() == ev.endDate().date()) {
0051             return false;
0052         }
0053         // don't split single day events that end at midnight either
0054         if (ev.startDate().secsTo(ev.endDate()) < 60 * 60 * 8 && ev.endDate().time().hour() == 0) {
0055             return false;
0056         }
0057         return true;
0058     }
0059 
0060     return JsonLd::isA<LodgingReservation>(res)
0061         || JsonLd::isA<RentalCarReservation>(res);
0062 }
0063 
0064 static QTimeZone timeZone(const QDateTime &dt)
0065 {
0066     return dt.timeSpec() == Qt::TimeZone ? dt.timeZone() : QTimeZone();
0067 }
0068 
0069 TimelineModel::TimelineModel(QObject *parent)
0070     : QAbstractListModel(parent)
0071 {
0072     connect(&m_dayUpdateTimer, &QTimer::timeout, this, &TimelineModel::dayChanged);
0073     m_dayUpdateTimer.setTimerType(Qt::VeryCoarseTimer);
0074     m_dayUpdateTimer.setSingleShot(true);
0075     m_dayUpdateTimer.setInterval((QTime::currentTime().secsTo({23, 59, 59}) + 1) * 1000);
0076     m_dayUpdateTimer.start();
0077 
0078     // make sure we properly update the empty today marker
0079     connect(this, &TimelineModel::todayRowChanged, this, [this]() {
0080         const auto idx = index(todayRow(), 0);
0081         if (m_todayEmpty == idx.data(TimelineModel::TodayEmptyRole).toBool()) {
0082             return;
0083         }
0084         m_todayEmpty = !m_todayEmpty;
0085         Q_EMIT dataChanged(idx, idx);
0086     });
0087 
0088     connect(&m_currentBatchTimer, &QTimer::timeout, this, &TimelineModel::currentBatchChanged);
0089     connect(&m_currentBatchTimer, &QTimer::timeout, this, &TimelineModel::updateTodayMarker);
0090     connect(&m_currentBatchTimer, &QTimer::timeout, this, &TimelineModel::scheduleCurrentBatchTimer);
0091     m_currentBatchTimer.setTimerType(Qt::VeryCoarseTimer);
0092     m_currentBatchTimer.setSingleShot(true);
0093 }
0094 
0095 TimelineModel::~TimelineModel() = default;
0096 
0097 void TimelineModel::setReservationManager(ReservationManager* mgr)
0098 {
0099     // for auto tests only
0100     if (Q_UNLIKELY(!mgr)) {
0101         beginResetModel();
0102         disconnect(m_resMgr, &ReservationManager::batchAdded, this, &TimelineModel::batchAdded);
0103         disconnect(m_resMgr, &ReservationManager::batchChanged, this, &TimelineModel::batchChanged);
0104         disconnect(m_resMgr, &ReservationManager::batchContentChanged, this, &TimelineModel::batchChanged);
0105         disconnect(m_resMgr, &ReservationManager::batchRenamed, this, &TimelineModel::batchRenamed);
0106         disconnect(m_resMgr, &ReservationManager::batchRemoved, this, &TimelineModel::batchRemoved);
0107         m_resMgr = mgr;
0108         m_elements.clear();
0109         endResetModel();
0110         return;
0111     }
0112 
0113     beginResetModel();
0114     m_resMgr = mgr;
0115     for (const auto &resId : mgr->batches()) {
0116         const auto res = m_resMgr->reservation(resId);
0117         auto elem = TimelineElement(this, resId, res, TimelineElement::SelfContained);
0118         if (!elem.isReservation()) { // a type we can't handle
0119             continue;
0120         }
0121         if (needsSplitting(res)) {
0122             m_elements.push_back(TimelineElement{this, resId, res, TimelineElement::RangeBegin});
0123             m_elements.push_back(TimelineElement{this, resId, res, TimelineElement::RangeEnd});
0124         } else {
0125             m_elements.push_back(std::move(elem));
0126         }
0127     }
0128     m_elements.push_back(TimelineElement{this, TimelineElement::TodayMarker,  QDateTime(today(), QTime(0, 0))});
0129     std::sort(m_elements.begin(), m_elements.end());
0130 
0131     connect(mgr, &ReservationManager::batchAdded, this, &TimelineModel::batchAdded);
0132     connect(mgr, &ReservationManager::batchChanged, this, &TimelineModel::batchChanged);
0133     connect(mgr, &ReservationManager::batchContentChanged, this, &TimelineModel::batchChanged);
0134     connect(mgr, &ReservationManager::batchRenamed, this, &TimelineModel::batchRenamed);
0135     connect(mgr, &ReservationManager::batchRemoved, this, &TimelineModel::batchRemoved);
0136     endResetModel();
0137 
0138     updateTodayMarker();
0139     updateInformationElements();
0140     Q_EMIT todayRowChanged();
0141 
0142     scheduleCurrentBatchTimer();
0143     Q_EMIT currentBatchChanged();
0144 }
0145 
0146 void TimelineModel::setWeatherForecastManager(WeatherForecastManager* mgr)
0147 {
0148     m_weatherMgr = mgr;
0149     updateWeatherElements();
0150     connect(m_weatherMgr, &WeatherForecastManager::forecastUpdated, this, &TimelineModel::updateWeatherElements);
0151 }
0152 
0153 TripGroupManager* TimelineModel::tripGroupManager() const
0154 {
0155     return m_tripGroupManager;
0156 }
0157 
0158 void TimelineModel::setTripGroupManager(TripGroupManager *mgr)
0159 {
0160     m_tripGroupManager = mgr;
0161     connect(mgr, &TripGroupManager::tripGroupAdded, this, &TimelineModel::tripGroupAdded);
0162     connect(mgr, &TripGroupManager::tripGroupChanged, this, &TimelineModel::tripGroupChanged);
0163     connect(mgr, &TripGroupManager::tripGroupRemoved, this, &TimelineModel::tripGroupRemoved);
0164     for (const auto &group : mgr->tripGroups()) {
0165         tripGroupAdded(group);
0166     }
0167 }
0168 
0169 void TimelineModel::setHomeCountryIsoCode(const QString &isoCode)
0170 {
0171     m_homeCountry = isoCode;
0172     updateInformationElements();
0173 }
0174 
0175 void TimelineModel::setTransferManager(TransferManager *mgr)
0176 {
0177     m_transferManager = mgr;
0178     connect(mgr, &TransferManager::transferAdded, this, &TimelineModel::transferChanged);
0179     connect(mgr, &TransferManager::transferChanged, this, &TimelineModel::transferChanged);
0180     connect(mgr, &TransferManager::transferRemoved, this, &TimelineModel::transferRemoved);
0181 
0182     // load existing transfers into the model
0183     for (const auto &batchId : m_resMgr->batches()) {
0184         updateTransfersForBatch(batchId);
0185     }
0186 }
0187 
0188 int TimelineModel::rowCount(const QModelIndex& parent) const
0189 {
0190     if (parent.isValid() || !m_resMgr) {
0191         return 0;
0192     }
0193     return m_elements.size();
0194 }
0195 
0196 QVariant TimelineModel::data(const QModelIndex& index, int role) const
0197 {
0198     if (!index.isValid() || !m_resMgr) {
0199         return {};
0200     }
0201 
0202     const auto &elem = m_elements.at(index.row());
0203     switch (role) {
0204         case SectionHeaderRole:
0205             // see TimelineSectionDelegateController
0206             return elem.dt.date().toString(Qt::ISODate);
0207         case BatchIdRole:
0208             return elem.isReservation() ? elem.batchId() : QString();
0209         case ElementTypeRole:
0210             return elem.elementType;
0211         case TodayEmptyRole:
0212             if (elem.elementType == TimelineElement::TodayMarker) {
0213                 return isDateEmpty(m_elements.at(index.row()).dt.date());
0214             }
0215             return {};
0216         case IsTodayRole:
0217             return elem.dt.date() == today();
0218         case ElementRangeRole:
0219             return elem.rangeType;
0220         case LocationInformationRole:
0221             if (elem.elementType == TimelineElement::LocationInfo)
0222                 return elem.content();
0223             break;
0224         case WeatherForecastRole:
0225             if (elem.elementType == TimelineElement::WeatherForecast)
0226                 return elem.content();
0227             break;
0228         case ReservationsRole:
0229         {
0230             if (!elem.isReservation()) {
0231                 return {};
0232             }
0233             const auto resIds = m_resMgr->reservationsForBatch(elem.batchId());
0234             QVector<QVariant> v;
0235             v.reserve(resIds.size());
0236             for (const auto &resId : resIds) {
0237                 v.push_back(m_resMgr->reservation(resId));
0238             }
0239             std::sort(v.begin(), v.end(), SortUtil::isBefore);
0240             return QVariant::fromValue(v);
0241         }
0242         case TripGroupIdRole:
0243             if (elem.elementType == TimelineElement::TripGroup) {
0244                 return elem.content();
0245             }
0246             break;
0247         case TripGroupRole:
0248             if (elem.elementType == TimelineElement::TripGroup)
0249                 return QVariant::fromValue(m_tripGroupManager->tripGroup(elem.content().toString()));
0250             break;
0251         case TransferRole:
0252             if (elem.elementType == TimelineElement::Transfer) {
0253                 return elem.content();
0254             }
0255             break;
0256         case StartDateTimeRole:
0257             return elem.dt;
0258         case EndDateTimeRole:
0259             return elem.endDateTime();
0260         case IsTimeboxedRole:
0261             return elem.isTimeBoxed();
0262         case IsCanceledRole:
0263             return elem.isCanceled();
0264     }
0265     return {};
0266 }
0267 
0268 QHash<int, QByteArray> TimelineModel::roleNames() const
0269 {
0270     auto names = QAbstractListModel::roleNames();
0271     names.insert(SectionHeaderRole, "sectionHeader");
0272     names.insert(BatchIdRole, "batchId");
0273     names.insert(ElementTypeRole, "type");
0274     names.insert(TodayEmptyRole, "isTodayEmpty");
0275     names.insert(IsTodayRole, "isToday");
0276     names.insert(ElementRangeRole, "rangeType");
0277     names.insert(LocationInformationRole, "locationInformation");
0278     names.insert(WeatherForecastRole, "weatherInformation");
0279     names.insert(ReservationsRole, "reservations");
0280     names.insert(TripGroupIdRole, "tripGroupId");
0281     names.insert(TripGroupRole, "tripGroup");
0282     names.insert(TransferRole, "transfer");
0283     return names;
0284 }
0285 
0286 int TimelineModel::todayRow() const
0287 {
0288     const auto it = std::find_if(m_elements.begin(), m_elements.end(), [](const auto &e) { return e.elementType == TimelineElement::TodayMarker; });
0289     return std::distance(m_elements.begin(), it);
0290 }
0291 
0292 void TimelineModel::batchAdded(const QString &resId)
0293 {
0294     const auto res = m_resMgr->reservation(resId);
0295     if (needsSplitting(res)) {
0296         insertElement(TimelineElement{this, resId, res, TimelineElement::RangeBegin});
0297         insertElement(TimelineElement{this, resId, res, TimelineElement::RangeEnd});
0298     } else {
0299         insertElement(TimelineElement{this, resId, res, TimelineElement::SelfContained});
0300     }
0301 
0302     updateInformationElements();
0303     updateTransfersForBatch(resId);
0304     Q_EMIT todayRowChanged();
0305 
0306     scheduleCurrentBatchTimer();
0307     Q_EMIT currentBatchChanged();
0308 }
0309 
0310 void TimelineModel::insertElement(TimelineElement &&elem)
0311 {
0312     if (elem.elementType == TimelineElement::Undefined) {
0313         return;
0314     }
0315 
0316     auto it = std::lower_bound(m_elements.begin(), m_elements.end(), elem);
0317     const auto row = std::distance(m_elements.begin(), it);
0318 
0319     beginInsertRows({}, row, row);
0320     m_elements.insert(it, std::move(elem));
0321     endInsertRows();
0322 }
0323 
0324 std::vector<TimelineElement>::iterator TimelineModel::insertOrUpdate(std::vector<TimelineElement>::iterator it, TimelineElement &&elem)
0325 {
0326     assert(elem.elementType != TimelineElement::Undefined);
0327 
0328     while (it != m_elements.end() && (*it) < elem) {
0329         ++it;
0330     }
0331 
0332     if (it != m_elements.end() && (*it) == elem) {
0333         const auto row = std::distance(m_elements.begin(), it);
0334         (*it) = std::move(elem);
0335         Q_EMIT dataChanged(index(row, 0), index(row, 0));
0336     } else {
0337         const auto row = std::distance(m_elements.begin(), it);
0338         beginInsertRows({}, row, row);
0339         it = m_elements.insert(it, std::move(elem));
0340         endInsertRows();
0341     }
0342     return it;
0343 }
0344 
0345 void TimelineModel::batchChanged(const QString &resId)
0346 {
0347     const auto res = m_resMgr->reservation(resId);
0348     if (needsSplitting(res)) {
0349         updateElement(resId, res, TimelineElement::RangeBegin);
0350         updateElement(resId, res, TimelineElement::RangeEnd);
0351     } else {
0352         updateElement(resId, res, TimelineElement::SelfContained);
0353     }
0354 
0355     updateInformationElements();
0356 
0357     scheduleCurrentBatchTimer();
0358     Q_EMIT currentBatchChanged();
0359 }
0360 
0361 void TimelineModel::batchRenamed(const QString& oldBatchId, const QString& newBatchId)
0362 {
0363     for (auto it = m_elements.begin(); it != m_elements.end(); ++it) {
0364         if (!(*it).isReservation() || (*it).batchId() != oldBatchId) {
0365             continue;
0366         }
0367 
0368         (*it).setContent(newBatchId);
0369         const auto idx = index(std::distance(m_elements.begin(), it), 0);
0370         Q_EMIT dataChanged(idx, idx);
0371 
0372         if ((*it).rangeType == TimelineElement::SelfContained || (*it).rangeType == TimelineElement::RangeEnd) {
0373             break;
0374         }
0375     }
0376 }
0377 
0378 void TimelineModel::updateElement(const QString &resId, const QVariant &res, TimelineElement::RangeType rangeType)
0379 {
0380     const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId, rangeType](const auto &e) {
0381         return e.isReservation() && e.batchId() == resId && e.rangeType == rangeType;
0382     });
0383     if (it == m_elements.end()) {
0384         return;
0385     }
0386     const auto row = std::distance(m_elements.begin(), it);
0387     const auto newDt = TimelineElement::relevantDateTime(res, rangeType);
0388 
0389     if ((*it).dt != newDt) {
0390         // element moved
0391         beginRemoveRows({}, row, row);
0392         m_elements.erase(it);
0393         endRemoveRows();
0394         insertElement(TimelineElement{this, resId, res, rangeType});
0395     } else {
0396         Q_EMIT dataChanged(index(row, 0), index(row, 0));
0397     }
0398 }
0399 
0400 void TimelineModel::batchRemoved(const QString &resId)
0401 {
0402     const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId](const auto &e) {
0403         return e.isReservation() && e.batchId() == resId;
0404     });
0405     if (it == m_elements.end()) {
0406         return;
0407     }
0408     const auto isSplit = (*it).rangeType == TimelineElement::RangeBegin;
0409     const auto row = std::distance(m_elements.begin(), it);
0410 
0411     beginRemoveRows({}, row, row);
0412     m_elements.erase(it);
0413     endRemoveRows();
0414     Q_EMIT todayRowChanged();
0415 
0416     if (isSplit) {
0417         batchRemoved(resId);
0418     }
0419 
0420     updateInformationElements();
0421 
0422     scheduleCurrentBatchTimer();
0423     Q_EMIT currentBatchChanged();
0424 }
0425 
0426 void TimelineModel::dayChanged()
0427 {
0428     updateTodayMarker();
0429     updateWeatherElements();
0430 
0431     m_dayUpdateTimer.setInterval((QTime::currentTime().secsTo({23, 59, 59}) + 1) * 1000);
0432     m_dayUpdateTimer.start();
0433 
0434     scheduleCurrentBatchTimer();
0435     Q_EMIT currentBatchChanged();
0436 }
0437 
0438 void TimelineModel::updateTodayMarker()
0439 {
0440     auto dt = now();
0441     auto it = std::lower_bound(m_elements.begin(), m_elements.end(), dt);
0442 
0443     if (it != m_elements.begin()) {
0444         const auto prevIt = std::prev(it);
0445         // check if the previous element is the old today marker, if so nothing to do
0446         if ((*prevIt).elementType == TimelineElement::TodayMarker) {
0447             (*prevIt).dt = dt;
0448             return;
0449         }
0450         // check if the previous element is still ongoing, in that case we want to be before that
0451         if ((*prevIt).dt.date() == today() && ((*prevIt).isTimeBoxed() && (*prevIt).endDateTime() > now())) {
0452             it = prevIt;
0453             dt = (*prevIt).dt;
0454         }
0455     }
0456 
0457     const auto newRow = std::distance(m_elements.begin(), it);
0458     const auto oldRow = todayRow();
0459     if (oldRow >= newRow) {
0460         return;
0461     }
0462 
0463     beginInsertRows({}, newRow, newRow);
0464     m_elements.insert(it, TimelineElement{this, TimelineElement::TodayMarker, dt});
0465     endInsertRows();
0466 
0467     beginRemoveRows({}, oldRow, oldRow);
0468     m_elements.erase(m_elements.begin() + oldRow);
0469     endRemoveRows();
0470     Q_EMIT todayRowChanged();
0471 }
0472 
0473 void TimelineModel::updateInformationElements()
0474 {
0475     // the location information is shown after location changes or before stationary elements
0476     // when transitioning into a location that:
0477     // - differs in one or more properties from the home country, and the difference
0478     // was introduced by this transtion
0479     // - differs in timezone from the previous location, and that timezone has a different
0480     // offset at the time of transition
0481 
0482     LocationInformation homeCountry;
0483     homeCountry.setIsoCode(m_homeCountry);
0484 
0485     auto previousCountry = homeCountry;
0486     for (auto it = m_elements.begin(); it != m_elements.end();) {
0487         if ((*it).elementType == TimelineElement::LocationInfo) { // this is one we didn't generate, otherwise it would be beyond that
0488             const auto row = std::distance(m_elements.begin(), it);
0489             beginRemoveRows({}, row, row);
0490             it = m_elements.erase(it);
0491             endRemoveRows();
0492             continue;
0493         }
0494 
0495         if ((*it).isCanceled() || (*it).isInformational()) {
0496             ++it;
0497             continue;
0498         }
0499 
0500         auto newCountry = homeCountry;
0501         newCountry.setIsoCode(LocationUtil::address((*it).destination()).addressCountry());
0502         newCountry.setTimeZone(previousCountry.timeZone(), (*it).dt);
0503         newCountry.setTimeZone(timeZone((*it).endDateTime()), (*it).dt);
0504         if (newCountry == previousCountry) {
0505             ++it;
0506             continue;
0507         }
0508         if (!(newCountry == homeCountry) || newCountry.hasRelevantTimeZoneChange(previousCountry)) {
0509             // for location changes, we want this after the corresponding element
0510             const auto dt = (*it).isLocationChange() ? (*it).endDateTime() : (*it).dt;
0511             it = insertOrUpdate(it, TimelineElement{this, TimelineElement::LocationInfo, dt, QVariant::fromValue(newCountry)});
0512         }
0513 
0514         ++it;
0515         previousCountry = newCountry;
0516     }
0517 
0518     updateWeatherElements();
0519 }
0520 
0521 void TimelineModel::updateWeatherElements()
0522 {
0523     if (!m_weatherMgr || !m_weatherMgr->allowNetworkAccess() || m_elements.empty()) {
0524         return;
0525     }
0526 
0527     qDebug() << "recomputing weather elements";
0528     GeoCoordinates geo;
0529     QString label;
0530 
0531     auto date = now();
0532     // round to next full hour
0533     date.setTime(QTime(date.time().hour(), 0));
0534     date = date.addSecs(60 * 60);
0535     const auto maxForecastTime = m_weatherMgr->maximumForecastTime(date.date());
0536 
0537     // look through the past, clean up weather elements there and figure out where we are
0538     auto it = m_elements.begin();
0539     for (; it != m_elements.end() && (*it).dt < now();) {
0540         if ((*it).elementType == TimelineElement::WeatherForecast) {
0541             const auto row = std::distance(m_elements.begin(), it);
0542             beginRemoveRows({}, row, row);
0543             it = m_elements.erase(it);
0544             endRemoveRows();
0545             continue;
0546         }
0547 
0548         if ((*it).isCanceled()) {
0549             ++it;
0550             continue;
0551         }
0552         const auto newGeo = LocationUtil::geo((*it).destination());
0553         if ((*it).isLocationChange() || newGeo.isValid()) {
0554             geo = newGeo;
0555             label = WeatherInformation::labelForPlace((*it).destination());
0556 
0557             // if we are in an ongoing location change, start afterwards
0558             const auto endDt = (*it).endDateTime();
0559             if ((*it).isLocationChange() && endDt.isValid() && date < endDt) {
0560                 date = endDt;
0561             }
0562         }
0563 
0564         ++it;
0565     }
0566 
0567     while(it != m_elements.end() && date < maxForecastTime) {
0568 
0569         if ((*it).dt < date || (*it).elementType == TimelineElement::TodayMarker) {
0570             // clean up outdated weather elements (happens when merging previously split ranges)
0571             if ((*it).elementType == TimelineElement::WeatherForecast) {
0572                 const auto row = std::distance(m_elements.begin(), it);
0573                 beginRemoveRows({}, row, row);
0574                 it = m_elements.erase(it);
0575                 endRemoveRows();
0576                 if (it == m_elements.end()) {
0577                     break;
0578                 }
0579                 continue;
0580             }
0581 
0582             // track where we are
0583             if ((*it).isCanceled()) {
0584                 ++it;
0585                 continue;
0586             }
0587             const auto newGeo = LocationUtil::geo((*it).destination());
0588             if ((*it).isLocationChange() || newGeo.isValid()) {
0589                 geo = newGeo;
0590                 label = WeatherInformation::labelForPlace((*it).destination());
0591             }
0592 
0593             ++it;
0594             continue;
0595         }
0596 
0597         // determine the length of the forecast range (at most until the end of the day)
0598         auto endTime = date;
0599         endTime.setTime(QTime(23, 59, 59));
0600         auto nextStartTime = endTime;
0601         GeoCoordinates newGeo = geo;
0602         QString newLabel = label;
0603         for (auto it2 = it; it2 != m_elements.end(); ++it2) {
0604             if ((*it2).dt >= endTime) {
0605                 break;
0606             }
0607             if ((*it2).isLocationChange()) {
0608                 // exclude the actual travel time from forecast ranges
0609                 endTime = std::min(endTime, (*it2).dt);
0610                 nextStartTime = std::max(endTime, (*it2).endDateTime());
0611                 newGeo = LocationUtil::geo((*it2).destination());
0612                 newLabel = WeatherInformation::labelForPlace((*it2).destination());
0613                 break;
0614             }
0615         }
0616 
0617         ::WeatherForecast fc;
0618         if (geo.isValid()) {
0619             m_weatherMgr->monitorLocation(geo.latitude(), geo.longitude());
0620             fc = m_weatherMgr->forecast(geo.latitude(), geo.longitude(), date, endTime);
0621         }
0622 
0623         // updated or new data
0624         if (fc.isValid()) {
0625             it = insertOrUpdate(it, TimelineElement{this, TimelineElement::WeatherForecast, date, QVariant::fromValue(WeatherInformation{fc, label})});
0626         }
0627         // we have no forecast data, but a matching weather element: remove
0628         else if ((*it).elementType == TimelineElement::WeatherForecast && (*it).dt == date) {
0629             const auto row = std::distance(m_elements.begin(), it);
0630             beginRemoveRows({}, row, row);
0631             it = m_elements.erase(it);
0632             endRemoveRows();
0633         }
0634 
0635         geo = newGeo;
0636         label = newLabel;
0637         date = nextStartTime.addSecs(1);
0638         ++it;
0639     }
0640 
0641     // append weather elements beyond the end of the list if necessary
0642     while (date < maxForecastTime && geo.isValid()) {
0643         auto endTime = date;
0644         endTime.setTime(QTime(23, 59, 59));
0645 
0646         m_weatherMgr->monitorLocation(geo.latitude(), geo.longitude());
0647         const auto fc = m_weatherMgr->forecast(geo.latitude(), geo.longitude(), date, endTime);
0648         if (fc.isValid()) {
0649             const auto row = std::distance(m_elements.begin(), it);
0650             beginInsertRows({}, row, row);
0651             it = m_elements.insert(it, TimelineElement{this, TimelineElement::WeatherForecast, date, QVariant::fromValue(WeatherInformation{fc, label})});
0652             ++it;
0653             endInsertRows();
0654         }
0655         date = endTime.addSecs(1);
0656     }
0657 
0658     qDebug() << "weather recomputation done";
0659 }
0660 
0661 void TimelineModel::updateTransfersForBatch(const QString& batchId)
0662 {
0663     if (!m_transferManager) {
0664         return;
0665     }
0666 
0667     auto transfer = m_transferManager->transfer(batchId, Transfer::Before);
0668     if (transfer.state() != Transfer::UndefinedState) {
0669         transferChanged(transfer);
0670     }
0671     transfer = m_transferManager->transfer(batchId, Transfer::After);
0672     if (transfer.state() != Transfer::UndefinedState) {
0673         transferChanged(transfer);
0674     }
0675 }
0676 
0677 QDateTime TimelineModel::now() const
0678 {
0679     if (Q_UNLIKELY(m_unitTestTime.isValid())) {
0680         return m_unitTestTime;
0681     }
0682     return QDateTime::currentDateTime();
0683 }
0684 
0685 QDate TimelineModel::today() const
0686 {
0687     if (Q_UNLIKELY(m_unitTestTime.isValid())) {
0688         return m_unitTestTime.date();
0689     }
0690     return QDate::currentDate();
0691 }
0692 
0693 void TimelineModel::setCurrentDateTime(const QDateTime &dt)
0694 {
0695     const auto dayDiffers = today() != dt.date();
0696     m_unitTestTime = dt;
0697     if (dayDiffers && !m_elements.empty()) {
0698         dayChanged();
0699     }
0700 
0701     scheduleCurrentBatchTimer();
0702 }
0703 
0704 void TimelineModel::tripGroupAdded(const QString& groupId)
0705 {
0706     const auto g = m_tripGroupManager->tripGroup(groupId);
0707 
0708     TimelineElement beginElem{this, TimelineElement::TripGroup, g.beginDateTime(), groupId};
0709     beginElem.rangeType = TimelineElement::RangeBegin;
0710     insertElement(std::move(beginElem));
0711 
0712     TimelineElement endElem{this, TimelineElement::TripGroup, g.endDateTime(), groupId};
0713     endElem.rangeType = TimelineElement::RangeEnd;
0714     insertElement(std::move(endElem));
0715 }
0716 
0717 void TimelineModel::tripGroupChanged(const QString& groupId)
0718 {
0719     // ### this can be done better probably
0720     tripGroupRemoved(groupId);
0721     tripGroupAdded(groupId);
0722 }
0723 
0724 void TimelineModel::tripGroupRemoved(const QString& groupId)
0725 {
0726     for (auto it = m_elements.begin(); it != m_elements.end();) {
0727         if ((*it).elementType != TimelineElement::TripGroup || (*it).content().toString() != groupId) {
0728             ++it;
0729             continue;
0730         }
0731 
0732         const auto row = std::distance(m_elements.begin(), it);
0733         beginRemoveRows({}, row, row);
0734         it = m_elements.erase(it);
0735         endRemoveRows();
0736     }
0737 }
0738 
0739 void TimelineModel::transferChanged(const Transfer& transfer)
0740 {
0741     if (transfer.state() == Transfer::UndefinedState) {
0742         return;
0743     }
0744     if (transfer.state() == Transfer::Discarded) {
0745         transferRemoved(transfer.reservationId(), transfer.alignment());
0746         return;
0747     }
0748 
0749     auto it = std::find_if(m_elements.begin(), m_elements.end(), [transfer](const auto &e) {
0750         return e.isReservation() && e.batchId() == transfer.reservationId();
0751     });
0752     if (it == m_elements.end()) {
0753         return;
0754     }
0755 
0756     if (transfer.alignment() == Transfer::Before) {
0757         if (it != m_elements.begin()) {
0758             --it;
0759         }
0760     }
0761     insertOrUpdate(it, TimelineElement(this, transfer));
0762 
0763     Q_EMIT todayRowChanged();
0764 }
0765 
0766 void TimelineModel::transferRemoved(const QString &resId, Transfer::Alignment alignment)
0767 {
0768     auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId, alignment](const auto &e) {
0769         return e.elementType == TimelineElement::Transfer && e.batchId() == resId
0770             && e.content().template value<Transfer>().alignment() == alignment;
0771     });
0772     if (it == m_elements.end()) {
0773         return;
0774     }
0775 
0776     const auto row = std::distance(m_elements.begin(), it);
0777     beginRemoveRows({}, row, row);
0778     m_elements.erase(it);
0779     endRemoveRows();
0780 
0781     Q_EMIT todayRowChanged();
0782 }
0783 
0784 static bool isSelectableElement(const TimelineElement &elem)
0785 {
0786     return elem.isReservation() && elem.rangeType == TimelineElement::SelfContained;
0787 }
0788 
0789 QString TimelineModel::currentBatchId() const
0790 {
0791     if (m_elements.empty()) {
0792         return {};
0793     }
0794 
0795     // find the next reservation
0796     auto it = std::lower_bound(m_elements.begin(), m_elements.end(), now());
0797     for (; it != m_elements.end() && !isSelectableElement(*it); ++it) {}
0798 
0799     QString nextResId;
0800     QDateTime nextStartTime;
0801     if (it != m_elements.end() && now().secsTo((*it).dt) < Constants::CurrentBatchLeadingMargin.count()) {
0802         nextResId = (*it).batchId();
0803         nextStartTime = (*it).dt;
0804     }
0805 
0806     // find the previous or current reservation
0807     if (it != m_elements.begin()) {
0808         --it;
0809         for (; it != m_elements.begin() && (it == m_elements.end() || !isSelectableElement(*it)); --it) {}
0810     }
0811 
0812     const auto resId = (*it).batchId();
0813     const auto res = m_resMgr->reservation(resId);
0814     auto endTime = SortUtil::endDateTime(res);
0815     if (endTime.secsTo(now()) > Constants::CurrentBatchTrailingMargin.count()) {
0816         endTime = {};
0817     }
0818 
0819     // only one side found
0820     if (!endTime.isValid()) {
0821         return nextResId;
0822     }
0823     if (!nextStartTime.isValid()) {
0824         return resId;
0825     }
0826 
0827     // (*it) is still active
0828     if (endTime >= now()) {
0829         return resId;
0830     }
0831 
0832     // take the one that is closer
0833     return endTime.secsTo(now()) < now().secsTo(nextStartTime) ? resId : nextResId;
0834 }
0835 
0836 void TimelineModel::scheduleCurrentBatchTimer()
0837 {
0838     if (m_elements.empty()) {
0839         return;
0840     }
0841 
0842     // we need the smallest valid time > now() of any of the following:
0843     // - end time of the current element + margin
0844     // - start time - margin of the next res
0845     // - end time of current + start time of next - endtime of current / 2
0846     // - end time of the next element (in case we are in the leading margin already)
0847 
0848     QDateTime triggerTime;
0849     const auto updateTriggerTime = [&triggerTime, this](const QDateTime &dt) {
0850         if (!dt.isValid() || dt <= now()) {
0851             return;
0852         }
0853         if (!triggerTime.isValid()) {
0854             triggerTime = dt;
0855         } else {
0856             triggerTime = std::min(triggerTime, dt);
0857         }
0858     };
0859 
0860     // find the next reservation
0861     auto it = std::lower_bound(m_elements.begin(), m_elements.end(), now());
0862     for (; it != m_elements.end() && !isSelectableElement(*it); ++it) {}
0863 
0864     QDateTime nextStartTime;
0865     if (it != m_elements.end()) {
0866         nextStartTime = (*it).dt;
0867         updateTriggerTime(nextStartTime.addSecs(-Constants::CurrentBatchLeadingMargin.count()));
0868         updateTriggerTime(SortUtil::endDateTime(m_resMgr->reservation((*it).batchId())));
0869     }
0870 
0871     // find the previous or current reservation
0872     if (it != m_elements.begin()) {
0873         --it;
0874         for (; it != m_elements.begin() && (it == m_elements.end() || !isSelectableElement(*it)); --it) {}
0875     }
0876 
0877     const auto res = m_resMgr->reservation((*it).batchId());
0878     auto endTime = SortUtil::endDateTime(res);
0879     updateTriggerTime(endTime.addSecs(Constants::CurrentBatchTrailingMargin.count()));
0880 
0881     if (nextStartTime.isValid() && endTime.isValid()) {
0882         updateTriggerTime(endTime.addSecs(endTime.secsTo(nextStartTime) / 2));
0883     }
0884 
0885     // QTimer only has 31bit for its msec interval, so don't schedule beyond a day
0886     // for longer distances we re-run this in the midnight timer above
0887     if (triggerTime.isValid() && triggerTime.date() == today()) {
0888         m_currentBatchTimer.setInterval(std::chrono::seconds(std::max<qint64>(60, now().secsTo(triggerTime))));
0889         m_currentBatchTimer.start();
0890     }
0891 }
0892 
0893 QVariant TimelineModel::locationAtTime(const QDateTime& dt) const
0894 {
0895     auto it = std::lower_bound(m_elements.begin(), m_elements.end(), dt);
0896     if (it == m_elements.begin()) {
0897         return {};
0898     }
0899 
0900     for (--it ;; --it) {
0901         // this is a still ongoing non-location change
0902         if (it != m_elements.end() && !(*it).isLocationChange() && (*it).endDateTime().isValid() && (*it).endDateTime() > dt) {
0903             if ((*it).isReservation()) {
0904                 auto loc = LocationUtil::location(m_resMgr->reservation((*it).batchId()));
0905                 if (LocationUtil::geo(loc).isValid() || !LocationUtil::address(loc).addressCountry().isEmpty()) {
0906                     return loc;
0907                 }
0908             }
0909         }
0910 
0911         if ((*it).isReservation() && (*it).isLocationChange()) {
0912             // TODO make this work for transfers too
0913             const auto res = m_resMgr->reservation((*it).batchId());
0914             return LocationUtil::arrivalLocation(res);
0915         }
0916 
0917         if (it == m_elements.begin()) {
0918             break;
0919         }
0920     }
0921 
0922     return {};
0923 }
0924 
0925 bool TimelineModel::isDateEmpty(const QDate &date) const
0926 {
0927     auto it = std::lower_bound(m_elements.begin(), m_elements.end(), date, [](const auto &lhs, auto rhs) {
0928         return lhs.dt.date() < rhs;
0929     });
0930     for (; it != m_elements.end(); ++it) {
0931         if ((*it).dt.date() == date && (*it).elementType != TimelineElement::TodayMarker) {
0932             return false;
0933         }
0934         if ((*it).dt.date() != date) {
0935             break;
0936         }
0937     }
0938     return true;
0939 }
0940 
0941 #include "moc_timelinemodel.cpp"