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

0001 /*
0002     SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "statisticsmodel.h"
0008 #include "locationhelper.h"
0009 #include "localizer.h"
0010 #include "reservationmanager.h"
0011 #include "tripgroupmanager.h"
0012 
0013 #include <KItinerary/LocationUtil>
0014 #include <KItinerary/Place>
0015 #include <KItinerary/Reservation>
0016 #include <KItinerary/SortUtil>
0017 
0018 #include <KCountry>
0019 #include <KLocalizedString>
0020 
0021 #include <QDebug>
0022 
0023 #include <algorithm>
0024 
0025 using namespace KItinerary;
0026 
0027 StatisticsItem::StatisticsItem() = default;
0028 
0029 StatisticsItem::StatisticsItem(const QString &label, const QString &value, StatisticsItem::Trend trend, bool hasData)
0030     : m_label(label)
0031     , m_value(value)
0032     , m_trend(trend)
0033     , m_hasData(hasData)
0034 {
0035 }
0036 
0037 StatisticsItem::~StatisticsItem() = default;
0038 
0039 StatisticsModel::StatisticsModel(QObject *parent)
0040     : QObject(parent)
0041 {
0042     connect(this, &StatisticsModel::setupChanged, this, &StatisticsModel::recompute);
0043     recompute();
0044 }
0045 
0046 StatisticsModel::~StatisticsModel() = default;
0047 
0048 ReservationManager* StatisticsModel::reservationManager() const
0049 {
0050     return m_resMgr;
0051 }
0052 
0053 void StatisticsModel::setReservationManager(ReservationManager *resMgr)
0054 {
0055     if (m_resMgr == resMgr) {
0056         return;
0057     }
0058     m_resMgr = resMgr;
0059     connect(m_resMgr, &ReservationManager::batchAdded, this, &StatisticsModel::recompute);
0060     Q_EMIT setupChanged();
0061 }
0062 
0063 TripGroupManager* StatisticsModel::tripGroupManager() const
0064 {
0065     return m_tripGroupMgr;
0066 }
0067 
0068 void StatisticsModel::setTripGroupManager(TripGroupManager* tripGroupMgr)
0069 {
0070     if (m_tripGroupMgr == tripGroupMgr) {
0071         return;
0072     }
0073     m_tripGroupMgr = tripGroupMgr;
0074     connect(m_tripGroupMgr, &TripGroupManager::tripGroupAdded, this, &StatisticsModel::recompute);
0075     Q_EMIT setupChanged();
0076 }
0077 
0078 void StatisticsModel::setTimeRange(const QDate &begin, const QDate &end)
0079 {
0080     if (m_begin == begin && end == m_end) {
0081         return;
0082     }
0083 
0084     m_begin = begin;
0085     m_end = end;
0086     recompute();
0087 }
0088 
0089 StatisticsItem StatisticsModel::totalCount() const
0090 {
0091     return StatisticsItem(i18n("Trips"), QLocale().toString(m_tripGroupCount), trend(m_tripGroupCount, m_prevTripGroupCount));
0092 }
0093 
0094 StatisticsItem StatisticsModel::totalDistance() const
0095 {
0096     return StatisticsItem(i18n("Distance"), Localizer::formatDistance(m_statData[Total][Distance]), trend(Total, Distance));
0097 }
0098 
0099 StatisticsItem StatisticsModel::totalNights() const
0100 {
0101     return StatisticsItem(i18n("Hotel nights"), QLocale().toString(m_hotelCount), trend(m_hotelCount, m_prevHotelCount));
0102 }
0103 
0104 StatisticsItem StatisticsModel::totalCO2() const
0105 {
0106     return StatisticsItem(i18n("CO₂"), Localizer::formatWeight(m_statData[Total][CO2]), trend(Total, CO2));
0107 }
0108 
0109 StatisticsItem StatisticsModel::visitedCountries() const
0110 {
0111     QStringList l;
0112     l.reserve(m_countries.size());
0113     std::transform(m_countries.begin(), m_countries.end(), std::back_inserter(l), [](const auto &iso) {
0114         return iso;
0115     });
0116     std::sort(l.begin(), l.end(), [](const auto &lhs, const auto &rhs) {
0117         return KCountry::fromAlpha2(lhs).name().localeAwareCompare(KCountry::fromAlpha2(rhs).name()) < 0;
0118     });
0119     return StatisticsItem(i18n("Visited countries"), l.join(QLatin1Char(' ')), StatisticsItem::TrendUnknown);
0120 }
0121 
0122 StatisticsItem StatisticsModel::flightCount() const
0123 {
0124     return StatisticsItem(i18n("Flights"), QLocale().toString(m_statData[Flight][TripCount]), trend(Flight, TripCount), m_hasData[Flight]);
0125 }
0126 
0127 StatisticsItem StatisticsModel::flightDistance() const
0128 {
0129     return StatisticsItem(i18n("Distance"), Localizer::formatDistance(m_statData[Flight][Distance]), trend(Flight, Distance), m_hasData[Flight]);
0130 }
0131 
0132 StatisticsItem StatisticsModel::flightCO2() const
0133 {
0134     return StatisticsItem(i18n("CO₂"), Localizer::formatWeight(m_statData[Flight][CO2]), trend(Flight, CO2), m_hasData[Flight]);
0135 }
0136 
0137 StatisticsItem StatisticsModel::trainCount() const
0138 {
0139     return StatisticsItem(i18n("Train rides"), QLocale().toString(m_statData[Train][TripCount]), trend(Train, TripCount), m_hasData[Train]);
0140 }
0141 
0142 StatisticsItem StatisticsModel::trainDistance() const
0143 {
0144     return StatisticsItem(i18n("Distance"), Localizer::formatDistance(m_statData[Train][Distance]), trend(Train, Distance), m_hasData[Train]);
0145 }
0146 
0147 StatisticsItem StatisticsModel::trainCO2() const
0148 {
0149     return StatisticsItem(i18n("CO₂"), Localizer::formatWeight(m_statData[Train][CO2]), trend(Train, CO2), m_hasData[Train]);
0150 }
0151 
0152 StatisticsItem StatisticsModel::busCount() const
0153 {
0154     return StatisticsItem(i18n("Bus rides"), QLocale().toString(m_statData[Bus][TripCount]), trend(Bus, TripCount), m_hasData[Bus]);
0155 }
0156 
0157 StatisticsItem StatisticsModel::busDistance() const
0158 {
0159     return StatisticsItem(i18n("Distance"), Localizer::formatDistance(m_statData[Bus][Distance]), trend(Bus, Distance), m_hasData[Bus]);
0160 }
0161 
0162 StatisticsItem StatisticsModel::busCO2() const
0163 {
0164     return StatisticsItem(i18n("CO₂"), Localizer::formatWeight(m_statData[Bus][CO2]), trend(Bus, CO2), m_hasData[Bus]);
0165 }
0166 
0167 StatisticsItem StatisticsModel::carCount() const
0168 {
0169     return StatisticsItem(i18n("Car rides"), QLocale().toString(m_statData[Car][TripCount]), trend(Car, TripCount), m_hasData[Car]);
0170 }
0171 
0172 StatisticsItem StatisticsModel::carDistance() const
0173 {
0174     return StatisticsItem(i18n("Distance"), Localizer::formatDistance(m_statData[Car][Distance]), trend(Car, Distance), m_hasData[Car]);
0175 }
0176 
0177 StatisticsItem StatisticsModel::carCO2() const
0178 {
0179     return StatisticsItem(i18n("CO₂"), Localizer::formatWeight(m_statData[Car][CO2]), trend(Car, CO2), m_hasData[Car]);
0180 }
0181 
0182 StatisticsItem StatisticsModel::boatCount() const
0183 {
0184     return StatisticsItem(i18n("Boat trips"), QLocale().toString(m_statData[Boat][TripCount]), trend(Boat, TripCount), m_hasData[Boat]);
0185 }
0186 
0187 StatisticsItem StatisticsModel::boatDistance() const
0188 {
0189     return StatisticsItem(i18n("Distance"), Localizer::formatDistance(m_statData[Boat][Distance]), trend(Boat, Distance), m_hasData[Boat]);
0190 }
0191 
0192 StatisticsItem StatisticsModel::boatCO2() const
0193 {
0194     return StatisticsItem(i18n("CO₂"), Localizer::formatWeight(m_statData[Boat][CO2]), trend(Boat, CO2), m_hasData[Boat]);
0195 }
0196 
0197 StatisticsModel::AggregateType StatisticsModel::typeForReservation(const QVariant &res) const
0198 {
0199     if (JsonLd::isA<FlightReservation>(res)) {
0200         return Flight;
0201     } else if (JsonLd::isA<TrainReservation>(res)) {
0202         return Train;
0203     } else if (JsonLd::isA<BusReservation>(res)) {
0204         return Bus;
0205     } else if (JsonLd::isA<BoatReservation>(res)) {
0206         return Boat;
0207     }
0208     return Car;
0209 }
0210 
0211 static int distance(const QVariant &res)
0212 {
0213     const auto dep = LocationUtil::departureLocation(res);
0214     const auto arr = LocationUtil::arrivalLocation(res);
0215     if (dep.isNull() || arr.isNull()) {
0216         return 0;
0217     }
0218     const auto depGeo = LocationUtil::geo(dep);
0219     const auto arrGeo = LocationUtil::geo(arr);
0220     if (!depGeo.isValid() || !arrGeo.isValid()) {
0221         return 0;
0222     }
0223     return std::max(0, LocationUtil::distance(depGeo, arrGeo));
0224 }
0225 
0226 // from https://en.wikipedia.org/wiki/Environmental_impact_of_transport
0227 static const int emissionPerKm[] = {
0228     0,
0229     285, // flight
0230     14, // train
0231     68, // bus
0232     158, // car
0233     113, // ferry
0234 };
0235 
0236 int StatisticsModel::co2emission(StatisticsModel::AggregateType type, int distance) const
0237 {
0238     return distance * emissionPerKm[type];
0239 }
0240 
0241 void StatisticsModel::computeStats(const QVariant& res, int (&statData)[AGGREGATE_TYPE_COUNT][STAT_TYPE_COUNT])
0242 {
0243     const auto type = typeForReservation(res);
0244     const auto dist = distance(res);
0245     const auto co2 = co2emission(type, dist / 1000);
0246 
0247     statData[type][TripCount]++;
0248     statData[type][Distance] += dist;
0249     statData[type][CO2] += co2;
0250 
0251     statData[Total][TripCount]++;
0252     statData[Total][Distance] += dist;
0253     statData[Total][CO2] += co2;
0254 }
0255 
0256 void StatisticsModel::recompute()
0257 {
0258     memset(m_statData, 0, AGGREGATE_TYPE_COUNT * STAT_TYPE_COUNT * sizeof(int));
0259     memset(m_prevStatData, 0, AGGREGATE_TYPE_COUNT * STAT_TYPE_COUNT * sizeof(int));
0260     memset(m_hasData, 0, AGGREGATE_TYPE_COUNT * sizeof(bool));
0261     m_hasData[Total] = true;
0262     m_hotelCount = 0;
0263     m_prevHotelCount = 0;
0264     m_countries.clear();
0265 
0266     if (!m_resMgr || !m_tripGroupMgr) {
0267         return;
0268     }
0269 
0270     QDate prevStart;
0271     if (m_begin.isValid() && m_end.isValid()) {
0272         prevStart = m_begin.addDays(m_end.daysTo(m_begin));
0273     }
0274 
0275     QSet<QString> tripGroups, prevTripGroups;
0276 
0277     const auto &batches = m_resMgr->batches();
0278     for (const auto &batchId : batches) {
0279         const auto res = m_resMgr->reservation(batchId);
0280         if (LocationUtil::isLocationChange(res)) {
0281             m_hasData[typeForReservation(res)] = true;
0282         }
0283         const auto dt = SortUtil::startDateTime(res);
0284 
0285         bool isPrev = false;
0286         if (m_end.isValid() && dt.date() > m_end) {
0287             continue;
0288         }
0289         if (prevStart.isValid()) {
0290             if (dt.date() < prevStart) {
0291                 continue;
0292             }
0293             isPrev = dt.date() < m_begin;
0294         }
0295 
0296         // don't count canceled reservations
0297         if (JsonLd::canConvert<Reservation>(res) && JsonLd::convert<Reservation>(res).reservationStatus() == Reservation::ReservationCancelled) {
0298             continue;
0299         }
0300 
0301         if (LocationUtil::isLocationChange(res)) {
0302             computeStats(res, isPrev ? m_prevStatData : m_statData);
0303         } else if (JsonLd::isA<LodgingReservation>(res)) {
0304             const auto hotel = res.value<LodgingReservation>();
0305             if (isPrev) {
0306                 m_prevHotelCount += hotel.checkinTime().daysTo(hotel.checkoutTime());
0307             } else {
0308                 m_hotelCount += hotel.checkinTime().daysTo(hotel.checkoutTime());
0309             }
0310         }
0311 
0312         const auto tgId = m_tripGroupMgr->tripGroupIdForReservation(batchId);
0313         if (!tgId.isEmpty()) {
0314             isPrev ? prevTripGroups.insert(tgId) : tripGroups.insert(tgId);
0315         }
0316 
0317         if (!isPrev) {
0318             auto c = LocationHelper::departureCountry(res);
0319             if (!c.isEmpty()) m_countries.insert(c);
0320             c = LocationHelper::destinationCountry(res);
0321             if (!c.isEmpty()) m_countries.insert(c);
0322         }
0323     }
0324 
0325     m_tripGroupCount = tripGroups.size();
0326     m_prevTripGroupCount = prevTripGroups.size();
0327 
0328     Q_EMIT changed();
0329 }
0330 
0331 StatisticsItem::Trend StatisticsModel::trend(int current, int prev) const
0332 {
0333     if (!m_begin.isValid() || !m_end.isValid()) {
0334         return StatisticsItem::TrendUnknown;
0335     }
0336 
0337     return current < prev ? StatisticsItem::TrendDown : current > prev ? StatisticsItem::TrendUp : StatisticsItem::TrendUnchanged;
0338 }
0339 
0340 StatisticsItem::Trend StatisticsModel::trend(StatisticsModel::AggregateType type, StatisticsModel::StatType stat) const
0341 {
0342     return trend(m_statData[type][stat], m_prevStatData[type][stat]);
0343 }
0344 
0345 #include "moc_statisticsmodel.cpp"