diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 87d2464..483e05f 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,111 +1,111 @@ set(itinerary_srcs countryinformation.cpp pkpassmanager.cpp reservationmanager.cpp timelinemodel.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 KPim::Itinerary KPim::PkPass KF5::I18n Qt5::Network ) add_executable(itinerary-app main.cpp applicationcontroller.cpp localizer.cpp pkpassimageprovider.cpp settings.cpp qml.qrc ) target_include_directories(itinerary-app PRIVATE ${CMAKE_BINARY_DIR}) target_link_libraries(itinerary-app PRIVATE itinerary - itinerary-weather Qt5::Quick KF5::Contacts ) if (ANDROID) # explicitly add runtime dependencies and transitive link dependencies, # so androiddeployqt picks them up target_link_libraries(itinerary-app PRIVATE KF5::Archive KF5::Kirigami2 Qt5::AndroidExtras Qt5::Svg KF5::Prison ) kirigami_package_breeze_icons(ICONS document-open edit-delete go-next-symbolic map-symbolic settings-configure view-calendar-day view-refresh weather-clear weather-clear-night weather-few-clouds weather-few-clouds-night weather-clouds weather-clouds-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-fog weather-showers weather-showers-scattered weather-hail weather-snow weather-snow-scattered weather-storm ) else () target_link_libraries(itinerary-app PRIVATE Qt5::Positioning) set_target_properties(itinerary-app PROPERTIES OUTPUT_NAME "itinerary") endif() qml_lint( main.qml BoardingPass.qml BusDelegate.qml BusPage.qml CountryInfoDelegate.qml DetailsPage.qml FlightDelegate.qml FlightPage.qml HotelDelegate.qml HotelPage.qml PkPassPage.qml PlaceDelegate.qml RestaurantDelegate.qml RestaurantPage.qml SettingsPage.qml TicketTokenDelegate.qml TimelineDelegate.qml TimelinePage.qml TouristAttractionDelegate.qml TrainDelegate.qml TrainPage.qml WeatherForecastDelegate.qml ) install(TARGETS itinerary-app ${INSTALL_TARGETS_DEFAULT_ARGS}) if (NOT ANDROID) install(PROGRAMS org.kde.itinerary.desktop DESTINATION ${KDE_INSTALL_APPDIR}) endif() diff --git a/src/app/main.cpp b/src/app/main.cpp index 9960c7f..a5ee8fd 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,150 +1,151 @@ /* 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 "countryinformation.h" #include "localizer.h" #include "pkpassmanager.h" #include "timelinemodel.h" #include "pkpassimageprovider.h" #include "reservationmanager.h" #include "settings.h" #include #include #include #include #include #include #include #ifdef Q_OS_ANDROID #include #include #endif #include #include #include #include void handleViewIntent(PkPassManager *passMgr) { #ifdef Q_OS_ANDROID // handle opened files const auto activity = QtAndroid::androidActivity(); if (!activity.isValid()) return; const auto intent = activity.callObjectMethod("getIntent", "()Landroid/content/Intent;"); if (!intent.isValid()) return; const auto uri = intent.callObjectMethod("getData", "()Landroid/net/Uri;"); if (!uri.isValid()) return; const auto scheme = uri.callObjectMethod("getScheme", "()Ljava/lang/String;"); if (scheme.toString() == QLatin1String("content")) { const auto tmpFile = activity.callObjectMethod("receiveContent", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object()); passMgr->importPassFromTempFile(tmpFile.toString()); } else if (scheme.toString() == QLatin1String("file")) { const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;"); passMgr->importPass(QUrl(uriStr.toString())); } else { const auto uriStr = uri.callObjectMethod("toString", "()Ljava/lang/String;"); qCWarning(Log) << "Unknown intent URI:" << uriStr.toString(); } #else Q_UNUSED(passMgr); #endif } #ifdef Q_OS_ANDROID Q_DECL_EXPORT #endif int main(int argc, char **argv) { QCoreApplication::setApplicationName(QStringLiteral("kde-itinerary")); QCoreApplication::setOrganizationName(QStringLiteral("KDE")); QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); QCoreApplication::setApplicationVersion(QStringLiteral(ITINERARY_VERSION_STRING)); QGuiApplication::setApplicationDisplayName(QStringLiteral("KDE Itinerary")); // TODO i18n QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QGuiApplication app(argc, argv); QGuiApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("map-globe"))); QCommandLineParser parser; parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument(QStringLiteral("pass"), QStringLiteral("PkPass file to import.")); parser.process(app); PkPassManager passMgr; ReservationManager resMgr; resMgr.setPkPassManager(&passMgr); TimelineModel timelineModel; timelineModel.setReservationManager(&resMgr); ApplicationController appController; Settings settings; WeatherForecastManager weatherForecastMgr; weatherForecastMgr.setAllowNetworkAccess(settings.weatherForecastEnabled()); QObject::connect(&settings, &Settings::weatherForecastEnabledChanged, &weatherForecastMgr, &WeatherForecastManager::setAllowNetworkAccess); + timelineModel.setWeatherForecastManager(&weatherForecastMgr); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Barcode", {}); qmlRegisterUncreatableType("org.kde.pkpass", 1, 0, "Field", {}); 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", {}); qmlRegisterUncreatableType("org.kde.itinerary", 1, 0, "TimelineModel", {}); qmlRegisterSingletonType("org.kde.itinerary", 1, 0, "Localizer", [](QQmlEngine*, QJSEngine*) -> QObject*{ return new Localizer; }); QQmlApplicationEngine engine; engine.addImageProvider(QStringLiteral("org.kde.pkpass"), new PkPassImageProvider(&passMgr)); engine.rootContext()->setContextProperty(QStringLiteral("_pkpassManager"), &passMgr); engine.rootContext()->setContextProperty(QStringLiteral("_reservationManager"), &resMgr); engine.rootContext()->setContextProperty(QStringLiteral("_timelineModel"), &timelineModel); engine.rootContext()->setContextProperty(QStringLiteral("_appController"), &appController); engine.rootContext()->setContextProperty(QStringLiteral("_settings"), &settings); engine.load(QStringLiteral(":/main.qml")); for (const auto &file : parser.positionalArguments()) { if (file.endsWith(QLatin1String(".pkpass"))) passMgr.importPass(QUrl::fromLocalFile(file)); else resMgr.importReservation(QUrl::fromLocalFile(file)); } handleViewIntent(&passMgr); return app.exec(); } diff --git a/src/app/timelinemodel.cpp b/src/app/timelinemodel.cpp index e19a0a7..54879e7 100644 --- a/src/app/timelinemodel.cpp +++ b/src/app/timelinemodel.cpp @@ -1,349 +1,413 @@ /* 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 "timelinemodel.h" #include "countryinformation.h" #include "pkpassmanager.h" #include "reservationmanager.h" +#include + #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; static bool needsSplitting(const QVariant &res) { return res.userType() == qMetaTypeId(); } static QDateTime relevantDateTime(const QVariant &res, TimelineModel::RangeType range) { if (range == TimelineModel::RangeBegin || range == TimelineModel::SelfContained) { return SortUtil::startDateTime(res); } if (range == TimelineModel::RangeEnd) { return SortUtil::endtDateTime(res); } return {}; } static QString passId(const QVariant &res) { const auto passTypeId = JsonLdDocument::readProperty(res, "pkpassPassTypeIdentifier").toString(); const auto serialNum = JsonLdDocument::readProperty(res, "pkpassSerialNumber").toString(); if (passTypeId.isEmpty() || serialNum.isEmpty()) { return {}; } return passTypeId + QLatin1Char('/') + QString::fromUtf8(serialNum.toUtf8().toBase64(QByteArray::Base64UrlEncoding)); } static TimelineModel::ElementType elementType(const QVariant &res) { if (JsonLd::isA(res)) { return TimelineModel::Flight; } if (JsonLd::isA(res)) { return TimelineModel::Hotel; } if (JsonLd::isA(res)) { return TimelineModel::TrainTrip; } if (JsonLd::isA(res)) { return TimelineModel::BusTrip; } if (JsonLd::isA(res)) { return TimelineModel::Restaurant; } if (JsonLd::isA(res)) { return TimelineModel::TouristAttraction; } return {}; } static QString destinationCountry(const QVariant &res) { if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalAirport().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalStation().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().arrivalStation().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().reservationFor().value().address().addressCountry(); } if (JsonLd::isA(res)) { return res.value().touristAttraction().address().addressCountry(); } return {}; } TimelineModel::TimelineModel(QObject *parent) : QAbstractListModel(parent) { } TimelineModel::~TimelineModel() = default; void TimelineModel::setReservationManager(ReservationManager* mgr) { beginResetModel(); m_resMgr = mgr; for (const auto &resId : mgr->reservations()) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { m_elements.push_back(Element{resId, {}, relevantDateTime(res, RangeBegin), elementType(res), RangeBegin}); m_elements.push_back(Element{resId, {}, relevantDateTime(res, RangeEnd), elementType(res), RangeEnd}); } else { m_elements.push_back(Element{resId, {}, relevantDateTime(res, SelfContained), elementType(res), SelfContained}); } } m_elements.push_back(Element{{}, {}, QDateTime(QDate::currentDate(), QTime(0, 0)), TodayMarker, SelfContained}); std::sort(m_elements.begin(), m_elements.end(), [](const Element &lhs, const Element &rhs) { return lhs.dt < rhs.dt; }); connect(mgr, &ReservationManager::reservationAdded, this, &TimelineModel::reservationAdded); connect(mgr, &ReservationManager::reservationUpdated, this, &TimelineModel::reservationUpdated); connect(mgr, &ReservationManager::reservationRemoved, this, &TimelineModel::reservationRemoved); endResetModel(); updateInformationElements(); emit todayRowChanged(); } +void TimelineModel::setWeatherForecastManager(WeatherForecastManager* mgr) +{ + m_weatherMgr = mgr; + insertWeatherElements(); + connect(m_weatherMgr, &WeatherForecastManager::forecastUpdated, this, &TimelineModel::updateWeatherElements); +} + int TimelineModel::rowCount(const QModelIndex& parent) const { if (parent.isValid() || !m_resMgr) { return 0; } return m_elements.size(); } QVariant TimelineModel::data(const QModelIndex& index, int role) const { if (!index.isValid() || !m_resMgr) { return {}; } const auto &elem = m_elements.at(index.row()); const auto res = m_resMgr->reservation(elem.id); switch (role) { case PassIdRole: return passId(res); case SectionHeader: { if (elem.dt.isNull()) { return {}; } if (elem.dt.date() == QDate::currentDate()) { return i18n("Today"); } return i18nc("weekday, date", "%1, %2", QLocale().dayName(elem.dt.date().dayOfWeek(), QLocale::LongFormat), QLocale().toString(elem.dt.date(), QLocale::ShortFormat)); } case ReservationRole: return res; case ReservationIdRole: return elem.id; case ElementTypeRole: return elem.elementType; case TodayEmptyRole: if (elem.elementType == TodayMarker) { return index.row() == (int)(m_elements.size() - 1) || m_elements.at(index.row() + 1).dt.date() > QDate::currentDate(); } return {}; case IsTodayRole: return elem.dt.date() == QDate::currentDate(); case ElementRangeRole: return elem.rangeType; case CountryInformationRole: + case WeatherForecastRole: return elem.content; } return {}; } QHash TimelineModel::roleNames() const { auto names = QAbstractListModel::roleNames(); names.insert(PassIdRole, "passId"); names.insert(SectionHeader, "sectionHeader"); names.insert(ReservationRole, "reservation"); names.insert(ReservationIdRole, "reservationId"); names.insert(ElementTypeRole, "type"); names.insert(TodayEmptyRole, "isTodayEmpty"); names.insert(IsTodayRole, "isToday"); names.insert(ElementRangeRole, "rangeType"); names.insert(CountryInformationRole, "countryInformation"); + names.insert(WeatherForecastRole, "weatherForecast"); return names; } int TimelineModel::todayRow() const { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [](const Element &e) { return e.elementType == TodayMarker; }); return std::distance(m_elements.begin(), it); } void TimelineModel::reservationAdded(const QString &resId) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { insertElement(Element{resId, {}, relevantDateTime(res, RangeBegin), elementType(res), RangeBegin}); insertElement(Element{resId, {}, relevantDateTime(res, RangeEnd), elementType(res), RangeEnd}); } else { insertElement(Element{resId, {}, relevantDateTime(res, SelfContained), elementType(res), SelfContained}); } updateInformationElements(); emit todayRowChanged(); } void TimelineModel::insertElement(Element &&elem) { auto it = std::lower_bound(m_elements.begin(), m_elements.end(), elem.dt, [](const Element &lhs, const QDateTime &rhs) { return lhs.dt < rhs; }); auto index = std::distance(m_elements.begin(), it); beginInsertRows({}, index, index); m_elements.insert(it, std::move(elem)); endInsertRows(); } void TimelineModel::reservationUpdated(const QString &resId) { const auto res = m_resMgr->reservation(resId); if (needsSplitting(res)) { updateElement(resId, res, RangeBegin); updateElement(resId, res, RangeEnd); } else { updateElement(resId, res, SelfContained); } updateInformationElements(); } void TimelineModel::updateElement(const QString &resId, const QVariant &res, TimelineModel::RangeType rangeType) { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId, rangeType](const Element &e) { return e.id == resId && e.rangeType == rangeType; }); if (it == m_elements.end()) { return; } const auto row = std::distance(m_elements.begin(), it); const auto newDt = relevantDateTime(res, rangeType); if ((*it).dt != newDt) { // element moved beginRemoveRows({}, row, row); m_elements.erase(it); endRemoveRows(); insertElement(Element{resId, {}, newDt, elementType(res), rangeType}); } else { emit dataChanged(index(row, 0), index(row, 0)); } } void TimelineModel::reservationRemoved(const QString &resId) { const auto it = std::find_if(m_elements.begin(), m_elements.end(), [resId](const Element &e) { return e.id == resId; }); if (it == m_elements.end()) { return; } const auto isSplit = (*it).rangeType == RangeBegin; const auto row = std::distance(m_elements.begin(), it); beginRemoveRows({}, row, row); m_elements.erase(it); endRemoveRows(); emit todayRowChanged(); if (isSplit) { reservationRemoved(resId); } updateInformationElements(); } void TimelineModel::updateInformationElements() { // the country information is shown before transitioning into a country that // differs in one or more properties from the home country and we where that // differences is introduced by the transition CountryInformation homeCountry; homeCountry.setIsoCode(QLatin1String("DE")); // TODO configurable home country auto previousCountry = homeCountry; for (auto it = m_elements.begin(); it != m_elements.end(); ++it) { switch ((*it).elementType) { case TodayMarker: + case WeatherForecast: it = erasePreviousCountyInfo(it); continue; case CountryInfo: previousCountry = (*it).content.value(); it = erasePreviousCountyInfo(it); // purge multiple consecutive country info elements continue; default: break; } auto newCountry = homeCountry; newCountry.setIsoCode(destinationCountry(m_resMgr->reservation((*it).id))); if (newCountry == previousCountry) { continue; } if (newCountry == homeCountry) { assert(it != m_elements.begin()); // previousCountry == homeCountry in this case // purge outdated country info element it = erasePreviousCountyInfo(it); previousCountry = newCountry; continue; } // add new country info element auto row = std::distance(m_elements.begin(), it); beginInsertRows({}, row, row); it = m_elements.insert(it, Element{{}, QVariant::fromValue(newCountry), (*it).dt, CountryInfo, SelfContained}); endInsertRows(); previousCountry = newCountry; } + + insertWeatherElements(); } std::vector::iterator TimelineModel::erasePreviousCountyInfo(std::vector::iterator it) { if (it == m_elements.begin()) { return it; } auto it2 = it; --it2; if ((*it2).elementType == CountryInfo) { - auto row = std::distance(m_elements.begin(), it2); + const auto row = std::distance(m_elements.begin(), it2); beginRemoveRows({}, row, row); it = m_elements.erase(it2); endRemoveRows(); } return it; } + +void TimelineModel::insertWeatherElements() +{ + if (!m_weatherMgr) { + return; + } + + auto it = std::find_if(m_elements.begin(), m_elements.end(), [](const Element &e) { return e.elementType == TodayMarker; }); + auto date = QDate::currentDate(); + for (; it != m_elements.end() && date < QDate::currentDate().addDays(9);) { + if ((*it).dt.date() < date || (*it).elementType == TodayMarker) { + ++it; + continue; + } + if (date == (*it).dt.date() && (*it).elementType == WeatherForecast) { // weather element already present + date = date.addDays(1); + ++it; + continue; + } + + // TODO determine correct location, merge full time range +// m_weatherMgr->monitorLocation(52, 13.5); + const auto fc = m_weatherMgr->forecast(52, 13.5, QDateTime(date, QTime(12, 0))); + if (!fc.isValid()) { + date = date.addDays(1); + ++it; + continue; + } + + const auto row = std::distance(m_elements.begin(), it); + beginInsertRows({}, row, row); + it = m_elements.insert(it, Element{{}, fc, QDateTime(date, QTime()), WeatherForecast, SelfContained}); + endInsertRows(); + date = date.addDays(1); + } +} + +void TimelineModel::updateWeatherElements() +{ + for (auto it = m_elements.begin(); it != m_elements.end(); ++it) { + if ((*it).elementType == WeatherForecast) { + // TODO see above + (*it).content = m_weatherMgr->forecast(52, 13.5, QDateTime((*it).dt.date(), QTime(12, 0))); + const auto idx = index(std::distance(m_elements.begin(), it), 0); + emit dataChanged(idx, idx); + } + } + + insertWeatherElements(); +} diff --git a/src/app/timelinemodel.h b/src/app/timelinemodel.h index f2ff721..bd60d1a 100644 --- a/src/app/timelinemodel.h +++ b/src/app/timelinemodel.h @@ -1,102 +1,108 @@ /* 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 TIMELINEMODEL_H #define TIMELINEMODEL_H #include #include class ReservationManager; +class WeatherForecastManager; class TimelineModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int todayRow READ todayRow NOTIFY todayRowChanged) public: enum Role { PassIdRole = Qt::UserRole + 1, SectionHeader, ReservationRole, ReservationIdRole, ElementTypeRole, TodayEmptyRole, IsTodayRole, ElementRangeRole, - CountryInformationRole + CountryInformationRole, + WeatherForecastRole }; enum ElementType { Undefined, Flight, TrainTrip, BusTrip, Hotel, Restaurant, TouristAttraction, TodayMarker, CountryInfo, WeatherForecast }; Q_ENUM(ElementType) // indicates whether an element is self-contained or the beginning/end of a longer timespan/range enum RangeType { SelfContained, RangeBegin, RangeEnd }; Q_ENUM(RangeType) explicit TimelineModel(QObject *parent = nullptr); ~TimelineModel(); void setReservationManager(ReservationManager *mgr); + void setWeatherForecastManager(WeatherForecastManager *mgr); QVariant data(const QModelIndex& index, int role) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QHash roleNames() const override; int todayRow() const; signals: void todayRowChanged(); private: struct Element { QString id; // reservation id QVariant content; // non-reservation content QDateTime dt; // relevant date/time ElementType elementType; RangeType rangeType; }; void reservationAdded(const QString &resId); void insertElement(Element &&elem); void reservationUpdated(const QString &resId); void updateElement(const QString &resId, const QVariant &res, RangeType rangeType); void reservationRemoved(const QString &resId); void updateInformationElements(); std::vector::iterator erasePreviousCountyInfo(std::vector::iterator it); + void insertWeatherElements(); + void updateWeatherElements(); ReservationManager *m_resMgr = nullptr; + WeatherForecastManager *m_weatherMgr = nullptr; std::vector m_elements; }; #endif // TIMELINEMODEL_H