Commit e124e116 authored by Volker Krause's avatar Volker Krause

Add country information elements to the timeline

Those are shown before transitioning into a country that differs in one or
more properties (driving side, power plugs, currency, etc) from the your
home country. So far only driving side is actually implemented though.
parent 4b701a10
[
{
"@context": "http://schema.org",
"@type": "FlightReservation",
"reservationFor": {
"@type": "Flight",
"airline": {
"@type": "Airline",
"iataCode": "UA",
"name": "United Airlines"
},
"arrivalAirport": {
"@type": "Airport",
"address": {
"@type": "PostalAddress",
"addressCountry": "GB"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 51.477500915527344,
"longitude": -0.4613890051841736
},
"iataCode": "LHR",
"name": "Heathrow"
},
"arrivalTime": {
"@type": "QDateTime",
"@value": "2017-01-04T08:25:00+00:00",
"timezone": "Europe/London"
},
"departureAirport": {
"@type": "Airport",
"address": {
"@type": "PostalAddress",
"addressCountry": "DE"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 52.55970001220703,
"longitude": 13.287799835205078
},
"iataCode": "TXL",
"name": "Tegel"
},
"departureDay": "2017-01-04",
"departureTime": {
"@type": "QDateTime",
"@value": "2017-01-04T07:20:00+01:00",
"timezone": "Europe/Berlin"
},
"flightNumber": "9668"
},
"reservationNumber": "XXX007"
},
{
"@context": "http://schema.org",
"@type": "FlightReservation",
"reservationFor": {
"@type": "Flight",
"airline": {
"@type": "Airline",
"iataCode": "UA",
"name": "United Airlines"
},
"arrivalAirport": {
"@type": "Airport",
"address": {
"@type": "PostalAddress",
"addressCountry": "US"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 37.618900299072266,
"longitude": -122.375
},
"iataCode": "SFO",
"name": "San Francisco International"
},
"arrivalTime": {
"@type": "QDateTime",
"@value": "2017-01-04T14:45:00-08:00",
"timezone": "America/Los_Angeles"
},
"departureAirport": {
"@type": "Airport",
"address": {
"@type": "PostalAddress",
"addressCountry": "GB"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 51.477500915527344,
"longitude": -0.4613890051841736
},
"iataCode": "LHR",
"name": "Heathrow"
},
"departureDay": "2017-01-04",
"departureTime": {
"@type": "QDateTime",
"@value": "2017-01-04T11:40:00+00:00",
"timezone": "Europe/London"
},
"flightNumber": "900"
},
"reservationNumber": "XXX007"
}
]
......@@ -15,6 +15,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <countryinformation.h>
#include <pkpassmanager.h>
#include <reservationmanager.h>
#include <timelinemodel.h>
......@@ -137,6 +138,36 @@ private slots:
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
}
void testCountryInfos()
{
ReservationManager resMgr;
clearReservations(&resMgr);
TimelineModel model;
model.setReservationManager(&resMgr);
QCOMPARE(model.rowCount(), 1);
QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
resMgr.importReservation(QUrl::fromLocalFile(QLatin1String(SOURCE_DIR "/data/flight-txl-lhr-sfo.json")));
QCOMPARE(model.rowCount(), 4); // GB country info, 2 flights, today marker
QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::CountryInfo);
auto countryInfo = model.index(0, 0).data(TimelineModel::CountryInformationRole).value<CountryInformation>();
qDebug() << (int)countryInfo.drivingSide() << countryInfo.drivingSideDiffers();
QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Left);
QCOMPARE(countryInfo.drivingSideDiffers(), true);
QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
// remove the GB flight, should also remove the GB country info
auto resId = model.index(1, 0).data(TimelineModel::ReservationIdRole).toString();
resMgr.removeReservation(resId);
QCOMPARE(model.rowCount(), 2);
QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineModel::Flight);
QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineModel::TodayMarker);
}
};
QTEST_GUILESS_MAIN(TimelineModelTest)
......
set(itinerary_srcs
countryinformation.cpp
pkpassmanager.cpp
reservationmanager.cpp
timelinemodel.cpp
......@@ -57,6 +58,7 @@ qml_lint(
BoardingPass.qml
BusDelegate.qml
BusPage.qml
CountryInfoDelegate.qml
DetailsPage.qml
FlightDelegate.qml
FlightPage.qml
......
/*
Copyright (C) 2018 Volker Krause <vkrause@kde.org>
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 <http://www.gnu.org/licenses/>.
*/
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.kitinerary 1.0 as KItinerary
import org.kde.itinerary 1.0
import "." as App
Kirigami.AbstractCard {
id: root
property var countryInfo;
header: Rectangle {
id: headerBackground
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
color: Kirigami.Theme.backgroundColor
implicitHeight: headerLayout.implicitHeight + Kirigami.Units.largeSpacing * 2
anchors.leftMargin: -root.leftPadding
anchors.topMargin: -root.topPadding
anchors.rightMargin: -root.rightPadding
RowLayout {
id: headerLayout
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
QQC2.Label {
text: qsTr("⚠ Entering %1").arg(countryInfo.isoCode) // TODO human readable country name
color: Kirigami.Theme.negativeTextColor
}
}
}
contentItem: ColumnLayout {
id: topLayout
QQC2.Label {
text: countryInfo.drivingSide == KItinerary.KnowledgeDb.DrivingSide.Right ?
qsTr("People are driving on the right side.") :
qsTr("People are driving on the wrong side.")
color: Kirigami.Theme.negativeTextColor
visible: countryInfo.drivingSideDiffers
}
}
}
......@@ -97,6 +97,12 @@ Kirigami.ScrollablePage {
}
}
}
Component {
id: countryInfoDelegate
App.CountryInfoDelegate {
countryInfo: modelData.countryInformation
}
}
Kirigami.CardsListView {
id: listView
......@@ -115,6 +121,7 @@ Kirigami.ScrollablePage {
case TimelineModel.BusTrip: return busDelegate;
case TimelineModel.Restaurant: return restaurantDelegate;
case TimelineModel.TodayMarker: return todayDelegate;
case TimelineModel.CountryInfo: return countryInfoDelegate;
}
}
}
......
/*
Copyright (C) 2018 Volker Krause <vkrause@kde.org>
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 <http://www.gnu.org/licenses/>.
*/
#include "countryinformation.h"
#include <QDebug>
using namespace KItinerary;
CountryInformation::CountryInformation()
{
}
CountryInformation::~CountryInformation() = default;
bool CountryInformation::operator==(const CountryInformation& other) const
{
return m_drivingSide == other.m_drivingSide || m_drivingSide == KnowledgeDb::DrivingSide::Unknown || other.m_drivingSide == KnowledgeDb::DrivingSide::Unknown;
}
QString CountryInformation::isoCode() const
{
return m_isoCode;
}
void CountryInformation::setIsoCode(const QString& isoCode)
{
if (m_isoCode == isoCode) {
return;
}
m_isoCode = isoCode;
const auto id = KnowledgeDb::CountryId{isoCode};
if (!id.isValid()) {
return;
}
const auto countryRecord = KnowledgeDb::countryForId(id);
setDrivingSide(countryRecord.drivingSide);
}
KnowledgeDb::DrivingSide CountryInformation::drivingSide() const
{
return m_drivingSide;
}
void CountryInformation::setDrivingSide(KnowledgeDb::DrivingSide drivingSide)
{
if (m_drivingSide == drivingSide || drivingSide == KnowledgeDb::DrivingSide::Unknown) {
return;
}
if (m_drivingSide != KnowledgeDb::DrivingSide::Unknown) {
m_drivingSideDiffers = true;
}
m_drivingSide = drivingSide;
}
bool CountryInformation::drivingSideDiffers() const
{
return m_drivingSideDiffers;
}
/*
Copyright (C) 2018 Volker Krause <vkrause@kde.org>
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 <http://www.gnu.org/licenses/>.
*/
#ifndef COUNTRYINFORMATION_H
#define COUNTRYINFORMATION_H
#include <KItinerary/CountryDb>
#include <QMetaType>
#include <QString>
/** Data for country information elements in the timeline model. */
class CountryInformation
{
Q_GADGET
Q_PROPERTY(QString isoCode READ isoCode)
Q_PROPERTY(KItinerary::KnowledgeDb::DrivingSide drivingSide READ drivingSide)
/** This indicates that the driving side information changed and needs to be displayed. */
Q_PROPERTY(bool drivingSideDiffers READ drivingSideDiffers)
public:
CountryInformation();
~CountryInformation();
bool operator==(const CountryInformation &other) const;
QString isoCode() const;
void setIsoCode(const QString &isoCode);
KItinerary::KnowledgeDb::DrivingSide drivingSide() const;
bool drivingSideDiffers() const;
private:
void setDrivingSide(KItinerary::KnowledgeDb::DrivingSide drivingSide);
QString m_isoCode;
KItinerary::KnowledgeDb::DrivingSide m_drivingSide = KItinerary::KnowledgeDb::DrivingSide::Unknown;
bool m_drivingSideDiffers = false;
};
Q_DECLARE_METATYPE(CountryInformation)
#endif // COUNTRYINFORMATION_H
......@@ -25,6 +25,7 @@
#include "pkpassimageprovider.h"
#include "reservationmanager.h"
#include <KItinerary/CountryDb>
#include <KItinerary/Ticket>
#include <KPkPass/Field>
......@@ -110,7 +111,9 @@ int main(int argc, char **argv)
qmlRegisterUncreatableType<KPkPass::Barcode>("org.kde.pkpass", 1, 0, "Barcode", {});
qmlRegisterUncreatableType<KPkPass::Field>("org.kde.pkpass", 1, 0, "Field", {});
qRegisterMetaType<KItinerary::KnowledgeDb::DrivingSide>();
qmlRegisterUncreatableType<KItinerary::Ticket>("org.kde.kitinerary", 1, 0, "Ticket", {});
qmlRegisterUncreatableMetaObject(KItinerary::KnowledgeDb::staticMetaObject, "org.kde.kitinerary", 1, 0, "KnowledgeDb", {});
qmlRegisterUncreatableType<TimelineModel>("org.kde.itinerary", 1, 0, "TimelineModel", {});
qmlRegisterSingletonType<Localizer>("org.kde.itinerary", 1, 0, "Localizer", [](QQmlEngine*, QJSEngine*) -> QObject*{
......
......@@ -4,6 +4,7 @@
<file>BoardingPass.qml</file>
<file>BusDelegate.qml</file>
<file>BusPage.qml</file>
<file>CountryInfoDelegate.qml</file>
<file>DetailsPage.qml</file>
<file>FlightDelegate.qml</file>
<file>FlightPage.qml</file>
......
......@@ -16,10 +16,12 @@
*/
#include "timelinemodel.h"
#include "countryinformation.h"
#include "pkpassmanager.h"
#include "reservationmanager.h"
#include <KItinerary/BusTrip>
#include <KItinerary/CountryDb>
#include <KItinerary/Flight>
#include <KItinerary/JsonLdDocument>
#include <KItinerary/Organization>
......@@ -34,6 +36,8 @@
#include <QDebug>
#include <QLocale>
#include <cassert>
using namespace KItinerary;
static bool needsSplitting(const QVariant &res)
......@@ -75,8 +79,9 @@ 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())
if (passTypeId.isEmpty() || serialNum.isEmpty()) {
return {};
}
return passTypeId + QLatin1Char('/') + QString::fromUtf8(serialNum.toUtf8().toBase64(QByteArray::Base64UrlEncoding));
}
......@@ -90,6 +95,26 @@ static TimelineModel::ElementType elementType(const QVariant &res)
return {};
}
static QString destinationCountry(const QVariant &res)
{
if (JsonLd::isA<FlightReservation>(res)) {
return res.value<FlightReservation>().reservationFor().value<Flight>().arrivalAirport().address().addressCountry();
}
if (JsonLd::isA<TrainReservation>(res)) {
return res.value<TrainReservation>().reservationFor().value<TrainTrip>().arrivalStation().address().addressCountry();
}
if (JsonLd::isA<LodgingReservation>(res)) {
return res.value<LodgingReservation>().reservationFor().value<LodgingBusiness>().address().addressCountry();
}
if (JsonLd::isA<BusReservation>(res)) {
return res.value<BusReservation>().reservationFor().value<BusTrip>().arrivalStation().address().addressCountry();
}
if (JsonLd::isA<FoodEstablishmentReservation>(res)) {
return res.value<FoodEstablishmentReservation>().reservationFor().value<FoodEstablishment>().address().addressCountry();
}
return {};
}
TimelineModel::TimelineModel(QObject *parent)
: QAbstractListModel(parent)
{
......@@ -109,13 +134,13 @@ void TimelineModel::setReservationManager(ReservationManager* 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});
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{resId, {}, relevantDateTime(res, SelfContained), elementType(res), SelfContained});
}
}
m_elements.push_back(Element{{}, QDateTime(QDate::currentDate(), QTime(0, 0)), TodayMarker, 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;
});
......@@ -123,20 +148,24 @@ void TimelineModel::setReservationManager(ReservationManager* mgr)
connect(mgr, &ReservationManager::reservationUpdated, this, &TimelineModel::reservationUpdated);
connect(mgr, &ReservationManager::reservationRemoved, this, &TimelineModel::reservationRemoved);
endResetModel();
updateInformationElements();
emit todayRowChanged();
}
int TimelineModel::rowCount(const QModelIndex& parent) const
{
if (parent.isValid() || !m_resMgr)
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)
if (!index.isValid() || !m_resMgr) {
return {};
}
const auto &elem = m_elements.at(index.row());
const auto res = m_resMgr->reservation(elem.id);
......@@ -147,10 +176,12 @@ QVariant TimelineModel::data(const QModelIndex& index, int role) const
return passId(res);
case SectionHeader:
{
if (elem.dt.isNull())
if (elem.dt.isNull()) {
return {};
if (elem.dt.date() == QDate::currentDate())
}
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:
......@@ -168,6 +199,8 @@ QVariant TimelineModel::data(const QModelIndex& index, int role) const
return elem.dt.date() == QDate::currentDate();
case ElementRangeRole:
return elem.rangeType;
case CountryInformationRole:
return elem.content;
}
return {};
}
......@@ -184,12 +217,13 @@ QHash<int, QByteArray> TimelineModel::roleNames() const
names.insert(TodayEmptyRole, "isTodayEmpty");
names.insert(IsTodayRole, "isToday");
names.insert(ElementRangeRole, "rangeType");
names.insert(CountryInformationRole, "countryInformation");
return names;
}
int TimelineModel::todayRow() const
{
const auto it = std::find_if(m_elements.begin(), m_elements.end(), [](const Element &e) { return e.id.isEmpty(); });
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);
}
......@@ -197,12 +231,13 @@ 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});
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});
insertElement(Element{resId, {}, relevantDateTime(res, SelfContained), elementType(res), SelfContained});
}
updateInformationElements();
emit todayRowChanged();
}
......@@ -226,6 +261,8 @@ void TimelineModel::reservationUpdated(const QString &resId)
} else {
updateElement(resId, res, SelfContained);
}
updateInformationElements();
}
void TimelineModel::updateElement(const QString &resId, const QVariant &res, TimelineModel::RangeType rangeType)
......@@ -242,7 +279,7 @@ void TimelineModel::updateElement(const QString &resId, const QVariant &res, Tim
beginRemoveRows({}, row, row);
m_elements.erase(it);
endRemoveRows();
insertElement(Element{resId, newDt, elementType(res), rangeType});
insertElement(Element{resId, {}, newDt, elementType(res), rangeType});
} else {
emit dataChanged(index(row, 0), index(row, 0));
}
......@@ -264,4 +301,58 @@ void TimelineModel::reservationRemoved(const QString &resId)
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:
continue;