1 /*
2     SPDX-FileCopyrightText: 2019 Volker Krause <vkrause@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "statisticsmodel.h"
8 #include "locationhelper.h"
9 #include "localizer.h"
10 #include "reservationmanager.h"
11 #include "tripgroupmanager.h"
12 
13 #include <KItinerary/LocationUtil>
14 #include <KItinerary/Place>
15 #include <KItinerary/Reservation>
16 #include <KItinerary/SortUtil>
17 
18 #include <KLocalizedString>
19 
20 #include <QDebug>
21 
22 using namespace KItinerary;
23 
24 StatisticsItem::StatisticsItem() = default;
25 
StatisticsItem(const QString & label,const QString & value,StatisticsItem::Trend trend)26 StatisticsItem::StatisticsItem(const QString &label, const QString &value, StatisticsItem::Trend trend)
27     : m_label(label)
28     , m_value(value)
29     , m_trend(trend)
30 {
31 }
32 
33 StatisticsItem::~StatisticsItem() = default;
34 
StatisticsModel(QObject * parent)35 StatisticsModel::StatisticsModel(QObject *parent)
36     : QObject(parent)
37 {
38     connect(this, &StatisticsModel::setupChanged, this, &StatisticsModel::recompute);
39     recompute();
40 }
41 
42 StatisticsModel::~StatisticsModel() = default;
43 
reservationManager() const44 ReservationManager* StatisticsModel::reservationManager() const
45 {
46     return m_resMgr;
47 }
48 
setReservationManager(ReservationManager * resMgr)49 void StatisticsModel::setReservationManager(ReservationManager *resMgr)
50 {
51     if (m_resMgr == resMgr) {
52         return;
53     }
54     m_resMgr = resMgr;
55     connect(m_resMgr, &ReservationManager::batchAdded, this, &StatisticsModel::recompute);
56     Q_EMIT setupChanged();
57 }
58 
tripGroupManager() const59 TripGroupManager* StatisticsModel::tripGroupManager() const
60 {
61     return m_tripGroupMgr;
62 }
63 
setTripGroupManager(TripGroupManager * tripGroupMgr)64 void StatisticsModel::setTripGroupManager(TripGroupManager* tripGroupMgr)
65 {
66     if (m_tripGroupMgr == tripGroupMgr) {
67         return;
68     }
69     m_tripGroupMgr = tripGroupMgr;
70     connect(m_tripGroupMgr, &TripGroupManager::tripGroupAdded, this, &StatisticsModel::recompute);
71     Q_EMIT setupChanged();
72 }
73 
setTimeRange(const QDate & begin,const QDate & end)74 void StatisticsModel::setTimeRange(const QDate &begin, const QDate &end)
75 {
76     if (m_begin == begin && end == m_end) {
77         return;
78     }
79 
80     m_begin = begin;
81     m_end = end;
82     recompute();
83 }
84 
formatCo2(int amount)85 static QString formatCo2(int amount)
86 {
87     if (amount >= 10000) {
88         // no decimals for large values
89         return i18n("%1 kg", amount / 1000);
90     }
91     return ki18n("%1 kg").subs(amount / 1000.0, 0, 'g', 2).toString();
92 }
93 
totalCount() const94 StatisticsItem StatisticsModel::totalCount() const
95 {
96     return StatisticsItem(i18n("Trips"), QLocale().toString(m_tripGroupCount), trend(m_tripGroupCount, m_prevTripGroupCount));
97 }
98 
totalDistance() const99 StatisticsItem StatisticsModel::totalDistance() const
100 {
101     return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Total][Distance] / 1000), trend(Total, Distance));
102 }
103 
totalNights() const104 StatisticsItem StatisticsModel::totalNights() const
105 {
106     return StatisticsItem(i18n("Hotel nights"), QLocale().toString(m_hotelCount), trend(m_hotelCount, m_prevHotelCount));
107 }
108 
totalCO2() const109 StatisticsItem StatisticsModel::totalCO2() const
110 {
111     return StatisticsItem(i18n("CO₂"), formatCo2(m_statData[Total][CO2]), trend(Total, CO2));
112 }
113 
visitedCountries() const114 StatisticsItem StatisticsModel::visitedCountries() const
115 {
116     QStringList l;
117     l.reserve(m_countries.size());
118     std::transform(m_countries.begin(), m_countries.end(), std::back_inserter(l), [](const auto &iso) {
119         return iso;
120     });
121     return StatisticsItem(i18n("Visited countries"), l.join(QLatin1Char(' ')), StatisticsItem::TrendUnknown);
122 }
123 
flightCount() const124 StatisticsItem StatisticsModel::flightCount() const
125 {
126     return StatisticsItem(i18n("Flights"), QLocale().toString(m_statData[Flight][TripCount]), trend(Flight, TripCount));
127 }
128 
flightDistance() const129 StatisticsItem StatisticsModel::flightDistance() const
130 {
131     return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Flight][Distance] / 1000), trend(Flight, Distance));
132 }
133 
flightCO2() const134 StatisticsItem StatisticsModel::flightCO2() const
135 {
136     return StatisticsItem(i18n("CO₂"), formatCo2(m_statData[Flight][CO2]), trend(Flight, CO2));
137 }
138 
trainCount() const139 StatisticsItem StatisticsModel::trainCount() const
140 {
141     return StatisticsItem(i18n("Train rides"), QLocale().toString(m_statData[Train][TripCount]), trend(Train, TripCount));
142 }
143 
trainDistance() const144 StatisticsItem StatisticsModel::trainDistance() const
145 {
146     return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Train][Distance] / 1000), trend(Train, Distance));
147 }
148 
trainCO2() const149 StatisticsItem StatisticsModel::trainCO2() const
150 {
151     return StatisticsItem(i18n("CO₂"), formatCo2(m_statData[Train][CO2]), trend(Train, CO2));
152 }
153 
busCount() const154 StatisticsItem StatisticsModel::busCount() const
155 {
156     return StatisticsItem(i18n("Bus rides"), QLocale().toString(m_statData[Bus][TripCount]), trend(Bus, TripCount));
157 }
158 
busDistance() const159 StatisticsItem StatisticsModel::busDistance() const
160 {
161     return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Bus][Distance] / 1000), trend(Bus, Distance));
162 }
163 
busCO2() const164 StatisticsItem StatisticsModel::busCO2() const
165 {
166     return StatisticsItem(i18n("CO₂"), formatCo2(m_statData[Bus][CO2]), trend(Bus, CO2));
167 }
168 
carCount() const169 StatisticsItem StatisticsModel::carCount() const
170 {
171     return StatisticsItem(i18n("Car rides"), QLocale().toString(m_statData[Car][TripCount]), trend(Car, TripCount));
172 }
173 
carDistance() const174 StatisticsItem StatisticsModel::carDistance() const
175 {
176     return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Car][Distance] / 1000), trend(Car, Distance));
177 }
178 
carCO2() const179 StatisticsItem StatisticsModel::carCO2() const
180 {
181     return StatisticsItem(i18n("CO₂"), formatCo2(m_statData[Car][CO2]), trend(Car, CO2));
182 }
183 
typeForReservation(const QVariant & res) const184 StatisticsModel::AggregateType StatisticsModel::typeForReservation(const QVariant &res) const
185 {
186     if (JsonLd::isA<FlightReservation>(res)) {
187         return Flight;
188     } else if (JsonLd::isA<TrainReservation>(res)) {
189         return Train;
190     } else if (JsonLd::isA<BusReservation>(res)) {
191         return Bus;
192     }
193     return Car;
194 }
195 
distance(const QVariant & res)196 static int distance(const QVariant &res)
197 {
198     const auto dep = LocationUtil::departureLocation(res);
199     const auto arr = LocationUtil::arrivalLocation(res);
200     if (dep.isNull() || arr.isNull()) {
201         return 0;
202     }
203     const auto depGeo = LocationUtil::geo(dep);
204     const auto arrGeo = LocationUtil::geo(arr);
205     if (!depGeo.isValid() || !arrGeo.isValid()) {
206         return 0;
207     }
208     return std::max(0, LocationUtil::distance(depGeo, arrGeo));
209 }
210 
211 // from https://en.wikipedia.org/wiki/Environmental_impact_of_transport
212 static const int emissionPerKm[] = {
213     0,
214     285, // flight
215     14, // train
216     68, // bus
217     158, // car
218 };
219 
co2emission(StatisticsModel::AggregateType type,int distance) const220 int StatisticsModel::co2emission(StatisticsModel::AggregateType type, int distance) const
221 {
222     return distance * emissionPerKm[type];
223 }
224 
computeStats(const QVariant & res,int (& statData)[AGGREGATE_TYPE_COUNT][STAT_TYPE_COUNT])225 void StatisticsModel::computeStats(const QVariant& res, int (&statData)[AGGREGATE_TYPE_COUNT][STAT_TYPE_COUNT])
226 {
227     const auto type = typeForReservation(res);
228     const auto dist = distance(res);
229     const auto co2 = co2emission(type, dist / 1000);
230 
231     statData[type][TripCount]++;
232     statData[type][Distance] += dist;
233     statData[type][CO2] += co2;
234 
235     statData[Total][TripCount]++;
236     statData[Total][Distance] += dist;
237     statData[Total][CO2] += co2;
238 }
239 
recompute()240 void StatisticsModel::recompute()
241 {
242     memset(m_statData, 0, AGGREGATE_TYPE_COUNT * STAT_TYPE_COUNT * sizeof(int));
243     memset(m_prevStatData, 0, AGGREGATE_TYPE_COUNT * STAT_TYPE_COUNT * sizeof(int));
244     m_hotelCount = 0;
245     m_prevHotelCount = 0;
246     m_countries.clear();
247 
248     if (!m_resMgr || !m_tripGroupMgr) {
249         return;
250     }
251 
252     QDate prevStart;
253     if (m_begin.isValid() && m_end.isValid()) {
254         prevStart = m_begin.addDays(m_end.daysTo(m_begin));
255     }
256 
257     QSet<QString> tripGroups, prevTripGroups;
258 
259     const auto &batches = m_resMgr->batches();
260     for (const auto &batchId : batches) {
261         const auto res = m_resMgr->reservation(batchId);
262         const auto dt = SortUtil::startDateTime(res);
263 
264         bool isPrev = false;
265         if (m_end.isValid() && dt.date() > m_end) {
266             continue;
267         }
268         if (prevStart.isValid()) {
269             if (dt.date() < prevStart) {
270                 continue;
271             }
272             isPrev = dt.date() < m_begin;
273         }
274 
275         // don't count canceled reservations
276         if (JsonLd::canConvert<Reservation>(res) && JsonLd::convert<Reservation>(res).reservationStatus() == Reservation::ReservationCancelled) {
277             continue;
278         }
279 
280         if (LocationUtil::isLocationChange(res)) {
281             computeStats(res, isPrev ? m_prevStatData : m_statData);
282         } else if (JsonLd::isA<LodgingReservation>(res)) {
283             const auto hotel = res.value<LodgingReservation>();
284             if (isPrev) {
285                 m_prevHotelCount += hotel.checkinTime().daysTo(hotel.checkoutTime());
286             } else {
287                 m_hotelCount += hotel.checkinTime().daysTo(hotel.checkoutTime());
288             }
289         }
290 
291         const auto tgId = m_tripGroupMgr->tripGroupIdForReservation(batchId);
292         if (!tgId.isEmpty()) {
293             isPrev ? prevTripGroups.insert(tgId) : tripGroups.insert(tgId);
294         }
295 
296         if (!isPrev) {
297             auto c = LocationHelper::departureCountry(res);
298             if (!c.isEmpty()) m_countries.insert(c);
299             c = LocationHelper::destinationCountry(res);
300             if (!c.isEmpty()) m_countries.insert(c);
301         }
302     }
303 
304     m_tripGroupCount = tripGroups.size();
305     m_prevTripGroupCount = prevTripGroups.size();
306 
307     Q_EMIT changed();
308 }
309 
trend(int current,int prev) const310 StatisticsItem::Trend StatisticsModel::trend(int current, int prev) const
311 {
312     if (!m_begin.isValid() || !m_end.isValid()) {
313         return StatisticsItem::TrendUnknown;
314     }
315 
316     return current < prev ? StatisticsItem::TrendDown : current > prev ? StatisticsItem::TrendUp : StatisticsItem::TrendUnchanged;
317 }
318 
trend(StatisticsModel::AggregateType type,StatisticsModel::StatType stat) const319 StatisticsItem::Trend StatisticsModel::trend(StatisticsModel::AggregateType type, StatisticsModel::StatType stat) const
320 {
321     return trend(m_statData[type][stat], m_prevStatData[type][stat]);
322 }
323