diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index a202895..efa57c0 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,17 +1,18 @@ add_definitions(-DSOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") ecm_add_test(jnisignaturetest.cpp LINK_LIBRARIES Qt5::Test) ecm_add_test(pkpassmanagertest.cpp LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(reservationmanagertest.cpp LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(applicationcontrollertest.cpp LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(tripgrouptest.cpp LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(timelinemodeltest.cpp modelverificationpoint.cpp TEST_NAME timelinemodeltest LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(tripgroupproxytest.cpp modelverificationpoint.cpp TEST_NAME tripgroupproxytest LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(tripgroupinfoprovidertest.cpp TEST_NAME tripgroupinfoprovidertest LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(publictransporttest.cpp TEST_NAME publictransporttest LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(timelinedelegatecontrollertest.cpp TEST_NAME timelinedelegatecontrollertest LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(documentmanagertest.cpp TEST_NAME documentmanagertest LINK_LIBRARIES Qt5::Test itinerary) +ecm_add_test(statisticstest.cpp LINK_LIBRARIES Qt5::Test itinerary) ecm_add_test(weathertest.cpp LINK_LIBRARIES Qt5::Test itinerary-weather) target_include_directories(weathertest PRIVATE ${CMAKE_BINARY_DIR}) diff --git a/autotests/statisticstest.cpp b/autotests/statisticstest.cpp new file mode 100644 index 0000000..9f00276 --- /dev/null +++ b/autotests/statisticstest.cpp @@ -0,0 +1,169 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include +#include +#include +#include +#include + +class StatisticsTest : public QObject +{ + Q_OBJECT +private: + void clearReservations(ReservationManager *mgr) + { + const auto batches = mgr->batches(); // copy, as this is getting modified in the process + for (const auto &id : batches) { + mgr->removeBatch(id); + } + QCOMPARE(mgr->batches().size(), 0); + } + + QByteArray readFile(const QString &fn) + { + QFile f(fn); + f.open(QFile::ReadOnly); + return f.readAll(); + } + +private Q_SLOTS: + void initTestCase() + { + qputenv("TZ", "UTC"); + qunsetenv("LANG"); + qunsetenv("LANGUAGE"); + qunsetenv("LC_CTYPE"); + QStandardPaths::setTestModeEnabled(true); + } + + void testStats() + { + ReservationManager resMgr; + clearReservations(&resMgr); + + StatisticsModel stats; + QSignalSpy changeSpy(&stats, &StatisticsModel::changed); + stats.setReservationManager(&resMgr); + + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/randa2017.json"))); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/akademy2017.json"))); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/akademy2018-program.json"))); + + stats.setTimeRange({}, {}); + QVERIFY(!changeSpy.isEmpty()); + auto item = stats.totalCount(); + QCOMPARE(item.m_value, QLatin1String("12")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + item = stats.totalNights(); + QCOMPARE(item.m_value, QLatin1String("13")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + item = stats.totalDistance(); + QCOMPARE(item.m_value, QLatin1String("6,182 km")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + item = stats.totalCO2(); + QCOMPARE(item.m_value, QLatin1String("1,673.44 kg")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + + item = stats.flightCount(); + QCOMPARE(item.m_value, QLatin1String("6")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + item = stats.flightDistance(); + QCOMPARE(item.m_value, QLatin1String("5,859 km")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + item = stats.flightCO2(); + QCOMPARE(item.m_value, QLatin1String("1,668.96 kg")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + + item = stats.trainCount(); + QCOMPARE(item.m_value, QLatin1String("4")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + item = stats.trainDistance(); + QCOMPARE(item.m_value, QLatin1String("323 km")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + item = stats.trainCO2(); + QCOMPARE(item.m_value, QLatin1String("4.48 kg")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUnknown); + + changeSpy.clear(); + stats.setTimeRange({2017, 9, 1}, {2018, 1, 1}); + QVERIFY(!changeSpy.isEmpty()); + item = stats.totalCount(); + QCOMPARE(item.m_value, QLatin1String("7")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUp); + item = stats.totalNights(); + QCOMPARE(item.m_value, QLatin1String("5")); + QCOMPARE(item.m_trend, StatisticsItem::TrendDown); + item = stats.totalDistance(); + QCOMPARE(item.m_value, QLatin1String("1,642 km")); + QCOMPARE(item.m_trend, StatisticsItem::TrendDown); + item = stats.totalCO2(); + QCOMPARE(item.m_value, QLatin1String("380.11 kg")); + QCOMPARE(item.m_trend, StatisticsItem::TrendDown); + + item = stats.flightCount(); + QCOMPARE(item.m_value, QLatin1String("2")); + QCOMPARE(item.m_trend, StatisticsItem::TrendDown); + item = stats.flightDistance(); + QCOMPARE(item.m_value, QLatin1String("1,319 km")); + QCOMPARE(item.m_trend, StatisticsItem::TrendDown); + item = stats.flightCO2(); + QCOMPARE(item.m_value, QLatin1String("375.63 kg")); + QCOMPARE(item.m_trend, StatisticsItem::TrendDown); + + item = stats.trainCount(); + QCOMPARE(item.m_value, QLatin1String("4")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUp); + item = stats.trainDistance(); + QCOMPARE(item.m_value, QLatin1String("323 km")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUp); + item = stats.trainCO2(); + QCOMPARE(item.m_value, QLatin1String("4.48 kg")); + QCOMPARE(item.m_trend, StatisticsItem::TrendUp); + } + + void testTimeRangeModel() + { + ReservationManager resMgr; + clearReservations(&resMgr); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/randa2017.json"))); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/akademy2017.json"))); + resMgr.importReservation(readFile(QLatin1String(SOURCE_DIR "/../tests/akademy2018-program.json"))); + + StatisticsTimeRangeModel model; + QAbstractItemModelTester tester(&model); + QCOMPARE(model.rowCount(), 1); + + model.setReservationManager(&resMgr); + QCOMPARE(model.rowCount(), 3); + QCOMPARE(model.data(model.index(1, 0), Qt::DisplayRole).toString(), QLatin1String("2018")); + QCOMPARE(model.data(model.index(2, 0), Qt::DisplayRole).toString(), QLatin1String("2017")); + + QCOMPARE(model.data(model.index(1, 0), StatisticsTimeRangeModel::BeginRole).toDate(), QDate(2018, 1, 1)); + QCOMPARE(model.data(model.index(1, 0), StatisticsTimeRangeModel::EndRole).toDate(), QDate(2018, 12, 31)); + QCOMPARE(model.data(model.index(2, 0), StatisticsTimeRangeModel::BeginRole).toDate(), QDate()); + QCOMPARE(model.data(model.index(2, 0), StatisticsTimeRangeModel::EndRole).toDate(), QDate(2017, 12, 31)); + } +}; + +QTEST_GUILESS_MAIN(StatisticsTest) + +#include "statisticstest.moc" diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 1c9a26e..6c16704 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,166 +1,169 @@ if (TARGET KF5::Notifications) SET(HAVE_NOTIFICATIONS TRUE) endif() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-itinerary.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-itinerary.h) set(itinerary_srcs applicationcontroller.cpp countryinformation.cpp documentmanager.cpp livedatamanager.cpp navigationcontroller.cpp pkpassmanager.cpp pkpassimageprovider.cpp publictransport.cpp reservationmanager.cpp + statisticsmodel.cpp + statisticstimerangemodel.cpp timelinedelegatecontroller.cpp timelinemodel.cpp tripgroup.cpp tripgroupinfoprovider.cpp tripgroupmanager.cpp tripgroupproxymodel.cpp util.cpp ) ecm_qt_declare_logging_category(itinerary_srcs HEADER logging.h IDENTIFIER Log CATEGORY_NAME org.kde.itinerary ) add_library(itinerary STATIC ${itinerary_srcs}) target_link_libraries(itinerary PUBLIC itinerary-weather KPublicTransport KPim::Itinerary KPim::PkPass KF5::I18n KF5::CoreAddons Qt5::Network Qt5::Quick ) if (TARGET KF5::Notifications) target_link_libraries(itinerary PUBLIC KF5::Notifications) endif() if (Qt5QuickCompiler_FOUND) qtquick_compiler_add_resources(qml_srcs qml.qrc) else () set(qml_srcs qml.qrc) endif() set(itinerary_app_srcs main.cpp countrymodel.cpp documentsmodel.cpp localizer.cpp settings.cpp tickettokenmodel.cpp weatherforecastmodel.cpp ${qml_srcs} brightnessmanager.cpp lockmanager.cpp ) if (ANDROID) list(APPEND itinerary_app_srcs androidbrightnessbackend.cpp androidlockbackend.cpp ) else() list(APPEND itinerary_app_srcs solidbrightnessbackend.cpp solidlockbackend.cpp ) qt5_add_dbus_interface(itinerary_app_srcs org.kde.Solid.PowerManagement.Actions.BrightnessControl.xml brightnesscontroldbusinterface) qt5_add_dbus_interface(itinerary_app_srcs org.freedesktop.ScreenSaver.xml screensaverdbusinterface) endif() add_executable(itinerary-app ${itinerary_app_srcs}) target_include_directories(itinerary-app PRIVATE ${CMAKE_BINARY_DIR}) target_link_libraries(itinerary-app PRIVATE itinerary KF5::Contacts ) if (ANDROID) target_include_directories(itinerary-app PRIVATE ${Qt5Core_PRIVATE_INCLUDE_DIRS}) # explicitly add runtime dependencies and transitive link dependencies, # so androiddeployqt picks them up target_link_libraries(itinerary PUBLIC Qt5::AndroidExtras KAndroidExtras) target_link_libraries(itinerary-app PRIVATE KF5::Archive KF5::Kirigami2 Qt5::Svg KF5::Prison OpenSSL::SSL ) kirigami_package_breeze_icons(ICONS application-pdf channel-insecure-symbolic channel-secure-symbolic checkmark clock dialog-cancel dialog-close document-edit document-open document-save documentinfo edit-delete edit-download edit-paste export-symbolic folder-documents-symbolic go-down-symbolic go-home go-next-symbolic go-up-symbolic help-about list-add map-symbolic meeting-attending question settings-configure view-calendar-day view-refresh + view-statistics weather-clear weather-clear-wind weather-clear-night weather-clear-wind-night weather-few-clouds weather-few-clouds-wind weather-few-clouds-night weather-few-clouds-wind-night weather-clouds weather-clouds-wind weather-clouds-night weather-clouds-wind-night weather-showers-day weather-showers-night weather-showers-scattered-day weather-showers-scattered-night weather-snow-scattered-day weather-snow-scattered-night weather-storm-day weather-storm-night weather-many-clouds -# weather-many-clouds-wind + weather-many-clouds-wind weather-fog weather-showers weather-showers-scattered weather-hail weather-snow weather-snow-scattered weather-storm ) else () target_link_libraries(itinerary PRIVATE Qt5::Positioning Qt5::DBus) target_link_libraries(itinerary-app PRIVATE KF5::DBusAddons Qt5::Widgets ) set_target_properties(itinerary-app PROPERTIES OUTPUT_NAME "itinerary") endif() install(TARGETS itinerary-app ${INSTALL_TARGETS_DEFAULT_ARGS}) install(PROGRAMS org.kde.itinerary.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES org.kde.itinerary.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) diff --git a/src/app/StatisticsDelegate.qml b/src/app/StatisticsDelegate.qml new file mode 100644 index 0000000..5c453b1 --- /dev/null +++ b/src/app/StatisticsDelegate.qml @@ -0,0 +1,42 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import QtQuick 2.5 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.1 as QQC2 +import org.kde.kirigami 2.4 as Kirigami +import org.kde.itinerary 1.0 +import "." as App + +RowLayout { + id: root + property var statItem + + Kirigami.FormData.label: root.statItem.label + + Kirigami.Icon { + source: root.statItem.trend == StatisticsItem.TrendUp ? "go-up" : root.statItem.trend == StatisticsItem.TrendDown ? "go-down" : "go-next" + color: root.statItem.trend == StatisticsItem.TrendUp ? Kirigami.Theme.negativeTextColor : root.statItem.trend == StatisticsItem.TrendDown ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.textColor + width: height + height: Kirigami.Units.gridUnit + visible: root.statItem.trend != StatisticsItem.TrendUnknown + } + + QQC2.Label { + text: root.statItem.value + } +} diff --git a/src/app/StatisticsPage.qml b/src/app/StatisticsPage.qml new file mode 100644 index 0000000..2c0c266 --- /dev/null +++ b/src/app/StatisticsPage.qml @@ -0,0 +1,92 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +import QtQuick 2.5 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.1 as QQC2 +import org.kde.kirigami 2.4 as Kirigami +import org.kde.itinerary 1.0 +import "." as App + +Kirigami.ScrollablePage { + id: root + title: i18n("Statistics") + + StatisticsModel { + id: model + reservationManager: _reservationManager + } + + StatisticsTimeRangeModel { + id: timeRangeModel + reservationManager: _reservationManager + } + + Kirigami.FormLayout { + + QQC2.ComboBox { + Kirigami.FormData.isSection: true + model: timeRangeModel + textRole: "display" + onActivated: { + var range = delegateModel.items.get(currentIndex) + model.setTimeRange(range.model.begin, range.model.end); + } + } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: i18n("Total") + } + StatisticsDelegate { statItem: model.totalCount } + StatisticsDelegate { statItem: model.totalDistance } + StatisticsDelegate { statItem: model.totalNights } + StatisticsDelegate { statItem: model.totalCO2 } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: i18n("Flights") + } + StatisticsDelegate { statItem: model.flightCount } + StatisticsDelegate { statItem: model.flightDistance } + StatisticsDelegate { statItem: model.flightCO2 } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: i18n("Trains") + } + StatisticsDelegate { statItem: model.trainCount } + StatisticsDelegate { statItem: model.trainDistance } + StatisticsDelegate { statItem: model.trainCO2 } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: i18n("Bus") + } + StatisticsDelegate { statItem: model.busCount } + StatisticsDelegate { statItem: model.busDistance } + StatisticsDelegate { statItem: model.busCO2 } + + Kirigami.Separator { + Kirigami.FormData.isSection: true + Kirigami.FormData.label: i18n("Car") + } + StatisticsDelegate { statItem: model.carCount } + StatisticsDelegate { statItem: model.carDistance } + StatisticsDelegate { statItem: model.carCO2 } + } +} diff --git a/src/app/main.cpp b/src/app/main.cpp index 737c21e..65d0de7 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,240 +1,245 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "itinerary_version.h" #include "logging.h" #include "applicationcontroller.h" #include "brightnessmanager.h" #include "countryinformation.h" #include "countrymodel.h" #include "documentmanager.h" #include "documentsmodel.h" #include "livedatamanager.h" #include "localizer.h" #include "lockmanager.h" #include "navigationcontroller.h" #include "pkpassmanager.h" #include "timelinemodel.h" #include "pkpassimageprovider.h" #include "publictransport.h" #include "reservationmanager.h" #include "settings.h" +#include "statisticsmodel.h" +#include "statisticstimerangemodel.h" #include "tickettokenmodel.h" +#include "timelinedelegatecontroller.h" #include "tripgroupinfoprovider.h" #include "tripgroupmanager.h" #include "tripgroupproxymodel.h" #include "util.h" -#include "timelinedelegatecontroller.h" #include "weatherforecastmodel.h" #include #include #include #include #include #include #ifndef Q_OS_ANDROID #include #endif #include #include #include #include #ifdef Q_OS_ANDROID #include #include #include #include #else #include #endif #include #include #include #include #include #include void handleViewIntent(ApplicationController *appController) { #ifdef Q_OS_ANDROID // handle opened files using namespace KAndroidExtras; appController->importFromUrl(Activity::getIntent().getData()); #else Q_UNUSED(appController); #endif } void handlePositionalArguments(ApplicationController *appController, const QStringList &args) { for (const auto &file : args) { const auto localUrl = QUrl::fromLocalFile(file); if (QFile::exists(localUrl.toLocalFile())) appController->importFromUrl(localUrl); else appController->importFromUrl(QUrl::fromUserInput(file)); } } #ifdef Q_OS_ANDROID Q_DECL_EXPORT #endif int main(int argc, char **argv) { QCoreApplication::setApplicationName(QStringLiteral("itinerary")); QCoreApplication::setOrganizationName(QStringLiteral("KDE")); QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); QCoreApplication::setApplicationVersion(QStringLiteral(ITINERARY_VERSION_STRING)); QGuiApplication::setApplicationDisplayName(i18n("KDE Itinerary")); QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #ifdef Q_OS_ANDROID QGuiApplication app(argc, argv); #else QApplication app(argc, argv); // for native file dialogs #endif QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("map-globe"))); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument(QStringLiteral("file"), i18n("PkPass or JSON-LD file to import.")); parser.process(app); #ifndef Q_OS_ANDROID KDBusService service(KDBusService::Unique); #endif Settings settings; PkPassManager passMgr; ReservationManager resMgr; DocumentManager docMgr; resMgr.setPkPassManager(&passMgr); TripGroupManager tripGroupMgr; tripGroupMgr.setReservationManager(&resMgr); ApplicationController appController; appController.setReservationManager(&resMgr); appController.setPkPassManager(&passMgr); appController.setDocumentManager(&docMgr); BrightnessManager brightnessManager; LockManager lockManager; LiveDataManager liveDataMgr; liveDataMgr.setPkPassManager(&passMgr); liveDataMgr.setReservationManager(&resMgr); liveDataMgr.setPollingEnabled(settings.queryLiveData()); QObject::connect(&settings, &Settings::queryLiveDataChanged, &liveDataMgr, &LiveDataManager::setPollingEnabled); #ifndef Q_OS_ANDROID QObject::connect(&service, &KDBusService::activateRequested, [&parser, &appController](const QStringList &args, const QString &workingDir) { qCDebug(Log) << "remote activation" << args << workingDir; if (!args.isEmpty()) { QDir::setCurrent(workingDir); parser.parse(args); handlePositionalArguments(&appController, parser.positionalArguments()); } if (!QGuiApplication::allWindows().isEmpty()) { QGuiApplication::allWindows().at(0)->requestActivate(); } }); #endif TimelineModel timelineModel; timelineModel.setHomeCountryIsoCode(settings.homeCountryIsoCode()); timelineModel.setReservationManager(&resMgr); QObject::connect(&settings, &Settings::homeCountryIsoCodeChanged, &timelineModel, &TimelineModel::setHomeCountryIsoCode); WeatherForecastManager weatherForecastMgr; weatherForecastMgr.setAllowNetworkAccess(settings.weatherForecastEnabled()); QObject::connect(&settings, &Settings::weatherForecastEnabledChanged, &weatherForecastMgr, &WeatherForecastManager::setAllowNetworkAccess); timelineModel.setWeatherForecastManager(&weatherForecastMgr); timelineModel.setTripGroupManager(&tripGroupMgr); TripGroupProxyModel tripGroupProxy; tripGroupProxy.setSourceModel(&timelineModel); TripGroupInfoProvider tripGroupInfoProvider; tripGroupInfoProvider.setReservationManager(&resMgr); tripGroupInfoProvider.setWeatherForecastManager(&weatherForecastMgr); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Barcode", {}); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Field", {}); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Pass", {}); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "BoardingPass", {}); qRegisterMetaType(); qmlRegisterUncreatableType("org.kde.kitinerary", 1, 0, "Ticket", {}); qmlRegisterUncreatableMetaObject(KItinerary::KnowledgeDb::staticMetaObject, "org.kde.kitinerary", 1, 0, "KnowledgeDb", {}); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "CountryInformation", {}); qmlRegisterType("org.kde.itinerary", 1, 0, "CountryModel"); qmlRegisterType("org.kde.itinerary", 1, 0, "DocumentsModel"); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "Localizer", [](QQmlEngine*, QJSEngine *engine) -> QJSValue { return engine->toScriptValue(Localizer()); }); qmlRegisterType("org.kde.itinerary", 1, 0, "TicketTokenModel"); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "TimelineModel", {}); qmlRegisterType("org.kde.itinerary", 1, 0, "TimelineDelegateController"); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "Util", [](QQmlEngine*, QJSEngine*) -> QObject*{ return new Util; }); qmlRegisterType("org.kde.itinerary", 1, 0, "WeatherForecastModel"); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "PublicTransport", [](QQmlEngine*, QJSEngine *engine) -> QJSValue { return engine->toScriptValue(PublicTransport()); }); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "ApplicationController", [](QQmlEngine *engine, QJSEngine*) -> QObject* { engine->setObjectOwnership(ApplicationController::instance(), QQmlEngine::CppOwnership); return ApplicationController::instance(); }); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "NavigationController", [](QQmlEngine*, QJSEngine *engine) -> QJSValue { return engine->toScriptValue(NavigationController()); }); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "DocumentManager", [](QQmlEngine *engine, QJSEngine*) -> QObject* { engine->setObjectOwnership(DocumentManager::instance(), QQmlEngine::CppOwnership); return DocumentManager::instance(); }); + qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "StatisticsItem", {}); + qmlRegisterType("org.kde.itinerary", 1, 0, "StatisticsModel"); + qmlRegisterType("org.kde.itinerary", 1, 0, "StatisticsTimeRangeModel"); QQmlApplicationEngine engine; engine.addImageProvider(QStringLiteral("org.kde.pkpass"), new PkPassImageProvider(&passMgr)); engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); engine.rootContext()->setContextProperty(QStringLiteral("_pkpassManager"), &passMgr); engine.rootContext()->setContextProperty(QStringLiteral("_reservationManager"), &resMgr); engine.rootContext()->setContextProperty(QStringLiteral("_timelineModel"), &tripGroupProxy); engine.rootContext()->setContextProperty(QStringLiteral("_settings"), &settings); engine.rootContext()->setContextProperty(QStringLiteral("_weatherForecastManager"), &weatherForecastMgr); engine.rootContext()->setContextProperty(QStringLiteral("_brightnessManager"), &brightnessManager); engine.rootContext()->setContextProperty(QStringLiteral("_lockManager"), &lockManager); engine.rootContext()->setContextProperty(QStringLiteral("_liveDataManager"), &liveDataMgr); engine.rootContext()->setContextProperty(QStringLiteral("_tripGroupInfoProvider"), QVariant::fromValue(tripGroupInfoProvider)); engine.load(QStringLiteral("qrc:/main.qml")); handlePositionalArguments(&appController, parser.positionalArguments()); handleViewIntent(&appController); return app.exec(); } diff --git a/src/app/main.qml b/src/app/main.qml index d3f970f..20f220b 100644 --- a/src/app/main.qml +++ b/src/app/main.qml @@ -1,134 +1,143 @@ /* Copyright (C) 2018 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import QtQuick 2.5 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.1 as QQC2 import Qt.labs.platform 1.1 import org.kde.kirigami 2.4 as Kirigami import org.kde.itinerary 1.0 import "." as App Kirigami.ApplicationWindow { title: i18n("KDE Itinerary") reachableModeEnabled: false width: 480 height: 720 FileDialog { id: fileDialog title: i18n("Import Reservation") folder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) nameFilters: [i18n("All Files (*.*)"), i18n("PkPass files (*.pkpass)"), i18n("PDF files (*.pdf)"), i18n("KDE Itinerary files (*.itinerary)")] onAccepted: ApplicationController.importFromUrl(file) } globalDrawer: Kirigami.GlobalDrawer { title: i18n("KDE Itinerary") titleIcon: "map-symbolic" actions: [ Kirigami.Action { text: i18n("Import...") iconName: "document-open" onTriggered: fileDialog.open() }, Kirigami.Action { text: i18n("Paste") iconName: "edit-paste" onTriggered: ApplicationController.importFromClipboard() enabled: ApplicationController.hasClipboardContent }, Kirigami.Action { text: i18n("Check Calendar") iconName: "view-calendar-day" onTriggered: ApplicationController.checkCalendar() visible: Qt.platform.os == "android" }, Kirigami.Action { text: i18n("Check for Updates") iconName: "view-refresh" onTriggered: { _liveDataManager.checkForUpdates(); } }, + Kirigami.Action { + text: i18n("Statistics") + iconName: "view-statistics" + onTriggered: pageStack.push(statisticsComponent) + }, Kirigami.Action { id: settingsAction text: i18n("Settings...") iconName: "settings-configure" onTriggered: pageStack.push(settingsComponent) }, Kirigami.Action { text: i18n("Export...") iconName: "export-symbolic" onTriggered: ApplicationController.exportData(); }, Kirigami.Action { id: aboutAction text: i18n("About") iconName: "help-about" onTriggered: pageStack.push(aboutComponent) } ] } contextDrawer: Kirigami.ContextDrawer { id: contextDrawer } pageStack.initialPage: mainPageComponent footer: Kirigami.InlineMessage { id: infoMessage Layout.fillWidth: true visible: false showCloseButton: true Connections { target: _reservationManager onInfoMessage: { infoMessage.text = msg; infoMessage.visible = true; } } } Component { id: mainPageComponent App.TimelinePage {} } Component { id: settingsComponent App.SettingsPage { id: settingsPage Binding { target: settingsAction property: "enabled" value: !settingsPage.isCurrentPage } } } Component { id: aboutComponent App.AboutPage { id: aboutPage Binding { target: aboutAction property: "enabled" value: !aboutPage.isCurrentPage } } } + Component { + id: statisticsComponent + App.StatisticsPage {} + } } diff --git a/src/app/qml.qrc b/src/app/qml.qrc index 1e5c19e..8887166 100644 --- a/src/app/qml.qrc +++ b/src/app/qml.qrc @@ -1,69 +1,71 @@ qtquickcontrols2.conf main.qml AboutPage.qml BoardingPass.qml BusDelegate.qml BusPage.qml CarRentalDelegate.qml CarRentalPage.qml CountryInfoDelegate.qml DepartureQueryPage.qml DateInput.qml DateTimeEdit.qml DetailsPage.qml DocumentsPage.qml EditorPage.qml EventDelegate.qml EventPage.qml EventTicket.qml FlightDelegate.qml FlightEditor.qml FlightPage.qml HotelDelegate.qml HotelPage.qml JourneyQueryPage.qml PkPassBarcode.qml PkPassPage.qml PlaceDelegate.qml PlaceEditor.qml PublicTransportBackendPage.qml RestaurantDelegate.qml RestaurantEditor.qml RestaurantPage.qml SettingsPage.qml + StatisticsDelegate.qml + StatisticsPage.qml TicketTokenDelegate.qml TimeInput.qml TimelineDelegate.qml TimelinePage.qml TouristAttractionDelegate.qml TouristAttractionPage.qml TrainDelegate.qml TrainEditor.qml TrainPage.qml TripGroupDelegate.qml WeatherForecastDelegate.qml WeatherForecastPage.qml images/bus.svg images/cablecar.svg images/car.svg images/coach.svg images/ferry.svg images/flight.svg images/foodestablishment.svg images/funicular.svg images/longdistancetrain.svg images/rapidtransit.svg images/shuttle.svg images/subway.svg images/taxi.svg images/train.svg images/tramway.svg images/transfer.svg images/wait.svg images/walk.svg diff --git a/src/app/statisticsmodel.cpp b/src/app/statisticsmodel.cpp new file mode 100644 index 0000000..5dd5dcd --- /dev/null +++ b/src/app/statisticsmodel.cpp @@ -0,0 +1,273 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "statisticsmodel.h" +#include "reservationmanager.h" + +#include +#include +#include +#include + +#include + +#include + +using namespace KItinerary; + +StatisticsItem::StatisticsItem() = default; + +StatisticsItem::StatisticsItem(const QString &label, const QString &value, StatisticsItem::Trend trend) + : m_label(label) + , m_value(value) + , m_trend(trend) +{ +} + +StatisticsItem::~StatisticsItem() = default; + +StatisticsModel::StatisticsModel(QObject *parent) + : QObject(parent) +{ + connect(this, &StatisticsModel::setupChanged, this, &StatisticsModel::recompute); + recompute(); +} + +StatisticsModel::~StatisticsModel() = default; + +ReservationManager* StatisticsModel::reservationManager() const +{ + return m_resMgr; +} + +void StatisticsModel::setReservationManager(ReservationManager *resMgr) +{ + if (m_resMgr == resMgr) { + return; + } + m_resMgr = resMgr; + connect(m_resMgr, &ReservationManager::batchAdded, this, &StatisticsModel::recompute); + emit setupChanged(); +} + +void StatisticsModel::setTimeRange(const QDate &begin, const QDate &end) +{ + if (m_begin == begin && end == m_end) { + return; + } + + m_begin = begin; + m_end = end; + recompute(); +} + +StatisticsItem StatisticsModel::totalCount() const +{ + return StatisticsItem(i18n("Trips"), QString::number(m_statData[Total][TripCount]), trend(Total, TripCount)); +} + +StatisticsItem StatisticsModel::totalDistance() const +{ + return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Total][Distance] / 1000), trend(Total, Distance)); +} + +StatisticsItem StatisticsModel::totalNights() const +{ + return StatisticsItem(i18n("Hotel nights"), QString::number(m_hotelCount), trend(m_hotelCount, m_prevHotelCount)); +} + +StatisticsItem StatisticsModel::totalCO2() const +{ + return StatisticsItem(i18n("CO₂"), i18n("%1 kg", m_statData[Total][CO2] / 1000.0), trend(Total, CO2)); +} + +StatisticsItem StatisticsModel::flightCount() const +{ + return StatisticsItem(i18n("Flights"), QString::number(m_statData[Flight][TripCount]), trend(Flight, TripCount)); +} + +StatisticsItem StatisticsModel::flightDistance() const +{ + return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Flight][Distance] / 1000), trend(Flight, Distance)); +} + +StatisticsItem StatisticsModel::flightCO2() const +{ + return StatisticsItem(i18n("CO₂"), i18n("%1 kg", m_statData[Flight][CO2] / 1000.0), trend(Flight, CO2)); +} + +StatisticsItem StatisticsModel::trainCount() const +{ + return StatisticsItem(i18n("Trips"), QString::number(m_statData[Train][TripCount]), trend(Train, TripCount)); +} + +StatisticsItem StatisticsModel::trainDistance() const +{ + return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Train][Distance] / 1000), trend(Train, Distance)); +} + +StatisticsItem StatisticsModel::trainCO2() const +{ + return StatisticsItem(i18n("CO₂"), i18n("%1 kg", m_statData[Train][CO2] / 1000.0), trend(Train, CO2)); +} + +StatisticsItem StatisticsModel::busCount() const +{ + return StatisticsItem(i18n("Trips"), QString::number(m_statData[Bus][TripCount]), trend(Bus, TripCount)); +} + +StatisticsItem StatisticsModel::busDistance() const +{ + return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Bus][Distance] / 1000), trend(Bus, Distance)); +} + +StatisticsItem StatisticsModel::busCO2() const +{ + return StatisticsItem(i18n("CO₂"), i18n("%1 kg", m_statData[Bus][CO2] / 1000.0), trend(Bus, CO2)); +} + +StatisticsItem StatisticsModel::carCount() const +{ + return StatisticsItem(i18n("Trips"), QString::number(m_statData[Car][TripCount]), trend(Car, TripCount)); +} + +StatisticsItem StatisticsModel::carDistance() const +{ + return StatisticsItem(i18n("Distance"), i18n("%1 km", m_statData[Car][Distance] / 1000), trend(Car, Distance)); +} + +StatisticsItem StatisticsModel::carCO2() const +{ + return StatisticsItem(i18n("CO₂"), i18n("%1 kg", m_statData[Car][CO2] / 1000.0), trend(Car, CO2)); +} + +StatisticsModel::AggregateType StatisticsModel::typeForReservation(const QVariant &res) const +{ + if (JsonLd::isA(res)) { + return Flight; + } else if (JsonLd::isA(res)) { + return Train; + } else if (JsonLd::isA(res)) { + return Bus; + } + return Car; +} + +static int distance(const QVariant &res) +{ + const auto dep = LocationUtil::departureLocation(res); + const auto arr = LocationUtil::arrivalLocation(res); + if (dep.isNull() || arr.isNull()) { + return 0; + } + const auto depGeo = LocationUtil::geo(dep); + const auto arrGeo = LocationUtil::geo(arr); + if (!depGeo.isValid() || !arrGeo.isValid()) { + return 0; + } + return std::max(0, LocationUtil::distance(depGeo, arrGeo)); +} + +// from https://en.wikipedia.org/wiki/Environmental_impact_of_transport +static const int emissionPerKm[] = { + 0, + 285, // flight + 14, // train + 68, // bus + 158, // car +}; + +int StatisticsModel::co2emission(StatisticsModel::AggregateType type, int distance) const +{ + return distance * emissionPerKm[type]; +} + +void StatisticsModel::computeStats(const QVariant& res, int (&statData)[AGGREGATE_TYPE_COUNT][STAT_TYPE_COUNT]) +{ + const auto type = typeForReservation(res); + const auto dist = distance(res); + const auto co2 = co2emission(type, dist / 1000); + + statData[type][TripCount]++; + statData[type][Distance] += dist; + statData[type][CO2] += co2; + + statData[Total][TripCount]++; + statData[Total][Distance] += dist; + statData[Total][CO2] += co2; +} + +void StatisticsModel::recompute() +{ + memset(m_statData, 0, AGGREGATE_TYPE_COUNT * STAT_TYPE_COUNT * sizeof(int)); + memset(m_prevStatData, 0, AGGREGATE_TYPE_COUNT * STAT_TYPE_COUNT * sizeof(int)); + m_hotelCount = 0; + m_prevHotelCount = 0; + + if (!m_resMgr) { + return; + } + + QDate prevStart; + if (m_begin.isValid() && m_end.isValid()) { + prevStart = m_begin.addDays(m_end.daysTo(m_begin)); + } + + const auto &batches = m_resMgr->batches(); + for (const auto &batchId : batches) { + const auto res = m_resMgr->reservation(batchId); + const auto dt = SortUtil::startDateTime(res); + + bool isPrev = false; + if (m_end.isValid() && dt.date() > m_end) { + continue; + } + if (prevStart.isValid()) { + if (dt.date() < prevStart) { + continue; + } + isPrev = dt.date() < m_begin; + } + + if (LocationUtil::isLocationChange(res)) { + computeStats(res, isPrev ? m_prevStatData : m_statData); + } else if (JsonLd::isA(res)) { + const auto hotel = res.value(); + if (isPrev) { + m_prevHotelCount += hotel.checkinTime().daysTo(hotel.checkoutTime()); + } else { + m_hotelCount += hotel.checkinTime().daysTo(hotel.checkoutTime()); + } + } + } + + emit changed(); +} + +StatisticsItem::Trend StatisticsModel::trend(int current, int prev) const +{ + if (!m_begin.isValid() || !m_end.isValid()) { + return StatisticsItem::TrendUnknown; + } + + return current < prev ? StatisticsItem::TrendDown : current > prev ? StatisticsItem::TrendUp : StatisticsItem::TrendUnchanged; +} + +StatisticsItem::Trend StatisticsModel::trend(StatisticsModel::AggregateType type, StatisticsModel::StatType stat) const +{ + return trend(m_statData[type][stat], m_prevStatData[type][stat]); +} diff --git a/src/app/statisticsmodel.h b/src/app/statisticsmodel.h new file mode 100644 index 0000000..7161e6a --- /dev/null +++ b/src/app/statisticsmodel.h @@ -0,0 +1,138 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef STATISTICSMODEL_H +#define STATISTICSMODEL_H + +#include +#include + +class ReservationManager; + +/** Statistics data item. */ +class StatisticsItem +{ + Q_GADGET + Q_PROPERTY(QString label MEMBER m_label CONSTANT) + Q_PROPERTY(QString value MEMBER m_value CONSTANT) + Q_PROPERTY(Trend trend MEMBER m_trend CONSTANT) + +public: + enum Trend { + TrendUnknown, + TrendUp, + TrendDown, + TrendUnchanged + }; + Q_ENUM(Trend) + + StatisticsItem(); + explicit StatisticsItem(const QString &label, const QString &value, StatisticsItem::Trend trend = TrendUnknown); + ~StatisticsItem(); + + QString m_label; + QString m_value; + Trend m_trend = TrendUnknown; +}; + +Q_DECLARE_METATYPE(StatisticsItem) + +/** Provides the data shown in the statistics page. */ +class StatisticsModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(StatisticsItem totalCount READ totalCount NOTIFY changed) + Q_PROPERTY(StatisticsItem totalDistance READ totalDistance NOTIFY changed) + Q_PROPERTY(StatisticsItem totalNights READ totalNights NOTIFY changed) + Q_PROPERTY(StatisticsItem totalCO2 READ totalCO2 NOTIFY changed) + + Q_PROPERTY(StatisticsItem flightCount READ flightCount NOTIFY changed) + Q_PROPERTY(StatisticsItem flightDistance READ flightDistance NOTIFY changed) + Q_PROPERTY(StatisticsItem flightCO2 READ flightCO2 NOTIFY changed) + + Q_PROPERTY(StatisticsItem trainCount READ trainCount NOTIFY changed) + Q_PROPERTY(StatisticsItem trainDistance READ trainDistance NOTIFY changed) + Q_PROPERTY(StatisticsItem trainCO2 READ trainCO2 NOTIFY changed) + + Q_PROPERTY(StatisticsItem busCount READ busCount NOTIFY changed) + Q_PROPERTY(StatisticsItem busDistance READ busDistance NOTIFY changed) + Q_PROPERTY(StatisticsItem busCO2 READ busCO2 NOTIFY changed) + + Q_PROPERTY(StatisticsItem carCount READ carCount NOTIFY changed) + Q_PROPERTY(StatisticsItem carDistance READ carDistance NOTIFY changed) + Q_PROPERTY(StatisticsItem carCO2 READ carCO2 NOTIFY changed) + + Q_PROPERTY(ReservationManager* reservationManager READ reservationManager WRITE setReservationManager NOTIFY setupChanged) + +public: + explicit StatisticsModel(QObject *parent = nullptr); + ~StatisticsModel(); + + ReservationManager* reservationManager() const; + void setReservationManager(ReservationManager *resMgr); + + Q_INVOKABLE void setTimeRange(const QDate &begin, const QDate &end); + + StatisticsItem totalCount() const; + StatisticsItem totalDistance() const; + StatisticsItem totalNights() const; + StatisticsItem totalCO2() const; + + StatisticsItem flightCount() const; + StatisticsItem flightDistance() const; + StatisticsItem flightCO2() const; + + StatisticsItem trainCount() const; + StatisticsItem trainDistance() const; + StatisticsItem trainCO2() const; + + StatisticsItem busCount() const; + StatisticsItem busDistance() const; + StatisticsItem busCO2() const; + + StatisticsItem carCount() const; + StatisticsItem carDistance() const; + StatisticsItem carCO2() const; + +Q_SIGNALS: + void setupChanged(); + void changed(); + +private: + void recompute(); + + ReservationManager *m_resMgr = nullptr; + QDate m_begin; + QDate m_end; + + enum AggregateType { Total, Flight, Train, Bus, Car, AGGREGATE_TYPE_COUNT }; + enum StatType { TripCount, Distance, CO2, STAT_TYPE_COUNT }; + + AggregateType typeForReservation(const QVariant &res) const; + int co2emission(AggregateType type, int distance) const; + void computeStats(const QVariant &res, int (&statData)[AGGREGATE_TYPE_COUNT][STAT_TYPE_COUNT]); + + StatisticsItem::Trend trend(int current, int prev) const; + StatisticsItem::Trend trend(AggregateType type, StatType stat) const; + + int m_statData[AGGREGATE_TYPE_COUNT][STAT_TYPE_COUNT]; + int m_prevStatData[AGGREGATE_TYPE_COUNT][STAT_TYPE_COUNT]; + int m_hotelCount = 0; + int m_prevHotelCount = 0; +}; + +#endif // STATISTICSMODEL_H diff --git a/src/app/statisticstimerangemodel.cpp b/src/app/statisticstimerangemodel.cpp new file mode 100644 index 0000000..a6cf161 --- /dev/null +++ b/src/app/statisticstimerangemodel.cpp @@ -0,0 +1,104 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "statisticstimerangemodel.h" +#include "reservationmanager.h" + +#include + +#include + +#include +#include +#include + +using namespace KItinerary; + +StatisticsTimeRangeModel::StatisticsTimeRangeModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +StatisticsTimeRangeModel::~StatisticsTimeRangeModel() = default; + +ReservationManager* StatisticsTimeRangeModel::reservationManager() const +{ + return m_resMgr; +} + +void StatisticsTimeRangeModel::setReservationManager(ReservationManager *resMgr) +{ + if (m_resMgr == resMgr) { + return; + } + m_resMgr = resMgr; + emit setupChanged(); + + beginResetModel(); + int y = 0; + + const auto &batches = m_resMgr->batches(); + for (const auto &batchId : batches) { + const auto res = m_resMgr->reservation(batchId); + const auto dt = SortUtil::startDateTime(res); + if (dt.date().year() != y) { + m_years.push_back(dt.date().year()); + y = dt.date().year(); + } + } + std::reverse(m_years.begin(), m_years.end()); + endResetModel(); +} + +int StatisticsTimeRangeModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return 1 + m_years.size(); +} + +QVariant StatisticsTimeRangeModel::data(const QModelIndex &index, int role) const +{ + switch (role) { + case Qt::DisplayRole: + if (index.row() == 0) { + return i18n("Total"); + } + return QLocale().toString(QDate(m_years[index.row() - 1], 1, 1), QStringLiteral("yyyy")); + case BeginRole: + if (index.row() == 0 || index.row() == (int)m_years.size()) { // first range is open ended here, to skip trend computation + return QDate(); + } + return QDate(m_years[index.row() - 1], 1, 1); + case EndRole: + if (index.row() == 0) { + return QDate(); + } + return QDate(m_years[index.row() - 1], 12, 31); + } + + return {}; +} + +QHash StatisticsTimeRangeModel::roleNames() const +{ + auto r = QAbstractListModel::roleNames(); + r.insert(BeginRole, "begin"); + r.insert(EndRole, "end"); + return r; +} diff --git a/src/app/statisticstimerangemodel.h b/src/app/statisticstimerangemodel.h new file mode 100644 index 0000000..cb2739a --- /dev/null +++ b/src/app/statisticstimerangemodel.h @@ -0,0 +1,55 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef STATISTICSTIMERANGEMODEL_H +#define STATISTICSTIMERANGEMODEL_H + +#include + +class ReservationManager; + +/** Selectable time ranges for the statistics page. */ +class StatisticsTimeRangeModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(ReservationManager* reservationManager READ reservationManager WRITE setReservationManager NOTIFY setupChanged) + +public: + explicit StatisticsTimeRangeModel(QObject *parent = nullptr); + ~StatisticsTimeRangeModel(); + + enum { + BeginRole = Qt::UserRole, + EndRole + }; + + ReservationManager* reservationManager() const; + void setReservationManager(ReservationManager *resMgr); + + int rowCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + +Q_SIGNALS: + void setupChanged(); + +private: + ReservationManager *m_resMgr = nullptr; + std::vector m_years; +}; + +#endif // STATISTICSTIMERANGEMODEL_H