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