summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJuho Annunen <juho.annunen@qt.io>2018-04-27 09:34:46 +0300
committerTeemu Holappa <teemu.holappa@qt.io>2018-05-07 09:32:28 +0000
commitd0833655c54c05925bd214f545bc904d53860fb3 (patch)
tree13fcd7b9032c602da56b9b594f0d99827e605e95
parentda6035233fc3bcfbc4a3d65075ee4099349c09d0 (diff)
Add new Ebike demo to basicsuite
Task-number: QTBUG-67917 Change-Id: I88e783ff7bcc530b13edfd3e21565953433293dc Reviewed-by: Teemu Holappa <teemu.holappa@qt.io>
-rw-r--r--basicsuite/demos.xml8
-rw-r--r--basicsuite/ebike-ui/.gitignore73
-rw-r--r--basicsuite/ebike-ui/BikeInfoTab.qml483
-rw-r--r--basicsuite/ebike-ui/BikeStyle/Colors.qml125
-rw-r--r--basicsuite/ebike-ui/BikeStyle/UILayout.qml257
-rw-r--r--basicsuite/ebike-ui/BikeStyle/qmldir2
-rw-r--r--basicsuite/ebike-ui/ClockView.qml72
-rw-r--r--basicsuite/ebike-ui/ColumnSpacer.qml45
-rw-r--r--basicsuite/ebike-ui/ConfigurationDrawer.qml147
-rw-r--r--basicsuite/ebike-ui/ConfigurationItem.qml63
-rw-r--r--basicsuite/ebike-ui/FpsItem.qml77
-rw-r--r--basicsuite/ebike-ui/GeneralTab.qml279
-rw-r--r--basicsuite/ebike-ui/IconifiedTabButton.qml70
-rw-r--r--basicsuite/ebike-ui/LightsBox.qml81
-rw-r--r--basicsuite/ebike-ui/MainPage.qml94
-rw-r--r--basicsuite/ebike-ui/ModeBox.qml101
-rw-r--r--basicsuite/ebike-ui/MusicPlayer.qml239
-rw-r--r--basicsuite/ebike-ui/NaviBox.qml129
-rw-r--r--basicsuite/ebike-ui/NaviButton.qml83
-rw-r--r--basicsuite/ebike-ui/NaviGuide.qml139
-rw-r--r--basicsuite/ebike-ui/NaviPage.qml691
-rw-r--r--basicsuite/ebike-ui/NaviTripInfo.qml147
-rw-r--r--basicsuite/ebike-ui/SpeedView.qml526
-rw-r--r--basicsuite/ebike-ui/StatsBox.qml157
-rw-r--r--basicsuite/ebike-ui/StatsPage.qml314
-rw-r--r--basicsuite/ebike-ui/StatsRow.qml102
-rw-r--r--basicsuite/ebike-ui/ToggleSwitch.qml66
-rw-r--r--basicsuite/ebike-ui/TripChart.qml236
-rw-r--r--basicsuite/ebike-ui/ViewTab.qml148
-rw-r--r--basicsuite/ebike-ui/app.pro141
-rw-r--r--basicsuite/ebike-ui/brightnesscontroller.cpp91
-rw-r--r--basicsuite/ebike-ui/brightnesscontroller.h69
-rw-r--r--basicsuite/ebike-ui/datamodelplugin/datamodelplugin.pro39
-rw-r--r--basicsuite/ebike-ui/datamodelplugin/plugin.cpp109
-rw-r--r--basicsuite/ebike-ui/datamodelplugin/qmldir2
-rw-r--r--basicsuite/ebike-ui/datastore.cpp300
-rw-r--r--basicsuite/ebike-ui/datastore.h163
-rw-r--r--basicsuite/ebike-ui/ebike-ui.pro7
-rw-r--r--basicsuite/ebike-ui/ebike_en.ts290
-rw-r--r--basicsuite/ebike-ui/ebike_fi.ts290
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-Black.ttfbin0 -> 194544 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-BlackItalic.ttfbin0 -> 190120 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-Bold.ttfbin0 -> 191648 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-BoldItalic.ttfbin0 -> 195604 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-ExtraBold.ttfbin0 -> 191644 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-ExtraBoldItalic.ttfbin0 -> 191664 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-ExtraLight.ttfbin0 -> 196472 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-ExtraLightItalic.ttfbin0 -> 194348 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-Italic.ttfbin0 -> 191936 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-Light.ttfbin0 -> 192116 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-LightItalic.ttfbin0 -> 192128 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-Medium.ttfbin0 -> 192488 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-MediumItalic.ttfbin0 -> 193096 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-Regular.ttfbin0 -> 190648 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-SemiBold.ttfbin0 -> 192268 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-SemiBoldItalic.ttfbin0 -> 192648 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-Thin.ttfbin0 -> 191468 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Montserrat-ThinItalic.ttfbin0 -> 192540 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/OFL.txt93
-rw-r--r--basicsuite/ebike-ui/fonts/Teko-Bold.ttfbin0 -> 305800 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Teko-Light.ttfbin0 -> 301528 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Teko-Medium.ttfbin0 -> 310028 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Teko-Regular.ttfbin0 -> 311780 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/Teko-SemiBold.ttfbin0 -> 317004 bytes
-rw-r--r--basicsuite/ebike-ui/fonts/fontawesome-webfont.ttfbin0 -> 165548 bytes
-rw-r--r--basicsuite/ebike-ui/fpscounter.cpp79
-rw-r--r--basicsuite/ebike-ui/fpscounter.h77
-rw-r--r--basicsuite/ebike-ui/images/arrow_left.pngbin0 -> 227 bytes
-rw-r--r--basicsuite/ebike-ui/images/arrow_right.pngbin0 -> 236 bytes
-rw-r--r--basicsuite/ebike-ui/images/assist.pngbin0 -> 415 bytes
-rw-r--r--basicsuite/ebike-ui/images/battery.pngbin0 -> 292 bytes
-rw-r--r--basicsuite/ebike-ui/images/bike-battery.pngbin0 -> 11471 bytes
-rw-r--r--basicsuite/ebike-ui/images/bike-brakes.pngbin0 -> 10578 bytes
-rw-r--r--basicsuite/ebike-ui/images/bike-chain.pngbin0 -> 10742 bytes
-rw-r--r--basicsuite/ebike-ui/images/bike-frontwheel.pngbin0 -> 11648 bytes
-rw-r--r--basicsuite/ebike-ui/images/bike-gears.pngbin0 -> 10874 bytes
-rw-r--r--basicsuite/ebike-ui/images/bike-light.pngbin0 -> 10457 bytes
-rw-r--r--basicsuite/ebike-ui/images/bike-rearwheel.pngbin0 -> 11595 bytes
-rw-r--r--basicsuite/ebike-ui/images/blue_circle_gps_area.pngbin0 -> 1089 bytes
-rw-r--r--basicsuite/ebike-ui/images/calories.pngbin0 -> 447 bytes
-rw-r--r--basicsuite/ebike-ui/images/checkmark.pngbin0 -> 208 bytes
-rw-r--r--basicsuite/ebike-ui/images/curtain_shadow_handle.pngbin0 -> 1787 bytes
-rw-r--r--basicsuite/ebike-ui/images/curtain_up_arrow.pngbin0 -> 210 bytes
-rw-r--r--basicsuite/ebike-ui/images/fps_icon.pngbin0 -> 1061 bytes
-rw-r--r--basicsuite/ebike-ui/images/info.pngbin0 -> 439 bytes
-rw-r--r--basicsuite/ebike-ui/images/info_selected.pngbin0 -> 368 bytes
-rw-r--r--basicsuite/ebike-ui/images/lights_off.pngbin0 -> 1325 bytes
-rw-r--r--basicsuite/ebike-ui/images/lights_on.pngbin0 -> 1310 bytes
-rw-r--r--basicsuite/ebike-ui/images/list.pngbin0 -> 179 bytes
-rw-r--r--basicsuite/ebike-ui/images/list_selected.pngbin0 -> 159 bytes
-rw-r--r--basicsuite/ebike-ui/images/map-marker.pngbin0 -> 815 bytes
-rw-r--r--basicsuite/ebike-ui/images/map_btn_shadow.pngbin0 -> 1188 bytes
-rw-r--r--basicsuite/ebike-ui/images/map_destination.pngbin0 -> 973 bytes
-rw-r--r--basicsuite/ebike-ui/images/map_locate.pngbin0 -> 366 bytes
-rw-r--r--basicsuite/ebike-ui/images/map_location_arrow.pngbin0 -> 611 bytes
-rw-r--r--basicsuite/ebike-ui/images/map_zoomin.pngbin0 -> 434 bytes
-rw-r--r--basicsuite/ebike-ui/images/map_zoomout.pngbin0 -> 421 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_bear_l.pngbin0 -> 870 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_bear_r.pngbin0 -> 878 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_hard_l.pngbin0 -> 1113 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_hard_r.pngbin0 -> 1057 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_left.pngbin0 -> 848 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_light_left.pngbin0 -> 1090 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_light_right.pngbin0 -> 1113 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_nodir.pngbin0 -> 820 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_right.pngbin0 -> 799 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_straight.pngbin0 -> 379 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_uturn_l.pngbin0 -> 1283 bytes
-rw-r--r--basicsuite/ebike-ui/images/nav_uturn_r.pngbin0 -> 1287 bytes
-rw-r--r--basicsuite/ebike-ui/images/navigation_widget_shadow.pngbin0 -> 3222 bytes
-rw-r--r--basicsuite/ebike-ui/images/nextsong.pngbin0 -> 344 bytes
-rw-r--r--basicsuite/ebike-ui/images/nextsong_pressed.pngbin0 -> 348 bytes
-rw-r--r--basicsuite/ebike-ui/images/ok.pngbin0 -> 425 bytes
-rw-r--r--basicsuite/ebike-ui/images/pause.pngbin0 -> 173 bytes
-rw-r--r--basicsuite/ebike-ui/images/pause_pressed.pngbin0 -> 172 bytes
-rw-r--r--basicsuite/ebike-ui/images/placeholder.pngbin0 -> 1769 bytes
-rw-r--r--basicsuite/ebike-ui/images/play.pngbin0 -> 362 bytes
-rw-r--r--basicsuite/ebike-ui/images/play_pressed.pngbin0 -> 362 bytes
-rw-r--r--basicsuite/ebike-ui/images/prevsong.pngbin0 -> 349 bytes
-rw-r--r--basicsuite/ebike-ui/images/prevsong_pressed.pngbin0 -> 353 bytes
-rw-r--r--basicsuite/ebike-ui/images/search.pngbin0 -> 463 bytes
-rw-r--r--basicsuite/ebike-ui/images/search_cancel.pngbin0 -> 322 bytes
-rw-r--r--basicsuite/ebike-ui/images/settings.pngbin0 -> 630 bytes
-rw-r--r--basicsuite/ebike-ui/images/settings_selected.pngbin0 -> 445 bytes
-rw-r--r--basicsuite/ebike-ui/images/small_input_box_shadow.pngbin0 -> 1141 bytes
-rw-r--r--basicsuite/ebike-ui/images/small_speedometer_arrow.pngbin0 -> 185 bytes
-rw-r--r--basicsuite/ebike-ui/images/small_speedometer_shadow.pngbin0 -> 3137 bytes
-rw-r--r--basicsuite/ebike-ui/images/speed.pngbin0 -> 400 bytes
-rw-r--r--basicsuite/ebike-ui/images/spinner.pngbin0 -> 3470 bytes
-rw-r--r--basicsuite/ebike-ui/images/top_curtain_drag.pngbin0 -> 1200 bytes
-rw-r--r--basicsuite/ebike-ui/images/trip.pngbin0 -> 381 bytes
-rw-r--r--basicsuite/ebike-ui/images/warning.pngbin0 -> 418 bytes
-rw-r--r--basicsuite/ebike-ui/main.qml350
-rw-r--r--basicsuite/ebike-ui/mapbox.cpp89
-rw-r--r--basicsuite/ebike-ui/mapbox.h68
-rw-r--r--basicsuite/ebike-ui/mapboxsuggestions.cpp109
-rw-r--r--basicsuite/ebike-ui/mapboxsuggestions.h89
-rw-r--r--basicsuite/ebike-ui/moment.js4551
-rw-r--r--basicsuite/ebike-ui/mostrecent.bsonbin0 -> 3440 bytes
-rw-r--r--basicsuite/ebike-ui/navigation.cpp97
-rw-r--r--basicsuite/ebike-ui/navigation.h103
-rw-r--r--basicsuite/ebike-ui/preview_l.jpgbin0 -> 28660 bytes
-rw-r--r--basicsuite/ebike-ui/qml.qrc107
-rw-r--r--basicsuite/ebike-ui/qtquickcontrols2.conf15
-rw-r--r--basicsuite/ebike-ui/socketclient.cpp152
-rw-r--r--basicsuite/ebike-ui/socketclient.h86
-rw-r--r--basicsuite/ebike-ui/suggestionsmodel.cpp157
-rw-r--r--basicsuite/ebike-ui/suggestionsmodel.h82
-rw-r--r--basicsuite/ebike-ui/tripdatamodel.cpp135
-rw-r--r--basicsuite/ebike-ui/tripdatamodel.h96
-rw-r--r--basicsuite/shared/main.cpp11
151 files changed, 13001 insertions, 0 deletions
diff --git a/basicsuite/demos.xml b/basicsuite/demos.xml
index e979a4d..4b7c3e7 100644
--- a/basicsuite/demos.xml
+++ b/basicsuite/demos.xml
@@ -93,6 +93,14 @@ Qt for Device Creation comes with Qt Virtual Keyboard - a framework that consist
</description>
</application>
+<application title="E-Bike" location="/data/user/qt/ebike" main="main.qml" icon="/data/user/qt/ebike/preview_l.jpg">
+<description>
+An E-bike instrument cluster concept designed and implemented by Qt.
+
+The entire concept is a testament that Qt brings the HMI designers and Software engineers together by using the same tools, allowing them to fast prototyping through collaborative workflow. In addition to that, Qt is optimized for running on low end SoC even the ones that don't have GPU acceleration for graphics.
+</description>
+</application>
+
<application title="Quick Controls 2" location="/data/user/qt/gallery" main="main.qml" icon="/data/user/qt/gallery/preview_l.jpg">
<description>
The gallery example is a simple application with a drawer menu that contains all the Qt Quick Controls 2.
diff --git a/basicsuite/ebike-ui/.gitignore b/basicsuite/ebike-ui/.gitignore
new file mode 100644
index 0000000..fab7372
--- /dev/null
+++ b/basicsuite/ebike-ui/.gitignore
@@ -0,0 +1,73 @@
+# This file is used to ignore files which are generated
+# ----------------------------------------------------------------------------
+
+*~
+*.autosave
+*.a
+*.core
+*.moc
+*.o
+*.obj
+*.orig
+*.rej
+*.so
+*.so.*
+*_pch.h.cpp
+*_resource.rc
+*.qm
+.#*
+*.*#
+core
+!core/
+tags
+.DS_Store
+.directory
+*.debug
+Makefile*
+*.prl
+*.app
+moc_*.cpp
+ui_*.h
+qrc_*.cpp
+Thumbs.db
+*.res
+*.rc
+/.qmake.cache
+/.qmake.stash
+
+# qtcreator generated files
+*.pro.user*
+
+# xemacs temporary files
+*.flc
+
+# Vim temporary files
+.*.swp
+
+# Visual Studio generated files
+*.ib_pdb_index
+*.idb
+*.ilk
+*.pdb
+*.sln
+*.suo
+*.vcproj
+*vcproj.*.*.user
+*.ncb
+*.sdf
+*.opensdf
+*.vcxproj
+*vcxproj.*
+
+# MinGW generated files
+*.Debug
+*.Release
+
+# Python byte code
+*.pyc
+
+# Binaries
+# --------
+*.dll
+*.exe
+
diff --git a/basicsuite/ebike-ui/BikeInfoTab.qml b/basicsuite/ebike-ui/BikeInfoTab.qml
new file mode 100644
index 0000000..0dc4f27
--- /dev/null
+++ b/basicsuite/ebike-ui/BikeInfoTab.qml
@@ -0,0 +1,483 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import DataStore 1.0
+
+import "./BikeStyle"
+
+Item {
+ property string selectedComponent: "battery"
+ property string bikeImageSource: "images/bike-battery.png"
+ property string componentName: "Battery"
+ property string componentStatusImageSource: "images/ok.png"
+ property color statusLineColor: selectedComponent === "rearwheel" ? Colors.bikeInfoLineWarning : Colors.bikeInfoLineOk
+ property int statusLineLength: 280
+ property string primaryDetails: "Health"
+ property string primaryDetailsValue: "85%"
+ property bool primaryDetailsOk: true
+ property var activeComponent: batteryCircle
+
+ function updateComponent() {
+ if (selectedComponent === "")
+ bikeImageSource = "images/bike.png";
+ else
+ bikeImageSource = "images/bike-" + selectedComponent + ".png";
+
+ if (selectedComponent === "battery") {
+ componentName = "Battery";
+ primaryDetails = "Health";
+ primaryDetailsValue = "85%";
+ primaryDetailsOk = true;
+ statusLineLength = 280;
+ } else if (selectedComponent === "brakes") {
+ componentName = "Brakes";
+ primaryDetails = "Health";
+ primaryDetailsValue = "85%";
+ primaryDetailsOk = true;
+ statusLineLength = 448;
+ } else if (selectedComponent === "chain") {
+ componentName = "Chain";
+ primaryDetails = "Health";
+ primaryDetailsValue = "85%";
+ primaryDetailsOk = true;
+ statusLineLength = 0;
+ } else if (selectedComponent === "gears") {
+ componentName = "Gears";
+ primaryDetails = "Health";
+ primaryDetailsValue = "85%";
+ primaryDetailsOk = true;
+ statusLineLength = 245;
+ } else if (selectedComponent === "light") {
+ componentName = "Light";
+ primaryDetails = "Health";
+ primaryDetailsValue = "85%";
+ primaryDetailsOk = true;
+ statusLineLength = 300;
+ } else if (selectedComponent === "frontwheel") {
+ componentName = "Front wheel";
+ primaryDetails = "Tire pressure";
+ primaryDetailsValue = "6.8 bar / 100 psi";
+ primaryDetailsOk = true;
+ statusLineLength = 340;
+ } else if (selectedComponent === "rearwheel") {
+ componentName = "Rear wheel";
+ primaryDetails = "Tire pressure";
+ primaryDetailsValue = "4.0 bar / 58 psi";
+ primaryDetailsOk = false;
+ statusLineLength = 210;
+ }
+ }
+
+ Text {
+ id: bikeInfoText
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ }
+ height: UILayout.configurationItemHeight
+ width: parent.width
+ text: qsTr("BIKE INFO")
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.configurationTitleSize
+ }
+ color: Colors.tabTitleColor
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ ColumnSpacer {
+ id: spacer
+ anchors.top: bikeInfoText.bottom
+ color: Colors.tabItemBorder
+ }
+
+
+ Image {
+ id: bikeImage
+ anchors {
+ top: spacer.bottom
+ right: parent.right
+ rightMargin: -30
+ }
+ source: bikeImageSource
+ }
+
+ Rectangle {
+ id: brakesCircle
+ width: 2 * UILayout.bikeInfoCircleRadius
+ height: 2 * UILayout.bikeInfoCircleRadius
+ radius: UILayout.bikeInfoCircleRadius
+ anchors {
+ verticalCenter: bikeImage.top
+ verticalCenterOffset: 43
+ horizontalCenter: bikeImage.left
+ horizontalCenterOffset: 243
+ }
+
+ color: "transparent"
+ border.width: UILayout.bikeInfoLineWidth
+ border.color: selectedComponent === "brakes" ? Colors.bikeInfoLineOk : Colors.bikeInfoDeselected
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ activeComponent = brakesCircle;
+ selectedComponent = "brakes"
+ updateComponent()
+ }
+ }
+ }
+
+ Rectangle {
+ id: lightCircle
+ width: 2 * UILayout.bikeInfoCircleRadius
+ height: 2 * UILayout.bikeInfoCircleRadius
+ radius: UILayout.bikeInfoCircleRadius
+ anchors {
+ verticalCenter: bikeImage.top
+ verticalCenterOffset: 77
+ horizontalCenter: bikeImage.left
+ horizontalCenterOffset: 252
+ }
+
+ color: "transparent"
+ border.width: UILayout.bikeInfoLineWidth
+ border.color: selectedComponent === "light" ? Colors.bikeInfoLineOk : Colors.bikeInfoDeselected
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ activeComponent = lightCircle;
+ selectedComponent = "light"
+ updateComponent()
+ }
+ }
+ }
+
+ Rectangle {
+ id: batteryCircle
+ width: 2 * UILayout.bikeInfoCircleRadius
+ height: 2 * UILayout.bikeInfoCircleRadius
+ radius: UILayout.bikeInfoCircleRadius
+ anchors {
+ verticalCenter: bikeImage.top
+ verticalCenterOffset: 106
+ horizontalCenter: bikeImage.left
+ horizontalCenterOffset: 200
+ }
+
+ color: "transparent"
+ border.width: UILayout.bikeInfoLineWidth
+ border.color: selectedComponent === "battery" ? Colors.bikeInfoLineOk : Colors.bikeInfoDeselected
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ activeComponent = batteryCircle;
+ selectedComponent = "battery"
+ updateComponent()
+ }
+ }
+ }
+
+ Rectangle {
+ id: gearsCircle
+ width: 2 * UILayout.bikeInfoCircleRadius
+ height: 2 * UILayout.bikeInfoCircleRadius
+ radius: UILayout.bikeInfoCircleRadius
+ anchors {
+ verticalCenter: bikeImage.top
+ verticalCenterOffset: 143
+ horizontalCenter: bikeImage.left
+ horizontalCenterOffset: 106
+ }
+
+ color: "transparent"
+ border.width: UILayout.bikeInfoLineWidth
+ border.color: selectedComponent === "gears" ? Colors.bikeInfoLineOk : Colors.bikeInfoDeselected
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ activeComponent = gearsCircle;
+ selectedComponent = "gears"
+ updateComponent()
+ }
+ }
+ }
+
+ Rectangle {
+ id: rearWheelCircle
+ width: 2 * UILayout.bikeInfoCircleRadius
+ height: 2 * UILayout.bikeInfoCircleRadius
+ radius: UILayout.bikeInfoCircleRadius
+ anchors {
+ verticalCenter: bikeImage.top
+ verticalCenterOffset: 144
+ horizontalCenter: bikeImage.left
+ horizontalCenterOffset: 56
+ }
+
+ color: "transparent"
+ border.width: UILayout.bikeInfoLineWidth
+ border.color: selectedComponent === "rearwheel" ? Colors.bikeInfoLineWarning : Colors.bikeInfoDeselected
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ activeComponent = rearWheelCircle;
+ selectedComponent = "rearwheel"
+ updateComponent()
+ }
+ }
+
+ Image {
+ anchors.centerIn: parent
+ source: "images/warning.png"
+ }
+ }
+
+ Rectangle {
+ id: frontWheelCircle
+ width: 2 * UILayout.bikeInfoCircleRadius
+ height: 2 * UILayout.bikeInfoCircleRadius
+ radius: UILayout.bikeInfoCircleRadius
+ anchors {
+ verticalCenter: bikeImage.top
+ verticalCenterOffset: 144
+ horizontalCenter: bikeImage.left
+ horizontalCenterOffset: 322
+ }
+
+ color: "transparent"
+ border.width: UILayout.bikeInfoLineWidth
+ border.color: selectedComponent === "frontwheel" ? Colors.bikeInfoLineOk : Colors.bikeInfoDeselected
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ activeComponent = frontWheelCircle;
+ selectedComponent = "frontwheel"
+ updateComponent()
+ }
+ }
+ }
+
+ Canvas {
+ id: slantedLine
+ anchors {
+ left: statusLineHorizontal.right
+ top: statusLineHorizontal.top
+ right: activeComponent.horizontalCenter
+ bottom: activeComponent.verticalCenter
+ }
+
+ onPaint: {
+ var ctx = getContext("2d");
+ ctx.reset();
+
+ // Calculate line length and subtract circle radius
+ var lineLength = Math.sqrt(slantedLine.width * slantedLine.width +
+ slantedLine.height * slantedLine.height);
+ lineLength -= UILayout.bikeInfoCircleRadius;
+
+ // Calculate angle
+ var angle = Math.atan2(slantedLine.height, slantedLine.width);
+
+ // Calculate new endpoints
+ var x = Math.cos(angle) * lineLength;
+ var y = Math.sin(angle) * lineLength;
+
+ ctx.lineCap = "round";
+ ctx.strokeStyle = statusLineColor;
+ ctx.lineWidth = UILayout.bikeInfoLineWidth;
+ ctx.beginPath();
+ ctx.moveTo(0, 1);
+ ctx.lineTo(x, y);
+ ctx.stroke();
+ }
+ }
+
+ Image {
+ id: componentStatusImage
+ anchors {
+ verticalCenter: spacer.bottom
+ verticalCenterOffset: (UILayout.bikeInfoComponentBaselineOffset + UILayout.bikeInfoComponentLineOffset) / 2
+ left: parent.left
+ }
+ source: componentStatusImageSource
+ visible: selectedComponent != ""
+ }
+
+ Text {
+ id: componentNameText
+ anchors {
+ baseline: spacer.bottom
+ baselineOffset: UILayout.bikeInfoComponentBaselineOffset
+ left: componentStatusImage.right
+ leftMargin: 5
+ }
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.bikeInfoComponentHeaderTextSize
+ }
+ color: Colors.bikeInfoComponentHeader
+ text: componentName
+ visible: selectedComponent != ""
+ }
+
+ // The line goes here somehow
+ Rectangle {
+ id: statusLineHorizontal
+ anchors {
+ top: componentNameText.baseline
+ topMargin: UILayout.bikeInfoComponentLineOffset
+ left: parent.left
+ }
+ height: UILayout.bikeInfoLineWidth
+ width: statusLineLength
+ color: statusLineColor
+ visible: selectedComponent != ""
+ }
+
+ Text {
+ id: primaryDetailsText
+ anchors {
+ baseline: statusLineHorizontal.bottom
+ baselineOffset: UILayout.bikeInfoLineDetailsMargin
+ left: parent.left
+ }
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.bikeInfoInfoHeaderTextSize
+ }
+ color: Colors.bikeInfoComponentText
+ text: primaryDetails
+ visible: selectedComponent != ""
+ }
+
+ Text {
+ id: primaryDetailsValueText
+ anchors {
+ baseline: primaryDetailsText.baseline
+ baselineOffset: UILayout.bikeInfoDetailsValueMargin
+ left: parent.left
+ }
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.bikeInfoInfoHeaderTextSize
+ }
+ color: primaryDetailsOk ? Colors.bikeInfoComponentOk : Colors.bikeInfoComponentWarning
+ text: primaryDetailsValue
+ visible: selectedComponent != ""
+ }
+
+ Text {
+ id: lastMaintenanceText
+ anchors {
+ baseline: primaryDetailsValueText.baseline
+ baselineOffset: UILayout.bikeInfoDetailsBaselineMargin
+ left: parent.left
+ }
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.bikeInfoInfoHeaderTextSize
+ }
+ color: Colors.bikeInfoComponentText
+ text: qsTr("Last maintenance")
+ visible: selectedComponent != ""
+ }
+
+ Text {
+ id: lastMaintenanceValueText
+ anchors {
+ baseline: lastMaintenanceText.baseline
+ baselineOffset: UILayout.bikeInfoDetailsValueMargin
+ left: parent.left
+ }
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.bikeInfoInfoHeaderTextSize
+ }
+ color: Colors.bikeInfoComponentOk
+ text: "10/3/2017"
+ visible: selectedComponent != ""
+ }
+
+ Text {
+ id: nextMaintenanceText
+ anchors {
+ baseline: lastMaintenanceValueText.baseline
+ baselineOffset: UILayout.bikeInfoDetailsBaselineMargin
+ left: parent.left
+ }
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.bikeInfoInfoHeaderTextSize
+ }
+ color: Colors.bikeInfoComponentText
+ text: qsTr("Scheduled maintenance")
+ visible: selectedComponent != ""
+ }
+
+ Text {
+ id: nextMaintenanceValueText
+ anchors {
+ baseline: nextMaintenanceText.baseline
+ baselineOffset: UILayout.bikeInfoDetailsValueMargin
+ left: parent.left
+ }
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.bikeInfoInfoHeaderTextSize
+ }
+ color: Colors.bikeInfoComponentOk
+ text: "10/3/2018"
+ visible: selectedComponent != ""
+ }
+}
diff --git a/basicsuite/ebike-ui/BikeStyle/Colors.qml b/basicsuite/ebike-ui/BikeStyle/Colors.qml
new file mode 100644
index 0000000..0086c7e
--- /dev/null
+++ b/basicsuite/ebike-ui/BikeStyle/Colors.qml
@@ -0,0 +1,125 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+pragma Singleton
+import QtQuick 2.9
+
+QtObject {
+ readonly property color clockText: "#d7dbe4"
+ readonly property color clockBackground: "#020611"
+ readonly property color mainBackground: "#1b1f2a"
+ readonly property color separator: "#040809"
+ readonly property color dottedRing: "#52576b"
+ readonly property color distanceText: "#ffffff"
+ readonly property color distanceUnit: "#acafbc"
+ readonly property color speedViewBackgroundCornered: "#020611"
+ readonly property color speedText: "#ffffff"
+ readonly property color speedUnit: "#acafbc"
+ readonly property color averageSpeedText: "#ffffff"
+ readonly property color averageSpeedUnit: "#acafbc"
+ readonly property color assistDistanceText: "#ffffff"
+ readonly property color assistDistanceUnit: "#acafbc"
+ readonly property color modeSelected: "#a0e63e"
+ readonly property color modeUnselected: "#acafbc"
+ readonly property color speedGradientStart: "#fcff2a"
+ readonly property color speedGradientEnd: "#41cd52"
+ readonly property color batteryGradientStart: "#34daea"
+ readonly property color batteryGradientEnd: "#3aeb95"
+ readonly property color assistPowerGradientStart: "#ffd200"
+ readonly property color assistPowerGradientEnd: "#f7971e"
+ readonly property color assistPowerEmpty: "#52576b"
+ readonly property color musicPlayerBackground: "#020611"
+ readonly property color musicPlayerSongText: "#d7dbe4"
+ readonly property color musicPlayerTimeText: "#989ba8"
+ readonly property color naviPageSuggestionBorder: "#a0e63e"
+ readonly property color naviPageSuggestionText: "#1b1f2a"
+ readonly property color naviPageSuggestionsDivider: "#989ba8"
+ readonly property color naviPageIconBackground: "#1b1f2a"
+ readonly property color naviPageIconPressedBackground: "#a0e63e"
+ readonly property color naviPageTripBackground: "#1b1f2a"
+ readonly property color naviPageTripDivider: "#acafbc"
+ readonly property color naviPageGuideBackground: "#1b1f2a"
+ readonly property color naviPageGuideTextColor: "#ffffff"
+ readonly property color naviPageGuideUnitColor: "#acafbc"
+ readonly property color naviPageGuideAddressColor: "#ffffff"
+ readonly property color curtainBackground: "#020611"
+ readonly property color tabBackground: "#020611"
+ readonly property color activeTabBorder: "#a0e63e"
+ readonly property color activeTabIcon: "#ffffff"
+ readonly property color tabIcon: "#989ba8"
+ readonly property color tabTitleColor: "#ffffff"
+ readonly property color tabItemColor: "#989ba8"
+ readonly property color tabItemBorder: "#1b1f2a"
+ readonly property color languageTextColor: "#acafbc"
+ readonly property color checkboxBorderColorChecked: "#a0e63e"
+ readonly property color checkboxBorderColor: "#acafbc"
+ readonly property color checkboxCheckedText: "#ffffff"
+ readonly property color checkboxUncheckedBackground: "#020611"
+ readonly property color sliderBackground: "#52576b"
+ readonly property color sliderInnerBackground: "#000000"
+ readonly property color sliderMinimumValue: "#fcff2a"
+ readonly property color sliderMaximumValue: "#41cd52"
+ readonly property color activeButtonBackground: "#a0e63e"
+ readonly property color inactiveButtonBackground: "#020611"
+ readonly property color activeButtonText: "#020611"
+ readonly property color inactiveButtonText: "#acafbc"
+ readonly property color inactiveButtonBorder: "#acafbc"
+ readonly property color switchOn: "#a0e63e"
+ readonly property color switchOff: "#ffffff"
+ readonly property color switchBackgroundOn: "#4da0e63e"
+ readonly property color switchBackgroundOff: "#52576b"
+ readonly property color bikeInfoDeselected: "#ffffff"
+ readonly property color bikeInfoLineOk: "#a0e63e"
+ readonly property color bikeInfoLineWarning: "#d4145a"
+ readonly property color bikeInfoComponentHeader: "#ffffff"
+ readonly property color bikeInfoComponentText: "#acafbc"
+ readonly property color bikeInfoComponentOk: "#ffffff"
+ readonly property color bikeInfoComponentWarning: "#d4145a"
+ readonly property color chartSpeed: "#a0e63e"
+ readonly property color chartAssistpower: "#34daea"
+ readonly property color chartLegend: "#ffffff"
+ readonly property color chartLabel: "#989ba8"
+ readonly property color chartTimeLabel: "#acafbc"
+ readonly property color chartGridLine: "#111520"
+ readonly property color statsButtonPressed: "#a0e63e"
+ readonly property color statsButtonInactive: "#52576b"
+ readonly property color statsButtonActive: "#848794"
+ readonly property color statsButtonInactiveText: "#52576b"
+ readonly property color statsButtonActiveText: "#ffffff"
+ readonly property color statsDescriptionText: "#989ba8"
+ readonly property color statsValueText: "#ffffff"
+ readonly property color statsSeparator: "#111520"
+}
diff --git a/basicsuite/ebike-ui/BikeStyle/UILayout.qml b/basicsuite/ebike-ui/BikeStyle/UILayout.qml
new file mode 100644
index 0000000..55f6baf
--- /dev/null
+++ b/basicsuite/ebike-ui/BikeStyle/UILayout.qml
@@ -0,0 +1,257 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+pragma Singleton
+import QtQuick 2.9
+
+QtObject {
+ readonly property int clockBaselineMargin: 25
+ readonly property int clockFontSize: 18
+
+ readonly property int statsIconTop: 56
+ readonly property int statsIconLeft: 50
+ readonly property int statsIconSeparator: 20
+ readonly property int statsIconWidth: 40
+ readonly property int statsIconHeight: 40
+ readonly property int statsTextSeparator: 15
+ readonly property int statsTextSize: 20
+ readonly property int statsTextTopOffset: 0
+ readonly property int statsUnitBaselineOffset: 0
+
+ readonly property int lightsIconBottom: 90
+ readonly property int lightsIconLeft: 50
+ readonly property int lightsIconWidth: 80
+ readonly property int lightsIconHeight: 80
+
+ readonly property int naviModeCenterMargin: 90
+
+ readonly property int naviIconTop: 45
+ readonly property int naviIconRight: 50
+ readonly property int naviIconWidth: 80
+ readonly property int naviIconHeight: 80
+ readonly property int naviTextSize: 26
+ readonly property int naviTextMargin: 30
+
+ readonly property int modeBottomOffset: 84
+ readonly property int modeDistance: 50
+ readonly property int modeTextSize: 20
+
+ readonly property int speedViewTop: 78
+ readonly property int speedViewRadius: 150 /* Normal mode */
+ readonly property int speedViewRadiusMinified: 90 /* Minified to corner mode */
+ readonly property int speedViewRadiusEnlarged: 205 /* Enlarged to full screen mode */
+ readonly property int speedViewDots: 96
+ readonly property int speedViewDotsMinified: 48
+ readonly property int speedViewDotsEnlarged: 128
+ readonly property int speedViewCornerLeftMargin: 15
+ readonly property int speedViewCornerBottomMargin: 15
+ readonly property int speedViewInnerRadius: 125
+ readonly property int speedViewInnerRadiusMinified: 65
+ readonly property int speedViewInnerRadiusEnlarged: 185
+ readonly property int speedViewInnerWidth: 12
+ readonly property int speedViewInnerWidthMinified: 8
+ readonly property double speedViewSpeedStart: Math.PI * 0.5 + Math.PI / 30
+ readonly property double speedViewSpeedEnd: Math.PI * 1.5 - Math.PI / 30
+ readonly property double speedViewBatteryStart: Math.PI * 0.5 - Math.PI / 30
+ readonly property double speedViewBatteryEnd: -Math.PI * 0.5 + Math.PI / 30
+ readonly property double speedViewAssistPowerStart: Math.PI * 0.5 + Math.PI / 34
+ readonly property double speedViewAssistPowerEnd: Math.PI * 0.5 - Math.PI / 34
+ readonly property int speedViewAssistPowerWidth: 6
+ readonly property int speedViewAssistPowerRadius: 230
+ readonly property int speedViewAssistPowerBottomOffset: 104
+ readonly property int speedBaselineOffset: 137
+ readonly property int speedBaselineOffsetMinified: 73
+ readonly property int speedBaselineOffsetEnlarged: 155
+ readonly property int speedTextSize: 108
+ readonly property int speedTextSizeMinified: 80
+ readonly property int speedTextSizeEnlarged: 190
+ readonly property int speedUnitsSize: 14
+ readonly property int speedUnitsSizeEnlarged: 18
+ readonly property int speedTextUnitMargin: 24
+ readonly property int speedTextUnitMarginMinified: 18
+ readonly property int speedTextUnitMarginEnlarged: 30
+ readonly property int speedIconsCenterOffset: 71
+ readonly property int speedIconsCenterOffsetEnlarged: 111
+ readonly property int speedInfoTextsOffsetEnlarged: -34
+ readonly property int speedInfoTextsSize: 38
+ readonly property int speedInfoTextsSizeEnlarged: 64
+ readonly property int speedInfoUnitsOffset: 24
+ readonly property int speedInfoUnitsOffsetEnlarged: 34
+ readonly property int averageSpeedIconMargin: -5
+ readonly property int averageSpeedIconWidth: 40
+ readonly property int averageSpeedIconHeight: 40
+ readonly property int assistDistanceIconMargin: -5
+ readonly property int assistDistanceIconWidth: 40
+ readonly property int assistDistanceIconHeight: 40
+ readonly property int assistPowerIconOffset: 49
+ readonly property int assistPowerIconOffsetEnlarged: 100
+ readonly property int assistPowerIconWidth: 30
+ readonly property int assistPowerIconHeight: 30
+ readonly property int assistPowerCircleRadius: 6
+ readonly property int assistPowerCircleOffset: 8
+ readonly property int assistPowerCircleVerticalOffset: 5
+ readonly property int assistPowerCircleTopMargin: 7
+ readonly property int speedometerCornerArrowWidth: 40
+ readonly property int speedometerCornerArrowHeight: 40
+ readonly property int ringValueText: 14
+
+ readonly property int musicPlayerWidth: 260
+ readonly property int musicPlayerHeight: 75
+ readonly property int musicPlayerCorner: 20
+ readonly property int musicPlayerIconWidth: 40
+ readonly property int musicPlayerIconHeight: 40
+ readonly property int musicPlayerIconBottom: 5
+ readonly property int musicPlayerIconSpacing: 50
+ readonly property int musicPlayerTextBottom: 5
+ readonly property int musicPlayerTextSize: 16
+
+ readonly property int naviPageLocationWidth: 300
+ readonly property int naviPageLocationHeight: 40
+ readonly property int naviPageLocationRadius: 20
+ readonly property int naviPageLocationTopMargin: 60
+ readonly property int naviPageLocationLeftPadding: 20
+ readonly property int naviPageIconBackgroundWidth: 50
+ readonly property int naviPageIconBackgroundHeight: 50
+ readonly property int naviPageIconBackgroundRadius: 25
+ readonly property int naviPageIconWidth: 40
+ readonly property int naviPageIconHeight: 40
+ readonly property int naviPageIconTopMargin: 15
+ readonly property int naviPageIconRightMargin: 15
+ readonly property int naviPageIconSpacing: 15
+ readonly property int naviPageSuggestionsOffset: 5
+ readonly property int naviPageSuggestionHeight: 40
+ readonly property int naviPageSuggestionTextSize: 16
+
+ readonly property int naviPageSearchIconWidth: 40
+ readonly property int naviPageSearchIconHeight: 40
+ readonly property int naviPageSearchIconMargin: 5
+ readonly property int naviPageSearchTextSize: 16
+
+ readonly property int naviPageTripWidth: 220
+ readonly property int naviPageTripHeight: 40
+ readonly property int naviPageTripRadius: 20
+ readonly property int naviPageTripDividerWidth: 2
+ readonly property int naviPageTripDividerHeight: 20
+ readonly property int naviPageTripBottomMargin: 15
+ readonly property int naviPageTripSearchMargin: 15
+ readonly property int naviPageTripTotalTextSize: 18
+ readonly property int naviPageTripTotalUnitSize: 18
+
+ readonly property int naviPageGuideRadius: 90
+ readonly property int naviPageGuideRightMargin: 15
+ readonly property int naviPageGuideBottomMargin: 15
+ readonly property int naviPageGuideArrowTopMargin: 30
+ readonly property int naviPageGuideArrowLeftMargin: 50
+ readonly property int naviPageGuideArrowWidth: 80
+ readonly property int naviPageGuideArrowHeight: 80
+ readonly property int naviPageGuideAddressBaselineMargin: 20
+ readonly property int naviPageGuideAddressRightMargin: 20
+ readonly property int naviPageGuideAddressTextSize: 14
+ readonly property int naviPageGuideDistanceBaselineMargin: 20
+ readonly property int naviPageGuideDistanceTextSize: 26
+ readonly property int naviPageGuideUnitTextSize: 26
+
+ readonly property int tabBarTabHeight: 60
+ readonly property int tabBarFontSize: 24
+ readonly property int tabButtonTopMargin: 13
+ readonly property int tabButtonIconWidth: 40
+ readonly property int tabButtonIconHeight: 40
+ readonly property int curtainMargin: 30
+ readonly property int curtainCloseHeight: 30
+ readonly property int configurationItemHeight: 59
+ readonly property int configurationItemSeparator: 1
+ readonly property int configurationTextSize: 18
+ readonly property int configurationTitleSize: 18
+ readonly property int languageTextSize: 18
+ readonly property int checkboxWidth: 20
+ readonly property int checkboxHeight: 20
+ readonly property int checkboxRadius: 5
+ readonly property int checkboxLabelSize: 16
+ readonly property int checkboxTextOffset: 10
+ readonly property int checkboxSliderOffset: 20
+ readonly property int sliderHandleRadius: 10
+ readonly property int sliderHandleRadiusInner: 6
+ readonly property int sliderWidth: 256
+ readonly property int sliderHeight: 4
+ readonly property int switchWidth: 50
+ readonly property int switchHeight: 20
+ readonly property int switchIndicatorRadius: 15
+ readonly property int unitButtonWidthMargin: 20
+ readonly property int unitButtonHeight: 40
+ readonly property int unitButtonSpacing: 10
+ readonly property int unitFontSize: 16
+ readonly property int bikeInfoComponentBaselineOffset: 30
+ readonly property int bikeInfoComponentLineOffset: 14
+ readonly property int bikeInfoLineWidth: 2
+ readonly property int bikeInfoLineDetailsMargin: 24
+ readonly property int bikeInfoDetailsValueMargin: 20
+ readonly property int bikeInfoDetailsBaselineMargin: 30
+ readonly property int bikeInfoComponentHeaderTextSize: 18
+ readonly property int bikeInfoInfoHeaderTextSize: 18
+ readonly property int bikeInfoCircleRadius: 17
+
+ readonly property int statsTripButtonWidth: 40
+ readonly property int statsTripButtonHeight: 40
+ readonly property int statsTripButtonMarginSide: 30
+ readonly property int statsTripButtonMarginTop: 60
+ readonly property int statsEndtripWidth: 150
+ readonly property int statsEndtripHeight: 40
+ readonly property int statsEndtripMargin: 60
+ readonly property int statsEndtripTextSize: 16
+ readonly property int statsDescriptionTextSize: 18
+ readonly property int statsValueTextSize: 18
+ readonly property int statsOdometerMarginRight: 30
+ readonly property int statsOdometerBaselineOffset: 40
+ readonly property int statsTopMargin: 28
+ readonly property int statsHeight: 39
+ readonly property int statsCenterOffset: 30
+ readonly property int chartWidth: 440
+ readonly property int chartHeight: 200
+ readonly property int chartBottomMargin: 0
+ readonly property int chartRightMargin: 0
+ readonly property int chartLegendTextSize: 14
+ readonly property int chartTimeLabelSize: 14
+ readonly property int chartSpeedLabelSize: 14
+ readonly property int chartAssistpowerLabelSize: 14
+
+ readonly property int topViewHeight: 229
+ readonly property int bottomViewHeight: 251
+ readonly property int horizontalViewSeparatorHeight: 1
+ readonly property int horizontalViewSeparatorWidth: 170
+ readonly property int verticalViewSeparatorHeightTop: 110
+ readonly property int verticalViewSeparatorHeightBottom: 132
+ readonly property int verticalViewSeparatorWidth: 1
+}
diff --git a/basicsuite/ebike-ui/BikeStyle/qmldir b/basicsuite/ebike-ui/BikeStyle/qmldir
new file mode 100644
index 0000000..e8e9d2f
--- /dev/null
+++ b/basicsuite/ebike-ui/BikeStyle/qmldir
@@ -0,0 +1,2 @@
+singleton Colors 1.0 Colors.qml
+singleton UILayout 1.0 UILayout.qml
diff --git a/basicsuite/ebike-ui/ClockView.qml b/basicsuite/ebike-ui/ClockView.qml
new file mode 100644
index 0000000..21310c0
--- /dev/null
+++ b/basicsuite/ebike-ui/ClockView.qml
@@ -0,0 +1,72 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+import "./BikeStyle"
+// Permanent placeholder for time display
+Item {
+ width: backgroundImage.width
+ height: backgroundImage.height
+ z: 1
+
+ // Timer that will show the current time at the top of the screen
+ Timer {
+ interval: 500; running: true; repeat: true
+ onTriggered: timeLabel.text = new Date().toLocaleTimeString(Qt.locale("en_US"), Locale.ShortFormat)
+ }
+
+ Image {
+ id: backgroundImage
+ source: "images/top_curtain_drag.png"
+ anchors.centerIn: parent
+ }
+
+ Text {
+ id: timeLabel
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ baseline: parent.top
+ baselineOffset: UILayout.clockBaselineMargin
+ }
+ color: Colors.clockText
+ text: "--:--"
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.clockFontSize
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/ColumnSpacer.qml b/basicsuite/ebike-ui/ColumnSpacer.qml
new file mode 100644
index 0000000..2cdfe98
--- /dev/null
+++ b/basicsuite/ebike-ui/ColumnSpacer.qml
@@ -0,0 +1,45 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.0
+
+import "./BikeStyle"
+
+Rectangle {
+ height: UILayout.configurationItemSeparator
+ width: parent.width
+ color: Colors.tabItemBorder
+}
diff --git a/basicsuite/ebike-ui/ConfigurationDrawer.qml b/basicsuite/ebike-ui/ConfigurationDrawer.qml
new file mode 100644
index 0000000..258a264
--- /dev/null
+++ b/basicsuite/ebike-ui/ConfigurationDrawer.qml
@@ -0,0 +1,147 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import "./BikeStyle"
+
+Drawer {
+ property alias bikeInfoTab: bikeInfoTab
+ property alias generalTab: generalTab
+ property alias viewTab: viewTab
+
+ background: Rectangle {
+ color: Colors.curtainBackground
+ width: parent.width
+ height: parent.height
+ }
+
+ TabBar {
+ id: bar
+ anchors {
+ left: parent.left
+ right: parent.right
+ leftMargin: UILayout.curtainMargin
+ rightMargin: UILayout.curtainMargin
+ }
+ height: UILayout.tabBarTabHeight
+ background: Rectangle {
+ color: Colors.curtainBackground
+ }
+
+ IconifiedTabButton {
+ id: bikeInfoTabButton
+ height: parent.height
+ bar: bar
+ deselectedIcon: "images/info.png"
+ selectedIcon: "images/info_selected.png"
+ }
+
+ IconifiedTabButton {
+ id: configurationTabButton
+ height: parent.height
+ bar: bar
+ deselectedIcon: "images/settings.png"
+ selectedIcon: "images/settings_selected.png"
+ }
+
+ IconifiedTabButton {
+ id: viewTabButton
+ height: parent.height
+ bar: bar
+ deselectedIcon: "images/list.png"
+ selectedIcon: "images/list_selected.png"
+ }
+ }
+
+ StackLayout {
+ id: stackLayout
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: bar.bottom
+ leftMargin: UILayout.curtainMargin
+ rightMargin: UILayout.curtainMargin
+ }
+ height: 290
+ currentIndex: bar.currentIndex
+
+ BikeInfoTab {
+ id: bikeInfoTab
+ }
+
+ GeneralTab {
+ id: generalTab
+ }
+
+ ViewTab {
+ id: viewTab
+
+ onResetDemo: {
+ // Reset trip data
+ datastore.resetDemo()
+ // Reset navigation
+ naviPage.resetDemo()
+ }
+ }
+ }
+
+ Rectangle {
+ id: drawerClose
+ anchors {
+ top: stackLayout.bottom
+ left: parent.left
+ right: parent.right
+ }
+
+ width: parent.width
+ height: drawerCloseImage.implicitHeight
+ color: "transparent"
+
+ Image {
+ id: drawerCloseImage
+ source: "images/curtain_shadow_handle.png"
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: drawer.close()
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/ConfigurationItem.qml b/basicsuite/ebike-ui/ConfigurationItem.qml
new file mode 100644
index 0000000..350e910
--- /dev/null
+++ b/basicsuite/ebike-ui/ConfigurationItem.qml
@@ -0,0 +1,63 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+
+import "./BikeStyle"
+
+Rectangle {
+ property string description
+
+ color: "transparent"
+ height: UILayout.configurationItemHeight
+ width: parent.width
+
+ Text {
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ left: parent.left
+ }
+ text: description
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.configurationTextSize
+ }
+ color: Colors.tabItemColor
+ verticalAlignment: Text.AlignVCenter
+ }
+}
diff --git a/basicsuite/ebike-ui/FpsItem.qml b/basicsuite/ebike-ui/FpsItem.qml
new file mode 100644
index 0000000..0ab183d
--- /dev/null
+++ b/basicsuite/ebike-ui/FpsItem.qml
@@ -0,0 +1,77 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.0
+
+Item {
+ id: fpsItem
+ property int frameCounter: 0
+ property int fpsValue: 0;
+
+ width: spinnerImage.width + fpsText.width
+ height: 48
+ z: 1
+
+ Image {
+ id: spinnerImage
+ source: "images/fps_icon.png"
+ NumberAnimation on rotation {
+ from: 0
+ to: 360
+ duration: 800
+ loops: Animation.Infinite
+ }
+ onRotationChanged: frameCounter++;
+ }
+
+ Text {
+ id: fpsText
+ anchors.right: parent.right
+ anchors.verticalCenter: spinnerImage.verticalCenter
+ color: "red"
+ text: "Fps: " + fpsItem.fpsValue
+ }
+
+ Timer {
+ interval: 2000
+ repeat: true
+ running: true
+ onTriggered: {
+ fpsValue = frameCounter / 2;
+ frameCounter = 0;
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/GeneralTab.qml b/basicsuite/ebike-ui/GeneralTab.qml
new file mode 100644
index 0000000..697a451
--- /dev/null
+++ b/basicsuite/ebike-ui/GeneralTab.qml
@@ -0,0 +1,279 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import DataStore 1.0
+
+import "./BikeStyle"
+
+Item {
+ Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ Text {
+ height: UILayout.configurationItemHeight
+ width: parent.width
+ text: qsTr("GENERAL")
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.configurationTitleSize
+ }
+ color: Colors.tabTitleColor
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ ColumnSpacer {
+ color: Colors.tabItemBorder
+ }
+
+ ConfigurationItem {
+ description: qsTr("Language")
+
+ Text {
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ right: parent.right
+ }
+ text: qsTr("English")
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.languageTextSize
+ }
+ color: Colors.languageTextColor
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ ColumnSpacer {
+ color: Colors.tabItemBorder
+ }
+
+ ConfigurationItem {
+ description: qsTr("Brightness")
+
+ Text {
+ anchors {
+ right: autoBrightness.left
+ rightMargin: UILayout.checkboxTextOffset
+ verticalCenter: autoBrightness.verticalCenter
+ }
+ text: qsTr("Auto")
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.checkboxLabelSize
+ }
+ color: Colors.checkboxCheckedText
+ }
+
+ CheckBox {
+ id: autoBrightness
+ width: UILayout.checkboxWidth
+ anchors {
+ right: brightnessSlider.left
+ rightMargin: UILayout.checkboxSliderOffset
+ verticalCenter: brightnessSlider.verticalCenter
+ }
+ checked: brightness.automatic
+
+ indicator: Rectangle {
+ implicitWidth: UILayout.checkboxWidth
+ implicitHeight: UILayout.checkboxHeight
+ y: parent.height / 2 - height / 2
+ radius: UILayout.checkboxRadius
+ color: Colors.checkboxUncheckedBackground
+ border.color: autoBrightness.checked ? Colors.checkboxBorderColorChecked : Colors.checkboxBorderColor
+ border.width: autoBrightness.checked ? 2 : 1
+
+ Image {
+ source: "images/checkmark.png"
+ anchors.centerIn: parent
+ visible: autoBrightness.checked
+ }
+ }
+
+ contentItem: Item {}
+
+ onToggled: brightness.automatic = checked
+ }
+
+ Slider {
+ id: brightnessSlider
+ value: 4
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ right: parent.right
+ }
+ from: 6
+ to: 1
+ stepSize: -1
+ snapMode: Slider.SnapAlways
+ onMoved: brightness.brightness = value
+
+ background: Rectangle {
+ x: brightnessSlider.leftPadding
+ y: brightnessSlider.topPadding + brightnessSlider.availableHeight / 2 - height / 2
+ implicitWidth: UILayout.sliderWidth
+ implicitHeight: UILayout.sliderHeight
+ width: brightnessSlider.availableWidth
+ height: implicitHeight
+ radius: UILayout.sliderHeight / 2
+ color: Colors.sliderBackground
+
+ Rectangle {
+ // Since gradient is only available vertically, we must draw and rotate
+ width: parent.height
+ height: brightnessSlider.visualPosition * parent.width
+ radius: UILayout.sliderHeight / 2
+ gradient: Gradient {
+ GradientStop { position: 0; color: Colors.sliderMinimumValue }
+ GradientStop { position: 1; color: Colors.sliderMaximumValue }
+ }
+
+ transform: Rotation { origin.x: 0; origin.y: UILayout.sliderHeight; angle: -90}
+ }
+ }
+
+ handle: Rectangle {
+ x: brightnessSlider.leftPadding + brightnessSlider.visualPosition * (brightnessSlider.availableWidth - width)
+ y: brightnessSlider.topPadding + brightnessSlider.availableHeight / 2 - height / 2
+ implicitWidth: 2 * UILayout.sliderHandleRadius
+ implicitHeight: 2 * UILayout.sliderHandleRadius
+ radius: UILayout.sliderHandleRadius
+ color: Colors.sliderMaximumValue
+
+ Rectangle {
+ anchors.centerIn: parent
+ implicitWidth: 2 * UILayout.sliderHandleRadiusInner
+ implicitHeight: 2 * UILayout.sliderHandleRadiusInner
+ radius: UILayout.sliderHandleRadiusInner
+ color: Colors.sliderInnerBackground
+ }
+ }
+ }
+ }
+
+ ColumnSpacer {
+ color: Colors.tabItemBorder
+ }
+
+ ConfigurationItem {
+ description: qsTr("Units")
+
+ RoundButton {
+ id: kmhButton
+ width: UILayout.unitButtonWidthMargin * 2 + kmhText.implicitWidth
+ height: UILayout.unitButtonHeight
+ radius: height / 2
+ anchors {
+ verticalCenter: parent.verticalCenter
+ right: parent.right
+ }
+
+ background: Rectangle {
+ width: parent.width
+ height: parent.height
+ radius: parent.radius
+ color: datastore.unit === DataStore.Kmh ? Colors.activeButtonBackground : Colors.inactiveButtonBackground
+ border.color: Colors.inactiveButtonBorder
+ border.width: datastore.unit === DataStore.Kmh ? 0 : 1
+ }
+
+ contentItem: Text {
+ id: kmhText
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.unitFontSize
+ }
+ text: "km/h"
+ color: datastore.unit === DataStore.Kmh ? Colors.activeButtonText : Colors.inactiveButtonText
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ onClicked: datastore.unit = DataStore.Kmh
+ }
+
+ RoundButton {
+ id: mphButton
+ width: UILayout.unitButtonWidthMargin * 2 + mphText.implicitWidth
+ height: UILayout.unitButtonHeight
+ radius: height / 2
+ anchors {
+ verticalCenter: parent.verticalCenter
+ right: kmhButton.left
+ rightMargin: UILayout.unitButtonSpacing
+ }
+
+ background: Rectangle {
+ width: parent.width
+ height: parent.height
+ radius: parent.radius
+ color: datastore.unit === DataStore.Mph ? Colors.activeButtonBackground : Colors.inactiveButtonBackground
+ border.color: Colors.inactiveButtonBorder
+ border.width: datastore.unit === DataStore.Mph ? 0 : 1
+ }
+
+ contentItem: Text {
+ id: mphText
+ text: "mph"
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.unitFontSize
+ }
+ color: datastore.unit === DataStore.Mph ? Colors.activeButtonText : Colors.inactiveButtonText
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ onClicked: datastore.unit = DataStore.Mph
+ }
+ }
+
+ ColumnSpacer {
+ color: Colors.tabItemBorder
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/IconifiedTabButton.qml b/basicsuite/ebike-ui/IconifiedTabButton.qml
new file mode 100644
index 0000000..572ce51
--- /dev/null
+++ b/basicsuite/ebike-ui/IconifiedTabButton.qml
@@ -0,0 +1,70 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+
+import "./BikeStyle"
+
+TabButton {
+ property string deselectedIcon
+ property string selectedIcon
+ property var bar
+
+ contentItem: Image {
+ width: UILayout.tabButtonIconWidth
+ height: UILayout.tabButtonIconHeight
+ source: bar.currentItem === parent ? selectedIcon : deselectedIcon
+ fillMode: Image.Pad
+ anchors {
+ top: parent.top
+ topMargin: UILayout.tabButtonTopMargin
+ horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ background: Rectangle {
+ color: Colors.tabBackground
+ height: parent.height
+
+ Rectangle {
+ visible: bar.currentItem === parent.parent
+ width: parent.width
+ height: 2
+ anchors.bottom: parent.bottom
+ color: Colors.activeTabBorder
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/LightsBox.qml b/basicsuite/ebike-ui/LightsBox.qml
new file mode 100644
index 0000000..327c819
--- /dev/null
+++ b/basicsuite/ebike-ui/LightsBox.qml
@@ -0,0 +1,81 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+import QtQuick.Extras 1.4
+
+import "./BikeStyle"
+
+// Bottom-left corner, controls
+Item {
+ width: 320
+ height: UILayout.bottomViewHeight
+
+ Image {
+ id: lightsIcon
+ width: UILayout.lightsIconWidth
+ height: UILayout.lightsIconHeight
+ source: datastore.lights ? "images/lights_on.png" : "images/lights_off.png"
+ fillMode: Image.PreserveAspectFit
+ anchors {
+ left: parent.left
+ leftMargin: UILayout.lightsIconLeft
+ bottom: parent.bottom
+ bottomMargin: UILayout.lightsIconBottom
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: datastore.lights = !datastore.lights
+ }
+
+ Rectangle {
+ width: UILayout.horizontalViewSeparatorWidth
+ height: UILayout.horizontalViewSeparatorHeight
+ anchors.top: parent.top
+ anchors.left: parent.left
+ color: Colors.separator
+ }
+
+ Rectangle {
+ width: UILayout.verticalViewSeparatorWidth
+ height: UILayout.verticalViewSeparatorHeightBottom
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ color: Colors.separator
+ }
+}
diff --git a/basicsuite/ebike-ui/MainPage.qml b/basicsuite/ebike-ui/MainPage.qml
new file mode 100644
index 0000000..ae36401
--- /dev/null
+++ b/basicsuite/ebike-ui/MainPage.qml
@@ -0,0 +1,94 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+import QtQuick.Controls 2.0
+import QtQuick.Extras 1.4
+
+import "./BikeStyle"
+
+Page {
+ background: Rectangle {
+ color: Colors.mainBackground
+ }
+ property alias statsButton: statsButton
+ property alias naviButton: naviButton
+ property alias lightsButton: lightsButton
+ property alias modeButton: modeButton
+ property string naviGuideArrowSource
+ property string naviGuideDistance
+ property string naviGuideAddress
+
+ StatsBox {
+ id: statsButton
+ anchors.left: parent.left
+ anchors.top: parent.top
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ swipeView.currentIndex = 0
+ }
+ }
+ }
+
+ NaviBox {
+ id: naviButton
+ anchors.right: parent.right
+ anchors.top: parent.top
+ arrowSource: naviGuideArrowSource
+ distance: naviGuideDistance
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ swipeView.currentIndex = 2
+ }
+ }
+ }
+
+ LightsBox {
+ id: lightsButton
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+ }
+
+ ModeBox {
+ id: modeButton
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ }
+}
diff --git a/basicsuite/ebike-ui/ModeBox.qml b/basicsuite/ebike-ui/ModeBox.qml
new file mode 100644
index 0000000..ad4eba7
--- /dev/null
+++ b/basicsuite/ebike-ui/ModeBox.qml
@@ -0,0 +1,101 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+import QtQuick.Extras 1.4
+import DataStore 1.0
+
+import "./BikeStyle"
+
+// Bottom-right corner, mode
+Item {
+ width: 320
+ height: UILayout.bottomViewHeight
+
+ Text {
+ id: sportModeText
+ anchors {
+ baseline: parent.bottom
+ baselineOffset: -UILayout.modeBottomOffset
+ horizontalCenter: parent.right
+ horizontalCenterOffset: -UILayout.naviModeCenterMargin
+ }
+ color: datastore.mode == DataStore.Sport ? Colors.modeSelected : Colors.modeUnselected
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.modeTextSize
+ }
+ text: qsTr("SPORT")
+ }
+
+ Text {
+ id: cruiseModeText
+ anchors {
+ baseline: sportModeText.baseline
+ baselineOffset: -UILayout.modeDistance
+ horizontalCenter: sportModeText.horizontalCenter
+ }
+ color: datastore.mode == DataStore.Cruise ? Colors.modeSelected : Colors.modeUnselected
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.modeTextSize
+ }
+ text: qsTr("CRUISE")
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: datastore.toggleMode()
+ }
+
+ Rectangle {
+ width: UILayout.horizontalViewSeparatorWidth
+ height: UILayout.horizontalViewSeparatorHeight
+ anchors.top: parent.top
+ anchors.right: parent.right
+ color: Colors.separator
+ }
+
+ Rectangle {
+ width: UILayout.verticalViewSeparatorWidth
+ height: UILayout.verticalViewSeparatorHeightBottom
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ color: Colors.separator
+ }
+}
diff --git a/basicsuite/ebike-ui/MusicPlayer.qml b/basicsuite/ebike-ui/MusicPlayer.qml
new file mode 100644
index 0000000..9d44534
--- /dev/null
+++ b/basicsuite/ebike-ui/MusicPlayer.qml
@@ -0,0 +1,239 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+
+import "./BikeStyle"
+
+Rectangle {
+ id: musicPlayer
+ width: UILayout.musicPlayerWidth
+ height: UILayout.musicPlayerHeight + UILayout.musicPlayerCorner
+ radius: UILayout.musicPlayerCorner
+ color: Colors.musicPlayerBackground
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ bottom: parent.bottom
+ bottomMargin: -UILayout.musicPlayerCorner
+ }
+ state: "hidden"
+ property bool isPlaying: false
+ property var songList: [
+ ["Post Malone - Rockstar", 218],
+ ["Ed Sheeran - Perfect", 263],
+ ["Imagine Dragons - Thunder", 187]
+ ]
+ property int currentSong: 0
+
+ function setupSong() {
+ var dur = songList[currentSong][1];
+ timeAnimation.stop();
+ songTimeText.songDuration = dur;
+ timeAnimation.from = dur;
+ timeAnimation.to = 0;
+ timeAnimation.duration = dur * 1000;
+ if (isPlaying)
+ timeAnimation.start();
+ }
+
+ function previousSong() {
+ if (currentSong === 0)
+ currentSong = songList.length - 1;
+ else
+ currentSong -= 1;
+ setupSong();
+ }
+
+ function nextSong() {
+ if (currentSong >= (songList.length - 1))
+ currentSong = 0;
+ else
+ currentSong += 1;
+ setupSong();
+ }
+
+ Component.onCompleted: setupSong()
+
+ Image {
+ id: playIcon
+ width: UILayout.musicPlayerIconWidth
+ height: UILayout.musicPlayerIconHeight
+ source: isPlaying
+ ? (playIconArea.pressed ? "images/pause_pressed.png" : "images/pause.png")
+ : (playIconArea.pressed ? "images/play_pressed.png" : "images/play.png")
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ bottom: parent.bottom
+ bottomMargin: UILayout.musicPlayerIconBottom + UILayout.musicPlayerCorner
+ }
+
+ MouseArea {
+ id: playIconArea
+ anchors {
+ fill: parent
+ margins: -UILayout.musicPlayerIconSpacing / 2
+ }
+ onClicked: {
+ isPlaying = !isPlaying
+ if (isPlaying) {
+ if (timeAnimation.running)
+ timeAnimation.resume()
+ else
+ timeAnimation.start()
+ } else
+ timeAnimation.pause()
+ }
+ }
+ }
+
+ Image {
+ id: previousIcon
+ width: UILayout.musicPlayerIconWidth
+ height: UILayout.musicPlayerIconHeight
+ source: previousIconArea.pressed ? "images/prevsong_pressed.png" : "images/prevsong.png"
+ anchors {
+ right: playIcon.left
+ rightMargin: UILayout.musicPlayerIconSpacing
+ bottom: playIcon.bottom
+ }
+
+ MouseArea {
+ id: previousIconArea
+ anchors {
+ fill: parent
+ margins: -UILayout.musicPlayerIconSpacing / 2
+ }
+ onClicked: previousSong()
+ }
+ }
+
+ Image {
+ id: nextIcon
+ width: UILayout.musicPlayerIconWidth
+ height: UILayout.musicPlayerIconHeight
+ source: nextIconArea.pressed ? "images/nextsong_pressed.png" : "images/nextsong.png"
+ anchors {
+ left: playIcon.right
+ leftMargin: UILayout.musicPlayerIconSpacing
+ bottom: playIcon.bottom
+ }
+
+ MouseArea {
+ id: nextIconArea
+ anchors {
+ fill: parent
+ margins: -UILayout.musicPlayerIconSpacing / 2
+ }
+ onClicked: nextSong()
+ }
+ }
+
+ Text {
+ id: songTitleText
+ anchors {
+ left: previousIcon.left
+ right: nextIcon.right
+ rightMargin: songTimeText.width + 5
+ baseline: previousIcon.top
+ baselineOffset: -UILayout.musicPlayerTextBottom
+ }
+
+ color: Colors.musicPlayerSongText
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.musicPlayerTextSize
+ }
+ text: songList[currentSong][0]
+ elide: Text.ElideRight
+ }
+
+ // Function for pretty-printing duration
+ function splitDuration(duration) {
+ var minutes = Math.floor(duration / 60);
+ var seconds = Math.floor(duration % 60);
+ if (seconds < 10)
+ seconds = "0" + seconds;
+ return minutes + ":" + seconds;
+ }
+
+ Text {
+ property int songDuration
+ id: songTimeText
+ anchors {
+ right: nextIcon.right
+ baseline: nextIcon.top
+ baselineOffset: -UILayout.musicPlayerTextBottom
+ }
+
+ color: Colors.musicPlayerTimeText
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.musicPlayerTextSize
+ }
+ text: splitDuration(songDuration)
+
+ NumberAnimation {
+ id: timeAnimation
+ target: songTimeText
+ property: "songDuration"
+ onStopped: {
+ if (isPlaying)
+ nextSong();
+ }
+ }
+ }
+
+ states: State {
+ name: "hidden"
+ PropertyChanges {
+ target: musicPlayer
+ anchors.bottomMargin: -musicPlayer.height
+ }
+ }
+
+ transitions: Transition {
+ from: ""
+ to: "hidden"
+ reversible: true
+ NumberAnimation {
+ properties: "anchors.bottomMargin"
+ duration: 250
+ easing.type: Easing.InOutQuad
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/NaviBox.qml b/basicsuite/ebike-ui/NaviBox.qml
new file mode 100644
index 0000000..53b55c8
--- /dev/null
+++ b/basicsuite/ebike-ui/NaviBox.qml
@@ -0,0 +1,129 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+import QtQuick.Controls 2.0
+
+import "./BikeStyle"
+
+// Top-right corner, navi
+Item {
+ width: 320
+ height: UILayout.topViewHeight
+ property string arrowSource: "images/nav_right.png"
+ property string distance: "0"
+ property string unit: "m"
+
+ Image {
+ id: naviIcon
+ width: UILayout.naviIconWidth
+ height: UILayout.naviIconHeight
+ source: arrowSource
+ anchors {
+ top: parent.top
+ topMargin: UILayout.naviIconTop
+ horizontalCenter: parent.right
+ horizontalCenterOffset: -UILayout.naviModeCenterMargin
+ }
+ }
+
+ Item {
+ id: container
+ anchors.horizontalCenter: naviIcon.horizontalCenter
+ anchors.top: naviIcon.bottom
+ height: 30
+ width: naviText.width + 5 + naviUnit.width
+ visible: navigation.active
+
+ Text {
+ id: naviText
+ anchors.baseline: container.bottom
+ color: Colors.distanceText
+ font {
+ family: "Montserrat, Bold"
+ weight: Font.Bold
+ pixelSize: UILayout.naviTextSize
+ }
+ text: Math.round(datastore.convertSmallDistance(distance) / 10) * 10
+ }
+
+ Text {
+ id: naviUnit
+ anchors {
+ baseline: container.bottom
+ left: naviText.right
+ leftMargin: 5
+ }
+ color: Colors.distanceUnit
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.naviTextSize
+ }
+ text: datastore.smallUnit
+ }
+ }
+
+ Text {
+ id: navigateText
+ anchors.horizontalCenter: naviIcon.horizontalCenter
+ anchors.top: naviIcon.bottom
+ visible: !navigation.active
+ color: Colors.modeUnselected
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.modeTextSize
+ }
+ text: qsTr("NAVIGATE")
+ }
+
+ Rectangle {
+ width: UILayout.horizontalViewSeparatorWidth
+ height: UILayout.horizontalViewSeparatorHeight
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ color: Colors.separator
+ }
+
+ Rectangle {
+ width: UILayout.verticalViewSeparatorWidth
+ height: UILayout.verticalViewSeparatorHeightTop
+ anchors.top: parent.top
+ anchors.left: parent.left
+ color: Colors.separator
+ }
+}
diff --git a/basicsuite/ebike-ui/NaviButton.qml b/basicsuite/ebike-ui/NaviButton.qml
new file mode 100644
index 0000000..a34d540
--- /dev/null
+++ b/basicsuite/ebike-ui/NaviButton.qml
@@ -0,0 +1,83 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+
+import "./BikeStyle"
+
+RoundButton {
+ property string iconSource
+
+ width: UILayout.naviPageIconBackgroundWidth
+ height: UILayout.naviPageIconBackgroundHeight
+ radius: UILayout.naviPageIconBackgroundRadius
+ z: 1
+
+ background: Item {
+ id: naviButtonBackground
+ width: parent.width
+ height: parent.height
+
+ Image {
+ id: naviButtonShadow
+ fillMode: Image.Pad
+ anchors {
+ horizontalCenter: naviButtonBackground.horizontalCenter
+ verticalCenter: naviButtonBackground.verticalCenter
+ horizontalCenterOffset: 1
+ verticalCenterOffset: 1
+ }
+ source: "images/map_btn_shadow.png"
+ }
+
+ Rectangle {
+ width: UILayout.naviPageIconBackgroundWidth
+ height: UILayout.naviPageIconBackgroundHeight
+ radius: UILayout.naviPageIconBackgroundRadius
+ color: parent.parent.down ? Colors.naviPageIconPressedBackground : Colors.naviPageIconBackground
+ }
+ }
+
+ contentItem: Item {}
+ Image {
+ anchors.centerIn: parent
+ width: UILayout.naviPageIconWidth
+ height: UILayout.naviPageIconHeight
+ source: iconSource
+ z: 3
+ }
+}
diff --git a/basicsuite/ebike-ui/NaviGuide.qml b/basicsuite/ebike-ui/NaviGuide.qml
new file mode 100644
index 0000000..1ba6f5e
--- /dev/null
+++ b/basicsuite/ebike-ui/NaviGuide.qml
@@ -0,0 +1,139 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+
+import "./BikeStyle"
+
+Rectangle {
+ property string arrowSource: "images/nav_nodir.png"
+ property string address: "-"
+ property string distance: "0"
+ property string unit: "m"
+
+ width: UILayout.naviPageGuideRadius * 2
+ height: width
+ radius: width
+ color: Colors.naviPageGuideBackground
+ z: 1
+
+ Rectangle {
+ width: UILayout.naviPageGuideRadius
+ height: width
+ radius: 10
+ color: Colors.naviPageGuideBackground
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ }
+
+ Image {
+ id: guideArrow
+ anchors {
+ top: parent.top
+ topMargin: UILayout.naviPageGuideArrowTopMargin
+ left: parent.left
+ leftMargin: UILayout.naviPageGuideArrowLeftMargin
+ }
+ source: arrowSource
+ width: UILayout.naviPageGuideArrowWidth
+ height: UILayout.naviPageGuideArrowHeight
+ }
+
+ Text {
+ id: naviAddressText
+ anchors {
+ baseline: parent.bottom
+ baselineOffset: -UILayout.naviPageGuideAddressBaselineMargin
+ right: parent.right
+ rightMargin: UILayout.naviPageGuideAddressRightMargin
+ }
+ width: 123
+ horizontalAlignment: Text.AlignRight
+ color: Colors.naviPageGuideAddressColor
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.naviPageGuideAddressTextSize
+ }
+ fontSizeMode: Text.Fit
+ wrapMode: Text.WordWrap
+ minimumPixelSize: 9
+ text: address
+ }
+
+ Text {
+ id: naviUnit
+ anchors {
+ baseline: naviAddressText.baseline
+ baselineOffset: -UILayout.naviPageGuideDistanceBaselineMargin
+ right: naviAddressText.right
+ }
+ color: Colors.naviPageGuideUnitColor
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.naviPageGuideUnitTextSize
+ }
+ text: datastore.smallUnit
+ }
+
+ Text {
+ id: naviDistance
+ anchors {
+ baseline: naviUnit.baseline
+ right: naviUnit.left
+ rightMargin: 10
+ }
+ color: Colors.naviPageGuideTextColor
+ font {
+ family: "Montserrat, Bold"
+ weight: Font.Bold
+ pixelSize: UILayout.naviPageGuideDistanceTextSize
+ }
+ text: Math.round(datastore.convertSmallDistance(distance) / 10) * 10
+ }
+
+ Image {
+ source: "images/navigation_widget_shadow.png"
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.verticalCenter
+ horizontalCenterOffset: 1
+ verticalCenterOffset: 1
+ }
+ z: -1
+ }
+}
diff --git a/basicsuite/ebike-ui/NaviPage.qml b/basicsuite/ebike-ui/NaviPage.qml
new file mode 100644
index 0000000..e6c8805
--- /dev/null
+++ b/basicsuite/ebike-ui/NaviPage.qml
@@ -0,0 +1,691 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+import QtQuick.Controls 2.2
+import QtPositioning 5.3
+import QtLocation 5.9
+import QtQuick.VirtualKeyboard 2.1
+
+import "./BikeStyle"
+
+Page {
+ id: mapContainer
+ property var startCoordinate: QtPositioning.coordinate(36.131961, -115.153048)
+ property var destinationCoordinate: QtPositioning.coordinate(90, 0)
+ property var targetPlace
+ property real totalDistance
+ property real metersPerSecond // This is calculated at the start of a trip
+ property real totalTravelTime: totalDistance / metersPerSecond
+ property real naviGuideSegmentDistance: 0
+ property string naviGuideArrowSource: "images/nav_nodir.png"
+ property string naviGuideDistance
+ property string naviGuideAddress: "-"
+ property alias routeQuery: routeQuery
+ property alias routeModel: routeModel
+ property alias targetEdit: targetEdit
+ property var routeSegmentList
+ property var currentSegment
+ property int routeSegment
+ property int pathSegment
+
+ function resetDemo() {
+ // Clear/reset everything
+ naviGuideArrowSource = "images/nav_nodir.png"
+ naviGuideDistance = "-"
+ naviGuideAddress = "-"
+ navigation.active = false
+ navigationArrowAnimation.stop()
+ targetEdit.clear()
+
+ map.focus = true
+ routeQuery.clearWaypoints()
+ routeModel.reset()
+ navigationArrowItem.coordinate = navigation.coordinate
+ destinationCoordinate = QtPositioning.coordinate(90, 0)
+ map.center = navigation.coordinate
+ }
+
+ TextField {
+ id: targetEdit
+ width: UILayout.naviPageLocationWidth
+ height: UILayout.naviPageLocationHeight
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ top: parent.top
+ topMargin: UILayout.naviPageLocationTopMargin
+ }
+ background: Rectangle {
+ radius: UILayout.naviPageLocationRadius
+ implicitWidth: UILayout.naviPageLocationWidth
+ implicitHeight: UILayout.naviPageLocationHeight
+ border.color: Colors.naviPageSuggestionBorder
+ border.width: targetEdit.activeFocus ? 2 : 0
+ }
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.naviPageSearchTextSize
+ }
+ cursorVisible: !navigation.active
+ inputMethodHints: Qt.ImhNoPredictiveText // This should disable lookup on the device
+
+ leftPadding: UILayout.naviPageLocationLeftPadding
+ rightPadding: UILayout.naviPageSearchIconMargin + UILayout.naviPageSearchIconWidth
+ placeholderText: qsTr("<i>Where do you want to go?</i>")
+ z: 1
+ autoScroll: false
+
+ // Update search text whenever text is edited
+ onTextEdited: suggest.search = text
+
+ // If in navigation mode, disable editing
+ readOnly: navigation.active
+
+ Image {
+ source: navigation.active ? "images/search_cancel.png" : "images/search.png"
+ width: UILayout.naviPageSearchIconWidth
+ height: UILayout.naviPageSearchIconHeight
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ right: parent.right
+ rightMargin: UILayout.naviPageSearchIconMargin
+ }
+ visible: !suggest.loading
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: navigation.active
+ onClicked: {
+ naviGuideArrowSource = "images/nav_nodir.png"
+ naviGuideDistance = "-"
+ naviGuideAddress = "-"
+ navigation.active = false
+ navigationArrowAnimation.stop()
+ targetEdit.clear()
+ }
+ }
+ }
+
+ // Show a busy indicator whenever suggestions are loading
+ BusyIndicator {
+ width: height
+ anchors {
+ top: targetEdit.top
+ bottom: targetEdit.bottom
+ right: targetEdit.right
+ rightMargin: UILayout.naviPageSearchIconMargin
+ }
+ running: suggest.loading
+ }
+
+ Image {
+ id: naviInputShadow
+ source: "images/small_input_box_shadow.png"
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.verticalCenter
+ horizontalCenterOffset: 1
+ verticalCenterOffset: 1
+ }
+ visible: false
+ z: -2
+ }
+ }
+
+ ListView {
+ id: targetList
+ anchors {
+ top: targetEdit.bottom;
+ topMargin: UILayout.naviPageSuggestionsOffset
+ horizontalCenter: targetEdit.horizontalCenter
+ }
+ width: UILayout.naviPageLocationWidth
+ height: 3 * UILayout.naviPageSuggestionHeight
+ model: suggestions
+ visible: targetEdit.activeFocus && !navigation.active
+ z: 1
+ currentIndex: -1
+
+ delegate: Component {
+ Rectangle {
+ width: parent.width
+ height: UILayout.naviPageSuggestionHeight
+ color: "white"
+ border.color: Colors.naviPageSuggestionsDivider
+ border.width: 1
+
+ Text {
+ width: parent.width
+ height: parent.height
+ verticalAlignment: Text.AlignVCenter
+ leftPadding: 10
+ elide: Text.ElideRight
+ text: placename
+ color: Colors.naviPageSuggestionText
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.naviPageSuggestionTextSize
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: targetList.currentIndex = index
+ }
+ }
+ }
+
+ onCurrentIndexChanged: {
+ // Called when the currentIndex is reset, ignore that
+ if (targetList.currentIndex == -1)
+ return;
+
+ // Get current place name
+ targetPlace = model.get(targetList.currentIndex)
+ suggestions.addToMostRecent(targetPlace)
+
+ // Reset search
+ targetList.currentIndex = -1
+ // Update current text
+ targetEdit.text = targetPlace.place_name
+ // Clear the model and stop any search
+ suggestions.clear()
+ suggest.stopSuggest()
+
+ targetEdit.cursorPosition = 0
+ targetEdit.ensureVisible(1)
+ navigation.active = true
+ map.focus = true
+
+ destinationCoordinate = QtPositioning.coordinate(targetPlace.center[1], targetPlace.center[0]);
+ routeQuery.clearWaypoints()
+ routeQuery.addWaypoint(map.center)
+ routeQuery.addWaypoint(destinationCoordinate)
+ routeQuery.travelModes = RouteQuery.BicycleTravel
+ routeQuery.routeOptimizations = RouteQuery.ShortestRoute
+ routeModel.update()
+ }
+ }
+
+ // Zoom and location icons
+ NaviButton {
+ id: gpsCenter
+ anchors {
+ top: parent.top
+ topMargin: UILayout.naviPageIconTopMargin
+ right: parent.right
+ rightMargin: UILayout.naviPageIconRightMargin
+ }
+ iconSource: "images/map_locate.png"
+ onClicked: {
+ navigationArrowItem.coordinate = navigation.coordinate
+ map.center = navigation.coordinate
+ }
+ }
+
+ NaviButton {
+ id: zoomIn
+ anchors {
+ top: gpsCenter.bottom
+ topMargin: UILayout.naviPageIconSpacing
+ right: parent.right
+ rightMargin: UILayout.naviPageIconRightMargin
+ }
+ iconSource: "images/map_zoomin.png"
+ autoRepeat: true
+ onClicked: navigation.zoomlevel += 0.1
+ }
+
+ NaviButton {
+ id: zoomOut
+ anchors {
+ top: zoomIn.bottom
+ topMargin: UILayout.naviPageIconSpacing
+ right: parent.right
+ rightMargin: UILayout.naviPageIconRightMargin
+ }
+ iconSource: "images/map_zoomout.png"
+ autoRepeat: true
+ onClicked: navigation.zoomlevel -= 0.1
+ }
+
+ NaviGuide {
+ id: naviGuide
+ anchors {
+ right: parent.right
+ bottom: parent.bottom
+ rightMargin: UILayout.naviPageGuideRightMargin
+ bottomMargin: UILayout.naviPageGuideBottomMargin
+ }
+ arrowSource: naviGuideArrowSource
+ distance: naviGuideDistance
+ address: naviGuideAddress
+ visible: navigation.active
+ }
+
+ NaviTripInfo {
+ id: totalTripInfo
+ z: 1
+ visible: navigation.active
+ anchors {
+ bottom: parent.bottom
+ bottomMargin: UILayout.naviPageTripBottomMargin
+ horizontalCenter: parent.horizontalCenter
+ }
+
+ remainingDistance: totalDistance
+ remainingTravelTime: totalTravelTime
+ }
+
+ RouteQuery {
+ id: routeQuery
+ numberAlternativeRoutes: 0
+ }
+
+ function updateNaviGuide(segment, nextsegment) {
+ var maneuver = segment.maneuver;
+ naviGuideSegmentDistance = maneuver.distanceToNextInstruction;
+ navigationArrowAnimation.pathDistance = naviGuideSegmentDistance;
+ naviGuideDistance = maneuver.distanceToNextInstruction;
+
+ if (nextsegment) {
+ var nextmaneuver = nextsegment.maneuver;
+ naviGuideAddress = nextmaneuver.instructionText;
+ switch (nextmaneuver.direction) {
+ case RouteManeuver.NoDirection:
+ naviGuideArrowSource = "images/nav_nodir.png";
+ break;
+ case RouteManeuver.DirectionForward:
+ naviGuideArrowSource = "images/nav_straight.png";
+ break;
+ case RouteManeuver.DirectionBearRight:
+ naviGuideArrowSource = "images/nav_bear_r.png";
+ break;
+ case RouteManeuver.DirectionLightRight:
+ naviGuideArrowSource = "images/nav_light_right.png";
+ break;
+ case RouteManeuver.DirectionRight:
+ naviGuideArrowSource = "images/nav_right.png";
+ break;
+ case RouteManeuver.DirectionHardRight:
+ naviGuideArrowSource = "images/nav_hard_r.png";
+ break;
+ case RouteManeuver.DirectionUTurnRight:
+ naviGuideArrowSource = "images/nav_uturn_r.png";
+ break;
+ case RouteManeuver.DirectionUTurnLeft:
+ naviGuideArrowSource = "images/nav_uturn_l.png";
+ break;
+ case RouteManeuver.DirectionHardLeft:
+ naviGuideArrowSource = "images/nav_hard_l.png";
+ break;
+ case RouteManeuver.DirectionLeft:
+ naviGuideArrowSource = "images/nav_left.png";
+ break;
+ case RouteManeuver.DirectionLightLeft:
+ naviGuideArrowSource = "images/nav_light_left.png";
+ break;
+ case RouteManeuver.DirectionBearLeft:
+ naviGuideArrowSource = "images/nav_bear_l.png";
+ break;
+ }
+ } else {
+ naviGuideAddress = "-";
+ naviGuideArrowSource = "images/nav_nodir.png";
+ }
+ }
+
+ function setNextAnimation() {
+ if (!navigation.active)
+ return;
+ var position = QtPositioning.coordinate(currentSegment.maneuver.position.longitude,
+ currentSegment.maneuver.position.latitude);
+
+ // Update the navigation instructions
+ if (pathSegment === 0) {
+ var nextSegment = routeSegmentList[routeSegment + 1];
+ updateNaviGuide(currentSegment, nextSegment);
+ }
+
+ var startPos = navigationArrowItem.coordinate;
+ var path = currentSegment.path;
+ pathSegment += 1;
+ if (pathSegment >= path.length) {
+ routeSegment += 1;
+ currentSegment = routeSegmentList[routeSegment];
+ if (!currentSegment) {
+ naviGuideArrowSource = "images/nav_nodir.png";
+ navigation.active = false;
+ targetEdit.clear();
+ return;
+ }
+ pathSegment = 0;
+ setNextAnimation();
+ return;
+ }
+ var endPos = path[pathSegment];
+
+ // Calculate new direction
+ var oldDir = navigationArrowAnimation.rotationDirection;
+ var newDir = startPos.azimuthTo(endPos);
+
+ // Calculate the duration of the animation
+ var diff = oldDir - newDir;
+ if (Math.abs(diff) < 15)
+ navigationArrowAnimation.rotationDuration = 0;
+ else if (diff < -180)
+ navigationArrowAnimation.rotationDuration = (diff + 360) * 5;
+ else if (diff > 180)
+ navigationArrowAnimation.rotationDuration = (360 - diff) * 5;
+ else
+ navigationArrowAnimation.rotationDuration = Math.abs(diff) * 5;
+
+ // Set animation details
+ var pathDistance = startPos.distanceTo(endPos);
+ var nextDistance = navigationArrowAnimation.pathDistance - pathDistance;
+ var nextRemainingDistance = navigationArrowAnimation.remainingDistance - pathDistance;
+ navigationArrowAnimation.coordinateDuration = pathDistance * 40;
+ navigationArrowAnimation.rotationDirection = startPos.azimuthTo(endPos);
+ navigationArrowAnimation.pathDistance = nextDistance;
+ navigationArrowAnimation.remainingDistance = nextRemainingDistance;
+ navigationArrowAnimation.sourceCoordinate = startPos;
+ navigationArrowAnimation.targetCoordinate = endPos;
+ navigationArrowAnimation.start();
+ }
+
+ RouteModel {
+ id: routeModel
+ plugin: map.plugin
+ query: routeQuery
+ onStatusChanged: {
+ if (status === RouteModel.Ready) {
+ switch (count) {
+ case 0:
+ // technically not an error
+ console.log('mapping error', errorString)
+ break
+ case 1:
+ break
+ }
+ } else if (status === RouteModel.Error) {
+ console.log('mapping error', errorString)
+ }
+ }
+ onRoutesChanged: {
+ if (count === 0)
+ return;
+ var route = routeModel.get(0);
+
+ totalDistance = route.distance;
+ metersPerSecond = route.distance / route.travelTime;
+ navigationArrowAnimation.remainingDistance = route.distance;
+
+ routeManeuverModel.clear();
+ currentSegment = route.segments[0];
+ routeSegmentList = route.segments;
+
+ routeSegment = 0;
+ pathSegment = 0;
+ setNextAnimation();
+ }
+ }
+
+ // Model that is used to display individual instructions
+ ListModel {
+ id: routeManeuverModel
+ }
+
+ Map {
+ id: map
+ gesture.enabled: true
+ anchors.fill: parent
+ plugin: mapboxgl
+
+ center: navigation.coordinate
+ zoomLevel: navigation.zoomlevel
+ bearing: navigation.direction
+
+ onCenterChanged: {
+ suggest.center = map.center
+ }
+
+ Behavior on bearing {
+ RotationAnimation {
+ duration: 250
+ direction: RotationAnimation.Shortest
+ }
+ }
+
+ Behavior on center {
+ id: centerBehavior
+ enabled: true
+ CoordinateAnimation { duration: 1500 }
+ }
+
+ MapQuickItem {
+ id: navigationArrowItem
+ z: 3
+ coordinate: navigation.routePosition
+ anchorPoint.x: navigationArrowImage.width / 2
+ anchorPoint.y: navigationArrowImage.height / 2
+ sourceItem: Image {
+ id: navigationArrowImage
+ source: "images/map_location_arrow.png"
+ width: 30
+ height: 30
+ }
+ }
+
+ MapQuickItem {
+ id: navigationCircleItem
+ z: 2
+ coordinate: navigation.routePosition
+ anchorPoint.x: navigationCircleImage.width / 2
+ anchorPoint.y: navigationCircleImage.height / 2
+ sourceItem: Image {
+ id: navigationCircleImage
+ source: "images/blue_circle_gps_area.png"
+ width: 100
+ height: 100
+ }
+ }
+
+ MapQuickItem {
+ id: destinationQuickItem
+ z: 3
+ coordinate: destinationCoordinate
+ anchorPoint.x: destinationImage.width / 2
+ anchorPoint.y: destinationImage.height - 10
+ sourceItem: Image {
+ id: destinationImage
+ source: "images/map_destination.png"
+ width: 40
+ height: 40
+ }
+ visible: navigation.active
+ }
+
+ MapItemView {
+ model: routeModel
+ delegate: routeDelegate
+ }
+
+ Component {
+ id: routeDelegate
+
+ MapRoute {
+ id: route
+ route: routeData
+ line.color: "#3698e8"
+ line.width: 6
+ smooth: true
+ visible: index === 0 // Show only one route (numberAlternativeRoutes not respected yet)
+ }
+ }
+
+ SequentialAnimation {
+ id: navigationArrowAnimation
+ property real rotationDuration: 0;
+ property real rotationDirection: 0;
+ property real coordinateDuration: 0;
+ property real pathDistance: 0;
+ property real remainingDistance: 0;
+ property var sourceCoordinate: navigation.routePosition;
+ property var targetCoordinate: startCoordinate;
+
+ RotationAnimation {
+ target: map
+ property: "bearing"
+ duration: navigationArrowAnimation.rotationDuration
+ to: navigationArrowAnimation.rotationDirection
+ direction: RotationAnimation.Shortest
+ }
+
+ ParallelAnimation {
+ CoordinateAnimation {
+ target: map
+ property: "center"
+ duration: navigationArrowAnimation.coordinateDuration
+ to: navigationArrowAnimation.targetCoordinate
+ }
+
+ CoordinateAnimation {
+ target: navigationArrowItem
+ property: "coordinate"
+ duration: navigationArrowAnimation.coordinateDuration
+ to: navigationArrowAnimation.targetCoordinate
+ }
+
+ CoordinateAnimation {
+ target: navigationCircleItem
+ property: "coordinate"
+ duration: navigationArrowAnimation.coordinateDuration
+ to: navigationArrowAnimation.targetCoordinate
+ }
+
+ NumberAnimation {
+ target: mapContainer
+ property: "naviGuideDistance"
+ duration: navigationArrowAnimation.coordinateDuration
+ to: navigationArrowAnimation.pathDistance
+ }
+
+ NumberAnimation {
+ target: mapContainer
+ property: "totalDistance"
+ duration: navigationArrowAnimation.coordinateDuration
+ to: navigationArrowAnimation.remainingDistance
+ }
+ }
+
+ onStopped: setNextAnimation()
+ }
+
+ MouseArea {
+ // Whenever the user taps on the map, move focus to it
+ anchors.fill: parent
+ onClicked: map.focus = true
+ }
+ }
+
+ Plugin {
+ id: mapboxgl
+ name: "mapbox"
+ PluginParameter {
+ name: "mapbox.access_token"
+ value: "pk.eyJ1IjoibWFwYm94NHF0IiwiYSI6ImNpd3J3eDE0eDEzdm8ydHM3YzhzajlrN2oifQ.keEkjqm79SiFDFjnesTcgQ"
+ }
+ PluginParameter {
+ name: "mapbox.mapping.map_id"
+ value: "mapbox.run-bike-hike"
+ }
+ }
+
+ states: [
+ State {
+ name: "";
+ when: !navigation.active
+ PropertyChanges {
+ target: targetEdit
+ width: UILayout.naviPageLocationWidth
+ anchors.topMargin: UILayout.naviPageLocationTopMargin
+ anchors.bottomMargin: 0
+ }
+ AnchorChanges {
+ target: targetEdit
+ anchors.top: parent.top
+ anchors.bottom: undefined
+ }
+ PropertyChanges {
+ target: naviInputShadow
+ visible: false
+ }
+ },
+ State {
+ name: "NAVIGATING";
+ when: navigation.active
+ PropertyChanges {
+ target: targetEdit
+ width: UILayout.naviPageTripWidth
+ anchors.topMargin: 0
+ anchors.bottomMargin: UILayout.naviPageTripSearchMargin
+ }
+ AnchorChanges {
+ target: targetEdit
+ anchors.top: undefined
+ anchors.bottom: totalTripInfo.top
+ }
+ PropertyChanges {
+ target: naviInputShadow
+ visible: true
+ }
+ }
+ ]
+
+ transitions: Transition {
+ NumberAnimation {
+ properties: "width"
+ easing.type: Easing.InOutQuad
+ duration: 250
+ }
+ AnchorAnimation {
+ duration: 250
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/NaviTripInfo.qml b/basicsuite/ebike-ui/NaviTripInfo.qml
new file mode 100644
index 0000000..e024aab
--- /dev/null
+++ b/basicsuite/ebike-ui/NaviTripInfo.qml
@@ -0,0 +1,147 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+
+import "./BikeStyle"
+
+Rectangle {
+ property real remainingDistance: 0
+ property real remainingTravelTime: 0
+ property var remainingDistanceSplit: datastore.splitDistance(remainingDistance, true)
+ property var remainingTravelTimeSplit: datastore.splitDuration(remainingTravelTime)
+
+ width: UILayout.naviPageTripWidth
+ height: UILayout.naviPageTripHeight
+ radius: UILayout.naviPageTripRadius
+ color: Colors.naviPageTripBackground
+
+ Rectangle {
+ width: UILayout.naviPageTripDividerWidth
+ height: UILayout.naviPageTripDividerHeight
+ radius: 2
+ color: Colors.naviPageTripDivider
+ anchors.centerIn: parent
+ }
+
+ Item {
+ height: parent.height
+ width: naviTripDistanceRemainingText.width + 5 + naviTripDistanceUnitText.width
+ anchors {
+ verticalCenter: parent.verticalCenter
+ horizontalCenter: parent.horizontalCenter
+ horizontalCenterOffset: -parent.width / 4
+ }
+
+ Text {
+ id: naviTripDistanceRemainingText
+ anchors.verticalCenter: parent.verticalCenter
+ color: Colors.naviPageGuideTextColor
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.naviPageTripTotalTextSize
+ }
+ text: remainingDistanceSplit.decimal ? remainingDistanceSplit.value.toFixed(1) : remainingDistanceSplit.value.toFixed(0)
+ }
+
+ Text {
+ id: naviTripDistanceUnitText
+ anchors {
+ verticalCenter: parent.verticalCenter
+ left: naviTripDistanceRemainingText.right
+ leftMargin: 5
+ }
+ color: Colors.naviPageGuideUnitColor
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.naviPageTripTotalUnitSize
+ }
+ text: remainingDistanceSplit.unit
+ }
+ }
+
+
+ Item {
+ height: parent.height
+ width: naviTripTimeRemainingText.width + 5 + naviTripTimeUnitText.width
+ anchors {
+ verticalCenter: parent.verticalCenter
+ horizontalCenter: parent.horizontalCenter
+ horizontalCenterOffset: parent.width / 4
+ }
+
+ Text {
+ id: naviTripTimeRemainingText
+ anchors.verticalCenter: parent.verticalCenter
+ color: Colors.naviPageGuideTextColor
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.naviPageTripTotalTextSize
+ }
+ text: remainingTravelTimeSplit.value.toFixed(0)
+ }
+
+ Text {
+ id: naviTripTimeUnitText
+ anchors {
+ verticalCenter: parent.verticalCenter
+ left: naviTripTimeRemainingText.right
+ leftMargin: 5
+ }
+ color: Colors.naviPageGuideUnitColor
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.naviPageTripTotalUnitSize
+ }
+ text: remainingTravelTimeSplit.unit
+ }
+ }
+
+ Image {
+ source: "images/small_input_box_shadow.png"
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.verticalCenter
+ horizontalCenterOffset: 1
+ verticalCenterOffset: 1
+ }
+ z: -1
+ }
+}
diff --git a/basicsuite/ebike-ui/SpeedView.qml b/basicsuite/ebike-ui/SpeedView.qml
new file mode 100644
index 0000000..d62e710
--- /dev/null
+++ b/basicsuite/ebike-ui/SpeedView.qml
@@ -0,0 +1,526 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import DataStore 1.0
+import "./BikeStyle"
+
+Rectangle {
+ width: UILayout.speedViewRadius * 2 + 2
+ height: UILayout.speedViewRadius * 2 + 2
+ color: "transparent"
+ radius: width * 0.5
+ z: 1
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ top: parent.top
+ topMargin: UILayout.speedViewTop
+ }
+ property int dotcount: UILayout.speedViewDots
+ property int curvewidth: UILayout.speedViewInnerWidth
+ property int speedTextSize: UILayout.speedTextSize
+ property int speedBaselineOffset: UILayout.speedBaselineOffset + 1
+ property int innerRadius: UILayout.speedViewInnerRadius
+ property int speedUnitsSize: UILayout.speedUnitsSize
+ property int speedUnitBaselineOffset: UILayout.speedTextUnitMargin
+ property int speedIconsOffset: UILayout.speedIconsCenterOffset
+ property int speedInfoTextsOffset: 0
+ property int speedInfoTextsSize: UILayout.speedInfoTextsSize
+ property int speedInfoUnitsOffset: UILayout.speedInfoUnitsOffset
+ property int assistPowerIconOffset: UILayout.assistPowerIconOffset
+ property bool enlarged: false
+ property alias cornerRectangle: cornerRectangle
+ property bool showZero: false
+
+ signal showMain()
+
+ // Speed info
+ Text {
+ id: speedText
+ anchors {
+ horizontalCenter: speedView.horizontalCenter
+ baseline: speedView.bottom
+ baselineOffset: -speedBaselineOffset
+ }
+
+ color: Colors.speedText
+ font {
+ family: "Teko, Light"
+ weight: Font.Light
+ pixelSize: speedTextSize
+ }
+ text: showZero ? "0" : datastore.speed.toFixed(0)
+ }
+
+ Text {
+ id: speedUnit
+ anchors {
+ horizontalCenter: speedText.horizontalCenter
+ baseline: speedText.baseline
+ baselineOffset: speedUnitBaselineOffset
+ }
+
+ color: Colors.speedUnit
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: speedUnitsSize
+ }
+ text: datastore.unit === DataStore.Kmh ? qsTr("km/h") : qsTr("mph")
+ }
+
+ // Average speed info
+ Text {
+ id: averageSpeedText
+ anchors {
+ baseline: speedText.baseline
+ baselineOffset: speedInfoTextsOffset
+ horizontalCenter: speedText.horizontalCenter
+ horizontalCenterOffset: -speedIconsOffset
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.averageSpeedText
+ font {
+ family: "Teko, Light"
+ weight: Font.Light
+ pixelSize: speedInfoTextsSize
+ }
+ text: datastore.averagespeed.toFixed(0)
+ }
+
+ Text {
+ id: averageSpeedUnit
+ anchors {
+ horizontalCenter: averageSpeedText.horizontalCenter
+ baseline: averageSpeedText.baseline
+ baselineOffset: speedInfoUnitsOffset
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.averageSpeedUnit
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: speedUnitsSize
+ }
+ horizontalAlignment: Text.AlignHCenter
+ text: datastore.unit === DataStore.Kmh ? qsTr("AVG\nkm/h") : qsTr("AVG\nmph")
+ }
+
+ Image {
+ id: averageSpeedIcon
+ width: UILayout.averageSpeedIconWidth
+ height: UILayout.averageSpeedIconHeight
+ source: "images/speed.png"
+ anchors {
+ horizontalCenter: averageSpeedText.horizontalCenter
+ bottom: averageSpeedText.top
+ bottomMargin: UILayout.averageSpeedIconMargin
+ }
+ visible: speedView.state !== "CORNERED"
+ }
+
+ // Assist info
+ Text {
+ id: assistDistanceText
+ anchors {
+ baseline: speedText.baseline
+ baselineOffset: speedInfoTextsOffset
+ horizontalCenter: speedText.horizontalCenter
+ horizontalCenterOffset: speedIconsOffset
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.assistDistanceText
+ font {
+ family: "Teko, Light"
+ weight: Font.Light
+ pixelSize: speedInfoTextsSize
+ }
+ text: datastore.assistdistance.toFixed(0)
+ }
+
+ Text {
+ id: assistDistanceUnit
+ anchors {
+ horizontalCenter: assistDistanceText.horizontalCenter
+ baseline: assistDistanceText.baseline
+ baselineOffset: speedInfoUnitsOffset
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.assistDistanceUnit
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: speedUnitsSize
+ }
+ horizontalAlignment: Text.AlignHCenter
+ text: datastore.unit === DataStore.Kmh ? qsTr("km\nassist\nleft") : qsTr("mi.\nassist\nleft")
+ }
+
+ Image {
+ id: assistDistanceIcon
+ width: UILayout.assistDistanceIconWidth
+ height: UILayout.assistDistanceIconHeight
+ source: "images/battery.png"
+ anchors {
+ horizontalCenter: assistDistanceText.horizontalCenter
+ bottom: assistDistanceText.top
+ bottomMargin: UILayout.assistDistanceIconMargin
+ }
+ visible: speedView.state !== "CORNERED"
+ }
+
+ // Assist power icon
+ Image {
+ id: assistPowerIcon
+ width: UILayout.assistPowerTconWidth
+ height: UILayout.assistPowerTconHeight
+ source: "images/assist.png"
+ anchors {
+ horizontalCenter: speedView.horizontalCenter
+ top: parent.verticalCenter
+ topMargin: assistPowerIconOffset
+ }
+ visible: speedView.state !== "CORNERED"
+ }
+
+ Gradient {
+ id: assistPowerGradient
+ GradientStop { position: 0.0; color: Colors.assistPowerGradientStart }
+ GradientStop { position: 1.0; color: Colors.assistPowerGradientEnd }
+ }
+
+ // Assist power circles
+ Rectangle {
+ id: assistPowerIcon1
+ width: UILayout.assistPowerCircleRadius * 2
+ height: width
+ radius: UILayout.assistPowerCircleRadius
+ anchors {
+ right: assistPowerIcon2.left
+ rightMargin: UILayout.assistPowerCircleOffset
+ bottom: assistPowerIcon2.bottom
+ bottomMargin: UILayout.assistPowerCircleVerticalOffset
+ }
+ color: Colors.assistPowerEmpty
+ gradient: datastore.assistpower > 2.0 ? assistPowerGradient : null
+ visible: speedView.state !== "CORNERED"
+ }
+
+ Rectangle {
+ id: assistPowerIcon2
+ width: UILayout.assistPowerCircleRadius * 2
+ height: width
+ radius: UILayout.assistPowerCircleRadius
+ anchors {
+ right: parent.horizontalCenter
+ rightMargin: UILayout.assistPowerCircleOffset / 2
+ top: assistPowerIcon.bottom
+ topMargin: UILayout.assistPowerCircleTopMargin
+ }
+ color: Colors.assistPowerEmpty
+ gradient: datastore.assistpower > 25.0 ? assistPowerGradient : null
+ visible: speedView.state !== "CORNERED"
+ }
+
+ Rectangle {
+ id: assistPowerIcon3
+ width: UILayout.assistPowerCircleRadius * 2
+ height: width
+ radius: UILayout.assistPowerCircleRadius
+ anchors {
+ left: parent.horizontalCenter
+ leftMargin: UILayout.assistPowerCircleOffset / 2
+ top: assistPowerIcon.bottom
+ topMargin: UILayout.assistPowerCircleTopMargin
+ }
+ color: Colors.assistPowerEmpty
+ gradient: datastore.assistpower > 50.0 ? assistPowerGradient : null
+ visible: speedView.state !== "CORNERED"
+ }
+
+ Rectangle {
+ id: assistPowerIcon4
+ width: UILayout.assistPowerCircleRadius * 2
+ height: width
+ radius: UILayout.assistPowerCircleRadius
+ anchors {
+ left: assistPowerIcon3.right
+ leftMargin: UILayout.assistPowerCircleOffset
+ bottom: assistPowerIcon3.bottom
+ bottomMargin: UILayout.assistPowerCircleVerticalOffset
+ }
+ color: Colors.assistPowerEmpty
+ gradient: datastore.assistpower > 75.0 ? assistPowerGradient : null
+ visible: speedView.state !== "CORNERED"
+ }
+
+ // Numbers for speed and battery level
+ Text {
+ anchors {
+ baseline: parent.bottom
+ baselineOffset: 20
+ right: parent.horizontalCenter
+ rightMargin: 10
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.dottedRing
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.ringValueText
+ }
+ text: "0"
+ }
+
+ Text {
+ anchors {
+ baseline: parent.bottom
+ baselineOffset: 20
+ left: parent.horizontalCenter
+ leftMargin: 10
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.dottedRing
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.ringValueText
+ }
+ text: "0 %"
+ }
+
+ Text {
+ anchors {
+ baseline: parent.verticalCenter
+ baselineOffset: -10
+ right: parent.left
+ rightMargin: 9
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.dottedRing
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.ringValueText
+ }
+ text: "30"
+ }
+
+ Text {
+ anchors {
+ baseline: parent.verticalCenter
+ baselineOffset: -10
+ left: parent.right
+ leftMargin: 9
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.dottedRing
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.ringValueText
+ }
+ text: "50 %"
+ }
+
+ Text {
+ anchors {
+ baseline: parent.top
+ baselineOffset: -10
+ right: parent.horizontalCenter
+ rightMargin: 10
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.dottedRing
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.ringValueText
+ }
+ text: "60"
+ }
+
+ Text {
+ anchors {
+ baseline: parent.top
+ baselineOffset: -10
+ left: parent.horizontalCenter
+ leftMargin: 10
+ }
+ visible: speedView.state !== "CORNERED"
+
+ color: Colors.dottedRing
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.ringValueText
+ }
+ text: "100 %"
+ }
+
+ Rectangle {
+ id: cornerRectangle
+ width: UILayout.speedViewRadiusMinified
+ height: width
+ // By default this is centered, and invisible
+ anchors.horizontalCenter: speedView.horizontalCenter
+ anchors.verticalCenter: speedView.verticalCenter
+ color: "transparent"
+ radius: 10
+ z: -1
+
+ Image {
+ source: "images/small_speedometer_arrow.png"
+ width: UILayout.speedometerCornerArrowWidth
+ height: UILayout.speedometerCornerArrowHeight
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+ visible: swipeView.currentIndex != 1
+ }
+ }
+
+ Image {
+ source: "images/small_speedometer_shadow.png"
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.verticalCenter
+ horizontalCenterOffset: 1
+ verticalCenterOffset: 1
+ }
+ z: -1
+
+ visible: speedView.state == "CORNERED"
+ }
+
+ Canvas {
+ id: speedArc
+ anchors.fill: parent
+
+ Component.onCompleted: {
+ datastore.onSpeedChanged.connect(speedArc.requestPaint)
+ datastore.onBatterylevelChanged.connect(speedArc.requestPaint)
+ }
+
+ onPaint: {
+ var ctx = getContext("2d");
+ ctx.reset();
+
+ var currentRadius = (speedView.width - 2) / 2;
+ var centerX = speedView.width / 2;
+ var centerY = speedView.height / 2;
+
+ // Draw the dotted circle (if not in corner mode)
+ if (speedView.state !== "CORNERED") {
+ ctx.fillStyle = Colors.dottedRing;
+ var angleStep = Math.PI * 2.0 / dotcount;
+ for (var angle = 0; angle <= Math.PI * 2; angle += angleStep) {
+ var x = currentRadius * Math.cos(angle);
+ var y = currentRadius * Math.sin(angle);
+ ctx.fillRect(centerX + x, centerY + y, 2, 2);
+ }
+ }
+
+ // Draw speed and battery view bases
+ ctx.lineCap = "round";
+ ctx.strokeStyle = Colors.dottedRing;
+ ctx.lineWidth = curvewidth;
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, innerRadius,
+ UILayout.speedViewSpeedStart, UILayout.speedViewSpeedEnd);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, innerRadius,
+ UILayout.speedViewBatteryStart, UILayout.speedViewBatteryEnd, true);
+ ctx.stroke();
+
+ // Draw speed gradient
+ if (!showZero) {
+ var speedArcLength = UILayout.speedViewSpeedEnd - UILayout.speedViewSpeedStart;
+ var speedAngle = Math.min(UILayout.speedViewSpeedEnd,
+ UILayout.speedViewSpeedStart + speedArcLength * (datastore.speed / 60));
+ var speedAngleX = Math.cos(speedAngle) * innerRadius;
+ var speedAngleY = Math.sin(speedAngle) * innerRadius;
+ var speedGradient = ctx.createLinearGradient(centerX, centerY + innerRadius,
+ centerX + speedAngleX, centerY + speedAngleY);
+ speedGradient.addColorStop(0.0, Colors.speedGradientStart);
+ speedGradient.addColorStop(1.0, Colors.speedGradientEnd);
+ ctx.strokeStyle = speedGradient;
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, innerRadius,
+ UILayout.speedViewSpeedStart, speedAngle);
+ ctx.stroke();
+ }
+
+ // Draw battery gradient
+ var batteryArcLength = UILayout.speedViewBatteryEnd - UILayout.speedViewBatteryStart;
+ var batteryAngle = Math.max(UILayout.speedViewBatteryEnd,
+ UILayout.speedViewBatteryStart + batteryArcLength * (datastore.batterylevel / 100));
+ var batteryAngleX = Math.cos(batteryAngle) * innerRadius;
+ var batteryAngleY = Math.sin(batteryAngle) * innerRadius;
+ var batteryGradient = ctx.createLinearGradient(centerX, centerY + innerRadius,
+ centerX + batteryAngleX, centerY + batteryAngleY);
+ //centerX, centerY - innerRadius);
+ batteryGradient.addColorStop(0.0, Colors.batteryGradientStart);
+ batteryGradient.addColorStop(1.0, Colors.batteryGradientEnd);
+ ctx.strokeStyle = batteryGradient;
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, innerRadius,
+ UILayout.speedViewBatteryStart, batteryAngle, true);
+ ctx.stroke();
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ if (enlarged)
+ enlarged = false
+ else if (swipeView.currentIndex === 1)
+ enlarged = true
+ else
+ speedView.showMain()
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/StatsBox.qml b/basicsuite/ebike-ui/StatsBox.qml
new file mode 100644
index 0000000..7d6f24c
--- /dev/null
+++ b/basicsuite/ebike-ui/StatsBox.qml
@@ -0,0 +1,157 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+import QtQuick.Controls 2.0
+import DataStore 1.0
+
+import "./BikeStyle"
+
+// Top-left corner, stats
+Item {
+ width: 320
+ height: UILayout.topViewHeight
+
+ Image {
+ id: tripIcon
+ width: UILayout.statsIconWidth
+ height: UILayout.statsIconHeight
+ source: "images/trip.png"
+ anchors {
+ top: parent.top
+ left: parent.left
+ topMargin: UILayout.statsIconTop
+ leftMargin: UILayout.statsIconLeft
+ }
+ }
+
+ Text {
+ id: tripText
+ color: Colors.distanceText
+ anchors {
+ top: tripIcon.top
+ topMargin: UILayout.statsTextTopOffset
+ left: tripIcon.right
+ leftMargin: UILayout.statsTextSeparator
+ }
+ font {
+ family: "Montserrat, Bold"
+ weight: Font.Bold
+ pixelSize: UILayout.statsTextSize
+ }
+ text: datastore.trip.toFixed(1)
+ }
+
+ Text {
+ id: tripUnitText
+ color: Colors.distanceUnit
+ anchors {
+ baseline: tripIcon.bottom
+ baselineOffset: -UILayout.statsUnitBaselineOffset
+ left: tripIcon.right
+ leftMargin: UILayout.statsTextSeparator
+ }
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.statsTextSize
+ }
+ text: datastore.unit === DataStore.Kmh ? "km" : "mi."
+ }
+
+ Image {
+ id: calIcon
+ width: UILayout.statsIconWidth
+ height: UILayout.statsIconHeight
+ source: "images/calories.png"
+ anchors {
+ top: tripIcon.bottom
+ left: parent.left
+ topMargin: UILayout.statsIconSeparator
+ leftMargin: UILayout.statsIconLeft
+ }
+ }
+
+ Text {
+ id: calText
+ color: Colors.distanceText
+ anchors {
+ top: calIcon.top
+ topMargin: UILayout.statsTextTopOffset
+ left: calIcon.right
+ leftMargin: UILayout.statsTextSeparator
+ }
+ font {
+ family: "Montserrat, Bold"
+ weight: Font.Bold
+ pixelSize: UILayout.statsTextSize
+ }
+ text: datastore.calories.toFixed(0)
+ }
+
+ Text {
+ id: calUnitText
+ color: Colors.distanceUnit
+ anchors {
+ baseline: calIcon.bottom
+ baselineOffset: -UILayout.statsUnitBaselineOffset
+ left: calIcon.right
+ leftMargin: UILayout.statsTextSeparator
+ }
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.statsTextSize
+ }
+ text: "kcal"
+ }
+
+ Rectangle {
+ width: UILayout.horizontalViewSeparatorWidth
+ height: UILayout.horizontalViewSeparatorHeight
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ color: Colors.separator
+ }
+
+ Rectangle {
+ width: UILayout.verticalViewSeparatorWidth
+ height: UILayout.verticalViewSeparatorHheightTop
+ anchors.top: parent.top
+ anchors.right: parent.right
+ color: Colors.separator
+ }
+}
diff --git a/basicsuite/ebike-ui/StatsPage.qml b/basicsuite/ebike-ui/StatsPage.qml
new file mode 100644
index 0000000..959b813
--- /dev/null
+++ b/basicsuite/ebike-ui/StatsPage.qml
@@ -0,0 +1,314 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+import QtCharts 2.2
+import DataStore 1.0
+
+import "./BikeStyle"
+import "moment.js" as Moment
+
+Page {
+ id: statsPage
+ background: Rectangle {
+ color: Colors.mainBackground
+ }
+
+ // Function for pretty-printing duration
+ function splitDuration(duration) {
+ var hours = Math.floor(duration / 3600);
+ var minutes = Math.floor((duration % 3600) / 60);
+ var seconds = Math.floor(duration % 60);
+ if (minutes < 10)
+ minutes = "0" + minutes;
+ return hours + ":" + minutes;
+ }
+
+ function timestampToReadable(timestamp) {
+ return Moment.moment.unix(timestamp).calendar();
+ }
+
+ // On new trip data (save clicked), switch index to new trip
+ Connections {
+ target: tripdata
+ onTripDataSaved: tripView.setCurrentIndex(index)
+ }
+
+ RoundButton {
+ id: endTrip
+ width: UILayout.statsEndtripWidth
+ height: UILayout.statsEndtripHeight
+ radius: height / 2
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ top: parent.top
+ topMargin: UILayout.statsEndtripMargin
+ }
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: parent.down ? Colors.statsButtonPressed : "transparent"
+ radius: parent.radius
+ border.color: Colors.statsButtonActive
+ border.width: parent.down ? 0 : 1
+ }
+
+ contentItem: Text {
+ color: Colors.statsButtonActiveText
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ text: qsTr("END TRIP")
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.statsEndtripTextSize
+ }
+ }
+ visible: tripView.currentIndex === 0
+ onClicked: tripdata.endTrip()
+ }
+
+ Text {
+ id: tripDateText
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ verticalCenter: parent.top
+ verticalCenterOffset: UILayout.statsEndtripMargin + UILayout.statsEndtripHeight / 2
+ }
+ color: Colors.statsButtonActiveText
+ text: qsTr("YESTERDAY")
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.statsEndtripTextSize
+ }
+ visible: tripView.currentIndex > 0
+ }
+
+ RoundButton {
+ id: previousChart
+ width: UILayout.statsTripButtonWidth
+ height: UILayout.statsTripButtonHeight
+ radius: height / 2
+ anchors {
+ left: parent.left
+ top: endTrip.top
+ leftMargin: UILayout.statsTripButtonMarginSide
+ }
+ enabled: tripView.currentIndex > 0
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: parent.down ? Colors.statsButtonPressed : "transparent"
+ radius: parent.radius
+ border.color: enabled ? Colors.statsButtonActive : Colors.statsButtonInactive
+ border.width: parent.down ? 0 : 1
+ }
+
+ contentItem: Item {}
+ Image {
+ anchors.centerIn: parent
+ source: "images/arrow_left.png"
+ opacity: parent.enabled ? 1.0 : 0.3
+ }
+
+ onClicked: tripView.decrementCurrentIndex()
+ }
+
+ RoundButton {
+ id: nextChart
+ width: UILayout.statsTripButtonWidth
+ height: UILayout.statsTripButtonHeight
+ radius: height / 2
+ anchors {
+ right: parent.right
+ top: endTrip.top
+ rightMargin: UILayout.statsTripButtonMarginSide
+ }
+ enabled: tripView.currentIndex < tripView.count - 1
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: parent.down ? Colors.statsButtonPressed : "transparent"
+ radius: parent.radius
+ border.color: enabled ? Colors.statsButtonActive : Colors.statsButtonInactive
+ border.width: parent.down ? 0 : 1
+ }
+
+ contentItem: Item {}
+ Image {
+ anchors.centerIn: parent
+ source: "images/arrow_right.png"
+ opacity: parent.enabled ? 1.0 : 0.3
+ }
+
+ onClicked: tripView.incrementCurrentIndex()
+ }
+
+ // Odometer
+ Item {
+ width: odometerText.width + odometerUnit.width + odometerDescription.width + 2 * 4
+ anchors {
+ right: parent.right
+ bottom: parent.top
+ rightMargin: UILayout.statsOdometerMarginRight
+ bottomMargin: -UILayout.statsOdometerBaselineOffset
+ }
+
+ Text {
+ id: odometerDescription
+ anchors.right: odometerText.left
+ anchors.rightMargin: 4
+ anchors.baseline: parent.bottom
+ color: Colors.statsDescriptionText
+ text: qsTr("Total")
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.statsDescriptionTextSize
+ }
+ }
+
+ Text {
+ id: odometerText
+ anchors.right: odometerUnit.left
+ anchors.rightMargin: 4
+ anchors.baseline: parent.bottom
+ color: Colors.statsValueText
+ text: datastore.odometer.toFixed(1)
+ font {
+ family: "Montserrat, Bold"
+ weight: Font.Bold
+ pixelSize: UILayout.statsValueTextSize
+ }
+ }
+
+ Text {
+ id: odometerUnit
+ anchors.right: parent.right
+ anchors.baseline: parent.bottom
+ color: Colors.statsDescriptionText
+ text: datastore.unit === DataStore.Kmh ? qsTr("km") : qsTr("mi.")
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.statsDescriptionTextSize
+ }
+ }
+ }
+
+ SwipeView {
+ id: tripView
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: endTrip.bottom
+ bottom: tripChart.top
+ leftMargin: UILayout.statsTripButtonMarginSide
+ rightMargin: UILayout.statsTripButtonMarginSide
+ topMargin: UILayout.statsTopMargin
+ }
+ // Hide any excess content, since we are using margins
+ clip: true
+
+ // Load data on first show
+ Component.onCompleted: tripdata.refresh()
+
+ onCurrentIndexChanged: tripDateText.text = timestampToReadable(tripdata.get(currentIndex).starttime)
+
+ Repeater {
+ model: tripdata
+
+ Column {
+ width: tripView.width
+ height: tripView.height
+
+ ColumnSpacer {
+ color: Colors.statsSeparator
+ }
+
+ StatsRow {
+ leftTitle: qsTr("Duration (h:m)")
+ leftValue: splitDuration(duration)
+ rightTitle: datastore.unit === DataStore.Kmh ? qsTr("Max. speed (km/h)") : qsTr("Max. speed (mph)")
+ rightValue: maxspeed.toFixed(1)
+ }
+
+ ColumnSpacer {
+ color: Colors.statsSeparator
+ }
+
+ StatsRow {
+ leftTitle: datastore.unit === DataStore.Kmh ? qsTr("Distance (km)") : qsTr("Distance (mi.)")
+ leftValue: distance.toFixed(1)
+ rightTitle: datastore.unit === DataStore.Kmh ? qsTr("Avg. speed (km/h)") : qsTr("Avg. speed (mph)")
+ rightValue: avgspeed.toFixed(1)
+ }
+
+ ColumnSpacer {
+ color: Colors.statsSeparator
+ }
+
+ StatsRow {
+ leftTitle: qsTr("Calories (kcal)")
+ leftValue: calories.toFixed(1)
+ rightTitle: qsTr("Ascent (m)")
+ rightValue: ascent.toFixed(1)
+ }
+
+ ColumnSpacer {
+ color: Colors.statsSeparator
+ }
+ }
+ }
+ }
+
+ TripChart {
+ id: tripChart
+ width: UILayout.chartWidth
+ height: UILayout.chartHeight
+ anchors {
+ bottom: parent.bottom
+ right: parent.right
+ bottomMargin: UILayout.chartBottomMargin
+ rightMargin: UILayout.chartRightMargin
+ }
+ animationRunning: tripView.currentIndex === 0
+ tripDetails: tripdata.get(currentIndex)
+ currentIndex: tripView.currentIndex
+ }
+}
diff --git a/basicsuite/ebike-ui/StatsRow.qml b/basicsuite/ebike-ui/StatsRow.qml
new file mode 100644
index 0000000..df0f523
--- /dev/null
+++ b/basicsuite/ebike-ui/StatsRow.qml
@@ -0,0 +1,102 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+
+import "./BikeStyle"
+
+Item {
+ property string leftTitle
+ property string leftValue
+ property string rightTitle
+ property string rightValue
+ height: UILayout.statsHeight
+ width: parent.width
+
+ Text {
+ text: leftTitle
+ anchors.left: parent.left
+ height: parent.height
+ color: Colors.statsDescriptionText
+ verticalAlignment: Text.AlignVCenter
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.statsDescriptionTextSize
+ }
+ }
+
+ Text {
+ text: leftValue
+ anchors.right: parent.horizontalCenter
+ anchors.rightMargin: UILayout.statsCenterOffset
+ height: parent.height
+ color: Colors.statsValueText
+ verticalAlignment: Text.AlignVCenter
+ font {
+ family: "Montserrat, Bold"
+ weight: Font.Bold
+ pixelSize: UILayout.statsValueTextSize
+ }
+ }
+
+ Text {
+ text: rightTitle
+ anchors.left: parent.horizontalCenter
+ anchors.leftMargin: UILayout.statsCenterOffset
+ height: parent.height
+ color: Colors.statsDescriptionText
+ verticalAlignment: Text.AlignVCenter
+ font {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.statsDescriptionTextSize
+ }
+ }
+
+ Text {
+ text: rightValue
+ anchors.right: parent.right
+ height: parent.height
+ color: Colors.statsValueText
+ verticalAlignment: Text.AlignVCenter
+ font {
+ family: "Montserrat, Bold"
+ weight: Font.Bold
+ pixelSize: UILayout.statsValueTextSize
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/ToggleSwitch.qml b/basicsuite/ebike-ui/ToggleSwitch.qml
new file mode 100644
index 0000000..94af651
--- /dev/null
+++ b/basicsuite/ebike-ui/ToggleSwitch.qml
@@ -0,0 +1,66 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+
+import "./BikeStyle"
+
+Switch {
+ id: control
+
+ implicitWidth: indicator.implicitWidth
+ implicitHeight: indicator.implicitHeight
+
+ indicator: Rectangle {
+ implicitWidth: UILayout.switchWidth
+ implicitHeight: UILayout.switchHeight
+ radius: height / 2
+ y: parent.height / 2 - height / 2
+ color: control.checked ? Colors.switchBackgroundOn : Colors.switchBackgroundOff
+
+ Rectangle {
+ x: control.checked ? parent.width - width : 0
+ y: UILayout.switchHeight / 2 - UILayout.switchIndicatorRadius
+ width: 2 * UILayout.switchIndicatorRadius
+ height: 2 * UILayout.switchIndicatorRadius
+ radius: UILayout.switchIndicatorRadius
+ color: control.checked ? Colors.switchOn : Colors.switchOff
+ }
+ }
+
+ contentItem: Text {}
+}
diff --git a/basicsuite/ebike-ui/TripChart.qml b/basicsuite/ebike-ui/TripChart.qml
new file mode 100644
index 0000000..72812a5
--- /dev/null
+++ b/basicsuite/ebike-ui/TripChart.qml
@@ -0,0 +1,236 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtCharts 2.2
+import DataStore 1.0
+
+import "./BikeStyle"
+
+ChartView {
+ property var exampleTrips: [
+ [[0, 0], [15, 250], [30, 0], [30, 0], [25, 450], [20, 400], [10, 200], [10, 200], [20, 350], [20, 300], [10, 200], [10, 100], [10, 100], [10, 175], [10, 150], [15, 200], [15, 250], [10, 200], [15, 250], [0, 0]],
+ [[0, 0], [5, 100], [10, 200], [10, 200], [10, 150], [15, 250], [15, 275], [15, 200], [20, 350], [5, 50], [15, 200], [15, 250], [15, 225], [20, 300], [25, 425], [25, 400], [15, 200], [8, 125], [10, 175], [0, 0]],
+ [[0, 0], [20, 375], [15, 250], [15, 300], [15, 275], [0, 0], [10, 200], [10, 100], [10, 100], [15, 250], [10, 200], [10, 100], [10, 100], [8, 100], [25, 50], [15, 200], [15, 250], [10, 200], [8, 100], [0, 0]],
+ [[0, 0], [15, 300], [15, 200], [15, 250], [10, 450], [20, 375], [20, 350], [20, 250], [15, 200], [15, 225], [8, 150], [8, 100], [8, 125], [8, 100], [10, 150], [15, 200], [15, 250], [20, 300], [15, 250], [0, 0]]
+ ]
+ property bool animationRunning: false
+ property int currentIndex
+ property var tripDetails
+
+ id: tripChart
+ antialiasing: true
+ backgroundColor: "transparent"
+ backgroundRoundness: 0
+ title: ""
+ legend {
+ markerShape: Legend.MarkerShapeCircle
+ reverseMarkers: true
+ labelColor: "#ffffff"
+ font {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.chartLegendTextSize
+ }
+ }
+
+ margins {
+ top: 0
+ bottom: 0
+ left: 0
+ right: 0
+ }
+
+ // X-axis is a timestamp value
+ DateTimeAxis {
+ id: axisX
+ format: Qt.locale("en_US").timeFormat(Locale.ShortFormat)
+ tickCount: 3
+ // No grid and no line
+ gridVisible: false
+ lineVisible: false
+ titleVisible: false
+ labelsColor: Colors.chartTimeLabel
+ labelsFont {
+ family: "Montserrat, Light"
+ weight: Font.Light
+ pixelSize: UILayout.chartTimeLabelSize
+ }
+ }
+
+ ValueAxis {
+ id: speedAxis
+ min: 0
+ max: 50
+ gridLineColor: Colors.chartGridLine
+ labelsColor: Colors.chartSpeed
+ labelsFont {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.chartSpeedLabelSize
+ }
+ titleVisible: false
+ lineVisible: false
+ labelFormat: "%.0f"
+ }
+
+ ValueAxis {
+ id: assistAxis
+ min: 0
+ max: 500
+ labelsColor: Colors.chartAssistpower
+ labelsFont {
+ family: "Montserrat, Regular"
+ weight: Font.Normal
+ pixelSize: UILayout.chartAssistpowerLabelSize
+ }
+ titleVisible: false
+ lineVisible: false
+ gridVisible: false
+ labelFormat: "%.0f"
+ }
+
+ SplineSeries {
+ id: assistSeries
+ name: qsTr("Pedal assist (W)")
+
+ axisX: axisX
+ axisYRight: assistAxis
+ pointsVisible: true
+ }
+
+ SplineSeries {
+ id: speedSeries
+ name: datastore.unit === DataStore.Kmh ? qsTr("Speed (km/h)") : qsTr("Speed (mph)")
+
+ axisX: axisX
+ axisY: speedAxis
+ pointsVisible: true
+ }
+
+ function updateTripGraph() {
+ if (currentIndex === 0 ) {
+ // Clear all current values (resets the graph)
+ speedSeries.removePoints(0, speedSeries.count);
+ assistSeries.removePoints(0, assistSeries.count);
+ } else if (currentIndex > 0) {
+ // Clear all current values
+ speedSeries.removePoints(0, speedSeries.count);
+ assistSeries.removePoints(0, assistSeries.count);
+ var seriesdata = exampleTrips[currentIndex % 4];
+
+ var now = tripDetails.starttime * 1000;
+ var duration = tripDetails.duration / seriesdata.length;
+
+ axisX.min = new Date(now - 60000);
+ for (var i = 0; i < seriesdata.length; i++) {
+ speedSeries.append(now, seriesdata[i][0]);
+ assistSeries.append(now, seriesdata[i][1]);
+ now += duration * 1000;
+ }
+ now -= duration * 1000;
+ axisX.max = new Date(now + 60000);
+ }
+ }
+
+ onTripDetailsChanged: updateTripGraph()
+
+ onCurrentIndexChanged: updateTripGraph()
+
+ // Make sure we have a proper value here
+ onAnimationRunningChanged: tripAnimationTimer.lastUpdate = new Date().getTime()
+
+ Timer {
+ id: tripAnimationTimer
+ property real lastUpdate: new Date().getTime()
+ property int currentIndex: 0
+ property var values: [[0, 0],
+ [10, 200],
+ [15, 250],
+ [15, 250],
+ [8, 150],
+ [5, 100],
+ [20, 400],
+ [20, 375],
+ [15, 275],
+ [15, 250],
+ [25, 450],
+ [15, 200],
+ [15, 200],
+ [10, 175],
+ [10, 150],
+ [5, 100],
+ [8, 125],
+ [10, 200],
+ [15, 250],
+ [0, 0]]
+
+ // Animate only if visible on screen
+ running: animationRunning && (swipeView.currentIndex === 0)
+ repeat: true
+ interval: 200
+ onTriggered: {
+ var now = new Date().getTime();
+ // Load a few initial numbers if empty
+ if (speedSeries.count === 0) {
+ speedSeries.append(now, values[0][0]);
+ assistSeries.append(now, values[0][1]);
+ speedSeries.append(now + 5000, values[1][0]);
+ assistSeries.append(now + 5000, values[1][1]);
+ speedSeries.append(now + 10000, values[2][0]);
+ assistSeries.append(now + 10000, values[2][1]);
+ speedSeries.append(now + 15000, values[3][0]);
+ assistSeries.append(now + 15000, values[3][1]);
+ currentIndex = 4;
+ }
+
+ if (now - lastUpdate > 5000) {
+ speedSeries.append(now + 15000, values[currentIndex][0]);
+ assistSeries.append(now + 15000, values[currentIndex][1]);
+ if (speedSeries.count > 17)
+ speedSeries.remove(0);
+ if (assistSeries.count > 17)
+ assistSeries.remove(0);
+ currentIndex += 1;
+ if (currentIndex == 20)
+ currentIndex = 0;
+ lastUpdate = now;
+ }
+ axisX.min = new Date(now - 50 * 1000);
+ axisX.max = new Date(now);
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/ViewTab.qml b/basicsuite/ebike-ui/ViewTab.qml
new file mode 100644
index 0000000..040436a
--- /dev/null
+++ b/basicsuite/ebike-ui/ViewTab.qml
@@ -0,0 +1,148 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+
+import "./BikeStyle"
+
+Item {
+ property alias musicPlayerSwitch: musicPlayerSwitch
+
+ signal resetDemo
+
+ Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+
+ Text {
+ height: UILayout.configurationItemHeight
+ width: parent.width
+ text: qsTr("VIEW")
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.configurationTitleSize
+ }
+ color: Colors.tabTitleColor
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ ColumnSpacer {
+ color: Colors.tabItemBorder
+ }
+
+ ConfigurationItem {
+ description: qsTr("Show FPS")
+
+ ToggleSwitch {
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ right: parent.right
+ }
+ checked: fps.visible
+ onCheckedChanged: fps.visible = checked
+ }
+ }
+
+ ColumnSpacer {
+ color: Colors.tabItemBorder
+ }
+
+ ConfigurationItem {
+ description: qsTr("Audio controls")
+
+ ToggleSwitch {
+ id: musicPlayerSwitch
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ right: parent.right
+ }
+ checked: false
+ onCheckedChanged: musicPlayer.state = (checked ? "" : "hidden")
+ }
+ }
+
+ ColumnSpacer {
+ color: Colors.tabItemBorder
+ }
+
+ ConfigurationItem {
+ description: qsTr("Reset demo")
+
+ RoundButton {
+ id: resetButton
+ width: UILayout.unitButtonWidthMargin * 2 + mphText.implicitWidth
+ height: UILayout.unitButtonHeight
+ radius: height / 2
+ anchors {
+ verticalCenter: parent.verticalCenter
+ right: parent.right
+ }
+
+ background: Rectangle {
+ anchors.fill: parent
+ color: parent.down ? Colors.statsButtonPressed : "transparent"
+ radius: parent.radius
+ border.color: Colors.statsButtonActive
+ border.width: parent.down ? 0 : 1
+ }
+
+ contentItem: Text {
+ id: mphText
+ text: "RESET DEMO"
+ font {
+ family: "Montserrat, Medium"
+ weight: Font.Medium
+ pixelSize: UILayout.unitFontSize
+ }
+ color: Colors.statsButtonActiveText
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ onClicked: resetDemo()
+ }
+ }
+
+ ColumnSpacer {
+ color: Colors.tabItemBorder
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/app.pro b/basicsuite/ebike-ui/app.pro
new file mode 100644
index 0000000..dd993f1
--- /dev/null
+++ b/basicsuite/ebike-ui/app.pro
@@ -0,0 +1,141 @@
+QT += quick
+CONFIG += c++11
+TARGET = ebike
+
+# The following define makes your compiler emit warnings if you use
+# any feature of Qt which as been marked deprecated (the exact warnings
+# depend on your compiler). Please consult the documentation of the
+# deprecated API in order to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if you use deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
+
+include(../shared/shared.pri)
+b2qtdemo_deploy_defaults()
+
+content.files = \
+ qtquickcontrols2.conf \
+ main.qml \
+ NaviPage.qml \
+ StatsPage.qml \
+ MainPage.qml \
+ SpeedView.qml \
+ StatsBox.qml \
+ NaviBox.qml \
+ LightsBox.qml \
+ ModeBox.qml \
+ ClockView.qml \
+ MusicPlayer.qml \
+ ConfigurationDrawer.qml \
+ IconifiedTabButton.qml \
+ GeneralTab.qml \
+ ColumnSpacer.qml \
+ BikeInfoTab.qml \
+ TripChart.qml \
+ FpsItem.qml \
+ NaviGuide.qml \
+ NaviTripInfo.qml \
+ ViewTab.qml \
+ moment.js \
+ StatsRow.qml \
+ ConfigurationItem.qml \
+ NaviButton.qml \
+ ToggleSwitch.qml \
+ mostrecent.bson
+
+content.path = $$DESTPATH
+
+style.files = \
+ BikeStyle/Colors.qml \
+ BikeStyle/qmldir \
+ BikeStyle/UILayout.qml
+
+
+images.files = \
+ images/lights_off.png \
+ images/lights_on.png \
+ images/map-marker.png \
+ images/trip.png \
+ images/calories.png \
+ images/nextsong.png \
+ images/nextsong_pressed.png \
+ images/play.png \
+ images/play_pressed.png \
+ images/prevsong.png \
+ images/prevsong_pressed.png \
+ images/speed.png \
+ images/battery.png \
+ images/assist.png \
+ images/arrow_left.png \
+ images/top_curtain_drag.png \
+ images/spinner.png \
+ images/checkmark.png \
+ images/nav_left.png \
+ images/nav_right.png \
+ images/nav_straight.png \
+ images/small_speedometer_arrow.png \
+ images/map_locate.png \
+ images/map_zoomin.png \
+ images/map_zoomout.png \
+ images/info.png \
+ images/info_selected.png \
+ images/list.png \
+ images/list_selected.png \
+ images/settings.png \
+ images/settings_selected.png \
+ images/curtain_up_arrow.png \
+ images/search.png \
+ images/search_cancel.png \
+ images/fps_icon.png \
+ images/arrow_right.png \
+ images/curtain_shadow_handle.png \
+ images/map_btn_shadow.png \
+ images/map_destination.png \
+ images/map_location_arrow.png \
+ images/small_speedometer_shadow.png \
+ images/navigation_widget_shadow.png \
+ images/small_input_box_shadow.png \
+ images/nav_bear_l.png \
+ images/nav_bear_r.png \
+ images/nav_hard_l.png \
+ images/nav_hard_r.png \
+ images/nav_light_left.png \
+ images/nav_light_right.png \
+ images/nav_nodir.png \
+ images/nav_uturn_l.png \
+ images/nav_uturn_r.png \
+ images/pause.png \
+ images/pause_pressed.png \
+ images/ok.png \
+ images/warning.png \
+ images/bike-battery.png \
+ images/bike-brakes.png \
+ images/bike-chain.png \
+ images/bike-frontwheel.png \
+ images/bike-gears.png \
+ images/bike-rearwheel.png \
+ images/bike-light.png \
+ images/blue_circle_gps_area.png
+
+OTHER_FILES += $${images.files}
+INSTALLS += images
+images.path = $$DESTPATH/images
+export(images.files)
+export(images.path)
+
+OTHER_FILES += $${style.files}
+INSTALLS += style
+style.path = $$DESTPATH/BikeStyle
+export(style.files)
+export(style.path)
+export(OTHER_FILES)
+export(INSTALLS)
+
+
+OTHER_FILES += $${content.files}
+
+INSTALLS += target content
+
diff --git a/basicsuite/ebike-ui/brightnesscontroller.cpp b/basicsuite/ebike-ui/brightnesscontroller.cpp
new file mode 100644
index 0000000..a6e2e30
--- /dev/null
+++ b/basicsuite/ebike-ui/brightnesscontroller.cpp
@@ -0,0 +1,91 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QFile>
+
+#include "brightnesscontroller.h"
+
+static const char brightnessSource[] = "/sys/class/backlight/backlight/brightness";
+static const int brightnessMax = 6;
+static const int brightnessMin = 1;
+
+BrightnessController::BrightnessController(QObject *parent)
+ : QObject(parent)
+ , m_automatic(false)
+{
+ QFile brightnessfile(brightnessSource);
+ if (brightnessfile.exists() && brightnessfile.open(QIODevice::ReadOnly)) {
+ char data[1];
+ if (brightnessfile.read(data, 1)) {
+ m_brightness = static_cast<int>(data[0] - '0');
+ }
+ } else {
+ // By default set to half
+ m_brightness = (brightnessMax + brightnessMin) / 2;
+ }
+}
+
+void BrightnessController::setBrightness(int brightness)
+{
+ if (m_brightness == brightness)
+ return;
+
+ // Valid values are between 1-6
+ if (brightness < brightnessMin || brightness > brightnessMax)
+ return;
+
+ QFile brightnessfile(brightnessSource);
+ if (brightnessfile.exists() && brightnessfile.open(QIODevice::WriteOnly)) {
+ char data[1] = {static_cast<char>('0' + brightness)};
+ if (brightnessfile.write(data, 1) == 1) {
+ m_brightness = brightness;
+ emit brightnessChanged(m_brightness);
+ }
+ } else {
+ // File does not exists, simulate changes
+ m_brightness = brightness;
+ emit brightnessChanged(m_brightness);
+ }
+}
+
+void BrightnessController::setAutomatic(bool automatic)
+{
+ if (m_automatic == automatic)
+ return;
+
+ m_automatic = automatic;
+ emit automaticChanged(m_automatic);
+}
diff --git a/basicsuite/ebike-ui/brightnesscontroller.h b/basicsuite/ebike-ui/brightnesscontroller.h
new file mode 100644
index 0000000..7b85f58
--- /dev/null
+++ b/basicsuite/ebike-ui/brightnesscontroller.h
@@ -0,0 +1,69 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BRIGHTNESSCONTROLLER_H
+#define BRIGHTNESSCONTROLLER_H
+
+#include <QObject>
+
+class BrightnessController : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(int brightness READ brightness WRITE setBrightness NOTIFY brightnessChanged)
+ Q_PROPERTY(bool automatic READ automatic WRITE setAutomatic NOTIFY automaticChanged)
+
+public:
+ explicit BrightnessController(QObject *parent = nullptr);
+
+public:
+ int brightness() const { return m_brightness; }
+ bool automatic() const { return m_automatic; }
+
+ void setBrightness(int brightness);
+ void setAutomatic(bool automatic);
+
+signals:
+ void brightnessChanged(int brightness);
+ void automaticChanged(bool automatic);
+
+public slots:
+
+private:
+ int m_brightness;
+ bool m_automatic;
+};
+
+#endif // BRIGHTNESSCONTROLLER_H
diff --git a/basicsuite/ebike-ui/datamodelplugin/datamodelplugin.pro b/basicsuite/ebike-ui/datamodelplugin/datamodelplugin.pro
new file mode 100644
index 0000000..ce22eec
--- /dev/null
+++ b/basicsuite/ebike-ui/datamodelplugin/datamodelplugin.pro
@@ -0,0 +1,39 @@
+TEMPLATE = lib
+CONFIG += plugin
+QT += qml quick positioning charts
+
+TARGET = ebikedatamodelplugin
+
+SOURCES += plugin.cpp \
+ $$PWD/../socketclient.cpp \
+ $$PWD/../datastore.cpp \
+ $$PWD/../navigation.cpp \
+ $$PWD/../mapboxsuggestions.cpp \
+ $$PWD/../suggestionsmodel.cpp \
+ $$PWD/../mapbox.cpp \
+ $$PWD/../brightnesscontroller.cpp \
+ $$PWD/../fpscounter.cpp \
+ $$PWD/../tripdatamodel.cpp
+
+HEADERS += \
+ $$PWD/../socketclient.h \
+ $$PWD/../datastore.h \
+ $$PWD/../navigation.h \
+ $$PWD/../mapboxsuggestions.h \
+ $$PWD/../suggestionsmodel.h \
+ $$PWD/../mapbox.h \
+ $$PWD/../brightnesscontroller.h \
+ $$PWD/../fpscounter.h \
+ $$PWD/../tripdatamodel.h
+
+INCLUDEPATH += $$PWD/../
+
+pluginfiles.files += \
+ qmldir \
+
+B2QT_DEPLOYPATH = /data/user/qt/qmlplugins/DataStore
+target.path += $$B2QT_DEPLOYPATH
+pluginfiles.path += $$B2QT_DEPLOYPATH
+
+INSTALLS += target pluginfiles
+
diff --git a/basicsuite/ebike-ui/datamodelplugin/plugin.cpp b/basicsuite/ebike-ui/datamodelplugin/plugin.cpp
new file mode 100644
index 0000000..a07e4e6
--- /dev/null
+++ b/basicsuite/ebike-ui/datamodelplugin/plugin.cpp
@@ -0,0 +1,109 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of Qt for Device Creation.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtQml/QQmlExtensionPlugin>
+#include <QtQml/qqml.h>
+#include <QtQml/QQmlEngine>
+#include <QtQml/QQmlContext>
+#include <qdebug.h>
+
+#include "datastore.h"
+#include "tripdatamodel.h"
+#include "navigation.h"
+#include "mapbox.h"
+#include "mapboxsuggestions.h"
+#include "suggestionsmodel.h"
+#include "brightnesscontroller.h"
+#include "fpscounter.h"
+
+class QExampleQmlPlugin : public QQmlExtensionPlugin
+{
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
+
+public:
+
+ void registerTypes(const char *uri)
+ {
+ qmlRegisterType<DataStore>("DataStore", 1, 0, "DataStore");
+ }
+
+ void initializeEngine(QQmlEngine *engine, const char *uri)
+ {
+ // Setup data store for connection to backend server
+ DataStore *datastore = new DataStore(engine);
+ datastore->connectToServer("datasocket");
+
+ // Setup mapbox and suggestions
+ MapBox *mapbox = new MapBox(engine);
+ MapBoxSuggestions *suggest = new MapBoxSuggestions(mapbox, engine);
+
+ // Setup navigation container
+ Navigation *navi = new Navigation(mapbox, engine);
+
+ // Brightness controller
+ BrightnessController *brightness = new BrightnessController(engine);
+
+ // FPS counter
+ FpsCounter *fps = new FpsCounter(engine);
+
+ QQmlContext *context = engine->rootContext();
+ context->setContextProperty("datastore", datastore);
+ context->setContextProperty("tripdata", datastore->tripDataModel());
+ context->setContextProperty("navigation", navi);
+ context->setContextProperty("suggest", suggest);
+ context->setContextProperty("suggestions", suggest->suggestions());
+ context->setContextProperty("brightness", brightness);
+ context->setContextProperty("fps", fps);
+ }
+};
+
+
+#include "plugin.moc"
diff --git a/basicsuite/ebike-ui/datamodelplugin/qmldir b/basicsuite/ebike-ui/datamodelplugin/qmldir
new file mode 100644
index 0000000..3018d21
--- /dev/null
+++ b/basicsuite/ebike-ui/datamodelplugin/qmldir
@@ -0,0 +1,2 @@
+module DataStore
+plugin ebikedatamodelplugin
diff --git a/basicsuite/ebike-ui/datastore.cpp b/basicsuite/ebike-ui/datastore.cpp
new file mode 100644
index 0000000..73567ac
--- /dev/null
+++ b/basicsuite/ebike-ui/datastore.cpp
@@ -0,0 +1,300 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QJsonArray>
+#include <QMetaObject>
+#include <QMetaMethod>
+
+#include "socketclient.h"
+#include "datastore.h"
+#include "tripdatamodel.h"
+
+// Speed conversions from m/s to km/h and mph
+static const double msToKmh = 3.6;
+static const double msToMph = 2.2369418519393043;
+// Distance conversions to and from m to km and mi
+static const double mToKm = 0.001;
+static const double mToMi = 0.000621371;
+static const double kmToM = 1000.0;
+static const double miToM = 1609.344;
+static const double mToYd = 1760.0 / 1609.344;
+
+DataStore::DataStore(QObject *parent)
+ : QObject(parent)
+ , m_client(new SocketClient(this))
+ , m_unit(Mph)
+ , m_trips(new TripDataModel(this, this))
+{
+ connect(m_client, &SocketClient::connected, this, &DataStore::requestStatus);
+ connect(m_client, &SocketClient::newMessage, this, &DataStore::parseMessage);
+}
+
+void DataStore::connectToServer(const QString &servername)
+{
+ m_client->connectToServer(servername);
+}
+
+void DataStore::getTrips()
+{
+ m_client->sendToServer(QJsonObject{{"method", "gettrips"}});
+}
+
+void DataStore::endTrip()
+{
+ m_client->sendToServer(QJsonObject{{"method", "endtrip"}});
+}
+
+void DataStore::toggleMode()
+{
+ setMode(mode() == Cruise ? Sport : Cruise);
+}
+
+void DataStore::resetDemo()
+{
+ m_client->sendToServer(QJsonObject{{"method", "reset"}});
+}
+
+double DataStore::speed() const
+{
+ return convertSpeed(m_properties.value("speed").toDouble());
+}
+
+double DataStore::topSpeed() const
+{
+ return convertSpeed(m_properties.value("topspeed").toDouble());
+}
+
+double DataStore::averageSpeed() const
+{
+ return convertSpeed(m_current.value("distance").toDouble() / m_current.value("duration").toDouble());
+}
+
+double DataStore::odometer() const
+{
+ return convertDistance(m_properties.value("odometer").toDouble());
+}
+
+double DataStore::trip() const
+{
+ return convertDistance(m_current.value("distance").toDouble());
+}
+
+double DataStore::calories() const
+{
+ return m_current.value("calories").toDouble();
+}
+
+double DataStore::assistDistance() const
+{
+ return convertDistance(m_properties.value("assistdistance").toDouble());
+}
+
+double DataStore::assistPower() const
+{
+ return m_properties.value("assistpower").toDouble();
+}
+
+double DataStore::batteryLevel() const
+{
+ return m_properties.value("batterylevel").toDouble();
+}
+
+bool DataStore::lights() const
+{
+ return m_properties.value("lights").toBool();
+}
+
+DataStore::Mode DataStore::mode() const
+{
+ return static_cast<Mode>(m_properties.value("mode").toInt());
+}
+
+DataStore::Unit DataStore::unit() const
+{
+ return m_unit;
+}
+
+int DataStore::arrow() const
+{
+ return m_properties.value("arrow").toInt();
+}
+
+double DataStore::legDistance() const
+{
+ return m_properties.value("legdistance").toDouble();
+}
+
+double DataStore::tripRemaining() const
+{
+ return m_properties.value("tripremaining").toDouble();
+}
+
+QString DataStore::smallUnit() const
+{
+ return m_unit == Kmh ? "m" : "yd";
+}
+
+void DataStore::setLights(bool lights)
+{
+ if (m_properties.value("lights").toBool() == lights)
+ return;
+
+ m_properties.insert("lights", lights);
+ m_client->sendToServer(QJsonObject{{"method", "set"}, {"key", "lights"}, {"value", lights}});
+ emit lightsChanged();
+}
+
+void DataStore::setMode(Mode mode)
+{
+ if (DataStore::mode() == mode)
+ return;
+
+ m_properties.insert("mode", mode);
+ m_client->sendToServer(QJsonObject{{"method", "set"}, {"key", "mode"}, {"value", static_cast<int>(mode)}});
+ emit modeChanged();
+}
+
+void DataStore::setUnit(DataStore::Unit unit)
+{
+ if (m_unit == unit)
+ return;
+
+ m_unit = unit;
+ emit unitChanged();
+ // Also emit all the other signals that are affected by mode change
+ emit speedChanged();
+ emit averagespeedChanged();
+ emit odometerChanged();
+ emit tripChanged();
+ emit assistdistanceChanged();
+ emit smallUnitChanged(smallUnit());
+}
+
+double DataStore::convertSpeed(double speed) const
+{
+ return speed * (m_unit == Kmh ? msToKmh : msToMph);
+}
+
+double DataStore::convertDistance(double distance) const
+{
+ return distance * (m_unit == Kmh ? mToKm : mToMi);
+}
+
+double DataStore::convertSmallDistance(double distance) const
+{
+ return distance * (m_unit == Kmh ? 1.0 : mToYd);
+}
+
+QString DataStore::getSmallUnit() const
+{
+ return m_unit == Kmh ? "m" : "yd";
+}
+
+QJsonObject DataStore::splitDistance(double distance, bool round) const
+{
+ if (m_unit == Kmh) {
+ if (distance >= kmToM)
+ return QJsonObject{{"value", distance * mToKm}, {"unit", "km"}, {"decimal", true}};
+ else {
+ if (round)
+ distance = qRound(distance / 10) * 10;
+ return QJsonObject{{"value", distance}, {"unit", "m"}, {"decimal", false}};
+ }
+ } else {
+ if (distance >= miToM)
+ return QJsonObject{{"value", distance * mToMi}, {"unit", "mi."}, {"decimal", true}};
+ else {
+ distance *= mToYd;
+ if (round)
+ distance = qRound(distance / 10) * 10;
+ return QJsonObject{{"value", distance}, {"unit", "yd"}, {"decimal", false}};
+ }
+ }
+}
+
+QJsonObject DataStore::splitDuration(double duration) const
+{
+ if (duration >= 60.0)
+ return QJsonObject{{"value", duration / 60.0}, {"unit", "min"}};
+ else
+ return QJsonObject{{"value", duration}, {"unit", "s"}};
+}
+
+void DataStore::requestStatus()
+{
+ m_client->sendToServer(QJsonObject{{"method", "getall"}});
+}
+
+void DataStore::emitByName(const QString &valuename)
+{
+ // Use QMetaObject information to find a proper signal
+ const QMetaObject *meta = metaObject();
+
+ // Find the notifier signal
+ QString signalName = QString("%1Changed()").arg(valuename);
+ int methodIndex = meta->indexOfSignal(signalName.toLatin1().constData());
+ meta->method(methodIndex).invoke(this, Qt::AutoConnection);
+}
+
+void DataStore::parseMessage(const QJsonObject &message)
+{
+ QString method = message.value("method").toString();
+
+ // If we are updating just one, simply insert new value and emit
+ if (method == "updateone") {
+ QString key = message.value("key").toString();
+ m_properties.insert(key, message.value("value"));
+ emitByName(key);
+ // If we are updating many, then iterate over the list and update each value
+ } else if (method == "updatemany") {
+ foreach (const QJsonValue &value, message.value("values").toArray()) {
+ QJsonObject obj = value.toObject();
+ QString key = obj.value("key").toString();
+ m_properties.insert(key, obj.value("value"));
+ emitByName(key);
+ }
+ } else if (method == "trips") {
+ m_trips->setTrips(message.value("trips").toArray());
+ } else if (method == "trip") {
+ m_trips->addTrip(message.value("trip").toObject());
+ } else if (method == "currenttrip") {
+ m_current = message.value("currenttrip").toObject();
+ m_trips->setCurrentTrip(m_current);
+ emit currentTripChanged();
+ } else if (method == "reset") {
+ emit demoReset();
+ }
+}
diff --git a/basicsuite/ebike-ui/datastore.h b/basicsuite/ebike-ui/datastore.h
new file mode 100644
index 0000000..20fa388
--- /dev/null
+++ b/basicsuite/ebike-ui/datastore.h
@@ -0,0 +1,163 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef DATASTORE_H
+#define DATASTORE_H
+
+#include <QObject>
+#include <QJsonObject>
+
+class SocketClient;
+class TripDataModel;
+
+class DataStore: public QObject
+{
+ Q_OBJECT
+
+ // Different measured properties
+ Q_PROPERTY(double speed READ speed NOTIFY speedChanged)
+ Q_PROPERTY(double topspeed READ topSpeed NOTIFY topspeedChanged)
+ Q_PROPERTY(double averagespeed READ averageSpeed NOTIFY currentTripChanged)
+ Q_PROPERTY(double odometer READ odometer NOTIFY odometerChanged)
+ Q_PROPERTY(double trip READ trip NOTIFY currentTripChanged)
+ Q_PROPERTY(double calories READ calories NOTIFY currentTripChanged)
+ Q_PROPERTY(double assistdistance READ assistDistance NOTIFY assistdistanceChanged)
+ Q_PROPERTY(double assistpower READ assistPower NOTIFY assistpowerChanged)
+ Q_PROPERTY(double batterylevel READ batteryLevel NOTIFY batterylevelChanged)
+
+ // Toggles for lights and mode
+ Q_PROPERTY(bool lights READ lights WRITE setLights NOTIFY lightsChanged)
+ Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged)
+
+ // Current units
+ Q_PROPERTY(Unit unit READ unit WRITE setUnit NOTIFY unitChanged)
+ Q_PROPERTY(QString smallUnit READ smallUnit NOTIFY smallUnitChanged)
+
+ // Navigation
+ Q_PROPERTY(int arrow READ arrow NOTIFY arrowChanged)
+ Q_PROPERTY(double legdistance READ legDistance NOTIFY legdistanceChanged)
+ Q_PROPERTY(double tripremaining READ tripRemaining NOTIFY tripremainingChanged)
+
+public:
+ explicit DataStore(QObject *parent = nullptr);
+
+ enum Mode { Cruise, Sport };
+ Q_ENUM(Mode)
+
+ enum Unit { Kmh, Mph };
+ Q_ENUM(Unit)
+
+public:
+ // Getters
+ double speed() const;
+ double topSpeed() const;
+ double averageSpeed() const;
+ double odometer() const;
+ double trip() const;
+ double calories() const;
+ double assistDistance() const;
+ double assistPower() const;
+ double batteryLevel() const;
+ bool lights() const;
+ Mode mode() const;
+ Unit unit() const;
+ int arrow() const;
+ double legDistance() const;
+ double tripRemaining() const;
+ QString smallUnit() const;
+
+ // Setters
+ void setLights(bool lights);
+ void setMode(Mode mode);
+ void setUnit(Unit unit);
+
+ // Get trip data model
+ TripDataModel *tripDataModel() const { return m_trips; }
+
+ // Convert speed and distance to proper units
+ Q_INVOKABLE double convertSpeed(double speed) const;
+ Q_INVOKABLE double convertDistance(double distance) const;
+ Q_INVOKABLE double convertSmallDistance(double distance) const;
+ Q_INVOKABLE QString getSmallUnit() const;
+
+ // Split and convert distance and duration to value and unit
+ Q_INVOKABLE QJsonObject splitDistance(double distance, bool round=false) const;
+ Q_INVOKABLE QJsonObject splitDuration(double duration) const;
+
+private:
+ void emitByName(const QString &valuename);
+
+signals:
+ void speedChanged();
+ void topspeedChanged();
+ void averagespeedChanged();
+ void odometerChanged();
+ void tripChanged();
+ void caloriesChanged();
+ void assistdistanceChanged();
+ void assistpowerChanged();
+ void batterylevelChanged();
+ void lightsChanged();
+ void modeChanged();
+ void unitChanged();
+ void arrowChanged();
+ void legdistanceChanged();
+ void tripremainingChanged();
+ void currentTripChanged();
+ void smallUnitChanged(QString smallUnit);
+ void demoReset();
+
+public slots:
+ void connectToServer(const QString &servername);
+ void getTrips();
+ void endTrip();
+ void toggleMode();
+ void resetDemo();
+
+private slots:
+ void requestStatus();
+ void parseMessage(const QJsonObject &message);
+
+private:
+ SocketClient *m_client;
+ QJsonObject m_properties;
+ QJsonObject m_current;
+ Unit m_unit;
+ TripDataModel *m_trips;
+ int m_smallUnit;
+};
+
+#endif // DATASTORE_H
diff --git a/basicsuite/ebike-ui/ebike-ui.pro b/basicsuite/ebike-ui/ebike-ui.pro
new file mode 100644
index 0000000..70443af
--- /dev/null
+++ b/basicsuite/ebike-ui/ebike-ui.pro
@@ -0,0 +1,7 @@
+TEMPLATE = subdirs
+CONFIG += ordered
+
+SUBDIRS += \
+ datamodelplugin \
+ app.pro
+
diff --git a/basicsuite/ebike-ui/ebike_en.ts b/basicsuite/ebike-ui/ebike_en.ts
new file mode 100644
index 0000000..14679be
--- /dev/null
+++ b/basicsuite/ebike-ui/ebike_en.ts
@@ -0,0 +1,290 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>BikeInfoTab</name>
+ <message>
+ <location filename="BikeInfoTab.qml" line="52"/>
+ <source>BIKE INFO</source>
+ <translation>BIKE INFO</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="67"/>
+ <source>Next maintenance</source>
+ <translation>Next maintenance</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="75"/>
+ <source>15000 km</source>
+ <translation>15000 km</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="75"/>
+ <source>10000 mi.</source>
+ <translation>10000 mi.</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="91"/>
+ <source>Battery health</source>
+ <translation>Battery health</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="99"/>
+ <source>Excellent</source>
+ <translation>Excellent</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="115"/>
+ <source>System Log</source>
+ <translation>System Log</translation>
+ </message>
+</context>
+<context>
+ <name>ConfigurationDrawer</name>
+ <message>
+ <source>LIST</source>
+ <translation type="vanished">LIST</translation>
+ </message>
+ <message>
+ <source>TBD</source>
+ <translation type="vanished">TBD</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralTab</name>
+ <message>
+ <location filename="GeneralTab.qml" line="52"/>
+ <source>GENERAL</source>
+ <translation>GENERAL</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="67"/>
+ <source>Language</source>
+ <translation>Language</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="75"/>
+ <source>English</source>
+ <translation>English</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="91"/>
+ <source>Brightness</source>
+ <translation>Brightness</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="99"/>
+ <source>Auto</source>
+ <translation>Auto</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="201"/>
+ <source>Units</source>
+ <translation>Units</translation>
+ </message>
+</context>
+<context>
+ <name>ModeBox</name>
+ <message>
+ <location filename="ModeBox.qml" line="62"/>
+ <source>SPORT</source>
+ <translation>SPORT</translation>
+ </message>
+ <message>
+ <location filename="ModeBox.qml" line="78"/>
+ <source>CRUISE</source>
+ <translation>CRUISE</translation>
+ </message>
+</context>
+<context>
+ <name>MusicPlayer</name>
+ <message>
+ <location filename="MusicPlayer.qml" line="133"/>
+ <source>Singer McSongface - Some really long title here</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>NaviPage</name>
+ <message>
+ <location filename="NaviPage.qml" line="89"/>
+ <source>&lt;i&gt;Where do you want to go?&lt;/i&gt;</source>
+ <translation>&lt;i&gt;Where do you want to go?&lt;/i&gt;</translation>
+ </message>
+</context>
+<context>
+ <name>SpeedView</name>
+ <message>
+ <location filename="SpeedView.qml" line="102"/>
+ <source>km/h</source>
+ <translation>km/h</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="102"/>
+ <source>mph</source>
+ <translation>mph</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="141"/>
+ <source>AVG
+km/h</source>
+ <translation>AVG
+km/h</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="141"/>
+ <source>AVG
+mph</source>
+ <translation>AVG
+mph</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="193"/>
+ <source>km
+assist
+left</source>
+ <translation>km
+assist
+left</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="193"/>
+ <source>mi.
+assist
+left</source>
+ <translation>mi.
+assist
+left</translation>
+ </message>
+</context>
+<context>
+ <name>StatsPage</name>
+ <message>
+ <location filename="StatsPage.qml" line="94"/>
+ <source>END TRIP</source>
+ <translation>END TRIP</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="113"/>
+ <source>YESTERDAY</source>
+ <translation>YESTERDAY</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="198"/>
+ <source>Total</source>
+ <translation>Total</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="225"/>
+ <source>km</source>
+ <translation>km</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="225"/>
+ <source>mi.</source>
+ <translation>mi.</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="265"/>
+ <source>Duration (h:m)</source>
+ <translation>Duration (h:m)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="267"/>
+ <source>Max. speed (km/h)</source>
+ <translation>Max. speed (km/h)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="267"/>
+ <source>Max. speed (mph)</source>
+ <translation>Max. speed (mph)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="276"/>
+ <source>Distance (km)</source>
+ <translation>Distance (km)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="276"/>
+ <source>Distance (mi.)</source>
+ <translation>Distance (mi.)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="278"/>
+ <source>Avg. speed (km/h)</source>
+ <translation>Avg. speed (km/h)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="278"/>
+ <source>Avg. speed (mph)</source>
+ <translation>Avg. speed (mph)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="287"/>
+ <source>Calories (kcal)</source>
+ <translation>Calories (kcal)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="289"/>
+ <source>Ascent (m)</source>
+ <translation>Ascent (m)</translation>
+ </message>
+</context>
+<context>
+ <name>SuggestionsModel</name>
+ <message>
+ <location filename="suggestionsmodel.cpp" line="67"/>
+ <source>Place</source>
+ <translation>Place</translation>
+ </message>
+</context>
+<context>
+ <name>TripChart</name>
+ <message>
+ <location filename="TripChart.qml" line="112"/>
+ <source>Speed (km/h)</source>
+ <translation>Speed (km/h)</translation>
+ </message>
+ <message>
+ <location filename="TripChart.qml" line="112"/>
+ <source>Speed (mph)</source>
+ <translation>Speed (mph)</translation>
+ </message>
+ <message>
+ <location filename="TripChart.qml" line="128"/>
+ <source>Pedal assist (W)</source>
+ <translation>Pedal assist (W)</translation>
+ </message>
+</context>
+<context>
+ <name>ViewTab</name>
+ <message>
+ <location filename="ViewTab.qml" line="51"/>
+ <source>VIEW</source>
+ <translation>VIEW</translation>
+ </message>
+ <message>
+ <location filename="ViewTab.qml" line="66"/>
+ <source>Show FPS</source>
+ <translation>Show FPS</translation>
+ </message>
+ <message>
+ <location filename="ViewTab.qml" line="84"/>
+ <source>Audio controls</source>
+ <translation>Audio controls</translation>
+ </message>
+ <message>
+ <location filename="ViewTab.qml" line="102"/>
+ <source>TBD</source>
+ <translation>TBD</translation>
+ </message>
+</context>
+<context>
+ <name>mainview</name>
+ <message>
+ <location filename="mainview.qml" line="50"/>
+ <source>Qt eBike</source>
+ <translation>Qt eBike</translation>
+ </message>
+</context>
+</TS>
diff --git a/basicsuite/ebike-ui/ebike_fi.ts b/basicsuite/ebike-ui/ebike_fi.ts
new file mode 100644
index 0000000..9601fc7
--- /dev/null
+++ b/basicsuite/ebike-ui/ebike_fi.ts
@@ -0,0 +1,290 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="fi_FI">
+<context>
+ <name>BikeInfoTab</name>
+ <message>
+ <location filename="BikeInfoTab.qml" line="52"/>
+ <source>BIKE INFO</source>
+ <translation>PYÖRÄN TIEDOT</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="67"/>
+ <source>Next maintenance</source>
+ <translation>Seuraava huolto</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="75"/>
+ <source>15000 km</source>
+ <translation>15000 km</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="75"/>
+ <source>10000 mi.</source>
+ <translation>10000 mi.</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="91"/>
+ <source>Battery health</source>
+ <translation>Akun tila</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="99"/>
+ <source>Excellent</source>
+ <translation>Loistava</translation>
+ </message>
+ <message>
+ <location filename="BikeInfoTab.qml" line="115"/>
+ <source>System Log</source>
+ <translation>Järjestelmälogi</translation>
+ </message>
+</context>
+<context>
+ <name>ConfigurationDrawer</name>
+ <message>
+ <source>LIST</source>
+ <translation type="vanished">LISTA</translation>
+ </message>
+ <message>
+ <source>TBD</source>
+ <translation type="vanished">MYÖH</translation>
+ </message>
+</context>
+<context>
+ <name>GeneralTab</name>
+ <message>
+ <location filename="GeneralTab.qml" line="52"/>
+ <source>GENERAL</source>
+ <translation>YLEINEN</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="67"/>
+ <source>Language</source>
+ <translation>Kieli</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="75"/>
+ <source>English</source>
+ <translation>englanti</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="91"/>
+ <source>Brightness</source>
+ <translation>Kirkkaus</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="99"/>
+ <source>Auto</source>
+ <translation>Auto</translation>
+ </message>
+ <message>
+ <location filename="GeneralTab.qml" line="201"/>
+ <source>Units</source>
+ <translation>Yksiköt</translation>
+ </message>
+</context>
+<context>
+ <name>ModeBox</name>
+ <message>
+ <location filename="ModeBox.qml" line="62"/>
+ <source>SPORT</source>
+ <translation>SPORT</translation>
+ </message>
+ <message>
+ <location filename="ModeBox.qml" line="78"/>
+ <source>CRUISE</source>
+ <translation>CRUISE</translation>
+ </message>
+</context>
+<context>
+ <name>MusicPlayer</name>
+ <message>
+ <location filename="MusicPlayer.qml" line="133"/>
+ <source>Singer McSongface - Some really long title here</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>NaviPage</name>
+ <message>
+ <location filename="NaviPage.qml" line="89"/>
+ <source>&lt;i&gt;Where do you want to go?&lt;/i&gt;</source>
+ <translation>&lt;i&gt;Mihin haluat mennä?&lt;/i&gt;</translation>
+ </message>
+</context>
+<context>
+ <name>SpeedView</name>
+ <message>
+ <location filename="SpeedView.qml" line="102"/>
+ <source>km/h</source>
+ <translation>km/h</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="102"/>
+ <source>mph</source>
+ <translation>mph</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="141"/>
+ <source>AVG
+km/h</source>
+ <translation>KESK
+km/h</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="141"/>
+ <source>AVG
+mph</source>
+ <translation>KESK
+mph</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="193"/>
+ <source>km
+assist
+left</source>
+ <translation>km
+avustus
+jäljellä</translation>
+ </message>
+ <message>
+ <location filename="SpeedView.qml" line="193"/>
+ <source>mi.
+assist
+left</source>
+ <translation>mi.
+avustus
+jäljellä</translation>
+ </message>
+</context>
+<context>
+ <name>StatsPage</name>
+ <message>
+ <location filename="StatsPage.qml" line="94"/>
+ <source>END TRIP</source>
+ <translation>LOPETA MATKA</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="113"/>
+ <source>YESTERDAY</source>
+ <translation>EILEN</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="198"/>
+ <source>Total</source>
+ <translation>Yhteensä</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="225"/>
+ <source>km</source>
+ <translation>km</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="225"/>
+ <source>mi.</source>
+ <translation>mi.</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="265"/>
+ <source>Duration (h:m)</source>
+ <translation>Kesto (h:m)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="267"/>
+ <source>Max. speed (km/h)</source>
+ <translation>Huippunopeus (km/h)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="267"/>
+ <source>Max. speed (mph)</source>
+ <translation>Huippunopeus (mph)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="276"/>
+ <source>Distance (km)</source>
+ <translation>Etäisyys (km)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="276"/>
+ <source>Distance (mi.)</source>
+ <translation>Etäisyys (mi.)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="278"/>
+ <source>Avg. speed (km/h)</source>
+ <translation>Keskinopeus (km/h)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="278"/>
+ <source>Avg. speed (mph)</source>
+ <translation>Keskinopeus (mph)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="287"/>
+ <source>Calories (kcal)</source>
+ <translation>Kalorit (kcal)</translation>
+ </message>
+ <message>
+ <location filename="StatsPage.qml" line="289"/>
+ <source>Ascent (m)</source>
+ <translation>Nousu (m)</translation>
+ </message>
+</context>
+<context>
+ <name>SuggestionsModel</name>
+ <message>
+ <location filename="suggestionsmodel.cpp" line="67"/>
+ <source>Place</source>
+ <translation>Paikka</translation>
+ </message>
+</context>
+<context>
+ <name>TripChart</name>
+ <message>
+ <location filename="TripChart.qml" line="112"/>
+ <source>Speed (km/h)</source>
+ <translation>Nopeus (km/h)</translation>
+ </message>
+ <message>
+ <location filename="TripChart.qml" line="112"/>
+ <source>Speed (mph)</source>
+ <translation>Nopeus (mph)</translation>
+ </message>
+ <message>
+ <location filename="TripChart.qml" line="128"/>
+ <source>Pedal assist (W)</source>
+ <translation>Avustusteho (W)</translation>
+ </message>
+</context>
+<context>
+ <name>ViewTab</name>
+ <message>
+ <location filename="ViewTab.qml" line="51"/>
+ <source>VIEW</source>
+ <translation>NÄYTÄ</translation>
+ </message>
+ <message>
+ <location filename="ViewTab.qml" line="66"/>
+ <source>Show FPS</source>
+ <translation>Näytä FPS</translation>
+ </message>
+ <message>
+ <location filename="ViewTab.qml" line="84"/>
+ <source>Audio controls</source>
+ <translation>Musiikkisoitin</translation>
+ </message>
+ <message>
+ <location filename="ViewTab.qml" line="102"/>
+ <source>TBD</source>
+ <translation>MYÖH</translation>
+ </message>
+</context>
+<context>
+ <name>mainview</name>
+ <message>
+ <location filename="mainview.qml" line="50"/>
+ <source>Qt eBike</source>
+ <translation>Qt eBike</translation>
+ </message>
+</context>
+</TS>
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Black.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Black.ttf
new file mode 100644
index 0000000..bf5443c
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-Black.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-BlackItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-BlackItalic.ttf
new file mode 100644
index 0000000..3eee3a7
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-BlackItalic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Bold.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Bold.ttf
new file mode 100644
index 0000000..8e9a5f3
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-Bold.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-BoldItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-BoldItalic.ttf
new file mode 100644
index 0000000..2c33630
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-BoldItalic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ExtraBold.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ExtraBold.ttf
new file mode 100644
index 0000000..1e3692d
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-ExtraBold.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ExtraBoldItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ExtraBoldItalic.ttf
new file mode 100644
index 0000000..5f6c382
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-ExtraBoldItalic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ExtraLight.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ExtraLight.ttf
new file mode 100644
index 0000000..7490dc7
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-ExtraLight.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ExtraLightItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ExtraLightItalic.ttf
new file mode 100644
index 0000000..24e1354
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-ExtraLightItalic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Italic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Italic.ttf
new file mode 100644
index 0000000..c5a36e5
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-Italic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Light.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Light.ttf
new file mode 100644
index 0000000..e66dc5b
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-Light.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-LightItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-LightItalic.ttf
new file mode 100644
index 0000000..b78b8b7
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-LightItalic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Medium.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Medium.ttf
new file mode 100644
index 0000000..88d70b8
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-Medium.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-MediumItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-MediumItalic.ttf
new file mode 100644
index 0000000..225fd18
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-MediumItalic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Regular.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Regular.ttf
new file mode 100644
index 0000000..626355a
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-Regular.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-SemiBold.ttf b/basicsuite/ebike-ui/fonts/Montserrat-SemiBold.ttf
new file mode 100644
index 0000000..6157045
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-SemiBold.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-SemiBoldItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-SemiBoldItalic.ttf
new file mode 100644
index 0000000..c6dd977
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-SemiBoldItalic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Thin.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Thin.ttf
new file mode 100644
index 0000000..dc16a02
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-Thin.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ThinItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ThinItalic.ttf
new file mode 100644
index 0000000..b9e12f4
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Montserrat-ThinItalic.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/OFL.txt b/basicsuite/ebike-ui/fonts/OFL.txt
new file mode 100644
index 0000000..74605df
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com).
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/basicsuite/ebike-ui/fonts/Teko-Bold.ttf b/basicsuite/ebike-ui/fonts/Teko-Bold.ttf
new file mode 100644
index 0000000..d061824
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Teko-Bold.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Teko-Light.ttf b/basicsuite/ebike-ui/fonts/Teko-Light.ttf
new file mode 100644
index 0000000..ec5194a
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Teko-Light.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Teko-Medium.ttf b/basicsuite/ebike-ui/fonts/Teko-Medium.ttf
new file mode 100644
index 0000000..cc38086
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Teko-Medium.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Teko-Regular.ttf b/basicsuite/ebike-ui/fonts/Teko-Regular.ttf
new file mode 100644
index 0000000..3161e63
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Teko-Regular.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/Teko-SemiBold.ttf b/basicsuite/ebike-ui/fonts/Teko-SemiBold.ttf
new file mode 100644
index 0000000..bc17e5a
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/Teko-SemiBold.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fonts/fontawesome-webfont.ttf b/basicsuite/ebike-ui/fonts/fontawesome-webfont.ttf
new file mode 100644
index 0000000..35acda2
--- /dev/null
+++ b/basicsuite/ebike-ui/fonts/fontawesome-webfont.ttf
Binary files differ
diff --git a/basicsuite/ebike-ui/fpscounter.cpp b/basicsuite/ebike-ui/fpscounter.cpp
new file mode 100644
index 0000000..137dcb4
--- /dev/null
+++ b/basicsuite/ebike-ui/fpscounter.cpp
@@ -0,0 +1,79 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QQuickWindow>
+
+#include "fpscounter.h"
+
+FpsCounter::FpsCounter(QObject *parent)
+ : QObject(parent)
+ , m_frameCounter(0)
+ , m_fps(0.0)
+ , m_visible(false)
+{
+}
+
+void FpsCounter::setVisible(bool visible)
+{
+ if (m_visible == visible)
+ return;
+
+ m_visible = visible;
+ emit visibleChanged(m_visible);
+}
+
+void FpsCounter::setWindow(QQuickWindow *window)
+{
+ connect(window, &QQuickWindow::frameSwapped, this, &FpsCounter::frameUpdated);
+ startTimer(1000);
+ m_timer.start();
+}
+
+void FpsCounter::timerEvent(QTimerEvent *)
+{
+ // Calculate new FPS
+ qreal newfps = qRound(m_frameCounter * 1000.0 / m_timer.elapsed());
+ m_frameCounter = 0;
+ m_timer.start();
+
+ // If there is no change, do nothing
+ if (qFuzzyCompare(m_fps, newfps))
+ return;
+
+ // Otherwise emit new fps
+ m_fps = newfps;
+ emit fpsChanged(m_fps);
+}
diff --git a/basicsuite/ebike-ui/fpscounter.h b/basicsuite/ebike-ui/fpscounter.h
new file mode 100644
index 0000000..7828844
--- /dev/null
+++ b/basicsuite/ebike-ui/fpscounter.h
@@ -0,0 +1,77 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef FPSCOUNTER_H
+#define FPSCOUNTER_H
+
+#include <QObject>
+#include <QElapsedTimer>
+
+class QQuickWindow;
+
+class FpsCounter : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(qreal fps READ fps NOTIFY fpsChanged)
+ Q_PROPERTY(bool visible READ visible WRITE setVisible NOTIFY visibleChanged)
+
+public:
+ explicit FpsCounter(QObject *parent = nullptr);
+
+public:
+ qreal fps() const { return m_fps; }
+ bool visible() const { return m_visible; }
+ void setVisible(bool visible);
+ void setWindow(QQuickWindow *window);
+
+protected:
+ void timerEvent(QTimerEvent *);
+
+signals:
+ void fpsChanged(qreal fps);
+ void visibleChanged(bool visible);
+
+private slots:
+ void frameUpdated() { m_frameCounter++; }
+
+private:
+ QElapsedTimer m_timer;
+ int m_frameCounter;
+ qreal m_fps;
+ bool m_visible;
+};
+
+#endif // FPSCOUNTER_H
diff --git a/basicsuite/ebike-ui/images/arrow_left.png b/basicsuite/ebike-ui/images/arrow_left.png
new file mode 100644
index 0000000..6c67a2c
--- /dev/null
+++ b/basicsuite/ebike-ui/images/arrow_left.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/arrow_right.png b/basicsuite/ebike-ui/images/arrow_right.png
new file mode 100644
index 0000000..b8cfb2e
--- /dev/null
+++ b/basicsuite/ebike-ui/images/arrow_right.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/assist.png b/basicsuite/ebike-ui/images/assist.png
new file mode 100644
index 0000000..4a2bc95
--- /dev/null
+++ b/basicsuite/ebike-ui/images/assist.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/battery.png b/basicsuite/ebike-ui/images/battery.png
new file mode 100644
index 0000000..170c48d
--- /dev/null
+++ b/basicsuite/ebike-ui/images/battery.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/bike-battery.png b/basicsuite/ebike-ui/images/bike-battery.png
new file mode 100644
index 0000000..e354d33
--- /dev/null
+++ b/basicsuite/ebike-ui/images/bike-battery.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/bike-brakes.png b/basicsuite/ebike-ui/images/bike-brakes.png
new file mode 100644
index 0000000..c271d75
--- /dev/null
+++ b/basicsuite/ebike-ui/images/bike-brakes.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/bike-chain.png b/basicsuite/ebike-ui/images/bike-chain.png
new file mode 100644
index 0000000..07879a4
--- /dev/null
+++ b/basicsuite/ebike-ui/images/bike-chain.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/bike-frontwheel.png b/basicsuite/ebike-ui/images/bike-frontwheel.png
new file mode 100644
index 0000000..ab9297f
--- /dev/null
+++ b/basicsuite/ebike-ui/images/bike-frontwheel.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/bike-gears.png b/basicsuite/ebike-ui/images/bike-gears.png
new file mode 100644
index 0000000..c977644
--- /dev/null
+++ b/basicsuite/ebike-ui/images/bike-gears.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/bike-light.png b/basicsuite/ebike-ui/images/bike-light.png
new file mode 100644
index 0000000..b18da3c
--- /dev/null
+++ b/basicsuite/ebike-ui/images/bike-light.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/bike-rearwheel.png b/basicsuite/ebike-ui/images/bike-rearwheel.png
new file mode 100644
index 0000000..fb02923
--- /dev/null
+++ b/basicsuite/ebike-ui/images/bike-rearwheel.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/blue_circle_gps_area.png b/basicsuite/ebike-ui/images/blue_circle_gps_area.png
new file mode 100644
index 0000000..06e1b6e
--- /dev/null
+++ b/basicsuite/ebike-ui/images/blue_circle_gps_area.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/calories.png b/basicsuite/ebike-ui/images/calories.png
new file mode 100644
index 0000000..fe3cbb1
--- /dev/null
+++ b/basicsuite/ebike-ui/images/calories.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/checkmark.png b/basicsuite/ebike-ui/images/checkmark.png
new file mode 100644
index 0000000..0e1b387
--- /dev/null
+++ b/basicsuite/ebike-ui/images/checkmark.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/curtain_shadow_handle.png b/basicsuite/ebike-ui/images/curtain_shadow_handle.png
new file mode 100644
index 0000000..afaf3ec
--- /dev/null
+++ b/basicsuite/ebike-ui/images/curtain_shadow_handle.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/curtain_up_arrow.png b/basicsuite/ebike-ui/images/curtain_up_arrow.png
new file mode 100644
index 0000000..97095d0
--- /dev/null
+++ b/basicsuite/ebike-ui/images/curtain_up_arrow.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/fps_icon.png b/basicsuite/ebike-ui/images/fps_icon.png
new file mode 100644
index 0000000..4cecab7
--- /dev/null
+++ b/basicsuite/ebike-ui/images/fps_icon.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/info.png b/basicsuite/ebike-ui/images/info.png
new file mode 100644
index 0000000..6ad9192
--- /dev/null
+++ b/basicsuite/ebike-ui/images/info.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/info_selected.png b/basicsuite/ebike-ui/images/info_selected.png
new file mode 100644
index 0000000..25056ff
--- /dev/null
+++ b/basicsuite/ebike-ui/images/info_selected.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/lights_off.png b/basicsuite/ebike-ui/images/lights_off.png
new file mode 100644
index 0000000..4ad0abd
--- /dev/null
+++ b/basicsuite/ebike-ui/images/lights_off.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/lights_on.png b/basicsuite/ebike-ui/images/lights_on.png
new file mode 100644
index 0000000..6da9893
--- /dev/null
+++ b/basicsuite/ebike-ui/images/lights_on.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/list.png b/basicsuite/ebike-ui/images/list.png
new file mode 100644
index 0000000..2e1633d
--- /dev/null
+++ b/basicsuite/ebike-ui/images/list.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/list_selected.png b/basicsuite/ebike-ui/images/list_selected.png
new file mode 100644
index 0000000..1c61a5e
--- /dev/null
+++ b/basicsuite/ebike-ui/images/list_selected.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/map-marker.png b/basicsuite/ebike-ui/images/map-marker.png
new file mode 100644
index 0000000..e2dd669
--- /dev/null
+++ b/basicsuite/ebike-ui/images/map-marker.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/map_btn_shadow.png b/basicsuite/ebike-ui/images/map_btn_shadow.png
new file mode 100644
index 0000000..c40b9b1
--- /dev/null
+++ b/basicsuite/ebike-ui/images/map_btn_shadow.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/map_destination.png b/basicsuite/ebike-ui/images/map_destination.png
new file mode 100644
index 0000000..a41b134
--- /dev/null
+++ b/basicsuite/ebike-ui/images/map_destination.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/map_locate.png b/basicsuite/ebike-ui/images/map_locate.png
new file mode 100644
index 0000000..2579c94
--- /dev/null
+++ b/basicsuite/ebike-ui/images/map_locate.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/map_location_arrow.png b/basicsuite/ebike-ui/images/map_location_arrow.png
new file mode 100644
index 0000000..9ec4d44
--- /dev/null
+++ b/basicsuite/ebike-ui/images/map_location_arrow.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/map_zoomin.png b/basicsuite/ebike-ui/images/map_zoomin.png
new file mode 100644
index 0000000..aea24fe
--- /dev/null
+++ b/basicsuite/ebike-ui/images/map_zoomin.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/map_zoomout.png b/basicsuite/ebike-ui/images/map_zoomout.png
new file mode 100644
index 0000000..e3f90da
--- /dev/null
+++ b/basicsuite/ebike-ui/images/map_zoomout.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_bear_l.png b/basicsuite/ebike-ui/images/nav_bear_l.png
new file mode 100644
index 0000000..b078fbf
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_bear_l.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_bear_r.png b/basicsuite/ebike-ui/images/nav_bear_r.png
new file mode 100644
index 0000000..84b48c1
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_bear_r.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_hard_l.png b/basicsuite/ebike-ui/images/nav_hard_l.png
new file mode 100644
index 0000000..7235cc3
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_hard_l.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_hard_r.png b/basicsuite/ebike-ui/images/nav_hard_r.png
new file mode 100644
index 0000000..4a19b65
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_hard_r.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_left.png b/basicsuite/ebike-ui/images/nav_left.png
new file mode 100644
index 0000000..5a5aa26
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_left.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_light_left.png b/basicsuite/ebike-ui/images/nav_light_left.png
new file mode 100644
index 0000000..7c88914
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_light_left.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_light_right.png b/basicsuite/ebike-ui/images/nav_light_right.png
new file mode 100644
index 0000000..3bb07ff
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_light_right.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_nodir.png b/basicsuite/ebike-ui/images/nav_nodir.png
new file mode 100644
index 0000000..7fc92a7
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_nodir.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_right.png b/basicsuite/ebike-ui/images/nav_right.png
new file mode 100644
index 0000000..8eecf13
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_right.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_straight.png b/basicsuite/ebike-ui/images/nav_straight.png
new file mode 100644
index 0000000..44a58c8
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_straight.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_uturn_l.png b/basicsuite/ebike-ui/images/nav_uturn_l.png
new file mode 100644
index 0000000..c9714d3
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_uturn_l.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nav_uturn_r.png b/basicsuite/ebike-ui/images/nav_uturn_r.png
new file mode 100644
index 0000000..a4aee58
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nav_uturn_r.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/navigation_widget_shadow.png b/basicsuite/ebike-ui/images/navigation_widget_shadow.png
new file mode 100644
index 0000000..4de81eb
--- /dev/null
+++ b/basicsuite/ebike-ui/images/navigation_widget_shadow.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nextsong.png b/basicsuite/ebike-ui/images/nextsong.png
new file mode 100644
index 0000000..90d6520
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nextsong.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/nextsong_pressed.png b/basicsuite/ebike-ui/images/nextsong_pressed.png
new file mode 100644
index 0000000..b764100
--- /dev/null
+++ b/basicsuite/ebike-ui/images/nextsong_pressed.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/ok.png b/basicsuite/ebike-ui/images/ok.png
new file mode 100644
index 0000000..6d862a4
--- /dev/null
+++ b/basicsuite/ebike-ui/images/ok.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/pause.png b/basicsuite/ebike-ui/images/pause.png
new file mode 100644
index 0000000..9425527
--- /dev/null
+++ b/basicsuite/ebike-ui/images/pause.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/pause_pressed.png b/basicsuite/ebike-ui/images/pause_pressed.png
new file mode 100644
index 0000000..b90e029
--- /dev/null
+++ b/basicsuite/ebike-ui/images/pause_pressed.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/placeholder.png b/basicsuite/ebike-ui/images/placeholder.png
new file mode 100644
index 0000000..a0e647e
--- /dev/null
+++ b/basicsuite/ebike-ui/images/placeholder.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/play.png b/basicsuite/ebike-ui/images/play.png
new file mode 100644
index 0000000..d31ae43
--- /dev/null
+++ b/basicsuite/ebike-ui/images/play.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/play_pressed.png b/basicsuite/ebike-ui/images/play_pressed.png
new file mode 100644
index 0000000..3cd498b
--- /dev/null
+++ b/basicsuite/ebike-ui/images/play_pressed.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/prevsong.png b/basicsuite/ebike-ui/images/prevsong.png
new file mode 100644
index 0000000..63d2619
--- /dev/null
+++ b/basicsuite/ebike-ui/images/prevsong.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/prevsong_pressed.png b/basicsuite/ebike-ui/images/prevsong_pressed.png
new file mode 100644
index 0000000..dbe5690
--- /dev/null
+++ b/basicsuite/ebike-ui/images/prevsong_pressed.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/search.png b/basicsuite/ebike-ui/images/search.png
new file mode 100644
index 0000000..f840e72
--- /dev/null
+++ b/basicsuite/ebike-ui/images/search.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/search_cancel.png b/basicsuite/ebike-ui/images/search_cancel.png
new file mode 100644
index 0000000..679ce32
--- /dev/null
+++ b/basicsuite/ebike-ui/images/search_cancel.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/settings.png b/basicsuite/ebike-ui/images/settings.png
new file mode 100644
index 0000000..9651de6
--- /dev/null
+++ b/basicsuite/ebike-ui/images/settings.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/settings_selected.png b/basicsuite/ebike-ui/images/settings_selected.png
new file mode 100644
index 0000000..2e8cd7f
--- /dev/null
+++ b/basicsuite/ebike-ui/images/settings_selected.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/small_input_box_shadow.png b/basicsuite/ebike-ui/images/small_input_box_shadow.png
new file mode 100644
index 0000000..401c0f2
--- /dev/null
+++ b/basicsuite/ebike-ui/images/small_input_box_shadow.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/small_speedometer_arrow.png b/basicsuite/ebike-ui/images/small_speedometer_arrow.png
new file mode 100644
index 0000000..4959e8d
--- /dev/null
+++ b/basicsuite/ebike-ui/images/small_speedometer_arrow.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/small_speedometer_shadow.png b/basicsuite/ebike-ui/images/small_speedometer_shadow.png
new file mode 100644
index 0000000..5b37857
--- /dev/null
+++ b/basicsuite/ebike-ui/images/small_speedometer_shadow.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/speed.png b/basicsuite/ebike-ui/images/speed.png
new file mode 100644
index 0000000..17d2e6d
--- /dev/null
+++ b/basicsuite/ebike-ui/images/speed.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/spinner.png b/basicsuite/ebike-ui/images/spinner.png
new file mode 100644
index 0000000..e59efb2
--- /dev/null
+++ b/basicsuite/ebike-ui/images/spinner.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/top_curtain_drag.png b/basicsuite/ebike-ui/images/top_curtain_drag.png
new file mode 100644
index 0000000..28388c6
--- /dev/null
+++ b/basicsuite/ebike-ui/images/top_curtain_drag.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/trip.png b/basicsuite/ebike-ui/images/trip.png
new file mode 100644
index 0000000..5a1761c
--- /dev/null
+++ b/basicsuite/ebike-ui/images/trip.png
Binary files differ
diff --git a/basicsuite/ebike-ui/images/warning.png b/basicsuite/ebike-ui/images/warning.png
new file mode 100644
index 0000000..0c37552
--- /dev/null
+++ b/basicsuite/ebike-ui/images/warning.png
Binary files differ
diff --git a/basicsuite/ebike-ui/main.qml b/basicsuite/ebike-ui/main.qml
new file mode 100644
index 0000000..33fe5e8
--- /dev/null
+++ b/basicsuite/ebike-ui/main.qml
@@ -0,0 +1,350 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+import QtQuick 2.7
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+import QtQuick.VirtualKeyboard 2.1
+import QtQml 2.2
+import DataStore 1.0
+
+import "./BikeStyle"
+
+Rectangle {
+ visible: true
+ width: 640
+ height: 480
+ color: "#111520"
+ id: root
+
+ // Permanent placeholder for time display
+ ClockView {
+ id: clockButton
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+
+ MouseArea {
+ anchors.rightMargin: -20
+ anchors.leftMargin: -20
+ anchors.bottomMargin: -20
+ anchors.fill: parent
+ onClicked: drawer.open()
+ }
+ }
+
+ FpsItem {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ visible: fps.visible
+ }
+
+ // The always-visible speed view
+ SpeedView {
+ id: speedView
+ onShowMain: swipeView.currentIndex = 1
+ showZero: naviPage.targetEdit.activeFocus
+
+ states: [
+ State {
+ name: ""
+ when: swipeView.currentIndex == 1 && (!speedView.enlarged)
+ PropertyChanges {
+ target: speedView
+ width: UILayout.speedViewRadius * 2 + 2
+ height: UILayout.speedViewRadius * 2 + 2
+ anchors.topMargin: UILayout.speedViewTop
+ anchors.leftMargin: 0
+ anchors.bottomMargin: 0
+ color: "transparent"
+ dotcount: UILayout.speedViewDots
+ curvewidth: UILayout.speedViewInnerWidth
+ speedTextSize: UILayout.speedTextSize
+ speedBaselineOffset: UILayout.speedBaselineOffset + 1
+ innerRadius: UILayout.speedViewInnerRadius
+ speedUnitsSize: UILayout.speedUnitsSize
+ speedUnitBaselineOffset: UILayout.speedTextUnitMargin
+ speedIconsOffset: UILayout.speedIconsCenterOffset
+ speedInfoTextsOffset: 0
+ speedInfoTextsSize: UILayout.speedInfoTextsSize
+ speedInfoUnitsOffset: UILayout.speedInfoUnitsOffset
+ assistPowerIconOffset: UILayout.assistPowerIconOffset
+ }
+ AnchorChanges {
+ target: speedView
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.left: undefined
+ anchors.bottom: undefined
+ }
+ PropertyChanges {
+ target: speedView.cornerRectangle
+ color: "transparent"
+ }
+ AnchorChanges {
+ target: speedView.cornerRectangle
+ anchors.horizontalCenter: speedView.horizontalCenter
+ anchors.verticalCenter: speedView.verticalCenter
+ anchors.left: undefined
+ anchors.bottom: undefined
+ }
+ StateChangeScript {
+ script: {
+ if (musicPlayer.lastMusicPlayerState === "" && (!drawer.viewTab.musicPlayerSwitch.checked))
+ musicPlayer.state = "";
+ }
+ }
+ },
+ State {
+ name: "CORNERED"
+ when: swipeView.currentIndex != 1
+ PropertyChanges {
+ target: speedView
+ width: UILayout.speedViewRadiusMinified * 2
+ height: UILayout.speedViewRadiusMinified * 2
+ anchors.topMargin: 0
+ anchors.leftMargin: UILayout.speedViewCornerLeftMargin
+ anchors.bottomMargin: UILayout.speedViewCornerBottomMargin
+ color: Colors.speedViewBackgroundCornered
+ dotcount: UILayout.speedViewDotsMinified
+ curvewidth: UILayout.speedViewInnerWidthMinified
+ speedTextSize: UILayout.speedTextSizeMinified
+ speedBaselineOffset: UILayout.speedBaselineOffsetMinified
+ innerRadius: UILayout.speedViewInnerRadiusMinified
+ speedUnitBaselineOffset: UILayout.speedTextUnitMarginMinified
+ }
+ AnchorChanges {
+ target: speedView
+ anchors.horizontalCenter: undefined
+ anchors.top: undefined
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+ }
+ PropertyChanges {
+ target: speedView.cornerRectangle
+ color: Colors.speedViewBackgroundCornered
+ }
+ AnchorChanges {
+ target: speedView.cornerRectangle
+ anchors.horizontalCenter: undefined
+ anchors.verticalCenter: undefined
+ anchors.left: speedView.left
+ anchors.bottom: speedView.bottom
+ }
+ StateChangeScript {
+ script: {
+ musicPlayer.lastMusicPlayerState = musicPlayer.state;
+ musicPlayer.state = "hidden";
+ }
+ }
+ },
+ State {
+ name: "ENLARGED"
+ when: swipeView.currentIndex == 1 && speedView.enlarged
+ PropertyChanges {
+ target: speedView
+ width: UILayout.speedViewRadiusEnlarged * 2
+ height: UILayout.speedViewRadiusEnlarged * 2
+ anchors.topMargin: 35
+ dotcount: UILayout.speedViewDotsEnlarged
+ speedTextSize: UILayout.speedTextSizeEnlarged
+ speedBaselineOffset: UILayout.speedBaselineOffsetEnlarged
+ innerRadius: UILayout.speedViewInnerRadiusEnlarged
+ speedUnitsSize: UILayout.speedUnitsSizeEnlarged
+ speedUnitBaselineOffset: UILayout.speedTextUnitMarginEnlarged
+ speedIconsOffset: UILayout.speedIconsCenterOffsetEnlarged
+ speedInfoTextsOffset: UILayout.speedInfoTextsOffsetEnlarged
+ speedInfoTextsSize: UILayout.speedInfoTextsSizeEnlarged
+ speedInfoUnitsOffset: UILayout.speedInfoUnitsOffsetEnlarged
+ assistPowerIconOffset: UILayout.assistPowerIconOffsetEnlarged
+ }
+ PropertyChanges {
+ target: mainPage.statsButton
+ anchors.leftMargin: -mainPage.statsButton.width
+ anchors.topMargin: -mainPage.statsButton.height
+ }
+ PropertyChanges {
+ target: mainPage.naviButton
+ anchors.rightMargin: -mainPage.naviButton.width
+ anchors.topMargin: -mainPage.naviButton.height
+ }
+ PropertyChanges {
+ target: mainPage.lightsButton
+ anchors.leftMargin: -mainPage.statsButton.width
+ anchors.bottomMargin: -mainPage.statsButton.height
+ }
+ PropertyChanges {
+ target: mainPage.modeButton
+ anchors.rightMargin: -mainPage.statsButton.width
+ anchors.bottomMargin: -mainPage.statsButton.height
+ }
+ PropertyChanges {
+ target: clockButton
+ anchors.topMargin: -clockButton.height
+ }
+ AnchorChanges {
+ target: speedView
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.left: undefined
+ anchors.bottom: undefined
+ }
+ StateChangeScript {
+ script: {
+ musicPlayer.lastMusicPlayerState = musicPlayer.state;
+ musicPlayer.state = "hidden";
+ }
+ }
+ }
+ ]
+
+ transitions: [
+ Transition {
+ from: ""; to: "ENLARGED"
+ NumberAnimation {
+ properties: "anchors.leftMargin,anchors.rightMargin,anchors.topMargin,anchors.bottomMargin"
+ duration: 250
+ }
+ NumberAnimation {
+ properties: "width,height,dotcount,speedTextSize,speedBaselineOffset,innerRadius,speedUnitsSize,speedUnitBaselineOffset,speedIconsOffset,speedInfoTextsOffset,speedInfoTextsSize,speedInfoUnitsOffset,assistPowerIconOffset"
+ duration: 250
+ }
+ AnchorAnimation {
+ duration: 250
+ }
+ },
+ Transition {
+ from: "ENLARGED"; to: ""
+ NumberAnimation {
+ properties: "anchors.leftMargin,anchors.rightMargin,anchors.topMargin,anchors.bottomMargin"
+ easing.type: Easing.OutBack
+ duration: 250
+ }
+ NumberAnimation {
+ properties: "width,height,dotcount,speedTextSize,speedBaselineOffset,innerRadius,speedUnitsSize,speedUnitBaselineOffset,speedIconsOffset,speedInfoTextsOffset,speedInfoTextsSize,speedInfoUnitsOffset,assistPowerIconOffset"
+ duration: 250
+ }
+ AnchorAnimation {
+ duration: 250
+ }
+ },
+ Transition {
+ NumberAnimation {
+ properties: "width,height,curvewidth,speedTextSize,speedBaselineOffset,innerRadius,speedUnitMargin"
+ easing.type: Easing.OutBack
+ duration: 250
+ }
+ ColorAnimation {
+ duration: 250
+ }
+ AnchorAnimation {
+ easing.type: Easing.OutBack
+ duration: 250
+ }
+ }
+ ]
+ }
+
+ // Configuration and settings drawer
+ ConfigurationDrawer {
+ id: drawer
+ height: 350
+ width: root.width
+ edge: Qt.TopEdge
+ dragMargin: 20
+ }
+
+ // Inactive swipe view, for animations
+ SwipeView {
+ id: swipeView
+ anchors.fill: parent
+ currentIndex: 1
+ interactive: false
+
+ // List of pages
+ StatsPage {}
+ MainPage {
+ id: mainPage
+ naviGuideArrowSource: naviPage.naviGuideArrowSource
+ naviGuideDistance: naviPage.naviGuideDistance
+ naviGuideAddress: naviPage.naviGuideAddress
+ }
+ NaviPage {
+ id: naviPage;
+ }
+ }
+
+ // Music player
+ MusicPlayer {
+ id: musicPlayer
+ property string lastMusicPlayerState: "unknown"
+ z: 1
+ }
+
+ Connections {
+ target: datastore
+ onDemoReset: drawer.close()
+ }
+
+ // Virtual keyboard
+ InputPanel {
+ id: inputPanel
+ z: 99
+ x: 0
+ y: root.height
+ width: root.width
+
+ states: State {
+ name: "visible"
+ when: inputPanel.active
+ PropertyChanges {
+ target: inputPanel
+ y: root.height - inputPanel.height
+ }
+ }
+ transitions: Transition {
+ from: ""
+ to: "visible"
+ reversible: true
+ ParallelAnimation {
+ NumberAnimation {
+ properties: "y"
+ duration: 250
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+ }
+}
diff --git a/basicsuite/ebike-ui/mapbox.cpp b/basicsuite/ebike-ui/mapbox.cpp
new file mode 100644
index 0000000..9d2303d
--- /dev/null
+++ b/basicsuite/ebike-ui/mapbox.cpp
@@ -0,0 +1,89 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include <QNetworkAccessManager>
+#include <QUrlQuery>
+
+#include "mapbox.h"
+
+#define MAPBOX_URL "https://api.mapbox.com/"
+#define MAPBOX_TOKEN "pk.eyJ1IjoibWFwYm94NHF0IiwiYSI6ImNpd3J3eDE0eDEzdm8ydHM3YzhzajlrN2oifQ.keEkjqm79SiFDFjnesTcgQ"
+
+MapBox::MapBox(QObject *parent)
+ : QObject(parent)
+ , m_nam(new QNetworkAccessManager(this))
+{
+}
+
+const QUrl MapBox::createUrl(const QString &path, QUrlQuery params) const
+{
+ // Create URL and set path
+ QUrl url(MAPBOX_URL);
+ url.setPath(path, QUrl::TolerantMode);
+
+ // Add access token to query params
+ params.addQueryItem("access_token", MAPBOX_TOKEN);
+ url.setQuery(params);
+
+ return url;
+}
+
+QNetworkReply *MapBox::get(const QUrl &url) const
+{
+ return m_nam->get(QNetworkRequest(url));
+}
+
+QNetworkReply *MapBox::getGeocoding(const QString &query, const QGeoCoordinate &proximity)
+{
+ QUrlQuery params;
+ params.addQueryItem("autocomplete", "true");
+ params.addQueryItem("limit", "3");
+ if (proximity.isValid())
+ params.addQueryItem("proximity", QString("%1,%2").arg(proximity.longitude()).arg(proximity.latitude()));
+ QUrl url = createUrl(QString("/geocoding/v5/mapbox.places/%1.json").arg(QString(QUrl::toPercentEncoding(query))), params);
+
+ return get(url);
+}
+
+QNetworkReply *MapBox::getDirections(const QGeoCoordinate &source, const QGeoCoordinate &destination, const QString &type)
+{
+ QString where = QString("%1,%2;%3,%4").arg(source.longitude()).arg(source.latitude()).arg(destination.longitude()).arg(destination.latitude());
+ QUrlQuery params;
+ params.addQueryItem("steps", "true");
+ params.addQueryItem("geometries", "geojson");
+ QUrl url = createUrl(QString("/directions/v5/mapbox/%1/%2.json").arg(type).arg(QString(QUrl::toPercentEncoding(where))), params);
+
+ return get(url);
+}
diff --git a/basicsuite/ebike-ui/mapbox.h b/basicsuite/ebike-ui/mapbox.h
new file mode 100644
index 0000000..5bd295f
--- /dev/null
+++ b/basicsuite/ebike-ui/mapbox.h
@@ -0,0 +1,68 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef MAPBOX_H
+#define MAPBOX_H
+
+#include <QObject>
+#include <QUrl>
+#include <QGeoCoordinate>
+
+class QNetworkAccessManager;
+class QNetworkReply;
+
+class MapBox : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit MapBox(QObject *parent = nullptr);
+
+public:
+ const QUrl createUrl(const QString &path, QUrlQuery params) const;
+ QNetworkReply *get(const QUrl &url) const;
+ QNetworkReply *getGeocoding(const QString &query, const QGeoCoordinate &proximity=QGeoCoordinate());
+ QNetworkReply *getDirections(const QGeoCoordinate &source, const QGeoCoordinate &destination, const QString &type=QString("cycling"));
+
+signals:
+
+public slots:
+
+private:
+ QNetworkAccessManager *m_nam;
+};
+
+#endif // MAPBOX_H
diff --git a/basicsuite/ebike-ui/mapboxsuggestions.cpp b/basicsuite/ebike-ui/mapboxsuggestions.cpp
new file mode 100644
index 0000000..3f66766
--- /dev/null
+++ b/basicsuite/ebike-ui/mapboxsuggestions.cpp
@@ -0,0 +1,109 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QTimer>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QUrl>
+#include <QUrlQuery>
+#include <QJsonDocument>
+#include <QJsonParseError>
+#include <QJsonObject>
+
+#include "mapboxsuggestions.h"
+#include "mapbox.h"
+#include "suggestionsmodel.h"
+
+MapBoxSuggestions::MapBoxSuggestions(MapBox *mapbox, QObject *parent)
+ : QObject(parent)
+ , m_mapbox(mapbox)
+ , m_timer(new QTimer(this))
+ , m_suggestions(new SuggestionsModel(this))
+{
+ // Setup timer to request 500ms after user stops typing
+ m_timer->setSingleShot(true);
+ m_timer->setInterval(500);
+
+ // Connect timer signal
+ connect(m_timer, &QTimer::timeout, this, &MapBoxSuggestions::loadSuggestions);
+}
+
+void MapBoxSuggestions::stopSuggest()
+{
+ m_timer->stop();
+}
+
+void MapBoxSuggestions::setSearch(const QString &search)
+{
+ if (m_search == search)
+ return;
+
+ m_search = search;
+ m_timer->start();
+ emit searchChanged(m_search);
+}
+
+void MapBoxSuggestions::loadSuggestions()
+{
+ QNetworkReply *reply = m_mapbox->getGeocoding(m_search, m_center);
+ connect(reply, &QNetworkReply::finished, this, &MapBoxSuggestions::handleReply);
+ m_requests.append(reply);
+ emit loadingChanged();
+}
+
+void MapBoxSuggestions::handleReply()
+{
+ QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
+ QJsonParseError error;
+ QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error);
+ if (error.error == QJsonParseError::NoError) {
+ QJsonObject obj = doc.object();
+ m_suggestions->setSuggestions(obj.value("features").toArray());
+ emit suggestionsChanged();
+ }
+
+ m_requests.removeOne(reply);
+ emit loadingChanged();
+ reply->deleteLater();
+}
+
+void MapBoxSuggestions::setCenter(const QGeoCoordinate &center)
+{
+ if (m_center != center) {
+ m_center = center;
+ emit centerChanged();
+ }
+}
diff --git a/basicsuite/ebike-ui/mapboxsuggestions.h b/basicsuite/ebike-ui/mapboxsuggestions.h
new file mode 100644
index 0000000..8dc1506
--- /dev/null
+++ b/basicsuite/ebike-ui/mapboxsuggestions.h
@@ -0,0 +1,89 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef MAPBOXSUGGESTIONS_H
+#define MAPBOXSUGGESTIONS_H
+
+#include <QObject>
+#include <QGeoCoordinate>
+
+class MapBox;
+class QNetworkReply;
+class QTimer;
+class SuggestionsModel;
+
+class MapBoxSuggestions : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(SuggestionsModel suggestions READ suggestions NOTIFY suggestionsChanged)
+ Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
+ Q_PROPERTY(QGeoCoordinate center READ center WRITE setCenter NOTIFY centerChanged)
+ Q_PROPERTY(QString search READ search WRITE setSearch NOTIFY searchChanged)
+
+public:
+ explicit MapBoxSuggestions(MapBox *mapbox, QObject *parent = nullptr);
+
+public:
+ SuggestionsModel *suggestions() const { return m_suggestions; }
+ bool loading() const { return m_requests.size() > 0; }
+ const QGeoCoordinate center() { return m_center; }
+ void setCenter(const QGeoCoordinate &center);
+ const QString search() const { return m_search; }
+ void setSearch(const QString &search);
+
+signals:
+ void suggestionsChanged();
+ void loadingChanged();
+ void centerChanged();
+ void searchChanged(QString search);
+
+public slots:
+ void stopSuggest();
+
+private slots:
+ void loadSuggestions();
+ void handleReply();
+
+private:
+ MapBox *m_mapbox;
+ QTimer *m_timer;
+ QList<QNetworkReply *> m_requests;
+ SuggestionsModel *m_suggestions;
+ QGeoCoordinate m_center;
+ QString m_search;
+};
+
+#endif // MAPBOXSUGGESTIONS_H
diff --git a/basicsuite/ebike-ui/moment.js b/basicsuite/ebike-ui/moment.js
new file mode 100644
index 0000000..eaf827d
--- /dev/null
+++ b/basicsuite/ebike-ui/moment.js
@@ -0,0 +1,4551 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+//! moment.js
+//! version : 2.19.2
+//! authors : Tim Wood, Iskren Chernev, Moment.js contributors
+//! license : MIT
+//! momentjs.com
+
+;(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ global.moment = factory()
+}(this, (function () { 'use strict';
+
+var hookCallback;
+
+function hooks () {
+ return hookCallback.apply(null, arguments);
+}
+
+// This is done to register the method called with moment()
+// without creating circular dependencies.
+function setHookCallback (callback) {
+ hookCallback = callback;
+}
+
+function isArray(input) {
+ return input instanceof Array || Object.prototype.toString.call(input) === '[object Array]';
+}
+
+function isObject(input) {
+ // IE8 will treat undefined and null as object if it wasn't for
+ // input != null
+ return input != null && Object.prototype.toString.call(input) === '[object Object]';
+}
+
+function isObjectEmpty(obj) {
+ if (Object.getOwnPropertyNames) {
+ return (Object.getOwnPropertyNames(obj).length === 0);
+ } else {
+ var k;
+ for (k in obj) {
+ if (obj.hasOwnProperty(k)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+function isUndefined(input) {
+ return input === void 0;
+}
+
+function isNumber(input) {
+ return typeof input === 'number' || Object.prototype.toString.call(input) === '[object Number]';
+}
+
+function isDate(input) {
+ return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
+}
+
+function map(arr, fn) {
+ var res = [], i;
+ for (i = 0; i < arr.length; ++i) {
+ res.push(fn(arr[i], i));
+ }
+ return res;
+}
+
+function hasOwnProp(a, b) {
+ return Object.prototype.hasOwnProperty.call(a, b);
+}
+
+function extend(a, b) {
+ for (var i in b) {
+ if (hasOwnProp(b, i)) {
+ a[i] = b[i];
+ }
+ }
+
+ if (hasOwnProp(b, 'toString')) {
+ a.toString = b.toString;
+ }
+
+ if (hasOwnProp(b, 'valueOf')) {
+ a.valueOf = b.valueOf;
+ }
+
+ return a;
+}
+
+function createUTC (input, format, locale, strict) {
+ return createLocalOrUTC(input, format, locale, strict, true).utc();
+}
+
+function defaultParsingFlags() {
+ // We need to deep clone this object.
+ return {
+ empty : false,
+ unusedTokens : [],
+ unusedInput : [],
+ overflow : -2,
+ charsLeftOver : 0,
+ nullInput : false,
+ invalidMonth : null,
+ invalidFormat : false,
+ userInvalidated : false,
+ iso : false,
+ parsedDateParts : [],
+ meridiem : null,
+ rfc2822 : false,
+ weekdayMismatch : false
+ };
+}
+
+function getParsingFlags(m) {
+ if (m._pf == null) {
+ m._pf = defaultParsingFlags();
+ }
+ return m._pf;
+}
+
+var some;
+if (Array.prototype.some) {
+ some = Array.prototype.some;
+} else {
+ some = function (fun) {
+ var t = Object(this);
+ var len = t.length >>> 0;
+
+ for (var i = 0; i < len; i++) {
+ if (i in t && fun.call(this, t[i], i, t)) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+}
+
+function isValid(m) {
+ if (m._isValid == null) {
+ var flags = getParsingFlags(m);
+ var parsedParts = some.call(flags.parsedDateParts, function (i) {
+ return i != null;
+ });
+ var isNowValid = !isNaN(m._d.getTime()) &&
+ flags.overflow < 0 &&
+ !flags.empty &&
+ !flags.invalidMonth &&
+ !flags.invalidWeekday &&
+ !flags.weekdayMismatch &&
+ !flags.nullInput &&
+ !flags.invalidFormat &&
+ !flags.userInvalidated &&
+ (!flags.meridiem || (flags.meridiem && parsedParts));
+
+ if (m._strict) {
+ isNowValid = isNowValid &&
+ flags.charsLeftOver === 0 &&
+ flags.unusedTokens.length === 0 &&
+ flags.bigHour === undefined;
+ }
+
+ if (Object.isFrozen == null || !Object.isFrozen(m)) {
+ m._isValid = isNowValid;
+ }
+ else {
+ return isNowValid;
+ }
+ }
+ return m._isValid;
+}
+
+function createInvalid (flags) {
+ var m = createUTC(NaN);
+ if (flags != null) {
+ extend(getParsingFlags(m), flags);
+ }
+ else {
+ getParsingFlags(m).userInvalidated = true;
+ }
+
+ return m;
+}
+
+// Plugins that add properties should also add the key here (null value),
+// so we can properly clone ourselves.
+var momentProperties = hooks.momentProperties = [];
+
+function copyConfig(to, from) {
+ var i, prop, val;
+
+ if (!isUndefined(from._isAMomentObject)) {
+ to._isAMomentObject = from._isAMomentObject;
+ }
+ if (!isUndefined(from._i)) {
+ to._i = from._i;
+ }
+ if (!isUndefined(from._f)) {
+ to._f = from._f;
+ }
+ if (!isUndefined(from._l)) {
+ to._l = from._l;
+ }
+ if (!isUndefined(from._strict)) {
+ to._strict = from._strict;
+ }
+ if (!isUndefined(from._tzm)) {
+ to._tzm = from._tzm;
+ }
+ if (!isUndefined(from._isUTC)) {
+ to._isUTC = from._isUTC;
+ }
+ if (!isUndefined(from._offset)) {
+ to._offset = from._offset;
+ }
+ if (!isUndefined(from._pf)) {
+ to._pf = getParsingFlags(from);
+ }
+ if (!isUndefined(from._locale)) {
+ to._locale = from._locale;
+ }
+
+ if (momentProperties.length > 0) {
+ for (i = 0; i < momentProperties.length; i++) {
+ prop = momentProperties[i];
+ val = from[prop];
+ if (!isUndefined(val)) {
+ to[prop] = val;
+ }
+ }
+ }
+
+ return to;
+}
+
+var updateInProgress = false;
+
+// Moment prototype object
+function Moment(config) {
+ copyConfig(this, config);
+ this._d = new Date(config._d != null ? config._d.getTime() : NaN);
+ if (!this.isValid()) {
+ this._d = new Date(NaN);
+ }
+ // Prevent infinite loop in case updateOffset creates new moment
+ // objects.
+ if (updateInProgress === false) {
+ updateInProgress = true;
+ hooks.updateOffset(this);
+ updateInProgress = false;
+ }
+}
+
+function isMoment (obj) {
+ return obj instanceof Moment || (obj != null && obj._isAMomentObject != null);
+}
+
+function absFloor (number) {
+ if (number < 0) {
+ // -0 -> 0
+ return Math.ceil(number) || 0;
+ } else {
+ return Math.floor(number);
+ }
+}
+
+function toInt(argumentForCoercion) {
+ var coercedNumber = +argumentForCoercion,
+ value = 0;
+
+ if (coercedNumber !== 0 && isFinite(coercedNumber)) {
+ value = absFloor(coercedNumber);
+ }
+
+ return value;
+}
+
+// compare two arrays, return the number of differences
+function compareArrays(array1, array2, dontConvert) {
+ var len = Math.min(array1.length, array2.length),
+ lengthDiff = Math.abs(array1.length - array2.length),
+ diffs = 0,
+ i;
+ for (i = 0; i < len; i++) {
+ if ((dontConvert && array1[i] !== array2[i]) ||
+ (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
+ diffs++;
+ }
+ }
+ return diffs + lengthDiff;
+}
+
+function warn(msg) {
+ if (hooks.suppressDeprecationWarnings === false &&
+ (typeof console !== 'undefined') && console.warn) {
+ console.warn('Deprecation warning: ' + msg);
+ }
+}
+
+function deprecate(msg, fn) {
+ var firstTime = true;
+
+ return extend(function () {
+ if (hooks.deprecationHandler != null) {
+ hooks.deprecationHandler(null, msg);
+ }
+ if (firstTime) {
+ var args = [];
+ var arg;
+ for (var i = 0; i < arguments.length; i++) {
+ arg = '';
+ if (typeof arguments[i] === 'object') {
+ arg += '\n[' + i + '] ';
+ for (var key in arguments[0]) {
+ arg += key + ': ' + arguments[0][key] + ', ';
+ }
+ arg = arg.slice(0, -2); // Remove trailing comma and space
+ } else {
+ arg = arguments[i];
+ }
+ args.push(arg);
+ }
+ warn(msg + '\nArguments: ' + Array.prototype.slice.call(args).join('') + '\n' + (new Error()).stack);
+ firstTime = false;
+ }
+ return fn.apply(this, arguments);
+ }, fn);
+}
+
+var deprecations = {};
+
+function deprecateSimple(name, msg) {
+ if (hooks.deprecationHandler != null) {
+ hooks.deprecationHandler(name, msg);
+ }
+ if (!deprecations[name]) {
+ warn(msg);
+ deprecations[name] = true;
+ }
+}
+
+hooks.suppressDeprecationWarnings = false;
+hooks.deprecationHandler = null;
+
+function isFunction(input) {
+ return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]';
+}
+
+function set (config) {
+ var prop, i;
+ for (i in config) {
+ prop = config[i];
+ if (isFunction(prop)) {
+ this[i] = prop;
+ } else {
+ this['_' + i] = prop;
+ }
+ }
+ this._config = config;
+ // Lenient ordinal parsing accepts just a number in addition to
+ // number + (possibly) stuff coming from _dayOfMonthOrdinalParse.
+ // TODO: Remove "ordinalParse" fallback in next major release.
+ this._dayOfMonthOrdinalParseLenient = new RegExp(
+ (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) +
+ '|' + (/\d{1,2}/).source);
+}
+
+function mergeConfigs(parentConfig, childConfig) {
+ var res = extend({}, parentConfig), prop;
+ for (prop in childConfig) {
+ if (hasOwnProp(childConfig, prop)) {
+ if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) {
+ res[prop] = {};
+ extend(res[prop], parentConfig[prop]);
+ extend(res[prop], childConfig[prop]);
+ } else if (childConfig[prop] != null) {
+ res[prop] = childConfig[prop];
+ } else {
+ delete res[prop];
+ }
+ }
+ }
+ for (prop in parentConfig) {
+ if (hasOwnProp(parentConfig, prop) &&
+ !hasOwnProp(childConfig, prop) &&
+ isObject(parentConfig[prop])) {
+ // make sure changes to properties don't modify parent config
+ res[prop] = extend({}, res[prop]);
+ }
+ }
+ return res;
+}
+
+function Locale(config) {
+ if (config != null) {
+ this.set(config);
+ }
+}
+
+var keys;
+
+if (Object.keys) {
+ keys = Object.keys;
+} else {
+ keys = function (obj) {
+ var i, res = [];
+ for (i in obj) {
+ if (hasOwnProp(obj, i)) {
+ res.push(i);
+ }
+ }
+ return res;
+ };
+}
+
+var defaultCalendar = {
+ sameDay : '[Today at] LT',
+ nextDay : '[Tomorrow at] LT',
+ nextWeek : 'dddd [at] LT',
+ lastDay : '[Yesterday at] LT',
+ lastWeek : '[Last] dddd [at] LT',
+ sameElse : 'L'
+};
+
+function calendar (key, mom, now) {
+ var output = this._calendar[key] || this._calendar['sameElse'];
+ return isFunction(output) ? output.call(mom, now) : output;
+}
+
+var defaultLongDateFormat = {
+ LTS : 'h:mm:ss A',
+ LT : 'h:mm A',
+ L : 'MM/DD/YYYY',
+ LL : 'MMMM D, YYYY',
+ LLL : 'MMMM D, YYYY h:mm A',
+ LLLL : 'dddd, MMMM D, YYYY h:mm A'
+};
+
+function longDateFormat (key) {
+ var format = this._longDateFormat[key],
+ formatUpper = this._longDateFormat[key.toUpperCase()];
+
+ if (format || !formatUpper) {
+ return format;
+ }
+
+ this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) {
+ return val.slice(1);
+ });
+
+ return this._longDateFormat[key];
+}
+
+var defaultInvalidDate = 'Invalid date';
+
+function invalidDate () {
+ return this._invalidDate;
+}
+
+var defaultOrdinal = '%d';
+var defaultDayOfMonthOrdinalParse = /\d{1,2}/;
+
+function ordinal (number) {
+ return this._ordinal.replace('%d', number);
+}
+
+var defaultRelativeTime = {
+ future : 'in %s',
+ past : '%s ago',
+ s : 'a few seconds',
+ ss : '%d seconds',
+ m : 'a minute',
+ mm : '%d minutes',
+ h : 'an hour',
+ hh : '%d hours',
+ d : 'a day',
+ dd : '%d days',
+ M : 'a month',
+ MM : '%d months',
+ y : 'a year',
+ yy : '%d years'
+};
+
+function relativeTime (number, withoutSuffix, string, isFuture) {
+ var output = this._relativeTime[string];
+ return (isFunction(output)) ?
+ output(number, withoutSuffix, string, isFuture) :
+ output.replace(/%d/i, number);
+}
+
+function pastFuture (diff, output) {
+ var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
+ return isFunction(format) ? format(output) : format.replace(/%s/i, output);
+}
+
+var aliases = {};
+
+function addUnitAlias (unit, shorthand) {
+ var lowerCase = unit.toLowerCase();
+ aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit;
+}
+
+function normalizeUnits(units) {
+ return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined;
+}
+
+function normalizeObjectUnits(inputObject) {
+ var normalizedInput = {},
+ normalizedProp,
+ prop;
+
+ for (prop in inputObject) {
+ if (hasOwnProp(inputObject, prop)) {
+ normalizedProp = normalizeUnits(prop);
+ if (normalizedProp) {
+ normalizedInput[normalizedProp] = inputObject[prop];
+ }
+ }
+ }
+
+ return normalizedInput;
+}
+
+var priorities = {};
+
+function addUnitPriority(unit, priority) {
+ priorities[unit] = priority;
+}
+
+function getPrioritizedUnits(unitsObj) {
+ var units = [];
+ for (var u in unitsObj) {
+ units.push({unit: u, priority: priorities[u]});
+ }
+ units.sort(function (a, b) {
+ return a.priority - b.priority;
+ });
+ return units;
+}
+
+function zeroFill(number, targetLength, forceSign) {
+ var absNumber = '' + Math.abs(number),
+ zerosToFill = targetLength - absNumber.length,
+ sign = number >= 0;
+ return (sign ? (forceSign ? '+' : '') : '-') +
+ Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber;
+}
+
+var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g;
+
+var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g;
+
+var formatFunctions = {};
+
+var formatTokenFunctions = {};
+
+// token: 'M'
+// padded: ['MM', 2]
+// ordinal: 'Mo'
+// callback: function () { this.month() + 1 }
+function addFormatToken (token, padded, ordinal, callback) {
+ var func = callback;
+ if (typeof callback === 'string') {
+ func = function () {
+ return this[callback]();
+ };
+ }
+ if (token) {
+ formatTokenFunctions[token] = func;
+ }
+ if (padded) {
+ formatTokenFunctions[padded[0]] = function () {
+ return zeroFill(func.apply(this, arguments), padded[1], padded[2]);
+ };
+ }
+ if (ordinal) {
+ formatTokenFunctions[ordinal] = function () {
+ return this.localeData().ordinal(func.apply(this, arguments), token);
+ };
+ }
+}
+
+function removeFormattingTokens(input) {
+ if (input.match(/\[[\s\S]/)) {
+ return input.replace(/^\[|\]$/g, '');
+ }
+ return input.replace(/\\/g, '');
+}
+
+function makeFormatFunction(format) {
+ var array = format.match(formattingTokens), i, length;
+
+ for (i = 0, length = array.length; i < length; i++) {
+ if (formatTokenFunctions[array[i]]) {
+ array[i] = formatTokenFunctions[array[i]];
+ } else {
+ array[i] = removeFormattingTokens(array[i]);
+ }
+ }
+
+ return function (mom) {
+ var output = '', i;
+ for (i = 0; i < length; i++) {
+ output += isFunction(array[i]) ? array[i].call(mom, format) : array[i];
+ }
+ return output;
+ };
+}
+
+// format date using native date object
+function formatMoment(m, format) {
+ if (!m.isValid()) {
+ return m.localeData().invalidDate();
+ }
+
+ format = expandFormat(format, m.localeData());
+ formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format);
+
+ return formatFunctions[format](m);
+}
+
+function expandFormat(format, locale) {
+ var i = 5;
+
+ function replaceLongDateFormatTokens(input) {
+ return locale.longDateFormat(input) || input;
+ }
+
+ localFormattingTokens.lastIndex = 0;
+ while (i >= 0 && localFormattingTokens.test(format)) {
+ format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
+ localFormattingTokens.lastIndex = 0;
+ i -= 1;
+ }
+
+ return format;
+}
+
+var match1 = /\d/; // 0 - 9
+var match2 = /\d\d/; // 00 - 99
+var match3 = /\d{3}/; // 000 - 999
+var match4 = /\d{4}/; // 0000 - 9999
+var match6 = /[+-]?\d{6}/; // -999999 - 999999
+var match1to2 = /\d\d?/; // 0 - 99
+var match3to4 = /\d\d\d\d?/; // 999 - 9999
+var match5to6 = /\d\d\d\d\d\d?/; // 99999 - 999999
+var match1to3 = /\d{1,3}/; // 0 - 999
+var match1to4 = /\d{1,4}/; // 0 - 9999
+var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999
+
+var matchUnsigned = /\d+/; // 0 - inf
+var matchSigned = /[+-]?\d+/; // -inf - inf
+
+var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z
+var matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi; // +00 -00 +00:00 -00:00 +0000 -0000 or Z
+
+var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123
+
+// any word (or two) characters or numbers including two/three word month in arabic.
+// includes scottish gaelic two word and hyphenated months
+var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i;
+
+
+var regexes = {};
+
+function addRegexToken (token, regex, strictRegex) {
+ regexes[token] = isFunction(regex) ? regex : function (isStrict, localeData) {
+ return (isStrict && strictRegex) ? strictRegex : regex;
+ };
+}
+
+function getParseRegexForToken (token, config) {
+ if (!hasOwnProp(regexes, token)) {
+ return new RegExp(unescapeFormat(token));
+ }
+
+ return regexes[token](config._strict, config._locale);
+}
+
+// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+function unescapeFormat(s) {
+ return regexEscape(s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
+ return p1 || p2 || p3 || p4;
+ }));
+}
+
+function regexEscape(s) {
+ return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
+var tokens = {};
+
+function addParseToken (token, callback) {
+ var i, func = callback;
+ if (typeof token === 'string') {
+ token = [token];
+ }
+ if (isNumber(callback)) {
+ func = function (input, array) {
+ array[callback] = toInt(input);
+ };
+ }
+ for (i = 0; i < token.length; i++) {
+ tokens[token[i]] = func;
+ }
+}
+
+function addWeekParseToken (token, callback) {
+ addParseToken(token, function (input, array, config, token) {
+ config._w = config._w || {};
+ callback(input, config._w, config, token);
+ });
+}
+
+function addTimeToArrayFromToken(token, input, config) {
+ if (input != null && hasOwnProp(tokens, token)) {
+ tokens[token](input, config._a, config, token);
+ }
+}
+
+var YEAR = 0;
+var MONTH = 1;
+var DATE = 2;
+var HOUR = 3;
+var MINUTE = 4;
+var SECOND = 5;
+var MILLISECOND = 6;
+var WEEK = 7;
+var WEEKDAY = 8;
+
+// FORMATTING
+
+addFormatToken('Y', 0, 0, function () {
+ var y = this.year();
+ return y <= 9999 ? '' + y : '+' + y;
+});
+
+addFormatToken(0, ['YY', 2], 0, function () {
+ return this.year() % 100;
+});
+
+addFormatToken(0, ['YYYY', 4], 0, 'year');
+addFormatToken(0, ['YYYYY', 5], 0, 'year');
+addFormatToken(0, ['YYYYYY', 6, true], 0, 'year');
+
+// ALIASES
+
+addUnitAlias('year', 'y');
+
+// PRIORITIES
+
+addUnitPriority('year', 1);
+
+// PARSING
+
+addRegexToken('Y', matchSigned);
+addRegexToken('YY', match1to2, match2);
+addRegexToken('YYYY', match1to4, match4);
+addRegexToken('YYYYY', match1to6, match6);
+addRegexToken('YYYYYY', match1to6, match6);
+
+addParseToken(['YYYYY', 'YYYYYY'], YEAR);
+addParseToken('YYYY', function (input, array) {
+ array[YEAR] = input.length === 2 ? hooks.parseTwoDigitYear(input) : toInt(input);
+});
+addParseToken('YY', function (input, array) {
+ array[YEAR] = hooks.parseTwoDigitYear(input);
+});
+addParseToken('Y', function (input, array) {
+ array[YEAR] = parseInt(input, 10);
+});
+
+// HELPERS
+
+function daysInYear(year) {
+ return isLeapYear(year) ? 366 : 365;
+}
+
+function isLeapYear(year) {
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
+}
+
+// HOOKS
+
+hooks.parseTwoDigitYear = function (input) {
+ return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
+};
+
+// MOMENTS
+
+var getSetYear = makeGetSet('FullYear', true);
+
+function getIsLeapYear () {
+ return isLeapYear(this.year());
+}
+
+function makeGetSet (unit, keepTime) {
+ return function (value) {
+ if (value != null) {
+ set$1(this, unit, value);
+ hooks.updateOffset(this, keepTime);
+ return this;
+ } else {
+ return get(this, unit);
+ }
+ };
+}
+
+function get (mom, unit) {
+ return mom.isValid() ?
+ mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() : NaN;
+}
+
+function set$1 (mom, unit, value) {
+ if (mom.isValid() && !isNaN(value)) {
+ if (unit === 'FullYear' && isLeapYear(mom.year()) && mom.month() === 1 && mom.date() === 29) {
+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value, mom.month(), daysInMonth(value, mom.month()));
+ }
+ else {
+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
+ }
+ }
+}
+
+// MOMENTS
+
+function stringGet (units) {
+ units = normalizeUnits(units);
+ if (isFunction(this[units])) {
+ return this[units]();
+ }
+ return this;
+}
+
+
+function stringSet (units, value) {
+ if (typeof units === 'object') {
+ units = normalizeObjectUnits(units);
+ var prioritized = getPrioritizedUnits(units);
+ for (var i = 0; i < prioritized.length; i++) {
+ this[prioritized[i].unit](units[prioritized[i].unit]);
+ }
+ } else {
+ units = normalizeUnits(units);
+ if (isFunction(this[units])) {
+ return this[units](value);
+ }
+ }
+ return this;
+}
+
+function mod(n, x) {
+ return ((n % x) + x) % x;
+}
+
+var indexOf;
+
+if (Array.prototype.indexOf) {
+ indexOf = Array.prototype.indexOf;
+} else {
+ indexOf = function (o) {
+ // I know
+ var i;
+ for (i = 0; i < this.length; ++i) {
+ if (this[i] === o) {
+ return i;
+ }
+ }
+ return -1;
+ };
+}
+
+function daysInMonth(year, month) {
+ if (isNaN(year) || isNaN(month)) {
+ return NaN;
+ }
+ var modMonth = mod(month, 12);
+ year += (month - modMonth) / 12;
+ return modMonth === 1 ? (isLeapYear(year) ? 29 : 28) : (31 - modMonth % 7 % 2);
+}
+
+// FORMATTING
+
+addFormatToken('M', ['MM', 2], 'Mo', function () {
+ return this.month() + 1;
+});
+
+addFormatToken('MMM', 0, 0, function (format) {
+ return this.localeData().monthsShort(this, format);
+});
+
+addFormatToken('MMMM', 0, 0, function (format) {
+ return this.localeData().months(this, format);
+});
+
+// ALIASES
+
+addUnitAlias('month', 'M');
+
+// PRIORITY
+
+addUnitPriority('month', 8);
+
+// PARSING
+
+addRegexToken('M', match1to2);
+addRegexToken('MM', match1to2, match2);
+addRegexToken('MMM', function (isStrict, locale) {
+ return locale.monthsShortRegex(isStrict);
+});
+addRegexToken('MMMM', function (isStrict, locale) {
+ return locale.monthsRegex(isStrict);
+});
+
+addParseToken(['M', 'MM'], function (input, array) {
+ array[MONTH] = toInt(input) - 1;
+});
+
+addParseToken(['MMM', 'MMMM'], function (input, array, config, token) {
+ var month = config._locale.monthsParse(input, token, config._strict);
+ // if we didn't find a month name, mark the date as invalid.
+ if (month != null) {
+ array[MONTH] = month;
+ } else {
+ getParsingFlags(config).invalidMonth = input;
+ }
+});
+
+// LOCALES
+
+var MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/;
+var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_');
+function localeMonths (m, format) {
+ if (!m) {
+ return isArray(this._months) ? this._months :
+ this._months['standalone'];
+ }
+ return isArray(this._months) ? this._months[m.month()] :
+ this._months[(this._months.isFormat || MONTHS_IN_FORMAT).test(format) ? 'format' : 'standalone'][m.month()];
+}
+
+var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_');
+function localeMonthsShort (m, format) {
+ if (!m) {
+ return isArray(this._monthsShort) ? this._monthsShort :
+ this._monthsShort['standalone'];
+ }
+ return isArray(this._monthsShort) ? this._monthsShort[m.month()] :
+ this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()];
+}
+
+function handleStrictParse(monthName, format, strict) {
+ var i, ii, mom, llc = monthName.toLocaleLowerCase();
+ if (!this._monthsParse) {
+ // this is not used
+ this._monthsParse = [];
+ this._longMonthsParse = [];
+ this._shortMonthsParse = [];
+ for (i = 0; i < 12; ++i) {
+ mom = createUTC([2000, i]);
+ this._shortMonthsParse[i] = this.monthsShort(mom, '').toLocaleLowerCase();
+ this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase();
+ }
+ }
+
+ if (strict) {
+ if (format === 'MMM') {
+ ii = indexOf.call(this._shortMonthsParse, llc);
+ return ii !== -1 ? ii : null;
+ } else {
+ ii = indexOf.call(this._longMonthsParse, llc);
+ return ii !== -1 ? ii : null;
+ }
+ } else {
+ if (format === 'MMM') {
+ ii = indexOf.call(this._shortMonthsParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._longMonthsParse, llc);
+ return ii !== -1 ? ii : null;
+ } else {
+ ii = indexOf.call(this._longMonthsParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._shortMonthsParse, llc);
+ return ii !== -1 ? ii : null;
+ }
+ }
+}
+
+function localeMonthsParse (monthName, format, strict) {
+ var i, mom, regex;
+
+ if (this._monthsParseExact) {
+ return handleStrictParse.call(this, monthName, format, strict);
+ }
+
+ if (!this._monthsParse) {
+ this._monthsParse = [];
+ this._longMonthsParse = [];
+ this._shortMonthsParse = [];
+ }
+
+ // TODO: add sorting
+ // Sorting makes sure if one month (or abbr) is a prefix of another
+ // see sorting in computeMonthsParse
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ mom = createUTC([2000, i]);
+ if (strict && !this._longMonthsParse[i]) {
+ this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i');
+ this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i');
+ }
+ if (!strict && !this._monthsParse[i]) {
+ regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
+ this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) {
+ return i;
+ } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) {
+ return i;
+ } else if (!strict && this._monthsParse[i].test(monthName)) {
+ return i;
+ }
+ }
+}
+
+// MOMENTS
+
+function setMonth (mom, value) {
+ var dayOfMonth;
+
+ if (!mom.isValid()) {
+ // No op
+ return mom;
+ }
+
+ if (typeof value === 'string') {
+ if (/^\d+$/.test(value)) {
+ value = toInt(value);
+ } else {
+ value = mom.localeData().monthsParse(value);
+ // TODO: Another silent failure?
+ if (!isNumber(value)) {
+ return mom;
+ }
+ }
+ }
+
+ dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value));
+ mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
+ return mom;
+}
+
+function getSetMonth (value) {
+ if (value != null) {
+ setMonth(this, value);
+ hooks.updateOffset(this, true);
+ return this;
+ } else {
+ return get(this, 'Month');
+ }
+}
+
+function getDaysInMonth () {
+ return daysInMonth(this.year(), this.month());
+}
+
+var defaultMonthsShortRegex = matchWord;
+function monthsShortRegex (isStrict) {
+ if (this._monthsParseExact) {
+ if (!hasOwnProp(this, '_monthsRegex')) {
+ computeMonthsParse.call(this);
+ }
+ if (isStrict) {
+ return this._monthsShortStrictRegex;
+ } else {
+ return this._monthsShortRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_monthsShortRegex')) {
+ this._monthsShortRegex = defaultMonthsShortRegex;
+ }
+ return this._monthsShortStrictRegex && isStrict ?
+ this._monthsShortStrictRegex : this._monthsShortRegex;
+ }
+}
+
+var defaultMonthsRegex = matchWord;
+function monthsRegex (isStrict) {
+ if (this._monthsParseExact) {
+ if (!hasOwnProp(this, '_monthsRegex')) {
+ computeMonthsParse.call(this);
+ }
+ if (isStrict) {
+ return this._monthsStrictRegex;
+ } else {
+ return this._monthsRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_monthsRegex')) {
+ this._monthsRegex = defaultMonthsRegex;
+ }
+ return this._monthsStrictRegex && isStrict ?
+ this._monthsStrictRegex : this._monthsRegex;
+ }
+}
+
+function computeMonthsParse () {
+ function cmpLenRev(a, b) {
+ return b.length - a.length;
+ }
+
+ var shortPieces = [], longPieces = [], mixedPieces = [],
+ i, mom;
+ for (i = 0; i < 12; i++) {
+ // make the regex if we don't have it already
+ mom = createUTC([2000, i]);
+ shortPieces.push(this.monthsShort(mom, ''));
+ longPieces.push(this.months(mom, ''));
+ mixedPieces.push(this.months(mom, ''));
+ mixedPieces.push(this.monthsShort(mom, ''));
+ }
+ // Sorting makes sure if one month (or abbr) is a prefix of another it
+ // will match the longer piece.
+ shortPieces.sort(cmpLenRev);
+ longPieces.sort(cmpLenRev);
+ mixedPieces.sort(cmpLenRev);
+ for (i = 0; i < 12; i++) {
+ shortPieces[i] = regexEscape(shortPieces[i]);
+ longPieces[i] = regexEscape(longPieces[i]);
+ }
+ for (i = 0; i < 24; i++) {
+ mixedPieces[i] = regexEscape(mixedPieces[i]);
+ }
+
+ this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+ this._monthsShortRegex = this._monthsRegex;
+ this._monthsStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i');
+ this._monthsShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i');
+}
+
+function createDate (y, m, d, h, M, s, ms) {
+ // can't just apply() to create a date:
+ // https://stackoverflow.com/q/181348
+ var date = new Date(y, m, d, h, M, s, ms);
+
+ // the date constructor remaps years 0-99 to 1900-1999
+ if (y < 100 && y >= 0 && isFinite(date.getFullYear())) {
+ date.setFullYear(y);
+ }
+ return date;
+}
+
+function createUTCDate (y) {
+ var date = new Date(Date.UTC.apply(null, arguments));
+
+ // the Date.UTC function remaps years 0-99 to 1900-1999
+ if (y < 100 && y >= 0 && isFinite(date.getUTCFullYear())) {
+ date.setUTCFullYear(y);
+ }
+ return date;
+}
+
+// start-of-first-week - start-of-year
+function firstWeekOffset(year, dow, doy) {
+ var // first-week day -- which january is always in the first week (4 for iso, 1 for other)
+ fwd = 7 + dow - doy,
+ // first-week day local weekday -- which local weekday is fwd
+ fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7;
+
+ return -fwdlw + fwd - 1;
+}
+
+// https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
+function dayOfYearFromWeeks(year, week, weekday, dow, doy) {
+ var localWeekday = (7 + weekday - dow) % 7,
+ weekOffset = firstWeekOffset(year, dow, doy),
+ dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset,
+ resYear, resDayOfYear;
+
+ if (dayOfYear <= 0) {
+ resYear = year - 1;
+ resDayOfYear = daysInYear(resYear) + dayOfYear;
+ } else if (dayOfYear > daysInYear(year)) {
+ resYear = year + 1;
+ resDayOfYear = dayOfYear - daysInYear(year);
+ } else {
+ resYear = year;
+ resDayOfYear = dayOfYear;
+ }
+
+ return {
+ year: resYear,
+ dayOfYear: resDayOfYear
+ };
+}
+
+function weekOfYear(mom, dow, doy) {
+ var weekOffset = firstWeekOffset(mom.year(), dow, doy),
+ week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1,
+ resWeek, resYear;
+
+ if (week < 1) {
+ resYear = mom.year() - 1;
+ resWeek = week + weeksInYear(resYear, dow, doy);
+ } else if (week > weeksInYear(mom.year(), dow, doy)) {
+ resWeek = week - weeksInYear(mom.year(), dow, doy);
+ resYear = mom.year() + 1;
+ } else {
+ resYear = mom.year();
+ resWeek = week;
+ }
+
+ return {
+ week: resWeek,
+ year: resYear
+ };
+}
+
+function weeksInYear(year, dow, doy) {
+ var weekOffset = firstWeekOffset(year, dow, doy),
+ weekOffsetNext = firstWeekOffset(year + 1, dow, doy);
+ return (daysInYear(year) - weekOffset + weekOffsetNext) / 7;
+}
+
+// FORMATTING
+
+addFormatToken('w', ['ww', 2], 'wo', 'week');
+addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek');
+
+// ALIASES
+
+addUnitAlias('week', 'w');
+addUnitAlias('isoWeek', 'W');
+
+// PRIORITIES
+
+addUnitPriority('week', 5);
+addUnitPriority('isoWeek', 5);
+
+// PARSING
+
+addRegexToken('w', match1to2);
+addRegexToken('ww', match1to2, match2);
+addRegexToken('W', match1to2);
+addRegexToken('WW', match1to2, match2);
+
+addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) {
+ week[token.substr(0, 1)] = toInt(input);
+});
+
+// HELPERS
+
+// LOCALES
+
+function localeWeek (mom) {
+ return weekOfYear(mom, this._week.dow, this._week.doy).week;
+}
+
+var defaultLocaleWeek = {
+ dow : 0, // Sunday is the first day of the week.
+ doy : 6 // The week that contains Jan 1st is the first week of the year.
+};
+
+function localeFirstDayOfWeek () {
+ return this._week.dow;
+}
+
+function localeFirstDayOfYear () {
+ return this._week.doy;
+}
+
+// MOMENTS
+
+function getSetWeek (input) {
+ var week = this.localeData().week(this);
+ return input == null ? week : this.add((input - week) * 7, 'd');
+}
+
+function getSetISOWeek (input) {
+ var week = weekOfYear(this, 1, 4).week;
+ return input == null ? week : this.add((input - week) * 7, 'd');
+}
+
+// FORMATTING
+
+addFormatToken('d', 0, 'do', 'day');
+
+addFormatToken('dd', 0, 0, function (format) {
+ return this.localeData().weekdaysMin(this, format);
+});
+
+addFormatToken('ddd', 0, 0, function (format) {
+ return this.localeData().weekdaysShort(this, format);
+});
+
+addFormatToken('dddd', 0, 0, function (format) {
+ return this.localeData().weekdays(this, format);
+});
+
+addFormatToken('e', 0, 0, 'weekday');
+addFormatToken('E', 0, 0, 'isoWeekday');
+
+// ALIASES
+
+addUnitAlias('day', 'd');
+addUnitAlias('weekday', 'e');
+addUnitAlias('isoWeekday', 'E');
+
+// PRIORITY
+addUnitPriority('day', 11);
+addUnitPriority('weekday', 11);
+addUnitPriority('isoWeekday', 11);
+
+// PARSING
+
+addRegexToken('d', match1to2);
+addRegexToken('e', match1to2);
+addRegexToken('E', match1to2);
+addRegexToken('dd', function (isStrict, locale) {
+ return locale.weekdaysMinRegex(isStrict);
+});
+addRegexToken('ddd', function (isStrict, locale) {
+ return locale.weekdaysShortRegex(isStrict);
+});
+addRegexToken('dddd', function (isStrict, locale) {
+ return locale.weekdaysRegex(isStrict);
+});
+
+addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) {
+ var weekday = config._locale.weekdaysParse(input, token, config._strict);
+ // if we didn't get a weekday name, mark the date as invalid
+ if (weekday != null) {
+ week.d = weekday;
+ } else {
+ getParsingFlags(config).invalidWeekday = input;
+ }
+});
+
+addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) {
+ week[token] = toInt(input);
+});
+
+// HELPERS
+
+function parseWeekday(input, locale) {
+ if (typeof input !== 'string') {
+ return input;
+ }
+
+ if (!isNaN(input)) {
+ return parseInt(input, 10);
+ }
+
+ input = locale.weekdaysParse(input);
+ if (typeof input === 'number') {
+ return input;
+ }
+
+ return null;
+}
+
+function parseIsoWeekday(input, locale) {
+ if (typeof input === 'string') {
+ return locale.weekdaysParse(input) % 7 || 7;
+ }
+ return isNaN(input) ? null : input;
+}
+
+// LOCALES
+
+var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_');
+function localeWeekdays (m, format) {
+ if (!m) {
+ return isArray(this._weekdays) ? this._weekdays :
+ this._weekdays['standalone'];
+ }
+ return isArray(this._weekdays) ? this._weekdays[m.day()] :
+ this._weekdays[this._weekdays.isFormat.test(format) ? 'format' : 'standalone'][m.day()];
+}
+
+var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_');
+function localeWeekdaysShort (m) {
+ return (m) ? this._weekdaysShort[m.day()] : this._weekdaysShort;
+}
+
+var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_');
+function localeWeekdaysMin (m) {
+ return (m) ? this._weekdaysMin[m.day()] : this._weekdaysMin;
+}
+
+function handleStrictParse$1(weekdayName, format, strict) {
+ var i, ii, mom, llc = weekdayName.toLocaleLowerCase();
+ if (!this._weekdaysParse) {
+ this._weekdaysParse = [];
+ this._shortWeekdaysParse = [];
+ this._minWeekdaysParse = [];
+
+ for (i = 0; i < 7; ++i) {
+ mom = createUTC([2000, 1]).day(i);
+ this._minWeekdaysParse[i] = this.weekdaysMin(mom, '').toLocaleLowerCase();
+ this._shortWeekdaysParse[i] = this.weekdaysShort(mom, '').toLocaleLowerCase();
+ this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase();
+ }
+ }
+
+ if (strict) {
+ if (format === 'dddd') {
+ ii = indexOf.call(this._weekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ } else if (format === 'ddd') {
+ ii = indexOf.call(this._shortWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ } else {
+ ii = indexOf.call(this._minWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ }
+ } else {
+ if (format === 'dddd') {
+ ii = indexOf.call(this._weekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._shortWeekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._minWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ } else if (format === 'ddd') {
+ ii = indexOf.call(this._shortWeekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._weekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._minWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ } else {
+ ii = indexOf.call(this._minWeekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._weekdaysParse, llc);
+ if (ii !== -1) {
+ return ii;
+ }
+ ii = indexOf.call(this._shortWeekdaysParse, llc);
+ return ii !== -1 ? ii : null;
+ }
+ }
+}
+
+function localeWeekdaysParse (weekdayName, format, strict) {
+ var i, mom, regex;
+
+ if (this._weekdaysParseExact) {
+ return handleStrictParse$1.call(this, weekdayName, format, strict);
+ }
+
+ if (!this._weekdaysParse) {
+ this._weekdaysParse = [];
+ this._minWeekdaysParse = [];
+ this._shortWeekdaysParse = [];
+ this._fullWeekdaysParse = [];
+ }
+
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+
+ mom = createUTC([2000, 1]).day(i);
+ if (strict && !this._fullWeekdaysParse[i]) {
+ this._fullWeekdaysParse[i] = new RegExp('^' + this.weekdays(mom, '').replace('.', '\.?') + '$', 'i');
+ this._shortWeekdaysParse[i] = new RegExp('^' + this.weekdaysShort(mom, '').replace('.', '\.?') + '$', 'i');
+ this._minWeekdaysParse[i] = new RegExp('^' + this.weekdaysMin(mom, '').replace('.', '\.?') + '$', 'i');
+ }
+ if (!this._weekdaysParse[i]) {
+ regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
+ this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
+ }
+ // test the regex
+ if (strict && format === 'dddd' && this._fullWeekdaysParse[i].test(weekdayName)) {
+ return i;
+ } else if (strict && format === 'ddd' && this._shortWeekdaysParse[i].test(weekdayName)) {
+ return i;
+ } else if (strict && format === 'dd' && this._minWeekdaysParse[i].test(weekdayName)) {
+ return i;
+ } else if (!strict && this._weekdaysParse[i].test(weekdayName)) {
+ return i;
+ }
+ }
+}
+
+// MOMENTS
+
+function getSetDayOfWeek (input) {
+ if (!this.isValid()) {
+ return input != null ? this : NaN;
+ }
+ var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
+ if (input != null) {
+ input = parseWeekday(input, this.localeData());
+ return this.add(input - day, 'd');
+ } else {
+ return day;
+ }
+}
+
+function getSetLocaleDayOfWeek (input) {
+ if (!this.isValid()) {
+ return input != null ? this : NaN;
+ }
+ var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7;
+ return input == null ? weekday : this.add(input - weekday, 'd');
+}
+
+function getSetISODayOfWeek (input) {
+ if (!this.isValid()) {
+ return input != null ? this : NaN;
+ }
+
+ // behaves the same as moment#day except
+ // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
+ // as a setter, sunday should belong to the previous week.
+
+ if (input != null) {
+ var weekday = parseIsoWeekday(input, this.localeData());
+ return this.day(this.day() % 7 ? weekday : weekday - 7);
+ } else {
+ return this.day() || 7;
+ }
+}
+
+var defaultWeekdaysRegex = matchWord;
+function weekdaysRegex (isStrict) {
+ if (this._weekdaysParseExact) {
+ if (!hasOwnProp(this, '_weekdaysRegex')) {
+ computeWeekdaysParse.call(this);
+ }
+ if (isStrict) {
+ return this._weekdaysStrictRegex;
+ } else {
+ return this._weekdaysRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_weekdaysRegex')) {
+ this._weekdaysRegex = defaultWeekdaysRegex;
+ }
+ return this._weekdaysStrictRegex && isStrict ?
+ this._weekdaysStrictRegex : this._weekdaysRegex;
+ }
+}
+
+var defaultWeekdaysShortRegex = matchWord;
+function weekdaysShortRegex (isStrict) {
+ if (this._weekdaysParseExact) {
+ if (!hasOwnProp(this, '_weekdaysRegex')) {
+ computeWeekdaysParse.call(this);
+ }
+ if (isStrict) {
+ return this._weekdaysShortStrictRegex;
+ } else {
+ return this._weekdaysShortRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_weekdaysShortRegex')) {
+ this._weekdaysShortRegex = defaultWeekdaysShortRegex;
+ }
+ return this._weekdaysShortStrictRegex && isStrict ?
+ this._weekdaysShortStrictRegex : this._weekdaysShortRegex;
+ }
+}
+
+var defaultWeekdaysMinRegex = matchWord;
+function weekdaysMinRegex (isStrict) {
+ if (this._weekdaysParseExact) {
+ if (!hasOwnProp(this, '_weekdaysRegex')) {
+ computeWeekdaysParse.call(this);
+ }
+ if (isStrict) {
+ return this._weekdaysMinStrictRegex;
+ } else {
+ return this._weekdaysMinRegex;
+ }
+ } else {
+ if (!hasOwnProp(this, '_weekdaysMinRegex')) {
+ this._weekdaysMinRegex = defaultWeekdaysMinRegex;
+ }
+ return this._weekdaysMinStrictRegex && isStrict ?
+ this._weekdaysMinStrictRegex : this._weekdaysMinRegex;
+ }
+}
+
+
+function computeWeekdaysParse () {
+ function cmpLenRev(a, b) {
+ return b.length - a.length;
+ }
+
+ var minPieces = [], shortPieces = [], longPieces = [], mixedPieces = [],
+ i, mom, minp, shortp, longp;
+ for (i = 0; i < 7; i++) {
+ // make the regex if we don't have it already
+ mom = createUTC([2000, 1]).day(i);
+ minp = this.weekdaysMin(mom, '');
+ shortp = this.weekdaysShort(mom, '');
+ longp = this.weekdays(mom, '');
+ minPieces.push(minp);
+ shortPieces.push(shortp);
+ longPieces.push(longp);
+ mixedPieces.push(minp);
+ mixedPieces.push(shortp);
+ mixedPieces.push(longp);
+ }
+ // Sorting makes sure if one weekday (or abbr) is a prefix of another it
+ // will match the longer piece.
+ minPieces.sort(cmpLenRev);
+ shortPieces.sort(cmpLenRev);
+ longPieces.sort(cmpLenRev);
+ mixedPieces.sort(cmpLenRev);
+ for (i = 0; i < 7; i++) {
+ shortPieces[i] = regexEscape(shortPieces[i]);
+ longPieces[i] = regexEscape(longPieces[i]);
+ mixedPieces[i] = regexEscape(mixedPieces[i]);
+ }
+
+ this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i');
+ this._weekdaysShortRegex = this._weekdaysRegex;
+ this._weekdaysMinRegex = this._weekdaysRegex;
+
+ this._weekdaysStrictRegex = new RegExp('^(' + longPieces.join('|') + ')', 'i');
+ this._weekdaysShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')', 'i');
+ this._weekdaysMinStrictRegex = new RegExp('^(' + minPieces.join('|') + ')', 'i');
+}
+
+// FORMATTING
+
+function hFormat() {
+ return this.hours() % 12 || 12;
+}
+
+function kFormat() {
+ return this.hours() || 24;
+}
+
+addFormatToken('H', ['HH', 2], 0, 'hour');
+addFormatToken('h', ['hh', 2], 0, hFormat);
+addFormatToken('k', ['kk', 2], 0, kFormat);
+
+addFormatToken('hmm', 0, 0, function () {
+ return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2);
+});
+
+addFormatToken('hmmss', 0, 0, function () {
+ return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2) +
+ zeroFill(this.seconds(), 2);
+});
+
+addFormatToken('Hmm', 0, 0, function () {
+ return '' + this.hours() + zeroFill(this.minutes(), 2);
+});
+
+addFormatToken('Hmmss', 0, 0, function () {
+ return '' + this.hours() + zeroFill(this.minutes(), 2) +
+ zeroFill(this.seconds(), 2);
+});
+
+function meridiem (token, lowercase) {
+ addFormatToken(token, 0, 0, function () {
+ return this.localeData().meridiem(this.hours(), this.minutes(), lowercase);
+ });
+}
+
+meridiem('a', true);
+meridiem('A', false);
+
+// ALIASES
+
+addUnitAlias('hour', 'h');
+
+// PRIORITY
+addUnitPriority('hour', 13);
+
+// PARSING
+
+function matchMeridiem (isStrict, locale) {
+ return locale._meridiemParse;
+}
+
+addRegexToken('a', matchMeridiem);
+addRegexToken('A', matchMeridiem);
+addRegexToken('H', match1to2);
+addRegexToken('h', match1to2);
+addRegexToken('k', match1to2);
+addRegexToken('HH', match1to2, match2);
+addRegexToken('hh', match1to2, match2);
+addRegexToken('kk', match1to2, match2);
+
+addRegexToken('hmm', match3to4);
+addRegexToken('hmmss', match5to6);
+addRegexToken('Hmm', match3to4);
+addRegexToken('Hmmss', match5to6);
+
+addParseToken(['H', 'HH'], HOUR);
+addParseToken(['k', 'kk'], function (input, array, config) {
+ var kInput = toInt(input);
+ array[HOUR] = kInput === 24 ? 0 : kInput;
+});
+addParseToken(['a', 'A'], function (input, array, config) {
+ config._isPm = config._locale.isPM(input);
+ config._meridiem = input;
+});
+addParseToken(['h', 'hh'], function (input, array, config) {
+ array[HOUR] = toInt(input);
+ getParsingFlags(config).bigHour = true;
+});
+addParseToken('hmm', function (input, array, config) {
+ var pos = input.length - 2;
+ array[HOUR] = toInt(input.substr(0, pos));
+ array[MINUTE] = toInt(input.substr(pos));
+ getParsingFlags(config).bigHour = true;
+});
+addParseToken('hmmss', function (input, array, config) {
+ var pos1 = input.length - 4;
+ var pos2 = input.length - 2;
+ array[HOUR] = toInt(input.substr(0, pos1));
+ array[MINUTE] = toInt(input.substr(pos1, 2));
+ array[SECOND] = toInt(input.substr(pos2));
+ getParsingFlags(config).bigHour = true;
+});
+addParseToken('Hmm', function (input, array, config) {
+ var pos = input.length - 2;
+ array[HOUR] = toInt(input.substr(0, pos));
+ array[MINUTE] = toInt(input.substr(pos));
+});
+addParseToken('Hmmss', function (input, array, config) {
+ var pos1 = input.length - 4;
+ var pos2 = input.length - 2;
+ array[HOUR] = toInt(input.substr(0, pos1));
+ array[MINUTE] = toInt(input.substr(pos1, 2));
+ array[SECOND] = toInt(input.substr(pos2));
+});
+
+// LOCALES
+
+function localeIsPM (input) {
+ // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
+ // Using charAt should be more compatible.
+ return ((input + '').toLowerCase().charAt(0) === 'p');
+}
+
+var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i;
+function localeMeridiem (hours, minutes, isLower) {
+ if (hours > 11) {
+ return isLower ? 'pm' : 'PM';
+ } else {
+ return isLower ? 'am' : 'AM';
+ }
+}
+
+
+// MOMENTS
+
+// Setting the hour should keep the time, because the user explicitly
+// specified which hour he wants. So trying to maintain the same hour (in
+// a new timezone) makes sense. Adding/subtracting hours does not follow
+// this rule.
+var getSetHour = makeGetSet('Hours', true);
+
+// months
+// week
+// weekdays
+// meridiem
+var baseConfig = {
+ calendar: defaultCalendar,
+ longDateFormat: defaultLongDateFormat,
+ invalidDate: defaultInvalidDate,
+ ordinal: defaultOrdinal,
+ dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse,
+ relativeTime: defaultRelativeTime,
+
+ months: defaultLocaleMonths,
+ monthsShort: defaultLocaleMonthsShort,
+
+ week: defaultLocaleWeek,
+
+ weekdays: defaultLocaleWeekdays,
+ weekdaysMin: defaultLocaleWeekdaysMin,
+ weekdaysShort: defaultLocaleWeekdaysShort,
+
+ meridiemParse: defaultLocaleMeridiemParse
+};
+
+// internal storage for locale config files
+var locales = {};
+var localeFamilies = {};
+var globalLocale;
+
+function normalizeLocale(key) {
+ return key ? key.toLowerCase().replace('_', '-') : key;
+}
+
+// pick the locale from the array
+// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
+// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
+function chooseLocale(names) {
+ var i = 0, j, next, locale, split;
+
+ while (i < names.length) {
+ split = normalizeLocale(names[i]).split('-');
+ j = split.length;
+ next = normalizeLocale(names[i + 1]);
+ next = next ? next.split('-') : null;
+ while (j > 0) {
+ locale = loadLocale(split.slice(0, j).join('-'));
+ if (locale) {
+ return locale;
+ }
+ if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
+ //the next array item is better than a shallower substring of this one
+ break;
+ }
+ j--;
+ }
+ i++;
+ }
+ return null;
+}
+
+function loadLocale(name) {
+ var oldLocale = null;
+ // TODO: Find a better way to register and load all the locales in Node
+ if (!locales[name] && (typeof module !== 'undefined') &&
+ module && module.exports) {
+ try {
+ oldLocale = globalLocale._abbr;
+ var aliasedRequire = require;
+ aliasedRequire('./locale/' + name);
+ getSetGlobalLocale(oldLocale);
+ } catch (e) {}
+ }
+ return locales[name];
+}
+
+// This function will load locale and then set the global locale. If
+// no arguments are passed in, it will simply return the current global
+// locale key.
+function getSetGlobalLocale (key, values) {
+ var data;
+ if (key) {
+ if (isUndefined(values)) {
+ data = getLocale(key);
+ }
+ else {
+ data = defineLocale(key, values);
+ }
+
+ if (data) {
+ // moment.duration._locale = moment._locale = data;
+ globalLocale = data;
+ }
+ }
+
+ return globalLocale._abbr;
+}
+
+function defineLocale (name, config) {
+ if (config !== null) {
+ var parentConfig = baseConfig;
+ config.abbr = name;
+ if (locales[name] != null) {
+ deprecateSimple('defineLocaleOverride',
+ 'use moment.updateLocale(localeName, config) to change ' +
+ 'an existing locale. moment.defineLocale(localeName, ' +
+ 'config) should only be used for creating a new locale ' +
+ 'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.');
+ parentConfig = locales[name]._config;
+ } else if (config.parentLocale != null) {
+ if (locales[config.parentLocale] != null) {
+ parentConfig = locales[config.parentLocale]._config;
+ } else {
+ if (!localeFamilies[config.parentLocale]) {
+ localeFamilies[config.parentLocale] = [];
+ }
+ localeFamilies[config.parentLocale].push({
+ name: name,
+ config: config
+ });
+ return null;
+ }
+ }
+ locales[name] = new Locale(mergeConfigs(parentConfig, config));
+
+ if (localeFamilies[name]) {
+ localeFamilies[name].forEach(function (x) {
+ defineLocale(x.name, x.config);
+ });
+ }
+
+ // backwards compat for now: also set the locale
+ // make sure we set the locale AFTER all child locales have been
+ // created, so we won't end up with the child locale set.
+ getSetGlobalLocale(name);
+
+
+ return locales[name];
+ } else {
+ // useful for testing
+ delete locales[name];
+ return null;
+ }
+}
+
+function updateLocale(name, config) {
+ if (config != null) {
+ var locale, tmpLocale, parentConfig = baseConfig;
+ // MERGE
+ tmpLocale = loadLocale(name);
+ if (tmpLocale != null) {
+ parentConfig = tmpLocale._config;
+ }
+ config = mergeConfigs(parentConfig, config);
+ locale = new Locale(config);
+ locale.parentLocale = locales[name];
+ locales[name] = locale;
+
+ // backwards compat for now: also set the locale
+ getSetGlobalLocale(name);
+ } else {
+ // pass null for config to unupdate, useful for tests
+ if (locales[name] != null) {
+ if (locales[name].parentLocale != null) {
+ locales[name] = locales[name].parentLocale;
+ } else if (locales[name] != null) {
+ delete locales[name];
+ }
+ }
+ }
+ return locales[name];
+}
+
+// returns locale data
+function getLocale (key) {
+ var locale;
+
+ if (key && key._locale && key._locale._abbr) {
+ key = key._locale._abbr;
+ }
+
+ if (!key) {
+ return globalLocale;
+ }
+
+ if (!isArray(key)) {
+ //short-circuit everything else
+ locale = loadLocale(key);
+ if (locale) {
+ return locale;
+ }
+ key = [key];
+ }
+
+ return chooseLocale(key);
+}
+
+function listLocales() {
+ return keys(locales);
+}
+
+function checkOverflow (m) {
+ var overflow;
+ var a = m._a;
+
+ if (a && getParsingFlags(m).overflow === -2) {
+ overflow =
+ a[MONTH] < 0 || a[MONTH] > 11 ? MONTH :
+ a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE :
+ a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR :
+ a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE :
+ a[SECOND] < 0 || a[SECOND] > 59 ? SECOND :
+ a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND :
+ -1;
+
+ if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
+ overflow = DATE;
+ }
+ if (getParsingFlags(m)._overflowWeeks && overflow === -1) {
+ overflow = WEEK;
+ }
+ if (getParsingFlags(m)._overflowWeekday && overflow === -1) {
+ overflow = WEEKDAY;
+ }
+
+ getParsingFlags(m).overflow = overflow;
+ }
+
+ return m;
+}
+
+// Pick the first defined of two or three arguments.
+function defaults(a, b, c) {
+ if (a != null) {
+ return a;
+ }
+ if (b != null) {
+ return b;
+ }
+ return c;
+}
+
+function currentDateArray(config) {
+ // hooks is actually the exported moment object
+ var nowValue = new Date(hooks.now());
+ if (config._useUTC) {
+ return [nowValue.getUTCFullYear(), nowValue.getUTCMonth(), nowValue.getUTCDate()];
+ }
+ return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()];
+}
+
+// convert an array to a date.
+// the array should mirror the parameters below
+// note: all values past the year are optional and will default to the lowest possible value.
+// [year, month, day , hour, minute, second, millisecond]
+function configFromArray (config) {
+ var i, date, input = [], currentDate, yearToUse;
+
+ if (config._d) {
+ return;
+ }
+
+ currentDate = currentDateArray(config);
+
+ //compute day of the year from weeks and weekdays
+ if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
+ dayOfYearFromWeekInfo(config);
+ }
+
+ //if the day of the year is set, figure out what it is
+ if (config._dayOfYear != null) {
+ yearToUse = defaults(config._a[YEAR], currentDate[YEAR]);
+
+ if (config._dayOfYear > daysInYear(yearToUse) || config._dayOfYear === 0) {
+ getParsingFlags(config)._overflowDayOfYear = true;
+ }
+
+ date = createUTCDate(yearToUse, 0, config._dayOfYear);
+ config._a[MONTH] = date.getUTCMonth();
+ config._a[DATE] = date.getUTCDate();
+ }
+
+ // Default to current date.
+ // * if no year, month, day of month are given, default to today
+ // * if day of month is given, default month and year
+ // * if month is given, default only year
+ // * if year is given, don't default anything
+ for (i = 0; i < 3 && config._a[i] == null; ++i) {
+ config._a[i] = input[i] = currentDate[i];
+ }
+
+ // Zero out whatever was not defaulted, including time
+ for (; i < 7; i++) {
+ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
+ }
+
+ // Check for 24:00:00.000
+ if (config._a[HOUR] === 24 &&
+ config._a[MINUTE] === 0 &&
+ config._a[SECOND] === 0 &&
+ config._a[MILLISECOND] === 0) {
+ config._nextDay = true;
+ config._a[HOUR] = 0;
+ }
+
+ config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input);
+ // Apply timezone offset from input. The actual utcOffset can be changed
+ // with parseZone.
+ if (config._tzm != null) {
+ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+ }
+
+ if (config._nextDay) {
+ config._a[HOUR] = 24;
+ }
+
+ // check for mismatching day of week
+ if (config._w && typeof config._w.d !== 'undefined' && config._w.d !== config._d.getDay()) {
+ getParsingFlags(config).weekdayMismatch = true;
+ }
+}
+
+function dayOfYearFromWeekInfo(config) {
+ var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow;
+
+ w = config._w;
+ if (w.GG != null || w.W != null || w.E != null) {
+ dow = 1;
+ doy = 4;
+
+ // TODO: We need to take the current isoWeekYear, but that depends on
+ // how we interpret now (local, utc, fixed offset). So create
+ // a now version of current config (take local/utc/offset flags, and
+ // create now).
+ weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(createLocal(), 1, 4).year);
+ week = defaults(w.W, 1);
+ weekday = defaults(w.E, 1);
+ if (weekday < 1 || weekday > 7) {
+ weekdayOverflow = true;
+ }
+ } else {
+ dow = config._locale._week.dow;
+ doy = config._locale._week.doy;
+
+ var curWeek = weekOfYear(createLocal(), dow, doy);
+
+ weekYear = defaults(w.gg, config._a[YEAR], curWeek.year);
+
+ // Default to current week.
+ week = defaults(w.w, curWeek.week);
+
+ if (w.d != null) {
+ // weekday -- low day numbers are considered next week
+ weekday = w.d;
+ if (weekday < 0 || weekday > 6) {
+ weekdayOverflow = true;
+ }
+ } else if (w.e != null) {
+ // local weekday -- counting starts from beginning of week
+ weekday = w.e + dow;
+ if (w.e < 0 || w.e > 6) {
+ weekdayOverflow = true;
+ }
+ } else {
+ // default to beginning of week
+ weekday = dow;
+ }
+ }
+ if (week < 1 || week > weeksInYear(weekYear, dow, doy)) {
+ getParsingFlags(config)._overflowWeeks = true;
+ } else if (weekdayOverflow != null) {
+ getParsingFlags(config)._overflowWeekday = true;
+ } else {
+ temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy);
+ config._a[YEAR] = temp.year;
+ config._dayOfYear = temp.dayOfYear;
+ }
+}
+
+// iso 8601 regex
+// 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00)
+var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/;
+var basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/;
+
+var tzRegex = /Z|[+-]\d\d(?::?\d\d)?/;
+
+var isoDates = [
+ ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/],
+ ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/],
+ ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/],
+ ['GGGG-[W]WW', /\d{4}-W\d\d/, false],
+ ['YYYY-DDD', /\d{4}-\d{3}/],
+ ['YYYY-MM', /\d{4}-\d\d/, false],
+ ['YYYYYYMMDD', /[+-]\d{10}/],
+ ['YYYYMMDD', /\d{8}/],
+ // YYYYMM is NOT allowed by the standard
+ ['GGGG[W]WWE', /\d{4}W\d{3}/],
+ ['GGGG[W]WW', /\d{4}W\d{2}/, false],
+ ['YYYYDDD', /\d{7}/]
+];
+
+// iso time formats and regexes
+var isoTimes = [
+ ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/],
+ ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/],
+ ['HH:mm:ss', /\d\d:\d\d:\d\d/],
+ ['HH:mm', /\d\d:\d\d/],
+ ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/],
+ ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/],
+ ['HHmmss', /\d\d\d\d\d\d/],
+ ['HHmm', /\d\d\d\d/],
+ ['HH', /\d\d/]
+];
+
+var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i;
+
+// date from iso format
+function configFromISO(config) {
+ var i, l,
+ string = config._i,
+ match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string),
+ allowTime, dateFormat, timeFormat, tzFormat;
+
+ if (match) {
+ getParsingFlags(config).iso = true;
+
+ for (i = 0, l = isoDates.length; i < l; i++) {
+ if (isoDates[i][1].exec(match[1])) {
+ dateFormat = isoDates[i][0];
+ allowTime = isoDates[i][2] !== false;
+ break;
+ }
+ }
+ if (dateFormat == null) {
+ config._isValid = false;
+ return;
+ }
+ if (match[3]) {
+ for (i = 0, l = isoTimes.length; i < l; i++) {
+ if (isoTimes[i][1].exec(match[3])) {
+ // match[2] should be 'T' or space
+ timeFormat = (match[2] || ' ') + isoTimes[i][0];
+ break;
+ }
+ }
+ if (timeFormat == null) {
+ config._isValid = false;
+ return;
+ }
+ }
+ if (!allowTime && timeFormat != null) {
+ config._isValid = false;
+ return;
+ }
+ if (match[4]) {
+ if (tzRegex.exec(match[4])) {
+ tzFormat = 'Z';
+ } else {
+ config._isValid = false;
+ return;
+ }
+ }
+ config._f = dateFormat + (timeFormat || '') + (tzFormat || '');
+ configFromStringAndFormat(config);
+ } else {
+ config._isValid = false;
+ }
+}
+
+// RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3
+var rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/;
+
+function extractFromRFC2822Strings(yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr) {
+ var result = [
+ untruncateYear(yearStr),
+ defaultLocaleMonthsShort.indexOf(monthStr),
+ parseInt(dayStr, 10),
+ parseInt(hourStr, 10),
+ parseInt(minuteStr, 10)
+ ];
+
+ if (secondStr) {
+ result.push(parseInt(secondStr, 10));
+ }
+
+ return result;
+}
+
+function untruncateYear(yearStr) {
+ var year = parseInt(yearStr, 10);
+ if (year <= 49) {
+ return 2000 + year;
+ } else if (year <= 999) {
+ return 1900 + year;
+ }
+ return year;
+}
+
+function preprocessRFC2822(s) {
+ // Remove comments and folding whitespace and replace multiple-spaces with a single space
+ return s.replace(/\([^)]*\)|[\n\t]/g, ' ').replace(/(\s\s+)/g, ' ').trim();
+}
+
+function checkWeekday(weekdayStr, parsedInput, config) {
+ if (weekdayStr) {
+ // TODO: Replace the vanilla JS Date object with an indepentent day-of-week check.
+ var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr),
+ weekdayActual = new Date(parsedInput[0], parsedInput[1], parsedInput[2]).getDay();
+ if (weekdayProvided !== weekdayActual) {
+ getParsingFlags(config).weekdayMismatch = true;
+ config._isValid = false;
+ return false;
+ }
+ }
+ return true;
+}
+
+var obsOffsets = {
+ UT: 0,
+ GMT: 0,
+ EDT: -4 * 60,
+ EST: -5 * 60,
+ CDT: -5 * 60,
+ CST: -6 * 60,
+ MDT: -6 * 60,
+ MST: -7 * 60,
+ PDT: -7 * 60,
+ PST: -8 * 60
+};
+
+function calculateOffset(obsOffset, militaryOffset, numOffset) {
+ if (obsOffset) {
+ return obsOffsets[obsOffset];
+ } else if (militaryOffset) {
+ // the only allowed military tz is Z
+ return 0;
+ } else {
+ var hm = parseInt(numOffset, 10);
+ var m = hm % 100, h = (hm - m) / 100;
+ return h * 60 + m;
+ }
+}
+
+// date and time from ref 2822 format
+function configFromRFC2822(config) {
+ var match = rfc2822.exec(preprocessRFC2822(config._i));
+ if (match) {
+ var parsedArray = extractFromRFC2822Strings(match[4], match[3], match[2], match[5], match[6], match[7]);
+ if (!checkWeekday(match[1], parsedArray, config)) {
+ return;
+ }
+
+ config._a = parsedArray;
+ config._tzm = calculateOffset(match[8], match[9], match[10]);
+
+ config._d = createUTCDate.apply(null, config._a);
+ config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm);
+
+ getParsingFlags(config).rfc2822 = true;
+ } else {
+ config._isValid = false;
+ }
+}
+
+// date from iso format or fallback
+function configFromString(config) {
+ var matched = aspNetJsonRegex.exec(config._i);
+
+ if (matched !== null) {
+ config._d = new Date(+matched[1]);
+ return;
+ }
+
+ configFromISO(config);
+ if (config._isValid === false) {
+ delete config._isValid;
+ } else {
+ return;
+ }
+
+ configFromRFC2822(config);
+ if (config._isValid === false) {
+ delete config._isValid;
+ } else {
+ return;
+ }
+
+ // Final attempt, use Input Fallback
+ hooks.createFromInputFallback(config);
+}
+
+hooks.createFromInputFallback = deprecate(
+ 'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' +
+ 'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' +
+ 'discouraged and will be removed in an upcoming major release. Please refer to ' +
+ 'http://momentjs.com/guides/#/warnings/js-date/ for more info.',
+ function (config) {
+ config._d = new Date(config._i + (config._useUTC ? ' UTC' : ''));
+ }
+);
+
+// constant that refers to the ISO standard
+hooks.ISO_8601 = function () {};
+
+// constant that refers to the RFC 2822 form
+hooks.RFC_2822 = function () {};
+
+// date from string and format string
+function configFromStringAndFormat(config) {
+ // TODO: Move this to another part of the creation flow to prevent circular deps
+ if (config._f === hooks.ISO_8601) {
+ configFromISO(config);
+ return;
+ }
+ if (config._f === hooks.RFC_2822) {
+ configFromRFC2822(config);
+ return;
+ }
+ config._a = [];
+ getParsingFlags(config).empty = true;
+
+ // This array is used to make a Date, either with `new Date` or `Date.UTC`
+ var string = '' + config._i,
+ i, parsedInput, tokens, token, skipped,
+ stringLength = string.length,
+ totalParsedInputLength = 0;
+
+ tokens = expandFormat(config._f, config._locale).match(formattingTokens) || [];
+
+ for (i = 0; i < tokens.length; i++) {
+ token = tokens[i];
+ parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
+ // console.log('token', token, 'parsedInput', parsedInput,
+ // 'regex', getParseRegexForToken(token, config));
+ if (parsedInput) {
+ skipped = string.substr(0, string.indexOf(parsedInput));
+ if (skipped.length > 0) {
+ getParsingFlags(config).unusedInput.push(skipped);
+ }
+ string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
+ totalParsedInputLength += parsedInput.length;
+ }
+ // don't parse if it's not a known token
+ if (formatTokenFunctions[token]) {
+ if (parsedInput) {
+ getParsingFlags(config).empty = false;
+ }
+ else {
+ getParsingFlags(config).unusedTokens.push(token);
+ }
+ addTimeToArrayFromToken(token, parsedInput, config);
+ }
+ else if (config._strict && !parsedInput) {
+ getParsingFlags(config).unusedTokens.push(token);
+ }
+ }
+
+ // add remaining unparsed input length to the string
+ getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength;
+ if (string.length > 0) {
+ getParsingFlags(config).unusedInput.push(string);
+ }
+
+ // clear _12h flag if hour is <= 12
+ if (config._a[HOUR] <= 12 &&
+ getParsingFlags(config).bigHour === true &&
+ config._a[HOUR] > 0) {
+ getParsingFlags(config).bigHour = undefined;
+ }
+
+ getParsingFlags(config).parsedDateParts = config._a.slice(0);
+ getParsingFlags(config).meridiem = config._meridiem;
+ // handle meridiem
+ config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem);
+
+ configFromArray(config);
+ checkOverflow(config);
+}
+
+
+function meridiemFixWrap (locale, hour, meridiem) {
+ var isPm;
+
+ if (meridiem == null) {
+ // nothing to do
+ return hour;
+ }
+ if (locale.meridiemHour != null) {
+ return locale.meridiemHour(hour, meridiem);
+ } else if (locale.isPM != null) {
+ // Fallback
+ isPm = locale.isPM(meridiem);
+ if (isPm && hour < 12) {
+ hour += 12;
+ }
+ if (!isPm && hour === 12) {
+ hour = 0;
+ }
+ return hour;
+ } else {
+ // this is not supposed to happen
+ return hour;
+ }
+}
+
+// date from string and array of format strings
+function configFromStringAndArray(config) {
+ var tempConfig,
+ bestMoment,
+
+ scoreToBeat,
+ i,
+ currentScore;
+
+ if (config._f.length === 0) {
+ getParsingFlags(config).invalidFormat = true;
+ config._d = new Date(NaN);
+ return;
+ }
+
+ for (i = 0; i < config._f.length; i++) {
+ currentScore = 0;
+ tempConfig = copyConfig({}, config);
+ if (config._useUTC != null) {
+ tempConfig._useUTC = config._useUTC;
+ }
+ tempConfig._f = config._f[i];
+ configFromStringAndFormat(tempConfig);
+
+ if (!isValid(tempConfig)) {
+ continue;
+ }
+
+ // if there is any input that was not parsed add a penalty for that format
+ currentScore += getParsingFlags(tempConfig).charsLeftOver;
+
+ //or tokens
+ currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10;
+
+ getParsingFlags(tempConfig).score = currentScore;
+
+ if (scoreToBeat == null || currentScore < scoreToBeat) {
+ scoreToBeat = currentScore;
+ bestMoment = tempConfig;
+ }
+ }
+
+ extend(config, bestMoment || tempConfig);
+}
+
+function configFromObject(config) {
+ if (config._d) {
+ return;
+ }
+
+ var i = normalizeObjectUnits(config._i);
+ config._a = map([i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond], function (obj) {
+ return obj && parseInt(obj, 10);
+ });
+
+ configFromArray(config);
+}
+
+function createFromConfig (config) {
+ var res = new Moment(checkOverflow(prepareConfig(config)));
+ if (res._nextDay) {
+ // Adding is smart enough around DST
+ res.add(1, 'd');
+ res._nextDay = undefined;
+ }
+
+ return res;
+}
+
+function prepareConfig (config) {
+ var input = config._i,
+ format = config._f;
+
+ config._locale = config._locale || getLocale(config._l);
+
+ if (input === null || (format === undefined && input === '')) {
+ return createInvalid({nullInput: true});
+ }
+
+ if (typeof input === 'string') {
+ config._i = input = config._locale.preparse(input);
+ }
+
+ if (isMoment(input)) {
+ return new Moment(checkOverflow(input));
+ } else if (isDate(input)) {
+ config._d = input;
+ } else if (isArray(format)) {
+ configFromStringAndArray(config);
+ } else if (format) {
+ configFromStringAndFormat(config);
+ } else {
+ configFromInput(config);
+ }
+
+ if (!isValid(config)) {
+ config._d = null;
+ }
+
+ return config;
+}
+
+function configFromInput(config) {
+ var input = config._i;
+ if (isUndefined(input)) {
+ config._d = new Date(hooks.now());
+ } else if (isDate(input)) {
+ config._d = new Date(input.valueOf());
+ } else if (typeof input === 'string') {
+ configFromString(config);
+ } else if (isArray(input)) {
+ config._a = map(input.slice(0), function (obj) {
+ return parseInt(obj, 10);
+ });
+ configFromArray(config);
+ } else if (isObject(input)) {
+ configFromObject(config);
+ } else if (isNumber(input)) {
+ // from milliseconds
+ config._d = new Date(input);
+ } else {
+ hooks.createFromInputFallback(config);
+ }
+}
+
+function createLocalOrUTC (input, format, locale, strict, isUTC) {
+ var c = {};
+
+ if (locale === true || locale === false) {
+ strict = locale;
+ locale = undefined;
+ }
+
+ if ((isObject(input) && isObjectEmpty(input)) ||
+ (isArray(input) && input.length === 0)) {
+ input = undefined;
+ }
+ // object construction must be done this way.
+ // https://github.com/moment/moment/issues/1423
+ c._isAMomentObject = true;
+ c._useUTC = c._isUTC = isUTC;
+ c._l = locale;
+ c._i = input;
+ c._f = format;
+ c._strict = strict;
+
+ return createFromConfig(c);
+}
+
+function createLocal (input, format, locale, strict) {
+ return createLocalOrUTC(input, format, locale, strict, false);
+}
+
+var prototypeMin = deprecate(
+ 'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/',
+ function () {
+ var other = createLocal.apply(null, arguments);
+ if (this.isValid() && other.isValid()) {
+ return other < this ? this : other;
+ } else {
+ return createInvalid();
+ }
+ }
+);
+
+var prototypeMax = deprecate(
+ 'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/',
+ function () {
+ var other = createLocal.apply(null, arguments);
+ if (this.isValid() && other.isValid()) {
+ return other > this ? this : other;
+ } else {
+ return createInvalid();
+ }
+ }
+);
+
+// Pick a moment m from moments so that m[fn](other) is true for all
+// other. This relies on the function fn to be transitive.
+//
+// moments should either be an array of moment objects or an array, whose
+// first element is an array of moment objects.
+function pickBy(fn, moments) {
+ var res, i;
+ if (moments.length === 1 && isArray(moments[0])) {
+ moments = moments[0];
+ }
+ if (!moments.length) {
+ return createLocal();
+ }
+ res = moments[0];
+ for (i = 1; i < moments.length; ++i) {
+ if (!moments[i].isValid() || moments[i][fn](res)) {
+ res = moments[i];
+ }
+ }
+ return res;
+}
+
+// TODO: Use [].sort instead?
+function min () {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isBefore', args);
+}
+
+function max () {
+ var args = [].slice.call(arguments, 0);
+
+ return pickBy('isAfter', args);
+}
+
+var now = function () {
+ return Date.now ? Date.now() : +(new Date());
+};
+
+var ordering = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond'];
+
+function isDurationValid(m) {
+ for (var key in m) {
+ if (!(indexOf.call(ordering, key) !== -1 && (m[key] == null || !isNaN(m[key])))) {
+ return false;
+ }
+ }
+
+ var unitHasDecimal = false;
+ for (var i = 0; i < ordering.length; ++i) {
+ if (m[ordering[i]]) {
+ if (unitHasDecimal) {
+ return false; // only allow non-integers for smallest unit
+ }
+ if (parseFloat(m[ordering[i]]) !== toInt(m[ordering[i]])) {
+ unitHasDecimal = true;
+ }
+ }
+ }
+
+ return true;
+}
+
+function isValid$1() {
+ return this._isValid;
+}
+
+function createInvalid$1() {
+ return createDuration(NaN);
+}
+
+function Duration (duration) {
+ var normalizedInput = normalizeObjectUnits(duration),
+ years = normalizedInput.year || 0,
+ quarters = normalizedInput.quarter || 0,
+ months = normalizedInput.month || 0,
+ weeks = normalizedInput.week || 0,
+ days = normalizedInput.day || 0,
+ hours = normalizedInput.hour || 0,
+ minutes = normalizedInput.minute || 0,
+ seconds = normalizedInput.second || 0,
+ milliseconds = normalizedInput.millisecond || 0;
+
+ this._isValid = isDurationValid(normalizedInput);
+
+ // representation for dateAddRemove
+ this._milliseconds = +milliseconds +
+ seconds * 1e3 + // 1000
+ minutes * 6e4 + // 1000 * 60
+ hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978
+ // Because of dateAddRemove treats 24 hours as different from a
+ // day when working around DST, we need to store them separately
+ this._days = +days +
+ weeks * 7;
+ // It is impossible to translate months into days without knowing
+ // which months you are are talking about, so we have to store
+ // it separately.
+ this._months = +months +
+ quarters * 3 +
+ years * 12;
+
+ this._data = {};
+
+ this._locale = getLocale();
+
+ this._bubble();
+}
+
+function isDuration (obj) {
+ return obj instanceof Duration;
+}
+
+function absRound (number) {
+ if (number < 0) {
+ return Math.round(-1 * number) * -1;
+ } else {
+ return Math.round(number);
+ }
+}
+
+// FORMATTING
+
+function offset (token, separator) {
+ addFormatToken(token, 0, 0, function () {
+ var offset = this.utcOffset();
+ var sign = '+';
+ if (offset < 0) {
+ offset = -offset;
+ sign = '-';
+ }
+ return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2);
+ });
+}
+
+offset('Z', ':');
+offset('ZZ', '');
+
+// PARSING
+
+addRegexToken('Z', matchShortOffset);
+addRegexToken('ZZ', matchShortOffset);
+addParseToken(['Z', 'ZZ'], function (input, array, config) {
+ config._useUTC = true;
+ config._tzm = offsetFromString(matchShortOffset, input);
+});
+
+// HELPERS
+
+// timezone chunker
+// '+10:00' > ['10', '00']
+// '-1530' > ['-15', '30']
+var chunkOffset = /([\+\-]|\d\d)/gi;
+
+function offsetFromString(matcher, string) {
+ var matches = (string || '').match(matcher);
+
+ if (matches === null) {
+ return null;
+ }
+
+ var chunk = matches[matches.length - 1] || [];
+ var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0];
+ var minutes = +(parts[1] * 60) + toInt(parts[2]);
+
+ return minutes === 0 ?
+ 0 :
+ parts[0] === '+' ? minutes : -minutes;
+}
+
+// Return a moment from input, that is local/utc/zone equivalent to model.
+function cloneWithOffset(input, model) {
+ var res, diff;
+ if (model._isUTC) {
+ res = model.clone();
+ diff = (isMoment(input) || isDate(input) ? input.valueOf() : createLocal(input).valueOf()) - res.valueOf();
+ // Use low-level api, because this fn is low-level api.
+ res._d.setTime(res._d.valueOf() + diff);
+ hooks.updateOffset(res, false);
+ return res;
+ } else {
+ return createLocal(input).local();
+ }
+}
+
+function getDateOffset (m) {
+ // On Firefox.24 Date#getTimezoneOffset returns a floating point.
+ // https://github.com/moment/moment/pull/1871
+ return -Math.round(m._d.getTimezoneOffset() / 15) * 15;
+}
+
+// HOOKS
+
+// This function will be called whenever a moment is mutated.
+// It is intended to keep the offset in sync with the timezone.
+hooks.updateOffset = function () {};
+
+// MOMENTS
+
+// keepLocalTime = true means only change the timezone, without
+// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]-->
+// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset
+// +0200, so we adjust the time as needed, to be valid.
+//
+// Keeping the time actually adds/subtracts (one hour)
+// from the actual represented time. That is why we call updateOffset
+// a second time. In case it wants us to change the offset again
+// _changeInProgress == true case, then we have to adjust, because
+// there is no such time in the given timezone.
+function getSetOffset (input, keepLocalTime, keepMinutes) {
+ var offset = this._offset || 0,
+ localAdjust;
+ if (!this.isValid()) {
+ return input != null ? this : NaN;
+ }
+ if (input != null) {
+ if (typeof input === 'string') {
+ input = offsetFromString(matchShortOffset, input);
+ if (input === null) {
+ return this;
+ }
+ } else if (Math.abs(input) < 16 && !keepMinutes) {
+ input = input * 60;
+ }
+ if (!this._isUTC && keepLocalTime) {
+ localAdjust = getDateOffset(this);
+ }
+ this._offset = input;
+ this._isUTC = true;
+ if (localAdjust != null) {
+ this.add(localAdjust, 'm');
+ }
+ if (offset !== input) {
+ if (!keepLocalTime || this._changeInProgress) {
+ addSubtract(this, createDuration(input - offset, 'm'), 1, false);
+ } else if (!this._changeInProgress) {
+ this._changeInProgress = true;
+ hooks.updateOffset(this, true);
+ this._changeInProgress = null;
+ }
+ }
+ return this;
+ } else {
+ return this._isUTC ? offset : getDateOffset(this);
+ }
+}
+
+function getSetZone (input, keepLocalTime) {
+ if (input != null) {
+ if (typeof input !== 'string') {
+ input = -input;
+ }
+
+ this.utcOffset(input, keepLocalTime);
+
+ return this;
+ } else {
+ return -this.utcOffset();
+ }
+}
+
+function setOffsetToUTC (keepLocalTime) {
+ return this.utcOffset(0, keepLocalTime);
+}
+
+function setOffsetToLocal (keepLocalTime) {
+ if (this._isUTC) {
+ this.utcOffset(0, keepLocalTime);
+ this._isUTC = false;
+
+ if (keepLocalTime) {
+ this.subtract(getDateOffset(this), 'm');
+ }
+ }
+ return this;
+}
+
+function setOffsetToParsedOffset () {
+ if (this._tzm != null) {
+ this.utcOffset(this._tzm, false, true);
+ } else if (typeof this._i === 'string') {
+ var tZone = offsetFromString(matchOffset, this._i);
+ if (tZone != null) {
+ this.utcOffset(tZone);
+ }
+ else {
+ this.utcOffset(0, true);
+ }
+ }
+ return this;
+}
+
+function hasAlignedHourOffset (input) {
+ if (!this.isValid()) {
+ return false;
+ }
+ input = input ? createLocal(input).utcOffset() : 0;
+
+ return (this.utcOffset() - input) % 60 === 0;
+}
+
+function isDaylightSavingTime () {
+ return (
+ this.utcOffset() > this.clone().month(0).utcOffset() ||
+ this.utcOffset() > this.clone().month(5).utcOffset()
+ );
+}
+
+function isDaylightSavingTimeShifted () {
+ if (!isUndefined(this._isDSTShifted)) {
+ return this._isDSTShifted;
+ }
+
+ var c = {};
+
+ copyConfig(c, this);
+ c = prepareConfig(c);
+
+ if (c._a) {
+ var other = c._isUTC ? createUTC(c._a) : createLocal(c._a);
+ this._isDSTShifted = this.isValid() &&
+ compareArrays(c._a, other.toArray()) > 0;
+ } else {
+ this._isDSTShifted = false;
+ }
+
+ return this._isDSTShifted;
+}
+
+function isLocal () {
+ return this.isValid() ? !this._isUTC : false;
+}
+
+function isUtcOffset () {
+ return this.isValid() ? this._isUTC : false;
+}
+
+function isUtc () {
+ return this.isValid() ? this._isUTC && this._offset === 0 : false;
+}
+
+// ASP.NET json date format regex
+var aspNetRegex = /^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/;
+
+// from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
+// somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
+// and further modified to allow for strings containing both week and day
+var isoRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;
+
+function createDuration (input, key) {
+ var duration = input,
+ // matching against regexp is expensive, do it on demand
+ match = null,
+ sign,
+ ret,
+ diffRes;
+
+ if (isDuration(input)) {
+ duration = {
+ ms : input._milliseconds,
+ d : input._days,
+ M : input._months
+ };
+ } else if (isNumber(input)) {
+ duration = {};
+ if (key) {
+ duration[key] = input;
+ } else {
+ duration.milliseconds = input;
+ }
+ } else if (!!(match = aspNetRegex.exec(input))) {
+ sign = (match[1] === '-') ? -1 : 1;
+ duration = {
+ y : 0,
+ d : toInt(match[DATE]) * sign,
+ h : toInt(match[HOUR]) * sign,
+ m : toInt(match[MINUTE]) * sign,
+ s : toInt(match[SECOND]) * sign,
+ ms : toInt(absRound(match[MILLISECOND] * 1000)) * sign // the millisecond decimal point is included in the match
+ };
+ } else if (!!(match = isoRegex.exec(input))) {
+ sign = (match[1] === '-') ? -1 : (match[1] === '+') ? 1 : 1;
+ duration = {
+ y : parseIso(match[2], sign),
+ M : parseIso(match[3], sign),
+ w : parseIso(match[4], sign),
+ d : parseIso(match[5], sign),
+ h : parseIso(match[6], sign),
+ m : parseIso(match[7], sign),
+ s : parseIso(match[8], sign)
+ };
+ } else if (duration == null) {// checks for null or undefined
+ duration = {};
+ } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) {
+ diffRes = momentsDifference(createLocal(duration.from), createLocal(duration.to));
+
+ duration = {};
+ duration.ms = diffRes.milliseconds;
+ duration.M = diffRes.months;
+ }
+
+ ret = new Duration(duration);
+
+ if (isDuration(input) && hasOwnProp(input, '_locale')) {
+ ret._locale = input._locale;
+ }
+
+ return ret;
+}
+
+createDuration.fn = Duration.prototype;
+createDuration.invalid = createInvalid$1;
+
+function parseIso (inp, sign) {
+ // We'd normally use ~~inp for this, but unfortunately it also
+ // converts floats to ints.
+ // inp may be undefined, so careful calling replace on it.
+ var res = inp && parseFloat(inp.replace(',', '.'));
+ // apply sign while we're at it
+ return (isNaN(res) ? 0 : res) * sign;
+}
+
+function positiveMomentsDifference(base, other) {
+ var res = {milliseconds: 0, months: 0};
+
+ res.months = other.month() - base.month() +
+ (other.year() - base.year()) * 12;
+ if (base.clone().add(res.months, 'M').isAfter(other)) {
+ --res.months;
+ }
+
+ res.milliseconds = +other - +(base.clone().add(res.months, 'M'));
+
+ return res;
+}
+
+function momentsDifference(base, other) {
+ var res;
+ if (!(base.isValid() && other.isValid())) {
+ return {milliseconds: 0, months: 0};
+ }
+
+ other = cloneWithOffset(other, base);
+ if (base.isBefore(other)) {
+ res = positiveMomentsDifference(base, other);
+ } else {
+ res = positiveMomentsDifference(other, base);
+ res.milliseconds = -res.milliseconds;
+ res.months = -res.months;
+ }
+
+ return res;
+}
+
+// TODO: remove 'name' arg after deprecation is removed
+function createAdder(direction, name) {
+ return function (val, period) {
+ var dur, tmp;
+ //invert the arguments, but complain about it
+ if (period !== null && !isNaN(+period)) {
+ deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period). ' +
+ 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.');
+ tmp = val; val = period; period = tmp;
+ }
+
+ val = typeof val === 'string' ? +val : val;
+ dur = createDuration(val, period);
+ addSubtract(this, dur, direction);
+ return this;
+ };
+}
+
+function addSubtract (mom, duration, isAdding, updateOffset) {
+ var milliseconds = duration._milliseconds,
+ days = absRound(duration._days),
+ months = absRound(duration._months);
+
+ if (!mom.isValid()) {
+ // No op
+ return;
+ }
+
+ updateOffset = updateOffset == null ? true : updateOffset;
+
+ if (months) {
+ setMonth(mom, get(mom, 'Month') + months * isAdding);
+ }
+ if (days) {
+ set$1(mom, 'Date', get(mom, 'Date') + days * isAdding);
+ }
+ if (milliseconds) {
+ mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding);
+ }
+ if (updateOffset) {
+ hooks.updateOffset(mom, days || months);
+ }
+}
+
+var add = createAdder(1, 'add');
+var subtract = createAdder(-1, 'subtract');
+
+function getCalendarFormat(myMoment, now) {
+ var diff = myMoment.diff(now, 'days', true);
+ return diff < -6 ? 'sameElse' :
+ diff < -1 ? 'lastWeek' :
+ diff < 0 ? 'lastDay' :
+ diff < 1 ? 'sameDay' :
+ diff < 2 ? 'nextDay' :
+ diff < 7 ? 'nextWeek' : 'sameElse';
+}
+
+function calendar$1 (time, formats) {
+ // We want to compare the start of today, vs this.
+ // Getting start-of-today depends on whether we're local/utc/offset or not.
+ var now = time || createLocal(),
+ sod = cloneWithOffset(now, this).startOf('day'),
+ format = hooks.calendarFormat(this, sod) || 'sameElse';
+
+ var output = formats && (isFunction(formats[format]) ? formats[format].call(this, now) : formats[format]);
+
+ return this.format(output || this.localeData().calendar(format, this, createLocal(now)));
+}
+
+function clone () {
+ return new Moment(this);
+}
+
+function isAfter (input, units) {
+ var localInput = isMoment(input) ? input : createLocal(input);
+ if (!(this.isValid() && localInput.isValid())) {
+ return false;
+ }
+ units = normalizeUnits(!isUndefined(units) ? units : 'millisecond');
+ if (units === 'millisecond') {
+ return this.valueOf() > localInput.valueOf();
+ } else {
+ return localInput.valueOf() < this.clone().startOf(units).valueOf();
+ }
+}
+
+function isBefore (input, units) {
+ var localInput = isMoment(input) ? input : createLocal(input);
+ if (!(this.isValid() && localInput.isValid())) {
+ return false;
+ }
+ units = normalizeUnits(!isUndefined(units) ? units : 'millisecond');
+ if (units === 'millisecond') {
+ return this.valueOf() < localInput.valueOf();
+ } else {
+ return this.clone().endOf(units).valueOf() < localInput.valueOf();
+ }
+}
+
+function isBetween (from, to, units, inclusivity) {
+ inclusivity = inclusivity || '()';
+ return (inclusivity[0] === '(' ? this.isAfter(from, units) : !this.isBefore(from, units)) &&
+ (inclusivity[1] === ')' ? this.isBefore(to, units) : !this.isAfter(to, units));
+}
+
+function isSame (input, units) {
+ var localInput = isMoment(input) ? input : createLocal(input),
+ inputMs;
+ if (!(this.isValid() && localInput.isValid())) {
+ return false;
+ }
+ units = normalizeUnits(units || 'millisecond');
+ if (units === 'millisecond') {
+ return this.valueOf() === localInput.valueOf();
+ } else {
+ inputMs = localInput.valueOf();
+ return this.clone().startOf(units).valueOf() <= inputMs && inputMs <= this.clone().endOf(units).valueOf();
+ }
+}
+
+function isSameOrAfter (input, units) {
+ return this.isSame(input, units) || this.isAfter(input,units);
+}
+
+function isSameOrBefore (input, units) {
+ return this.isSame(input, units) || this.isBefore(input,units);
+}
+
+function diff (input, units, asFloat) {
+ var that,
+ zoneDelta,
+ delta, output;
+
+ if (!this.isValid()) {
+ return NaN;
+ }
+
+ that = cloneWithOffset(input, this);
+
+ if (!that.isValid()) {
+ return NaN;
+ }
+
+ zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4;
+
+ units = normalizeUnits(units);
+
+ switch (units) {
+ case 'year': output = monthDiff(this, that) / 12; break;
+ case 'month': output = monthDiff(this, that); break;
+ case 'quarter': output = monthDiff(this, that) / 3; break;
+ case 'second': output = (this - that) / 1e3; break; // 1000
+ case 'minute': output = (this - that) / 6e4; break; // 1000 * 60
+ case 'hour': output = (this - that) / 36e5; break; // 1000 * 60 * 60
+ case 'day': output = (this - that - zoneDelta) / 864e5; break; // 1000 * 60 * 60 * 24, negate dst
+ case 'week': output = (this - that - zoneDelta) / 6048e5; break; // 1000 * 60 * 60 * 24 * 7, negate dst
+ default: output = this - that;
+ }
+
+ return asFloat ? output : absFloor(output);
+}
+
+function monthDiff (a, b) {
+ // difference in months
+ var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()),
+ // b is in (anchor - 1 month, anchor + 1 month)
+ anchor = a.clone().add(wholeMonthDiff, 'months'),
+ anchor2, adjust;
+
+ if (b - anchor < 0) {
+ anchor2 = a.clone().add(wholeMonthDiff - 1, 'months');
+ // linear across the month
+ adjust = (b - anchor) / (anchor - anchor2);
+ } else {
+ anchor2 = a.clone().add(wholeMonthDiff + 1, 'months');
+ // linear across the month
+ adjust = (b - anchor) / (anchor2 - anchor);
+ }
+
+ //check for negative zero, return zero if negative zero
+ return -(wholeMonthDiff + adjust) || 0;
+}
+
+hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ';
+hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]';
+
+function toString () {
+ return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ');
+}
+
+function toISOString() {
+ if (!this.isValid()) {
+ return null;
+ }
+ var m = this.clone().utc();
+ if (m.year() < 0 || m.year() > 9999) {
+ return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+ }
+ if (isFunction(Date.prototype.toISOString)) {
+ // native implementation is ~50x faster, use it when we can
+ return this.toDate().toISOString();
+ }
+ return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
+}
+
+/**
+ * Return a human readable representation of a moment that can
+ * also be evaluated to get a new moment which is the same
+ *
+ * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects
+ */
+function inspect () {
+ if (!this.isValid()) {
+ return 'moment.invalid(/* ' + this._i + ' */)';
+ }
+ var func = 'moment';
+ var zone = '';
+ if (!this.isLocal()) {
+ func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone';
+ zone = 'Z';
+ }
+ var prefix = '[' + func + '("]';
+ var year = (0 <= this.year() && this.year() <= 9999) ? 'YYYY' : 'YYYYYY';
+ var datetime = '-MM-DD[T]HH:mm:ss.SSS';
+ var suffix = zone + '[")]';
+
+ return this.format(prefix + year + datetime + suffix);
+}
+
+function format (inputString) {
+ if (!inputString) {
+ inputString = this.isUtc() ? hooks.defaultFormatUtc : hooks.defaultFormat;
+ }
+ var output = formatMoment(this, inputString);
+ return this.localeData().postformat(output);
+}
+
+function from (time, withoutSuffix) {
+ if (this.isValid() &&
+ ((isMoment(time) && time.isValid()) ||
+ createLocal(time).isValid())) {
+ return createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix);
+ } else {
+ return this.localeData().invalidDate();
+ }
+}
+
+function fromNow (withoutSuffix) {
+ return this.from(createLocal(), withoutSuffix);
+}
+
+function to (time, withoutSuffix) {
+ if (this.isValid() &&
+ ((isMoment(time) && time.isValid()) ||
+ createLocal(time).isValid())) {
+ return createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix);
+ } else {
+ return this.localeData().invalidDate();
+ }
+}
+
+function toNow (withoutSuffix) {
+ return this.to(createLocal(), withoutSuffix);
+}
+
+// If passed a locale key, it will set the locale for this
+// instance. Otherwise, it will return the locale configuration
+// variables for this instance.
+function locale (key) {
+ var newLocaleData;
+
+ if (key === undefined) {
+ return this._locale._abbr;
+ } else {
+ newLocaleData = getLocale(key);
+ if (newLocaleData != null) {
+ this._locale = newLocaleData;
+ }
+ return this;
+ }
+}
+
+var lang = deprecate(
+ 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.',
+ function (key) {
+ if (key === undefined) {
+ return this.localeData();
+ } else {
+ return this.locale(key);
+ }
+ }
+);
+
+function localeData () {
+ return this._locale;
+}
+
+function startOf (units) {
+ units = normalizeUnits(units);
+ // the following switch intentionally omits break keywords
+ // to utilize falling through the cases.
+ switch (units) {
+ case 'year':
+ this.month(0);
+ /* falls through */
+ case 'quarter':
+ case 'month':
+ this.date(1);
+ /* falls through */
+ case 'week':
+ case 'isoWeek':
+ case 'day':
+ case 'date':
+ this.hours(0);
+ /* falls through */
+ case 'hour':
+ this.minutes(0);
+ /* falls through */
+ case 'minute':
+ this.seconds(0);
+ /* falls through */
+ case 'second':
+ this.milliseconds(0);
+ }
+
+ // weeks are a special case
+ if (units === 'week') {
+ this.weekday(0);
+ }
+ if (units === 'isoWeek') {
+ this.isoWeekday(1);
+ }
+
+ // quarters are also special
+ if (units === 'quarter') {
+ this.month(Math.floor(this.month() / 3) * 3);
+ }
+
+ return this;
+}
+
+function endOf (units) {
+ units = normalizeUnits(units);
+ if (units === undefined || units === 'millisecond') {
+ return this;
+ }
+
+ // 'date' is an alias for 'day', so it should be considered as such.
+ if (units === 'date') {
+ units = 'day';
+ }
+
+ return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms');
+}
+
+function valueOf () {
+ return this._d.valueOf() - ((this._offset || 0) * 60000);
+}
+
+function unix () {
+ return Math.floor(this.valueOf() / 1000);
+}
+
+function toDate () {
+ return new Date(this.valueOf());
+}
+
+function toArray () {
+ var m = this;
+ return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()];
+}
+
+function toObject () {
+ var m = this;
+ return {
+ years: m.year(),
+ months: m.month(),
+ date: m.date(),
+ hours: m.hours(),
+ minutes: m.minutes(),
+ seconds: m.seconds(),
+ milliseconds: m.milliseconds()
+ };
+}
+
+function toJSON () {
+ // new Date(NaN).toJSON() === null
+ return this.isValid() ? this.toISOString() : null;
+}
+
+function isValid$2 () {
+ return isValid(this);
+}
+
+function parsingFlags () {
+ return extend({}, getParsingFlags(this));
+}
+
+function invalidAt () {
+ return getParsingFlags(this).overflow;
+}
+
+function creationData() {
+ return {
+ input: this._i,
+ format: this._f,
+ locale: this._locale,
+ isUTC: this._isUTC,
+ strict: this._strict
+ };
+}
+
+// FORMATTING
+
+addFormatToken(0, ['gg', 2], 0, function () {
+ return this.weekYear() % 100;
+});
+
+addFormatToken(0, ['GG', 2], 0, function () {
+ return this.isoWeekYear() % 100;
+});
+
+function addWeekYearFormatToken (token, getter) {
+ addFormatToken(0, [token, token.length], 0, getter);
+}
+
+addWeekYearFormatToken('gggg', 'weekYear');
+addWeekYearFormatToken('ggggg', 'weekYear');
+addWeekYearFormatToken('GGGG', 'isoWeekYear');
+addWeekYearFormatToken('GGGGG', 'isoWeekYear');
+
+// ALIASES
+
+addUnitAlias('weekYear', 'gg');
+addUnitAlias('isoWeekYear', 'GG');
+
+// PRIORITY
+
+addUnitPriority('weekYear', 1);
+addUnitPriority('isoWeekYear', 1);
+
+
+// PARSING
+
+addRegexToken('G', matchSigned);
+addRegexToken('g', matchSigned);
+addRegexToken('GG', match1to2, match2);
+addRegexToken('gg', match1to2, match2);
+addRegexToken('GGGG', match1to4, match4);
+addRegexToken('gggg', match1to4, match4);
+addRegexToken('GGGGG', match1to6, match6);
+addRegexToken('ggggg', match1to6, match6);
+
+addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) {
+ week[token.substr(0, 2)] = toInt(input);
+});
+
+addWeekParseToken(['gg', 'GG'], function (input, week, config, token) {
+ week[token] = hooks.parseTwoDigitYear(input);
+});
+
+// MOMENTS
+
+function getSetWeekYear (input) {
+ return getSetWeekYearHelper.call(this,
+ input,
+ this.week(),
+ this.weekday(),
+ this.localeData()._week.dow,
+ this.localeData()._week.doy);
+}
+
+function getSetISOWeekYear (input) {
+ return getSetWeekYearHelper.call(this,
+ input, this.isoWeek(), this.isoWeekday(), 1, 4);
+}
+
+function getISOWeeksInYear () {
+ return weeksInYear(this.year(), 1, 4);
+}
+
+function getWeeksInYear () {
+ var weekInfo = this.localeData()._week;
+ return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
+}
+
+function getSetWeekYearHelper(input, week, weekday, dow, doy) {
+ var weeksTarget;
+ if (input == null) {
+ return weekOfYear(this, dow, doy).year;
+ } else {
+ weeksTarget = weeksInYear(input, dow, doy);
+ if (week > weeksTarget) {
+ week = weeksTarget;
+ }
+ return setWeekAll.call(this, input, week, weekday, dow, doy);
+ }
+}
+
+function setWeekAll(weekYear, week, weekday, dow, doy) {
+ var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy),
+ date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear);
+
+ this.year(date.getUTCFullYear());
+ this.month(date.getUTCMonth());
+ this.date(date.getUTCDate());
+ return this;
+}
+
+// FORMATTING
+
+addFormatToken('Q', 0, 'Qo', 'quarter');
+
+// ALIASES
+
+addUnitAlias('quarter', 'Q');
+
+// PRIORITY
+
+addUnitPriority('quarter', 7);
+
+// PARSING
+
+addRegexToken('Q', match1);
+addParseToken('Q', function (input, array) {
+ array[MONTH] = (toInt(input) - 1) * 3;
+});
+
+// MOMENTS
+
+function getSetQuarter (input) {
+ return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
+}
+
+// FORMATTING
+
+addFormatToken('D', ['DD', 2], 'Do', 'date');
+
+// ALIASES
+
+addUnitAlias('date', 'D');
+
+// PRIOROITY
+addUnitPriority('date', 9);
+
+// PARSING
+
+addRegexToken('D', match1to2);
+addRegexToken('DD', match1to2, match2);
+addRegexToken('Do', function (isStrict, locale) {
+ // TODO: Remove "ordinalParse" fallback in next major release.
+ return isStrict ?
+ (locale._dayOfMonthOrdinalParse || locale._ordinalParse) :
+ locale._dayOfMonthOrdinalParseLenient;
+});
+
+addParseToken(['D', 'DD'], DATE);
+addParseToken('Do', function (input, array) {
+ array[DATE] = toInt(input.match(match1to2)[0], 10);
+});
+
+// MOMENTS
+
+var getSetDayOfMonth = makeGetSet('Date', true);
+
+// FORMATTING
+
+addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear');
+
+// ALIASES
+
+addUnitAlias('dayOfYear', 'DDD');
+
+// PRIORITY
+addUnitPriority('dayOfYear', 4);
+
+// PARSING
+
+addRegexToken('DDD', match1to3);
+addRegexToken('DDDD', match3);
+addParseToken(['DDD', 'DDDD'], function (input, array, config) {
+ config._dayOfYear = toInt(input);
+});
+
+// HELPERS
+
+// MOMENTS
+
+function getSetDayOfYear (input) {
+ var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1;
+ return input == null ? dayOfYear : this.add((input - dayOfYear), 'd');
+}
+
+// FORMATTING
+
+addFormatToken('m', ['mm', 2], 0, 'minute');
+
+// ALIASES
+
+addUnitAlias('minute', 'm');
+
+// PRIORITY
+
+addUnitPriority('minute', 14);
+
+// PARSING
+
+addRegexToken('m', match1to2);
+addRegexToken('mm', match1to2, match2);
+addParseToken(['m', 'mm'], MINUTE);
+
+// MOMENTS
+
+var getSetMinute = makeGetSet('Minutes', false);
+
+// FORMATTING
+
+addFormatToken('s', ['ss', 2], 0, 'second');
+
+// ALIASES
+
+addUnitAlias('second', 's');
+
+// PRIORITY
+
+addUnitPriority('second', 15);
+
+// PARSING
+
+addRegexToken('s', match1to2);
+addRegexToken('ss', match1to2, match2);
+addParseToken(['s', 'ss'], SECOND);
+
+// MOMENTS
+
+var getSetSecond = makeGetSet('Seconds', false);
+
+// FORMATTING
+
+addFormatToken('S', 0, 0, function () {
+ return ~~(this.millisecond() / 100);
+});
+
+addFormatToken(0, ['SS', 2], 0, function () {
+ return ~~(this.millisecond() / 10);
+});
+
+addFormatToken(0, ['SSS', 3], 0, 'millisecond');
+addFormatToken(0, ['SSSS', 4], 0, function () {
+ return this.millisecond() * 10;
+});
+addFormatToken(0, ['SSSSS', 5], 0, function () {
+ return this.millisecond() * 100;
+});
+addFormatToken(0, ['SSSSSS', 6], 0, function () {
+ return this.millisecond() * 1000;
+});
+addFormatToken(0, ['SSSSSSS', 7], 0, function () {
+ return this.millisecond() * 10000;
+});
+addFormatToken(0, ['SSSSSSSS', 8], 0, function () {
+ return this.millisecond() * 100000;
+});
+addFormatToken(0, ['SSSSSSSSS', 9], 0, function () {
+ return this.millisecond() * 1000000;
+});
+
+
+// ALIASES
+
+addUnitAlias('millisecond', 'ms');
+
+// PRIORITY
+
+addUnitPriority('millisecond', 16);
+
+// PARSING
+
+addRegexToken('S', match1to3, match1);
+addRegexToken('SS', match1to3, match2);
+addRegexToken('SSS', match1to3, match3);
+
+var token;
+for (token = 'SSSS'; token.length <= 9; token += 'S') {
+ addRegexToken(token, matchUnsigned);
+}
+
+function parseMs(input, array) {
+ array[MILLISECOND] = toInt(('0.' + input) * 1000);
+}
+
+for (token = 'S'; token.length <= 9; token += 'S') {
+ addParseToken(token, parseMs);
+}
+// MOMENTS
+
+var getSetMillisecond = makeGetSet('Milliseconds', false);
+
+// FORMATTING
+
+addFormatToken('z', 0, 0, 'zoneAbbr');
+addFormatToken('zz', 0, 0, 'zoneName');
+
+// MOMENTS
+
+function getZoneAbbr () {
+ return this._isUTC ? 'UTC' : '';
+}
+
+function getZoneName () {
+ return this._isUTC ? 'Coordinated Universal Time' : '';
+}
+
+var proto = Moment.prototype;
+
+proto.add = add;
+proto.calendar = calendar$1;
+proto.clone = clone;
+proto.diff = diff;
+proto.endOf = endOf;
+proto.format = format;
+proto.from = from;
+proto.fromNow = fromNow;
+proto.to = to;
+proto.toNow = toNow;
+proto.get = stringGet;
+proto.invalidAt = invalidAt;
+proto.isAfter = isAfter;
+proto.isBefore = isBefore;
+proto.isBetween = isBetween;
+proto.isSame = isSame;
+proto.isSameOrAfter = isSameOrAfter;
+proto.isSameOrBefore = isSameOrBefore;
+proto.isValid = isValid$2;
+proto.lang = lang;
+proto.locale = locale;
+proto.localeData = localeData;
+proto.max = prototypeMax;
+proto.min = prototypeMin;
+proto.parsingFlags = parsingFlags;
+proto.set = stringSet;
+proto.startOf = startOf;
+proto.subtract = subtract;
+proto.toArray = toArray;
+proto.toObject = toObject;
+proto.toDate = toDate;
+proto.toISOString = toISOString;
+proto.inspect = inspect;
+proto.toJSON = toJSON;
+proto.toString = toString;
+proto.unix = unix;
+proto.valueOf = valueOf;
+proto.creationData = creationData;
+
+// Year
+proto.year = getSetYear;
+proto.isLeapYear = getIsLeapYear;
+
+// Week Year
+proto.weekYear = getSetWeekYear;
+proto.isoWeekYear = getSetISOWeekYear;
+
+// Quarter
+proto.quarter = proto.quarters = getSetQuarter;
+
+// Month
+proto.month = getSetMonth;
+proto.daysInMonth = getDaysInMonth;
+
+// Week
+proto.week = proto.weeks = getSetWeek;
+proto.isoWeek = proto.isoWeeks = getSetISOWeek;
+proto.weeksInYear = getWeeksInYear;
+proto.isoWeeksInYear = getISOWeeksInYear;
+
+// Day
+proto.date = getSetDayOfMonth;
+proto.day = proto.days = getSetDayOfWeek;
+proto.weekday = getSetLocaleDayOfWeek;
+proto.isoWeekday = getSetISODayOfWeek;
+proto.dayOfYear = getSetDayOfYear;
+
+// Hour
+proto.hour = proto.hours = getSetHour;
+
+// Minute
+proto.minute = proto.minutes = getSetMinute;
+
+// Second
+proto.second = proto.seconds = getSetSecond;
+
+// Millisecond
+proto.millisecond = proto.milliseconds = getSetMillisecond;
+
+// Offset
+proto.utcOffset = getSetOffset;
+proto.utc = setOffsetToUTC;
+proto.local = setOffsetToLocal;
+proto.parseZone = setOffsetToParsedOffset;
+proto.hasAlignedHourOffset = hasAlignedHourOffset;
+proto.isDST = isDaylightSavingTime;
+proto.isLocal = isLocal;
+proto.isUtcOffset = isUtcOffset;
+proto.isUtc = isUtc;
+proto.isUTC = isUtc;
+
+// Timezone
+proto.zoneAbbr = getZoneAbbr;
+proto.zoneName = getZoneName;
+
+// Deprecations
+proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth);
+proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth);
+proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear);
+proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', getSetZone);
+proto.isDSTShifted = deprecate('isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', isDaylightSavingTimeShifted);
+
+function createUnix (input) {
+ return createLocal(input * 1000);
+}
+
+function createInZone () {
+ return createLocal.apply(null, arguments).parseZone();
+}
+
+function preParsePostFormat (string) {
+ return string;
+}
+
+var proto$1 = Locale.prototype;
+
+proto$1.calendar = calendar;
+proto$1.longDateFormat = longDateFormat;
+proto$1.invalidDate = invalidDate;
+proto$1.ordinal = ordinal;
+proto$1.preparse = preParsePostFormat;
+proto$1.postformat = preParsePostFormat;
+proto$1.relativeTime = relativeTime;
+proto$1.pastFuture = pastFuture;
+proto$1.set = set;
+
+// Month
+proto$1.months = localeMonths;
+proto$1.monthsShort = localeMonthsShort;
+proto$1.monthsParse = localeMonthsParse;
+proto$1.monthsRegex = monthsRegex;
+proto$1.monthsShortRegex = monthsShortRegex;
+
+// Week
+proto$1.week = localeWeek;
+proto$1.firstDayOfYear = localeFirstDayOfYear;
+proto$1.firstDayOfWeek = localeFirstDayOfWeek;
+
+// Day of Week
+proto$1.weekdays = localeWeekdays;
+proto$1.weekdaysMin = localeWeekdaysMin;
+proto$1.weekdaysShort = localeWeekdaysShort;
+proto$1.weekdaysParse = localeWeekdaysParse;
+
+proto$1.weekdaysRegex = weekdaysRegex;
+proto$1.weekdaysShortRegex = weekdaysShortRegex;
+proto$1.weekdaysMinRegex = weekdaysMinRegex;
+
+// Hours
+proto$1.isPM = localeIsPM;
+proto$1.meridiem = localeMeridiem;
+
+function get$1 (format, index, field, setter) {
+ var locale = getLocale();
+ var utc = createUTC().set(setter, index);
+ return locale[field](utc, format);
+}
+
+function listMonthsImpl (format, index, field) {
+ if (isNumber(format)) {
+ index = format;
+ format = undefined;
+ }
+
+ format = format || '';
+
+ if (index != null) {
+ return get$1(format, index, field, 'month');
+ }
+
+ var i;
+ var out = [];
+ for (i = 0; i < 12; i++) {
+ out[i] = get$1(format, i, field, 'month');
+ }
+ return out;
+}
+
+// ()
+// (5)
+// (fmt, 5)
+// (fmt)
+// (true)
+// (true, 5)
+// (true, fmt, 5)
+// (true, fmt)
+function listWeekdaysImpl (localeSorted, format, index, field) {
+ if (typeof localeSorted === 'boolean') {
+ if (isNumber(format)) {
+ index = format;
+ format = undefined;
+ }
+
+ format = format || '';
+ } else {
+ format = localeSorted;
+ index = format;
+ localeSorted = false;
+
+ if (isNumber(format)) {
+ index = format;
+ format = undefined;
+ }
+
+ format = format || '';
+ }
+
+ var locale = getLocale(),
+ shift = localeSorted ? locale._week.dow : 0;
+
+ if (index != null) {
+ return get$1(format, (index + shift) % 7, field, 'day');
+ }
+
+ var i;
+ var out = [];
+ for (i = 0; i < 7; i++) {
+ out[i] = get$1(format, (i + shift) % 7, field, 'day');
+ }
+ return out;
+}
+
+function listMonths (format, index) {
+ return listMonthsImpl(format, index, 'months');
+}
+
+function listMonthsShort (format, index) {
+ return listMonthsImpl(format, index, 'monthsShort');
+}
+
+function listWeekdays (localeSorted, format, index) {
+ return listWeekdaysImpl(localeSorted, format, index, 'weekdays');
+}
+
+function listWeekdaysShort (localeSorted, format, index) {
+ return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort');
+}
+
+function listWeekdaysMin (localeSorted, format, index) {
+ return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin');
+}
+
+getSetGlobalLocale('en', {
+ dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/,
+ ordinal : function (number) {
+ var b = number % 10,
+ output = (toInt(number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
+ }
+});
+
+// Side effect imports
+hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', getSetGlobalLocale);
+hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', getLocale);
+
+var mathAbs = Math.abs;
+
+function abs () {
+ var data = this._data;
+
+ this._milliseconds = mathAbs(this._milliseconds);
+ this._days = mathAbs(this._days);
+ this._months = mathAbs(this._months);
+
+ data.milliseconds = mathAbs(data.milliseconds);
+ data.seconds = mathAbs(data.seconds);
+ data.minutes = mathAbs(data.minutes);
+ data.hours = mathAbs(data.hours);
+ data.months = mathAbs(data.months);
+ data.years = mathAbs(data.years);
+
+ return this;
+}
+
+function addSubtract$1 (duration, input, value, direction) {
+ var other = createDuration(input, value);
+
+ duration._milliseconds += direction * other._milliseconds;
+ duration._days += direction * other._days;
+ duration._months += direction * other._months;
+
+ return duration._bubble();
+}
+
+// supports only 2.0-style add(1, 's') or add(duration)
+function add$1 (input, value) {
+ return addSubtract$1(this, input, value, 1);
+}
+
+// supports only 2.0-style subtract(1, 's') or subtract(duration)
+function subtract$1 (input, value) {
+ return addSubtract$1(this, input, value, -1);
+}
+
+function absCeil (number) {
+ if (number < 0) {
+ return Math.floor(number);
+ } else {
+ return Math.ceil(number);
+ }
+}
+
+function bubble () {
+ var milliseconds = this._milliseconds;
+ var days = this._days;
+ var months = this._months;
+ var data = this._data;
+ var seconds, minutes, hours, years, monthsFromDays;
+
+ // if we have a mix of positive and negative values, bubble down first
+ // check: https://github.com/moment/moment/issues/2166
+ if (!((milliseconds >= 0 && days >= 0 && months >= 0) ||
+ (milliseconds <= 0 && days <= 0 && months <= 0))) {
+ milliseconds += absCeil(monthsToDays(months) + days) * 864e5;
+ days = 0;
+ months = 0;
+ }
+
+ // The following code bubbles up values, see the tests for
+ // examples of what that means.
+ data.milliseconds = milliseconds % 1000;
+
+ seconds = absFloor(milliseconds / 1000);
+ data.seconds = seconds % 60;
+
+ minutes = absFloor(seconds / 60);
+ data.minutes = minutes % 60;
+
+ hours = absFloor(minutes / 60);
+ data.hours = hours % 24;
+
+ days += absFloor(hours / 24);
+
+ // convert days to months
+ monthsFromDays = absFloor(daysToMonths(days));
+ months += monthsFromDays;
+ days -= absCeil(monthsToDays(monthsFromDays));
+
+ // 12 months -> 1 year
+ years = absFloor(months / 12);
+ months %= 12;
+
+ data.days = days;
+ data.months = months;
+ data.years = years;
+
+ return this;
+}
+
+function daysToMonths (days) {
+ // 400 years have 146097 days (taking into account leap year rules)
+ // 400 years have 12 months === 4800
+ return days * 4800 / 146097;
+}
+
+function monthsToDays (months) {
+ // the reverse of daysToMonths
+ return months * 146097 / 4800;
+}
+
+function as (units) {
+ if (!this.isValid()) {
+ return NaN;
+ }
+ var days;
+ var months;
+ var milliseconds = this._milliseconds;
+
+ units = normalizeUnits(units);
+
+ if (units === 'month' || units === 'year') {
+ days = this._days + milliseconds / 864e5;
+ months = this._months + daysToMonths(days);
+ return units === 'month' ? months : months / 12;
+ } else {
+ // handle milliseconds separately because of floating point math errors (issue #1867)
+ days = this._days + Math.round(monthsToDays(this._months));
+ switch (units) {
+ case 'week' : return days / 7 + milliseconds / 6048e5;
+ case 'day' : return days + milliseconds / 864e5;
+ case 'hour' : return days * 24 + milliseconds / 36e5;
+ case 'minute' : return days * 1440 + milliseconds / 6e4;
+ case 'second' : return days * 86400 + milliseconds / 1000;
+ // Math.floor prevents floating point math errors here
+ case 'millisecond': return Math.floor(days * 864e5) + milliseconds;
+ default: throw new Error('Unknown unit ' + units);
+ }
+ }
+}
+
+// TODO: Use this.as('ms')?
+function valueOf$1 () {
+ if (!this.isValid()) {
+ return NaN;
+ }
+ return (
+ this._milliseconds +
+ this._days * 864e5 +
+ (this._months % 12) * 2592e6 +
+ toInt(this._months / 12) * 31536e6
+ );
+}
+
+function makeAs (alias) {
+ return function () {
+ return this.as(alias);
+ };
+}
+
+var asMilliseconds = makeAs('ms');
+var asSeconds = makeAs('s');
+var asMinutes = makeAs('m');
+var asHours = makeAs('h');
+var asDays = makeAs('d');
+var asWeeks = makeAs('w');
+var asMonths = makeAs('M');
+var asYears = makeAs('y');
+
+function clone$1 () {
+ return createDuration(this);
+}
+
+function get$2 (units) {
+ units = normalizeUnits(units);
+ return this.isValid() ? this[units + 's']() : NaN;
+}
+
+function makeGetter(name) {
+ return function () {
+ return this.isValid() ? this._data[name] : NaN;
+ };
+}
+
+var milliseconds = makeGetter('milliseconds');
+var seconds = makeGetter('seconds');
+var minutes = makeGetter('minutes');
+var hours = makeGetter('hours');
+var days = makeGetter('days');
+var months = makeGetter('months');
+var years = makeGetter('years');
+
+function weeks () {
+ return absFloor(this.days() / 7);
+}
+
+var round = Math.round;
+var thresholds = {
+ ss: 44, // a few seconds to seconds
+ s : 45, // seconds to minute
+ m : 45, // minutes to hour
+ h : 22, // hours to day
+ d : 26, // days to month
+ M : 11 // months to year
+};
+
+// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
+function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) {
+ return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
+}
+
+function relativeTime$1 (posNegDuration, withoutSuffix, locale) {
+ var duration = createDuration(posNegDuration).abs();
+ var seconds = round(duration.as('s'));
+ var minutes = round(duration.as('m'));
+ var hours = round(duration.as('h'));
+ var days = round(duration.as('d'));
+ var months = round(duration.as('M'));
+ var years = round(duration.as('y'));
+
+ var a = seconds <= thresholds.ss && ['s', seconds] ||
+ seconds < thresholds.s && ['ss', seconds] ||
+ minutes <= 1 && ['m'] ||
+ minutes < thresholds.m && ['mm', minutes] ||
+ hours <= 1 && ['h'] ||
+ hours < thresholds.h && ['hh', hours] ||
+ days <= 1 && ['d'] ||
+ days < thresholds.d && ['dd', days] ||
+ months <= 1 && ['M'] ||
+ months < thresholds.M && ['MM', months] ||
+ years <= 1 && ['y'] || ['yy', years];
+
+ a[2] = withoutSuffix;
+ a[3] = +posNegDuration > 0;
+ a[4] = locale;
+ return substituteTimeAgo.apply(null, a);
+}
+
+// This function allows you to set the rounding function for relative time strings
+function getSetRelativeTimeRounding (roundingFunction) {
+ if (roundingFunction === undefined) {
+ return round;
+ }
+ if (typeof(roundingFunction) === 'function') {
+ round = roundingFunction;
+ return true;
+ }
+ return false;
+}
+
+// This function allows you to set a threshold for relative time strings
+function getSetRelativeTimeThreshold (threshold, limit) {
+ if (thresholds[threshold] === undefined) {
+ return false;
+ }
+ if (limit === undefined) {
+ return thresholds[threshold];
+ }
+ thresholds[threshold] = limit;
+ if (threshold === 's') {
+ thresholds.ss = limit - 1;
+ }
+ return true;
+}
+
+function humanize (withSuffix) {
+ if (!this.isValid()) {
+ return this.localeData().invalidDate();
+ }
+
+ var locale = this.localeData();
+ var output = relativeTime$1(this, !withSuffix, locale);
+
+ if (withSuffix) {
+ output = locale.pastFuture(+this, output);
+ }
+
+ return locale.postformat(output);
+}
+
+var abs$1 = Math.abs;
+
+function sign(x) {
+ return ((x > 0) - (x < 0)) || +x;
+}
+
+function toISOString$1() {
+ // for ISO strings we do not use the normal bubbling rules:
+ // * milliseconds bubble up until they become hours
+ // * days do not bubble at all
+ // * months bubble up until they become years
+ // This is because there is no context-free conversion between hours and days
+ // (think of clock changes)
+ // and also not between days and months (28-31 days per month)
+ if (!this.isValid()) {
+ return this.localeData().invalidDate();
+ }
+
+ var seconds = abs$1(this._milliseconds) / 1000;
+ var days = abs$1(this._days);
+ var months = abs$1(this._months);
+ var minutes, hours, years;
+
+ // 3600 seconds -> 60 minutes -> 1 hour
+ minutes = absFloor(seconds / 60);
+ hours = absFloor(minutes / 60);
+ seconds %= 60;
+ minutes %= 60;
+
+ // 12 months -> 1 year
+ years = absFloor(months / 12);
+ months %= 12;
+
+
+ // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
+ var Y = years;
+ var M = months;
+ var D = days;
+ var h = hours;
+ var m = minutes;
+ var s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : '';
+ var total = this.asSeconds();
+
+ if (!total) {
+ // this is the same as C#'s (Noda) and python (isodate)...
+ // but not other JS (goog.date)
+ return 'P0D';
+ }
+
+ var totalSign = total < 0 ? '-' : '';
+ var ymSign = sign(this._months) !== sign(total) ? '-' : '';
+ var daysSign = sign(this._days) !== sign(total) ? '-' : '';
+ var hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : '';
+
+ return totalSign + 'P' +
+ (Y ? ymSign + Y + 'Y' : '') +
+ (M ? ymSign + M + 'M' : '') +
+ (D ? daysSign + D + 'D' : '') +
+ ((h || m || s) ? 'T' : '') +
+ (h ? hmsSign + h + 'H' : '') +
+ (m ? hmsSign + m + 'M' : '') +
+ (s ? hmsSign + s + 'S' : '');
+}
+
+var proto$2 = Duration.prototype;
+
+proto$2.isValid = isValid$1;
+proto$2.abs = abs;
+proto$2.add = add$1;
+proto$2.subtract = subtract$1;
+proto$2.as = as;
+proto$2.asMilliseconds = asMilliseconds;
+proto$2.asSeconds = asSeconds;
+proto$2.asMinutes = asMinutes;
+proto$2.asHours = asHours;
+proto$2.asDays = asDays;
+proto$2.asWeeks = asWeeks;
+proto$2.asMonths = asMonths;
+proto$2.asYears = asYears;
+proto$2.valueOf = valueOf$1;
+proto$2._bubble = bubble;
+proto$2.clone = clone$1;
+proto$2.get = get$2;
+proto$2.milliseconds = milliseconds;
+proto$2.seconds = seconds;
+proto$2.minutes = minutes;
+proto$2.hours = hours;
+proto$2.days = days;
+proto$2.weeks = weeks;
+proto$2.months = months;
+proto$2.years = years;
+proto$2.humanize = humanize;
+proto$2.toISOString = toISOString$1;
+proto$2.toString = toISOString$1;
+proto$2.toJSON = toISOString$1;
+proto$2.locale = locale;
+proto$2.localeData = localeData;
+
+// Deprecations
+proto$2.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', toISOString$1);
+proto$2.lang = lang;
+
+// Side effect imports
+
+// FORMATTING
+
+addFormatToken('X', 0, 0, 'unix');
+addFormatToken('x', 0, 0, 'valueOf');
+
+// PARSING
+
+addRegexToken('x', matchSigned);
+addRegexToken('X', matchTimestamp);
+addParseToken('X', function (input, array, config) {
+ config._d = new Date(parseFloat(input, 10) * 1000);
+});
+addParseToken('x', function (input, array, config) {
+ config._d = new Date(toInt(input));
+});
+
+// Side effect imports
+
+
+hooks.version = '2.19.2';
+
+setHookCallback(createLocal);
+
+hooks.fn = proto;
+hooks.min = min;
+hooks.max = max;
+hooks.now = now;
+hooks.utc = createUTC;
+hooks.unix = createUnix;
+hooks.months = listMonths;
+hooks.isDate = isDate;
+hooks.locale = getSetGlobalLocale;
+hooks.invalid = createInvalid;
+hooks.duration = createDuration;
+hooks.isMoment = isMoment;
+hooks.weekdays = listWeekdays;
+hooks.parseZone = createInZone;
+hooks.localeData = getLocale;
+hooks.isDuration = isDuration;
+hooks.monthsShort = listMonthsShort;
+hooks.weekdaysMin = listWeekdaysMin;
+hooks.defineLocale = defineLocale;
+hooks.updateLocale = updateLocale;
+hooks.locales = listLocales;
+hooks.weekdaysShort = listWeekdaysShort;
+hooks.normalizeUnits = normalizeUnits;
+hooks.relativeTimeRounding = getSetRelativeTimeRounding;
+hooks.relativeTimeThreshold = getSetRelativeTimeThreshold;
+hooks.calendarFormat = getCalendarFormat;
+hooks.prototype = proto;
+
+return hooks;
+
+})));
diff --git a/basicsuite/ebike-ui/mostrecent.bson b/basicsuite/ebike-ui/mostrecent.bson
new file mode 100644
index 0000000..5e9edea
--- /dev/null
+++ b/basicsuite/ebike-ui/mostrecent.bson
Binary files differ
diff --git a/basicsuite/ebike-ui/navigation.cpp b/basicsuite/ebike-ui/navigation.cpp
new file mode 100644
index 0000000..067636e
--- /dev/null
+++ b/basicsuite/ebike-ui/navigation.cpp
@@ -0,0 +1,97 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QNetworkReply>
+
+#include "navigation.h"
+#include "mapbox.h"
+
+Navigation::Navigation(MapBox *mapbox, QObject *parent)
+ : QObject(parent)
+ , m_mapbox(mapbox)
+ , m_position(QGeoCoordinate(36.131961, -115.153048), QDateTime::currentDateTime())
+ , m_zoomlevel(18)
+ , m_active(false)
+ , m_routeDirection(0)
+ , m_routePosition(m_position.coordinate())
+{
+ m_position.setAttribute(QGeoPositionInfo::Direction, 0);
+}
+
+void Navigation::setPosition(const QGeoPositionInfo &position)
+{
+ if (m_position != position) {
+ m_position = position;
+ emit positionChanged(m_position);
+ }
+}
+
+void Navigation::setCoordinate(const QGeoCoordinate &coordinate)
+{
+ if (m_position.coordinate() != coordinate) {
+ m_position.setCoordinate(coordinate);
+ emit coordinateChanged(m_position.coordinate());
+ }
+}
+
+void Navigation::setDirection(qreal direction)
+{
+ if (qFuzzyCompare(m_position.attribute(QGeoPositionInfo::Direction), direction))
+ return;
+
+ m_position.setAttribute(QGeoPositionInfo::Direction, direction);
+ emit directionChanged(direction);
+}
+
+void Navigation::setZoomLevel(qreal zoomlevel)
+{
+ if (qFuzzyCompare(m_zoomlevel, zoomlevel))
+ return;
+
+ m_zoomlevel = zoomlevel;
+ emit zoomLevelChanged(m_zoomlevel);
+}
+
+void Navigation::setActive(bool active)
+{
+ if (m_active == active)
+ return;
+
+ m_active = active;
+ emit activeChanged(m_active);
+}
diff --git a/basicsuite/ebike-ui/navigation.h b/basicsuite/ebike-ui/navigation.h
new file mode 100644
index 0000000..6c9533f
--- /dev/null
+++ b/basicsuite/ebike-ui/navigation.h
@@ -0,0 +1,103 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef NAVIGATION_H
+#define NAVIGATION_H
+
+#include <QObject>
+#include <QGeoPositionInfo>
+#include <QJsonObject>
+#include <QJSValue>
+#include <QTimer>
+
+#include <QDebug>
+
+class MapBox;
+
+class Navigation : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(QGeoPositionInfo position READ position WRITE setPosition NOTIFY positionChanged)
+ Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged)
+ Q_PROPERTY(qreal direction READ direction WRITE setDirection NOTIFY directionChanged)
+ Q_PROPERTY(qreal zoomlevel READ zoomLevel WRITE setZoomLevel NOTIFY zoomLevelChanged)
+ Q_PROPERTY(bool active READ active WRITE setActive NOTIFY activeChanged)
+ Q_PROPERTY(qreal routeDirection READ routeDirection NOTIFY routeDirectionChanged)
+ Q_PROPERTY(QGeoCoordinate routePosition READ routePosition NOTIFY routePositionChanged)
+
+public:
+ explicit Navigation(MapBox *mapbox, QObject *parent = nullptr);
+
+public:
+ // Getters
+ const QGeoPositionInfo &position() const { return m_position; }
+ QGeoCoordinate coordinate() const { return m_position.coordinate(); }
+ qreal direction() const { return m_position.attribute(QGeoPositionInfo::Direction); }
+ qreal zoomLevel() const { return m_zoomlevel; }
+ bool active() const { return m_active; }
+
+ qreal routeDirection() const { return m_routeDirection; }
+ QGeoCoordinate routePosition() const { return m_routePosition; }
+
+ // Setters
+ void setPosition(const QGeoPositionInfo &position);
+ void setCoordinate(const QGeoCoordinate &coordinate);
+ void setDirection(qreal direction);
+ void setZoomLevel(qreal zoomlevel);
+ void setActive(bool active);
+
+signals:
+ void positionChanged(QGeoPositionInfo position);
+ void coordinateChanged(QGeoCoordinate coordinate);
+ void directionChanged(qreal direction);
+ void zoomLevelChanged(qreal zoomlevel);
+ void activeChanged(bool active);
+
+ void routeDirectionChanged(qreal routeDirection);
+ void routePositionChanged(QGeoCoordinate routePosition);
+
+private:
+ MapBox *m_mapbox;
+ QGeoPositionInfo m_position;
+ qreal m_zoomlevel;
+ bool m_active;
+
+ qreal m_routeDirection;
+ QGeoCoordinate m_routePosition;
+};
+
+#endif // NAVIGATION_H
diff --git a/basicsuite/ebike-ui/preview_l.jpg b/basicsuite/ebike-ui/preview_l.jpg
new file mode 100644
index 0000000..4cd0851
--- /dev/null
+++ b/basicsuite/ebike-ui/preview_l.jpg
Binary files differ
diff --git a/basicsuite/ebike-ui/qml.qrc b/basicsuite/ebike-ui/qml.qrc
new file mode 100644
index 0000000..810c477
--- /dev/null
+++ b/basicsuite/ebike-ui/qml.qrc
@@ -0,0 +1,107 @@
+<RCC>
+ <qresource prefix="/">
+ <file>qtquickcontrols2.conf</file>
+ <file>mainview.qml</file>
+ <file>NaviPage.qml</file>
+ <file>StatsPage.qml</file>
+ <file>MainPage.qml</file>
+ <file>BikeStyle/Colors.qml</file>
+ <file>BikeStyle/qmldir</file>
+ <file>SpeedView.qml</file>
+ <file>fonts/Montserrat-Medium.ttf</file>
+ <file>fonts/Montserrat-Bold.ttf</file>
+ <file>fonts/Montserrat-Light.ttf</file>
+ <file>fonts/Montserrat-Regular.ttf</file>
+ <file>StatsBox.qml</file>
+ <file>BikeStyle/UILayout.qml</file>
+ <file>NaviBox.qml</file>
+ <file>LightsBox.qml</file>
+ <file>ModeBox.qml</file>
+ <file>ClockView.qml</file>
+ <file>images/lights_off.png</file>
+ <file>images/lights_on.png</file>
+ <file>fonts/Teko-Bold.ttf</file>
+ <file>fonts/Teko-Light.ttf</file>
+ <file>fonts/Teko-Medium.ttf</file>
+ <file>fonts/Teko-Regular.ttf</file>
+ <file>MusicPlayer.qml</file>
+ <file>images/map-marker.png</file>
+ <file>images/trip.png</file>
+ <file>images/calories.png</file>
+ <file>images/nextsong.png</file>
+ <file>images/nextsong_pressed.png</file>
+ <file>images/play.png</file>
+ <file>images/play_pressed.png</file>
+ <file>images/prevsong.png</file>
+ <file>images/prevsong_pressed.png</file>
+ <file>images/speed.png</file>
+ <file>images/battery.png</file>
+ <file>images/assist.png</file>
+ <file>ConfigurationDrawer.qml</file>
+ <file>IconifiedTabButton.qml</file>
+ <file>fonts/fontawesome-webfont.ttf</file>
+ <file>GeneralTab.qml</file>
+ <file>ColumnSpacer.qml</file>
+ <file>BikeInfoTab.qml</file>
+ <file>TripChart.qml</file>
+ <file>images/top_curtain_drag.png</file>
+ <file>FpsItem.qml</file>
+ <file>images/spinner.png</file>
+ <file>images/checkmark.png</file>
+ <file>images/nav_left.png</file>
+ <file>images/nav_right.png</file>
+ <file>images/nav_straight.png</file>
+ <file>images/small_speedometer_arrow.png</file>
+ <file>images/map_locate.png</file>
+ <file>images/map_zoomin.png</file>
+ <file>images/map_zoomout.png</file>
+ <file>images/info.png</file>
+ <file>images/info_selected.png</file>
+ <file>images/list.png</file>
+ <file>images/list_selected.png</file>
+ <file>images/settings.png</file>
+ <file>images/settings_selected.png</file>
+ <file>ConfigurationItem.qml</file>
+ <file>images/curtain_up_arrow.png</file>
+ <file>NaviButton.qml</file>
+ <file>images/search.png</file>
+ <file>images/search_cancel.png</file>
+ <file>NaviGuide.qml</file>
+ <file>images/arrow_left.png</file>
+ <file>images/arrow_right.png</file>
+ <file>StatsRow.qml</file>
+ <file>images/fps_icon.png</file>
+ <file>images/curtain_shadow_handle.png</file>
+ <file>images/map_btn_shadow.png</file>
+ <file>images/map_destination.png</file>
+ <file>images/map_location_arrow.png</file>
+ <file>NaviTripInfo.qml</file>
+ <file>moment.js</file>
+ <file>images/small_speedometer_shadow.png</file>
+ <file>images/navigation_widget_shadow.png</file>
+ <file>images/small_input_box_shadow.png</file>
+ <file>images/nav_bear_l.png</file>
+ <file>images/nav_bear_r.png</file>
+ <file>images/nav_hard_l.png</file>
+ <file>images/nav_hard_r.png</file>
+ <file>images/nav_light_left.png</file>
+ <file>images/nav_light_right.png</file>
+ <file>images/nav_nodir.png</file>
+ <file>images/nav_uturn_l.png</file>
+ <file>images/nav_uturn_r.png</file>
+ <file>ViewTab.qml</file>
+ <file>images/pause.png</file>
+ <file>images/pause_pressed.png</file>
+ <file>images/ok.png</file>
+ <file>images/warning.png</file>
+ <file>images/bike-battery.png</file>
+ <file>images/bike-brakes.png</file>
+ <file>images/bike-chain.png</file>
+ <file>images/bike-frontwheel.png</file>
+ <file>images/bike-gears.png</file>
+ <file>images/bike-rearwheel.png</file>
+ <file>images/bike-light.png</file>
+ <file>ToggleSwitch.qml</file>
+ <file>images/blue_circle_gps_area.png</file>
+ </qresource>
+</RCC>
diff --git a/basicsuite/ebike-ui/qtquickcontrols2.conf b/basicsuite/ebike-ui/qtquickcontrols2.conf
new file mode 100644
index 0000000..1764b16
--- /dev/null
+++ b/basicsuite/ebike-ui/qtquickcontrols2.conf
@@ -0,0 +1,15 @@
+; This file can be edited to change the style of the application
+; See Styling Qt Quick Controls 2 in the documentation for details:
+; http://doc.qt.io/qt-5/qtquickcontrols2-styles.html
+
+[Controls]
+Style=Default
+
+[Universal]
+Theme=Light
+;Accent=Steel
+
+[Material]
+Theme=Light
+;Accent=BlueGrey
+;Primary=BlueGray
diff --git a/basicsuite/ebike-ui/socketclient.cpp b/basicsuite/ebike-ui/socketclient.cpp
new file mode 100644
index 0000000..d40d268
--- /dev/null
+++ b/basicsuite/ebike-ui/socketclient.cpp
@@ -0,0 +1,152 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QDataStream>
+#include <QJsonDocument>
+
+#include "socketclient.h"
+
+SocketClient::SocketClient(QObject *parent)
+ : QObject(parent)
+ , m_socket(new QLocalSocket(this))
+{
+ // Connect socket signals
+ connect(m_socket, &QLocalSocket::connected, this, &SocketClient::connected);
+ connect(m_socket, &QLocalSocket::disconnected, this, &SocketClient::disconnected);
+ connect(m_socket, &QLocalSocket::readyRead, this, &SocketClient::readyRead);
+
+ // Setup timer to try to reconnect after disconnect
+ m_connectionTimer.setInterval(5000);
+ connect(&m_connectionTimer, &QTimer::timeout, this, &SocketClient::reconnect);
+ connect(m_socket, &QLocalSocket::connected, &m_connectionTimer, &QTimer::stop);
+ connect(m_socket, &QLocalSocket::disconnected,
+ &m_connectionTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
+ connect(m_socket, static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error),
+ &m_connectionTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
+}
+
+void SocketClient::connectToServer(const QString &servername)
+{
+ m_servername = servername;
+ reconnect();
+}
+
+void SocketClient::reconnect()
+{
+ qDebug("Connecting to server...");
+ m_socket->connectToServer(m_servername);
+}
+
+qint64 SocketClient::write(const QByteArray &data)
+{
+ return m_socket->write(data);
+}
+
+/**
+ * @brief send a QByteArray to the server
+ * @param message
+ *
+ * Adds the length of the message as a header and sends the message to the server.
+ */
+void SocketClient::sendToServer(const QByteArray &message)
+{
+ // Prepend the message length
+ QByteArray data;
+ QDataStream stream(&data, QIODevice::WriteOnly);
+ stream << static_cast<qint32>(message.size() + 4);
+ data.append(message);
+
+ write(data);
+}
+
+/**
+ * @brief send a Json object to the server
+ * @param message
+ *
+ * Sends a QJsonObject object to the server, encoded in Qt's internal binary format.
+ */
+void SocketClient::sendToServer(const QJsonObject &message)
+{
+ QJsonDocument doc(message);
+ sendToServer(doc.toBinaryData());
+}
+
+/**
+ * @brief reads incoming data from the server
+ *
+ * Parses only message headers and body as QByteArray, but does not care about
+ * the contents. All complete messages are processed at @see parseMessage.
+ */
+void SocketClient::readyRead()
+{
+ m_data += m_socket->readAll();
+
+ bool messagefound = true;
+ while (messagefound) {
+ messagefound = false;
+ // If we have at least some data
+ if (m_data.size() >= 4) {
+ // Extract message size
+ qint32 messagesize;
+ QDataStream stream(m_data.left(4));
+ stream >> messagesize;
+
+ // If we have enough data for at least one message
+ if (m_data.size() >= messagesize) {
+ // Extract actual message
+ QByteArray message = m_data.mid(4, messagesize - 4);
+ parseMessage(message);
+ // Drop necessary amount of bytes
+ m_data = m_data.mid(messagesize);
+ messagefound = true; // Try to parse another message
+ }
+ }
+ }
+}
+
+/**
+ * @brief parse the contents of a QByteArray
+ * @param message
+ *
+ * Contents are parsed from QJsonDocument's binary data. This separation allows
+ * the format to be changed later on, if need be.
+ */
+void SocketClient::parseMessage(const QByteArray &message)
+{
+ // Parse message from raw format
+ QJsonDocument doc = QJsonDocument::fromBinaryData(message);
+ emit newMessage(doc.object());
+}
diff --git a/basicsuite/ebike-ui/socketclient.h b/basicsuite/ebike-ui/socketclient.h
new file mode 100644
index 0000000..dc100a2
--- /dev/null
+++ b/basicsuite/ebike-ui/socketclient.h
@@ -0,0 +1,86 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef SOCKETCLIENT_H
+#define SOCKETCLIENT_H
+
+#include <QObject>
+#include <QLocalSocket>
+#include <QTimer>
+#include <QJsonObject>
+
+/**
+ * @brief The SocketClient class
+ *
+ * Socket container and message parser for client communications.
+ */
+class SocketClient : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit SocketClient(QObject *parent = nullptr);
+
+public:
+ QLocalSocket* socket(void) const { return m_socket; }
+ qint64 write(const QByteArray &data);
+ void sendToServer(const QByteArray &message);
+ void sendToServer(const QJsonObject &message);
+
+private:
+ void parseMessage(const QByteArray &message);
+
+signals:
+ void connected();
+ void disconnected();
+
+ void newMessage(const QJsonObject &message);
+
+public slots:
+ void connectToServer(const QString &servername);
+
+private slots:
+ void reconnect();
+ void readyRead();
+
+private:
+ QLocalSocket *m_socket;
+ QString m_servername;
+ QTimer m_connectionTimer;
+ QByteArray m_data;
+};
+
+#endif // SOCKETCLIENT_H
diff --git a/basicsuite/ebike-ui/suggestionsmodel.cpp b/basicsuite/ebike-ui/suggestionsmodel.cpp
new file mode 100644
index 0000000..fa028f0
--- /dev/null
+++ b/basicsuite/ebike-ui/suggestionsmodel.cpp
@@ -0,0 +1,157 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+#include <QCoreApplication>
+#include <QDir>
+#include <QFile>
+#include <QJsonDocument>
+
+#include "suggestionsmodel.h"
+
+#define EBIKE_DEMO_MODE
+
+static const char mostRecentFilename[] = "mostrecent.bson";
+
+SuggestionsModel::SuggestionsModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+ loadMostRecent();
+}
+
+SuggestionsModel::SuggestionsModel(const QJsonArray &suggestions, QObject *parent)
+ : QAbstractListModel(parent)
+ , m_suggestions(suggestions)
+{
+ loadMostRecent();
+}
+
+QVariant SuggestionsModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ // Only horizontal header
+ if (orientation == Qt::Vertical)
+ return QVariant();
+
+ if (role == Qt::DisplayRole && section == 0)
+ return tr("Place");
+
+ return QAbstractListModel::headerData(section, orientation, role);
+}
+
+int SuggestionsModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_suggestions.isEmpty() ? m_mostrecent.size() : m_suggestions.size();
+}
+
+QVariant SuggestionsModel::data(const QModelIndex &index, int role) const
+{
+ QJsonObject obj = get(index.row());
+ if (role == Qt::DisplayRole) {
+ return obj.value("place_name").toVariant();
+ } else if (role == PlaceNameRole) {
+ return obj.value("place_name").toVariant();
+ }
+
+ return QVariant();
+}
+
+QHash<int, QByteArray> SuggestionsModel::roleNames() const
+{
+ QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
+ roles[PlaceNameRole] = "placename";
+ return roles;
+}
+
+const QJsonObject SuggestionsModel::get(int index) const
+{
+ return m_suggestions.isEmpty() ?
+ m_mostrecent[index].toObject() :
+ m_suggestions[index].toObject();
+}
+
+void SuggestionsModel::setSuggestions(const QJsonArray &suggestions)
+{
+ beginResetModel();
+ m_suggestions = suggestions;
+ endResetModel();
+ emit emptyChanged();
+}
+
+void SuggestionsModel::clear()
+{
+ beginResetModel();
+ m_suggestions = QJsonArray();
+ endResetModel();
+ emit emptyChanged();
+}
+
+void SuggestionsModel::addToMostRecent(const QJsonObject &place)
+{
+ Q_UNUSED(place)
+ // For the demo, do not add new most recent places
+#ifndef EBIKE_DEMO_MODE
+ if (!m_mostrecent.contains(place))
+ m_mostrecent.prepend(place);
+ if (m_mostrecent.size() > 3)
+ m_mostrecent.pop_back();
+ saveMostRecent();
+#endif
+}
+
+void SuggestionsModel::loadMostRecent()
+{
+ QDir dir(QCoreApplication::applicationDirPath());
+
+ // Load most recent places
+ QString mostRecentFilepath = dir.absoluteFilePath(mostRecentFilename);
+ QFile mostRecentFile(mostRecentFilepath);
+ if (mostRecentFile.open(QIODevice::ReadOnly)) {
+ QJsonDocument doc = QJsonDocument::fromBinaryData(mostRecentFile.readAll());
+ m_mostrecent = doc.array();
+ }
+}
+
+void SuggestionsModel::saveMostRecent() const
+{
+ QDir dir(QCoreApplication::applicationDirPath());
+
+ // Load most recent places
+ QString mostRecentFilepath = dir.absoluteFilePath(mostRecentFilename);
+ QFile mostRecentFile(mostRecentFilepath);
+ if (mostRecentFile.open(QIODevice::WriteOnly)) {
+ QJsonDocument doc(m_mostrecent);
+ mostRecentFile.write(doc.toBinaryData());
+ }
+}
diff --git a/basicsuite/ebike-ui/suggestionsmodel.h b/basicsuite/ebike-ui/suggestionsmodel.h
new file mode 100644
index 0000000..5bf22b5
--- /dev/null
+++ b/basicsuite/ebike-ui/suggestionsmodel.h
@@ -0,0 +1,82 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef SUGGESTIONSMODEL_H
+#define SUGGESTIONSMODEL_H
+
+#include <QAbstractListModel>
+#include <QJsonArray>
+#include <QJsonObject>
+
+class SuggestionsModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(bool empty READ isEmpty RESET clear NOTIFY emptyChanged)
+
+public:
+ explicit SuggestionsModel(QObject *parent = nullptr);
+ explicit SuggestionsModel(const QJsonArray &suggestions, QObject *parent = nullptr);
+ enum SuggestionRoles {
+ PlaceNameRole = Qt::UserRole + 1
+ };
+
+public:
+ virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+ virtual int rowCount(const QModelIndex &parent) const;
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual QHash<int, QByteArray> roleNames() const;
+
+ Q_INVOKABLE const QJsonObject get(int index) const;
+ bool isEmpty() const { return m_suggestions.size() == 0; }
+
+ void setSuggestions(const QJsonArray &suggestions);
+ Q_INVOKABLE void clear();
+
+ Q_INVOKABLE void addToMostRecent(const QJsonObject &place);
+
+private:
+ void loadMostRecent();
+ void saveMostRecent() const;
+
+signals:
+ void emptyChanged();
+
+private:
+ QJsonArray m_mostrecent;
+ QJsonArray m_suggestions;
+};
+
+#endif // SUGGESTIONSMODEL_H
diff --git a/basicsuite/ebike-ui/tripdatamodel.cpp b/basicsuite/ebike-ui/tripdatamodel.cpp
new file mode 100644
index 0000000..b74605f
--- /dev/null
+++ b/basicsuite/ebike-ui/tripdatamodel.cpp
@@ -0,0 +1,135 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "datastore.h"
+#include "tripdatamodel.h"
+
+#include <QDebug>
+
+TripDataModel::TripDataModel(DataStore *datastore, QObject *parent)
+ : QAbstractListModel(parent)
+ , m_datastore(datastore)
+ , m_refreshing(false)
+ , m_saving(false)
+{
+}
+
+int TripDataModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent)
+ return m_trips.count() + 1; // +1 for current trip
+}
+
+QVariant TripDataModel::data(const QModelIndex &index, int role) const
+{
+ QJsonObject obj = get(index.row());
+ if (role == DurationRole)
+ return obj.value("duration").toDouble(); // Seconds
+ else if (role == DistanceRole)
+ return m_datastore->convertDistance(obj.value("distance").toDouble());
+ else if (role == CaloriesRole)
+ return obj.value("calories").toDouble();
+ else if (role == MaxspeedRole)
+ return m_datastore->convertSpeed(obj.value("maxspeed").toDouble());
+ else if (role == AvgspeedRole)
+ return m_datastore->convertSpeed(obj.value("distance").toDouble() / obj.value("duration").toDouble());
+ else if (role == AscentRole)
+ return obj.value("ascent").toDouble();
+ else if (role == StartTimeRole)
+ return obj.value("starttime").toDouble();
+
+ return QVariant();
+}
+
+QHash<int, QByteArray> TripDataModel::roleNames() const
+{
+ QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
+ roles[DurationRole] = "duration";
+ roles[DistanceRole] = "distance";
+ roles[CaloriesRole] = "calories";
+ roles[MaxspeedRole] = "maxspeed";
+ roles[AvgspeedRole] = "avgspeed";
+ roles[AscentRole] = "ascent";
+ roles[StartTimeRole] = "starttime";
+
+ return roles;
+}
+
+void TripDataModel::setTrips(const QJsonArray &trips)
+{
+ beginResetModel();
+ m_trips = trips;
+ endResetModel();
+ m_refreshing = false;
+ emit refreshingChanged(m_refreshing);
+ emit refreshed();
+}
+
+void TripDataModel::addTrip(const QJsonObject &trip)
+{
+ // Always append at the beginning of the list, easy to calculate
+ int newRow = 0;
+ beginInsertRows(QModelIndex(), newRow, newRow);
+ m_trips.append(trip);
+ endInsertRows();
+ emit tripDataSaved(newRow);
+}
+
+const QJsonObject TripDataModel::get(int index) const
+{
+ return index == 0 ? m_current : m_trips.at(m_trips.count() - index).toObject();
+}
+
+void TripDataModel::refresh()
+{
+ m_refreshing = true;
+ emit refreshingChanged(m_refreshing);
+ m_datastore->getTrips();
+}
+
+void TripDataModel::endTrip()
+{
+ m_saving = true;
+ emit savingChanged(m_saving);
+ m_datastore->endTrip();
+}
+
+void TripDataModel::setCurrentTrip(const QJsonObject &current)
+{
+ m_current = current;
+ QModelIndex currentIndex = index(0);
+ emit dataChanged(currentIndex, currentIndex);
+}
diff --git a/basicsuite/ebike-ui/tripdatamodel.h b/basicsuite/ebike-ui/tripdatamodel.h
new file mode 100644
index 0000000..56be84e
--- /dev/null
+++ b/basicsuite/ebike-ui/tripdatamodel.h
@@ -0,0 +1,96 @@
+/****************************************************************************
+**
+** Copyright (C) 2017 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the E-Bike demo project.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef TRIPDATAMODEL_H
+#define TRIPDATAMODEL_H
+
+#include <QAbstractListModel>
+#include <QJsonObject>
+#include <QJsonArray>
+
+class DataStore;
+
+class TripDataModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(bool refreshing READ refreshing NOTIFY refreshingChanged)
+ Q_PROPERTY(bool saving READ saving NOTIFY savingChanged)
+
+public:
+ explicit TripDataModel(DataStore *datastore, QObject *parent = nullptr);
+ enum TripDataRoles {
+ DurationRole = Qt::UserRole + 1,
+ DistanceRole,
+ CaloriesRole,
+ MaxspeedRole,
+ AvgspeedRole,
+ AscentRole,
+ StartTimeRole
+ };
+
+public:
+ virtual int rowCount(const QModelIndex &parent) const;
+ virtual QVariant data(const QModelIndex &index, int role) const;
+ virtual QHash<int, QByteArray> roleNames() const;
+
+ Q_INVOKABLE const QJsonObject get(int index) const;
+
+ void setTrips(const QJsonArray &trips);
+ void addTrip(const QJsonObject &trip);
+
+ bool refreshing() const { return m_refreshing; }
+ bool saving() const { return m_saving; }
+
+signals:
+ void refreshed();
+ void refreshingChanged(bool refreshing);
+ void savingChanged(bool saving);
+ void tripDataSaved(int index);
+
+public slots:
+ void refresh();
+ void endTrip();
+ void setCurrentTrip(const QJsonObject &current);
+
+private:
+ DataStore *m_datastore;
+ QJsonArray m_trips;
+ QJsonObject m_current;
+ bool m_refreshing;
+ bool m_saving;
+};
+
+#endif // TRIPDATAMODEL_H
diff --git a/basicsuite/shared/main.cpp b/basicsuite/shared/main.cpp
index 00ceab1..6fba612 100644
--- a/basicsuite/shared/main.cpp
+++ b/basicsuite/shared/main.cpp
@@ -90,6 +90,17 @@ int main(int argc, char **argv)
QFontDatabase::addApplicationFont(":/fonts/TitilliumWeb-Bold.ttf");
QFontDatabase::addApplicationFont(":/fonts/TitilliumWeb-Black.ttf");
+ //For eBike demo
+ QFontDatabase::addApplicationFont(":/fonts/Montserrat-Bold.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Montserrat-Light.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Montserrat-Medium.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Montserrat-Regular.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Teko-Bold.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Teko-Light.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Teko-Medium.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/Teko-Regular.ttf");
+ QFontDatabase::addApplicationFont(":/fonts/fontawesome-webfont.ttf");
+
QString path = app.applicationDirPath();
QPalette pal;