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"