diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index a0f0766..1aaa917 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -1,180 +1,181 @@
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)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/itinerary_version_detailed.h.in ${CMAKE_CURRENT_BINARY_DIR}/itinerary_version_detailed.h)
set(itinerary_srcs
applicationcontroller.cpp
countryinformation.cpp
documentmanager.cpp
favoritelocationmodel.cpp
json.cpp
livedatamanager.cpp
navigationcontroller.cpp
pkpassmanager.cpp
pkpassimageprovider.cpp
publictransport.cpp
reservationmanager.cpp
statisticsmodel.cpp
statisticstimerangemodel.cpp
timelinedelegatecontroller.cpp
timelineelement.cpp
timelinemodel.cpp
tripgroup.cpp
tripgroupinfoprovider.cpp
tripgroupmanager.cpp
tripgroupproxymodel.cpp
transfer.cpp
transfermanager.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
Qt5::QuickControls2
)
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
crosshairs
dialog-cancel
dialog-close
document-edit
document-open
document-save
documentinfo
edit-delete
edit-download
edit-paste
edit-rename
export-symbolic
folder-documents-symbolic
go-down-symbolic
go-home-symbolic
go-next-symbolic
go-up-symbolic
help-about-symbolic
+ help-contents
list-add
map-symbolic
meeting-attending
question
settings-configure
view-calendar-day
view-list-symbolic
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-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 Qt5::Widgets)
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})
ecm_install_icons(ICONS 48-apps-itinerary.svg DESTINATION ${KDE_INSTALL_ICONDIR})
diff --git a/src/app/WelcomePage.qml b/src/app/WelcomePage.qml
new file mode 100644
index 0000000..64a4207
--- /dev/null
+++ b/src/app/WelcomePage.qml
@@ -0,0 +1,72 @@
+/*
+ Copyright (C) 2020 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 QtPositioning 5.11
+import org.kde.kirigami 2.4 as Kirigami
+import org.kde.itinerary 1.0
+import "." as App
+
+Kirigami.ScrollablePage {
+ id: root
+ title: _reservationManager.isEmpty() ? i18n("Welcome!") : i18n("Help")
+
+ ColumnLayout {
+ Kirigami.Heading {
+ text: i18n("How to import data?")
+ }
+ QQC2.Label {
+ Layout.fillWidth: true
+ text: i18n("There's a number of ways to import data into KDE Itinerary:
+ - Directly opening PDF tickets or Apple Wallet passes.
+ - From the Android calendar, for entries made via the KMail, Nextcloud or Thunderbird itinerary plug-ins, and synced using DavDroid.
+ - Via KDE Connect from the KMail itinerary plug-in.
+ - By scanning boarding pass barcodes and pasting their content.
+
")
+ wrapMode: Text.WordWrap
+ }
+
+ Kirigami.Heading {
+ text: i18n("Check the settings!")
+ }
+ QQC2.Label {
+ Layout.fillWidth: true
+ text: i18n("KDE Itinerary has all features disabled by default that require online access, such as retrieving live traffice data or weather forecasts. You therefore might want to review these settings. While you are at it, you might want to configure your home location to enable the transfer assistant to automatically suggests ways to your next departure station or airport.")
+ wrapMode: Text.WordWrap
+ }
+
+ Kirigami.Heading {
+ text: i18n("More information")
+ }
+ QQC2.Label {
+ Layout.fillWidth: true
+ text: i18n("For more information about KDE Itinerary check out the wiki page.")
+ wrapMode: Text.WordWrap
+ onLinkActivated: Qt.openUrlExternally(link)
+ }
+
+
+ QQC2.Button {
+ text: i18n("Got it!")
+ onClicked: applicationWindow().pageStack.pop();
+ Layout.alignment: Qt.AlignRight
+ visible: _reservationManager.isEmpty()
+ }
+ }
+}
diff --git a/src/app/main.qml b/src/app/main.qml
index c83a2af..325717d 100644
--- a/src/app/main.qml
+++ b/src/app/main.qml
@@ -1,142 +1,157 @@
/*
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"
isMenu: true
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 {
id: statsAction
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 {
+ text: i18n("Help")
+ iconName: "help-contents"
+ onTriggered: pageStack.push(welcomeComponent)
+ },
Kirigami.Action {
id: aboutAction
text: i18n("About")
iconName: "help-about-symbolic"
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.onCompleted: {
+ if (_reservationManager.isEmpty()) {
+ pageStack.push(welcomeComponent);
+ }
+ }
+
Component {
id: mainPageComponent
App.TimelinePage {}
}
Component {
id: settingsComponent
App.SettingsPage {
id: settingsPage
onIsCurrentPageChanged: settingsAction.enabled = !settingsPage.isCurrentPage
}
}
Component {
id: aboutComponent
App.AboutPage {
id: aboutPage
onIsCurrentPageChanged: aboutAction.enabled = !aboutPage.isCurrentPage
}
}
Component {
id: statisticsComponent
App.StatisticsPage {
id: statsPage
reservationManager: _reservationManager
tripGroupManager: TripGroupManager
onIsCurrentPageChanged: statsAction.enabled = !statsPage.isCurrentPage
}
}
+ Component {
+ id: welcomeComponent
+ App.WelcomePage {}
+ }
}
diff --git a/src/app/qml.qrc b/src/app/qml.qrc
index 89b4b2a..28d8311 100644
--- a/src/app/qml.qrc
+++ b/src/app/qml.qrc
@@ -1,81 +1,82 @@
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
EventEditor.qml
EventPage.qml
EventTicket.qml
FavoriteLocationPage.qml
FlightDelegate.qml
FlightEditor.qml
FlightPage.qml
HotelDelegate.qml
HotelEditor.qml
HotelPage.qml
JourneyDelegateHeader.qml
JourneyQueryPage.qml
JourneySectionDelegate.qml
JourneySummaryDelegate.qml
LocationPicker.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
TransferDelegate.qml
TransferPage.qml
TripGroupDelegate.qml
VehicleLayoutPage.qml
WeatherForecastDelegate.qml
WeatherForecastPage.qml
+ WelcomePage.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/reservationmanager.cpp b/src/app/reservationmanager.cpp
index 9a105fd..97cac51 100644
--- a/src/app/reservationmanager.cpp
+++ b/src/app/reservationmanager.cpp
@@ -1,504 +1,509 @@
/*
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 "reservationmanager.h"
#include "pkpassmanager.h"
#include "logging.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace KItinerary;
static bool isSameTrip(const QVariant &lhs, const QVariant &rhs)
{
if (lhs.userType() != rhs.userType() || !JsonLd::canConvert(lhs) || !JsonLd::canConvert(rhs)) {
return false;
}
const auto lhsTrip = JsonLd::convert(lhs).reservationFor();
const auto rhsTrip = JsonLd::convert(rhs).reservationFor();
return MergeUtil::isSame(lhsTrip, rhsTrip);
}
ReservationManager::ReservationManager(QObject* parent)
: QObject(parent)
{
loadBatches();
}
ReservationManager::~ReservationManager() = default;
void ReservationManager::setPkPassManager(PkPassManager* mgr)
{
m_passMgr = mgr;
connect(mgr, &PkPassManager::passAdded, this, &ReservationManager::passAdded);
connect(mgr, &PkPassManager::passUpdated, this, &ReservationManager::passUpdated);
connect(mgr, &PkPassManager::passRemoved, this, &ReservationManager::passRemoved);
}
+bool ReservationManager::isEmpty() const
+{
+ return m_batchToResMap.empty();
+}
+
bool ReservationManager::hasBatch(const QString &batchId) const
{
return m_batchToResMap.contains(batchId);
}
QVariant ReservationManager::reservation(const QString& id) const
{
if (id.isEmpty()) {
return {};
}
const auto it = m_reservations.constFind(id);
if (it != m_reservations.constEnd()) {
return it.value();
}
const QString resPath = reservationsBasePath() + id + QLatin1String(".jsonld");
QFile f(resPath);
if (!f.open(QFile::ReadOnly)) {
qCWarning(Log) << "Failed to open JSON-LD reservation data file:" << resPath << f.errorString();
return {};
}
const auto doc = QJsonDocument::fromJson(f.readAll());
if (!(doc.isArray() && doc.array().size() == 1) && !doc.isObject()) {
qCWarning(Log) << "Invalid JSON-LD reservation data file:" << resPath;
return {};
}
const auto resData = JsonLdDocument::fromJson(doc.isArray() ? doc.array() : QJsonArray({doc.object()}));
if (resData.size() != 1) {
qCWarning(Log) << "Unable to parse JSON-LD reservation data file:" << resPath;
return {};
}
// re-run post-processing to benefit from newer augmentations
ExtractorPostprocessor postproc;
postproc.process(resData);
if (postproc.result().size() != 1) {
qCWarning(Log) << "Post-processing discarded the reservation:" << resPath;
return {};
}
const auto res = postproc.result().at(0);
m_reservations.insert(id, res);
return res;
}
QString ReservationManager::reservationsBasePath()
{
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/reservations/");
}
QString ReservationManager::batchesBasePath()
{
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/batches/");
}
QVector ReservationManager::importReservation(const QByteArray& data, const QString &fileName)
{
ExtractorEngine engine;
engine.setContextDate(QDateTime(QDate::currentDate(), QTime(0, 0)));
engine.setData(data, fileName);
return importReservations(JsonLdDocument::fromJson(engine.extract()));
}
QVector ReservationManager::importReservations(const QVector &resData)
{
ExtractorPostprocessor postproc;
postproc.setContextDate(QDateTime(QDate::currentDate(), QTime(0, 0)));
postproc.process(resData);
const auto data = postproc.result();
QVector ids;
ids.reserve(data.size());
for (const auto &res : data) {
if (JsonLd::isA(res)) { // promote Event to EventReservation
EventReservation ev;
ev.setReservationFor(res);
ids.push_back(addReservation(ev));
continue;
}
// filter out non-Reservation objects we can't handle yet
// TODO show UI asking for time ranges for LodgingBusiness, FoodEstablishment, etc
if (!JsonLd::canConvert(res) && !JsonLd::isA(res)) {
continue;
}
ids.push_back(addReservation(res));
}
emit infoMessage(i18np("One reservation imported.", "%1 reservations imported.", ids.size()));
return ids;
}
QString ReservationManager::addReservation(const QVariant &res)
{
// look for matching reservations, or matching batches
// we need to do that within a +/-24h range, so we find unbound elements too
// TODO in case this updates the time for an unbound element we need to re-sort, otherwise the prev/next logic fails!
const auto rangeBegin = SortUtil::startDateTime(res).addDays(-1);
const auto rangeEnd = rangeBegin.addDays(2);
const auto beginIt = std::lower_bound(m_batches.begin(), m_batches.end(), rangeBegin, [this](const auto &lhs, const auto &rhs) {
return SortUtil::startDateTime(reservation(lhs)) < rhs;
});
for (auto it = beginIt; it != m_batches.end(); ++it) {
const auto otherRes = reservation(*it);
if (SortUtil::startDateTime(otherRes) > rangeEnd) {
break; // no hit
}
if (MergeUtil::isSame(res, otherRes)) {
// this is actually an update of otherRes!
const auto newRes = MergeUtil::merge(otherRes, res);
updateReservation(*it, newRes);
return *it;
}
if (isSameTrip(res, otherRes)) {
// this is a multi-traveler element, check if we have it as one of the batch elements already
const auto &batch = m_batchToResMap.value(*it);
for (const auto &batchedId : batch) {
const auto batchedRes = reservation(batchedId);
if (MergeUtil::isSame(res, batchedRes)) {
// this is actually an update of a batched reservation
const auto newRes = MergeUtil::merge(otherRes, res);
updateReservation(batchedId, newRes);
return batchedId;
}
}
// truly new, and added to an existing batch
const QString resId = QUuid::createUuid().toString();
storeReservation(resId, res);
emit reservationAdded(resId);
m_batchToResMap[*it].push_back(resId);
m_resToBatchMap.insert(resId, *it);
emit batchChanged(*it);
storeBatch(*it);
return resId;
}
}
// truly new, and starting a new batch
const QString resId = QUuid::createUuid().toString();
storeReservation(resId, res);
emit reservationAdded(resId);
// search for the precise insertion place, beginIt is only the begin of our initial search range
const auto insertIt = std::lower_bound(m_batches.begin(), m_batches.end(), SortUtil::startDateTime(res), [this](const auto &lhs, const auto &rhs) {
return SortUtil::startDateTime(reservation(lhs)) < rhs;
});
m_batches.insert(insertIt, resId);
m_batchToResMap.insert(resId, {resId});
m_resToBatchMap.insert(resId, resId);
emit batchAdded(resId);
storeBatch(resId);
return resId;
}
void ReservationManager::updateReservation(const QString &resId, const QVariant &res)
{
const auto oldRes = reservation(resId);
storeReservation(resId, res);
emit reservationChanged(resId);
updateBatch(resId, res, oldRes);
}
void ReservationManager::storeReservation(const QString &resId, const QVariant &res) const
{
const QString basePath = reservationsBasePath();
QDir::root().mkpath(basePath);
const QString path = basePath + resId + QLatin1String(".jsonld");
QFile f(path);
if (!f.open(QFile::WriteOnly)) {
qCWarning(Log) << "Unable to open file:" << f.errorString();
return;
}
f.write(QJsonDocument(JsonLdDocument::toJson(res)).toJson());
m_reservations.insert(resId, res);
}
void ReservationManager::removeReservation(const QString& id)
{
const auto batchId = m_resToBatchMap.value(id);
removeFromBatch(id, batchId);
const QString basePath = reservationsBasePath();
QFile::remove(basePath + QLatin1Char('/') + id + QLatin1String(".jsonld"));
emit reservationRemoved(id);
m_reservations.remove(id);
}
const std::vector& ReservationManager::batches() const
{
return m_batches;
}
QString ReservationManager::batchForReservation(const QString &resId) const
{
return m_resToBatchMap.value(resId);
}
QStringList ReservationManager::reservationsForBatch(const QString &batchId) const
{
return m_batchToResMap.value(batchId);
}
void ReservationManager::removeBatch(const QString &batchId)
{
// TODO make this atomic, ie. don't emit batch range notifications
const auto res = m_batchToResMap.value(batchId);
for (const auto &id : res) {
if (id != batchId) {
removeReservation(id);
}
}
removeReservation(batchId);
}
void ReservationManager::passAdded(const QString& passId)
{
const auto pass = m_passMgr->pass(passId);
ExtractorEngine engine;
engine.setPass(pass);
const auto data = engine.extract();
const auto res = JsonLdDocument::fromJson(data);
importReservations(res);
}
void ReservationManager::passUpdated(const QString& passId)
{
passAdded(passId);
}
void ReservationManager::passRemoved(const QString& passId)
{
Q_UNUSED(passId);
// TODO
}
void ReservationManager::loadBatches()
{
Q_ASSERT(m_batches.empty());
const auto base = batchesBasePath();
if (!QDir::root().exists(base)) {
initialBatchCreate();
return;
}
for (QDirIterator it(base, QDir::NoDotAndDotDot | QDir::Files); it.hasNext();) {
it.next();
QFile batchFile(it.filePath());
if (!batchFile.open(QFile::ReadOnly)) {
qCWarning(Log) << "Failed to open batch file" << it.filePath() << batchFile.errorString();
continue;
}
const auto batchId = it.fileInfo().baseName();
m_batches.push_back(batchId);
const auto batchDoc = QJsonDocument::fromJson(batchFile.readAll());
const auto top = batchDoc.object();
const auto resArray = top.value(QLatin1String("elements")).toArray();
QStringList l;
l.reserve(resArray.size());
for (const auto &v : resArray) {
const auto resId = v.toString();
l.push_back(resId);
m_resToBatchMap.insert(resId, batchId);
}
m_batchToResMap.insert(batchId, l);
}
std::sort(m_batches.begin(), m_batches.end(), [this](const auto &lhs, const auto &rhs) {
return SortUtil::isBefore(reservation(lhs), reservation(rhs));
});
}
void ReservationManager::storeBatch(const QString &batchId) const
{
QJsonArray elems;
const auto &batch = m_batchToResMap.value(batchId);
std::copy(batch.begin(), batch.end(), std::back_inserter(elems));
QJsonObject top;
top.insert(QLatin1String("elements"), elems);
const QString path = batchesBasePath() + batchId + QLatin1String(".json");
QFile f(path);
if (!f.open(QFile::WriteOnly | QFile::Truncate)) {
qCWarning(Log) << "Failed to open batch file!" << path << f.errorString();
return;
}
f.write(QJsonDocument(top).toJson());
}
void ReservationManager::storeRemoveBatch(const QString &batchId) const
{
const QString path = batchesBasePath() + batchId + QLatin1String(".json");
QFile::remove(path);
}
void ReservationManager::initialBatchCreate()
{
const auto batchBase = batchesBasePath();
QDir::root().mkpath(batchBase);
qCDebug(Log) << batchBase;
const QSignalBlocker blocker(this);
const auto base = reservationsBasePath();
for (QDirIterator it(base, QDir::NoDotAndDotDot | QDir::Files); it.hasNext();) {
it.next();
const auto resId = it.fileInfo().baseName();
const auto res = reservation(resId);
updateBatch(resId, res, res);
}
}
void ReservationManager::updateBatch(const QString &resId, const QVariant &newRes, const QVariant &oldRes)
{
const auto oldBatchId = batchForReservation(resId);
QString newBatchId;
// find the destination batch
const auto beginIt = std::lower_bound(m_batches.begin(), m_batches.end(), newRes, [this](const auto &lhs, const auto &rhs) {
return SortUtil::startDateTime(reservation(lhs)) < SortUtil::startDateTime(rhs);
});
for (auto it = beginIt; it != m_batches.end(); ++it) {
const auto otherRes = (resId == (*it)) ? oldRes : reservation(*it);
if (SortUtil::startDateTime(otherRes) != SortUtil::startDateTime(newRes)) {
break; // no hit
}
if (isSameTrip(newRes, otherRes)) {
newBatchId = *it;
break;
}
}
// still in the same batch?
if (!oldBatchId.isEmpty() && oldBatchId == newBatchId) {
emit batchContentChanged(oldBatchId);
// no need to store here, as batching didn't actually change
return;
}
// move us out of the old batch
// WARNING: beginIt will become invalid after this!
removeFromBatch(resId, oldBatchId);
// insert us into the new batch
if (newBatchId.isEmpty()) {
// we are starting a new batch
// re-run search for insertion point, could be invalid due to the above deletions
const auto it = std::lower_bound(m_batches.begin(), m_batches.end(), newRes, [this](const auto &lhs, const auto &rhs) {
return SortUtil::startDateTime(reservation(lhs)) < SortUtil::startDateTime(rhs);
});
m_batches.insert(it, QString(resId));
m_batchToResMap.insert(resId, {resId});
m_resToBatchMap.insert(resId, resId);
emit batchAdded(resId);
storeBatch(resId);
} else {
m_batchToResMap[newBatchId].push_back(resId);
m_resToBatchMap.insert(resId, newBatchId);
emit batchChanged(newBatchId);
storeBatch(newBatchId);
}
}
void ReservationManager::removeFromBatch(const QString &resId, const QString &batchId)
{
if (batchId.isEmpty()) {
return;
}
auto &batches = m_batchToResMap[batchId];
m_resToBatchMap.remove(resId);
if (batches.size() == 1) { // we were alone there, remove old batch
m_batchToResMap.remove(batchId);
const auto it = std::find(m_batches.begin(), m_batches.end(), batchId);
m_batches.erase(it);
emit batchRemoved(batchId);
storeRemoveBatch(batchId);
} else if (resId == batchId) {
// our id was the batch id, so rename the old batch
batches.removeAll(resId);
const QString renamedBatchId = batches.first();
auto it = std::find(m_batches.begin(), m_batches.end(), batchId);
Q_ASSERT(it != m_batches.end());
*it = renamedBatchId;
for (const auto &id : batches) {
m_resToBatchMap[id] = renamedBatchId;
}
m_batchToResMap[renamedBatchId] = batches;
m_batchToResMap.remove(batchId);
emit batchRenamed(batchId, renamedBatchId);
storeRemoveBatch(batchId);
storeBatch(renamedBatchId);
} else {
// old batch remains
batches.removeAll(resId);
emit batchChanged(batchId);
storeBatch(batchId);
}
}
QString ReservationManager::previousBatch(const QString &batchId) const
{
// ### this can be optimized by relying on m_batches being sorted by start date
const auto it = std::find(m_batches.begin(), m_batches.end(), batchId);
if (it == m_batches.end() || it == m_batches.begin()) {
return {};
}
return *(it - 1);
}
QString ReservationManager::nextBatch(const QString& batchId) const
{
// ### this can be optimized by relying on m_batches being sorted by start date
const auto it = std::find(m_batches.begin(), m_batches.end(), batchId);
if (it == m_batches.end() || m_batches.size() < 2 || it == (m_batches.end() - 1)) {
return {};
}
return *(it + 1);
}
diff --git a/src/app/reservationmanager.h b/src/app/reservationmanager.h
index b1f0c4c..5f0843d 100644
--- a/src/app/reservationmanager.h
+++ b/src/app/reservationmanager.h
@@ -1,126 +1,127 @@
/*
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 .
*/
#ifndef RESERVATIONMANAGER_H
#define RESERVATIONMANAGER_H
#include
#include
#include
class PkPassManager;
class QUrl;
/** Manages JSON-LD reservation data.
* This is done on two levels:
* - the raw individual reservation elements (one per traveler and per trip)
* - reservation batches for multi-traveler trips
* Most consumers probably want to work with the multi-traveler batches rather
* than the raw elements.
* Batches are identified by a reservation id of a random element in that batch,
* that means you can directly retrieve reservation data using the batch id too.
*
* Identifiers are QStrings, which is super ugly, but needed for direct consumption
* by QML.
*/
class ReservationManager : public QObject
{
Q_OBJECT
public:
explicit ReservationManager(QObject *parent = nullptr);
~ReservationManager();
void setPkPassManager(PkPassManager *mgr);
+ Q_INVOKABLE bool isEmpty() const;
Q_INVOKABLE QVariant reservation(const QString &id) const;
/** Adds @p res if it's new, or merges it with an existing reservation or reservation batch.
* @returns The id of the new or existed merged reservation.
*/
Q_INVOKABLE QString addReservation(const QVariant &res);
Q_INVOKABLE void updateReservation(const QString &resId, const QVariant &res);
Q_INVOKABLE void removeReservation(const QString &id);
/** Attempts to extract reservation information from @p data.
* @returns A list of reservation ids for the extracted elements. Those can be reservation
* ids that previously existed, in case the extracted elements could be merged.
*/
QVector importReservation(const QByteArray &data, const QString &fileName = {});
QVector importReservations(const QVector &resData);
const std::vector& batches() const;
bool hasBatch(const QString &batchId) const;
QString batchForReservation(const QString &resId) const;
Q_INVOKABLE QStringList reservationsForBatch(const QString &batchId) const;
Q_INVOKABLE void removeBatch(const QString &batchId);
/** Returns the batch happening prior to @p batchId, if any. */
QString previousBatch(const QString &batchId) const;
/** Returns the batch happening after @p batchId, if any. */
QString nextBatch(const QString &batchId) const;
Q_SIGNALS:
void reservationAdded(const QString &id);
void reservationChanged(const QString &id);
void reservationRemoved(const QString &id);
void batchAdded(const QString &batchId);
/** This is emitted when elements are added or removed from the batch,
* but its content otherwise stays untouched.
*/
void batchChanged(const QString &batchId);
/** This is emitted when the batch content changed, but the batching
* as such remains the same.
*/
void batchContentChanged(const QString &batchId);
/** This is emitted when the reservation with @p oldBatchId was removed and
* it has been used to identify a non-empty batch.
*/
void batchRenamed(const QString &oldBatchId, const QString &newBatchId);
void batchRemoved(const QString &batchId);
/** Human readable information message. */
void infoMessage(const QString &msg);
private:
static QString reservationsBasePath();
static QString batchesBasePath();
void storeReservation(const QString &resId, const QVariant &res) const;
void passAdded(const QString &passId);
void passUpdated(const QString &passId);
void passRemoved(const QString &passId);
void loadBatches();
void initialBatchCreate();
void storeBatch(const QString &batchId) const;
void storeRemoveBatch(const QString &batchId) const;
void updateBatch(const QString &resId, const QVariant &newRes, const QVariant &oldRes);
void removeFromBatch(const QString &resId, const QString &batchId);
mutable QHash m_reservations;
std::vector m_batches;
QHash m_batchToResMap; // ### QStringList for direct consumption by QML
QHash m_resToBatchMap;
PkPassManager *m_passMgr = nullptr;
};
#endif // RESERVATIONMANAGER_H