diff options
Diffstat (limited to 'basicsuite/ebike-ui')
149 files changed, 12985 insertions, 0 deletions
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..098a6a8 --- /dev/null +++ b/basicsuite/ebike-ui/datamodelplugin/plugin.cpp @@ -0,0 +1,112 @@ +/**************************************************************************** +** +** 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) + { + Q_UNUSED(uri); + qmlRegisterType<DataStore>("DataStore", 1, 0, "DataStore"); + } + + void initializeEngine(QQmlEngine *engine, const char *uri) + { + Q_UNUSED(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><i>Where do you want to go?</i></source> + <translation><i>Where do you want to go?</i></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><i>Where do you want to go?</i></source> + <translation><i>Mihin haluat mennä?</i></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 Binary files differnew file mode 100644 index 0000000..bf5443c --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-Black.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-BlackItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-BlackItalic.ttf Binary files differnew file mode 100644 index 0000000..3eee3a7 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-BlackItalic.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Bold.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Bold.ttf Binary files differnew file mode 100644 index 0000000..8e9a5f3 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-Bold.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-BoldItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-BoldItalic.ttf Binary files differnew file mode 100644 index 0000000..2c33630 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-BoldItalic.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ExtraBold.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ExtraBold.ttf Binary files differnew file mode 100644 index 0000000..1e3692d --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-ExtraBold.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ExtraBoldItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ExtraBoldItalic.ttf Binary files differnew file mode 100644 index 0000000..5f6c382 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-ExtraBoldItalic.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ExtraLight.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ExtraLight.ttf Binary files differnew file mode 100644 index 0000000..7490dc7 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-ExtraLight.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ExtraLightItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ExtraLightItalic.ttf Binary files differnew file mode 100644 index 0000000..24e1354 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-ExtraLightItalic.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Italic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Italic.ttf Binary files differnew file mode 100644 index 0000000..c5a36e5 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-Italic.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Light.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Light.ttf Binary files differnew file mode 100644 index 0000000..e66dc5b --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-Light.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-LightItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-LightItalic.ttf Binary files differnew file mode 100644 index 0000000..b78b8b7 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-LightItalic.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Medium.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Medium.ttf Binary files differnew file mode 100644 index 0000000..88d70b8 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-Medium.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-MediumItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-MediumItalic.ttf Binary files differnew file mode 100644 index 0000000..225fd18 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-MediumItalic.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Regular.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Regular.ttf Binary files differnew file mode 100644 index 0000000..626355a --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-Regular.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-SemiBold.ttf b/basicsuite/ebike-ui/fonts/Montserrat-SemiBold.ttf Binary files differnew file mode 100644 index 0000000..6157045 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-SemiBold.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-SemiBoldItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-SemiBoldItalic.ttf Binary files differnew file mode 100644 index 0000000..c6dd977 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-SemiBoldItalic.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-Thin.ttf b/basicsuite/ebike-ui/fonts/Montserrat-Thin.ttf Binary files differnew file mode 100644 index 0000000..dc16a02 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-Thin.ttf diff --git a/basicsuite/ebike-ui/fonts/Montserrat-ThinItalic.ttf b/basicsuite/ebike-ui/fonts/Montserrat-ThinItalic.ttf Binary files differnew file mode 100644 index 0000000..b9e12f4 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Montserrat-ThinItalic.ttf 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 Binary files differnew file mode 100644 index 0000000..d061824 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Teko-Bold.ttf diff --git a/basicsuite/ebike-ui/fonts/Teko-Light.ttf b/basicsuite/ebike-ui/fonts/Teko-Light.ttf Binary files differnew file mode 100644 index 0000000..ec5194a --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Teko-Light.ttf diff --git a/basicsuite/ebike-ui/fonts/Teko-Medium.ttf b/basicsuite/ebike-ui/fonts/Teko-Medium.ttf Binary files differnew file mode 100644 index 0000000..cc38086 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Teko-Medium.ttf diff --git a/basicsuite/ebike-ui/fonts/Teko-Regular.ttf b/basicsuite/ebike-ui/fonts/Teko-Regular.ttf Binary files differnew file mode 100644 index 0000000..3161e63 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Teko-Regular.ttf diff --git a/basicsuite/ebike-ui/fonts/Teko-SemiBold.ttf b/basicsuite/ebike-ui/fonts/Teko-SemiBold.ttf Binary files differnew file mode 100644 index 0000000..bc17e5a --- /dev/null +++ b/basicsuite/ebike-ui/fonts/Teko-SemiBold.ttf diff --git a/basicsuite/ebike-ui/fonts/fontawesome-webfont.ttf b/basicsuite/ebike-ui/fonts/fontawesome-webfont.ttf Binary files differnew file mode 100644 index 0000000..35acda2 --- /dev/null +++ b/basicsuite/ebike-ui/fonts/fontawesome-webfont.ttf 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 Binary files differnew file mode 100644 index 0000000..6c67a2c --- /dev/null +++ b/basicsuite/ebike-ui/images/arrow_left.png diff --git a/basicsuite/ebike-ui/images/arrow_right.png b/basicsuite/ebike-ui/images/arrow_right.png Binary files differnew file mode 100644 index 0000000..b8cfb2e --- /dev/null +++ b/basicsuite/ebike-ui/images/arrow_right.png diff --git a/basicsuite/ebike-ui/images/assist.png b/basicsuite/ebike-ui/images/assist.png Binary files differnew file mode 100644 index 0000000..4a2bc95 --- /dev/null +++ b/basicsuite/ebike-ui/images/assist.png diff --git a/basicsuite/ebike-ui/images/battery.png b/basicsuite/ebike-ui/images/battery.png Binary files differnew file mode 100644 index 0000000..170c48d --- /dev/null +++ b/basicsuite/ebike-ui/images/battery.png diff --git a/basicsuite/ebike-ui/images/bike-battery.png b/basicsuite/ebike-ui/images/bike-battery.png Binary files differnew file mode 100644 index 0000000..e354d33 --- /dev/null +++ b/basicsuite/ebike-ui/images/bike-battery.png diff --git a/basicsuite/ebike-ui/images/bike-brakes.png b/basicsuite/ebike-ui/images/bike-brakes.png Binary files differnew file mode 100644 index 0000000..c271d75 --- /dev/null +++ b/basicsuite/ebike-ui/images/bike-brakes.png diff --git a/basicsuite/ebike-ui/images/bike-chain.png b/basicsuite/ebike-ui/images/bike-chain.png Binary files differnew file mode 100644 index 0000000..07879a4 --- /dev/null +++ b/basicsuite/ebike-ui/images/bike-chain.png diff --git a/basicsuite/ebike-ui/images/bike-frontwheel.png b/basicsuite/ebike-ui/images/bike-frontwheel.png Binary files differnew file mode 100644 index 0000000..ab9297f --- /dev/null +++ b/basicsuite/ebike-ui/images/bike-frontwheel.png diff --git a/basicsuite/ebike-ui/images/bike-gears.png b/basicsuite/ebike-ui/images/bike-gears.png Binary files differnew file mode 100644 index 0000000..c977644 --- /dev/null +++ b/basicsuite/ebike-ui/images/bike-gears.png diff --git a/basicsuite/ebike-ui/images/bike-light.png b/basicsuite/ebike-ui/images/bike-light.png Binary files differnew file mode 100644 index 0000000..b18da3c --- /dev/null +++ b/basicsuite/ebike-ui/images/bike-light.png diff --git a/basicsuite/ebike-ui/images/bike-rearwheel.png b/basicsuite/ebike-ui/images/bike-rearwheel.png Binary files differnew file mode 100644 index 0000000..fb02923 --- /dev/null +++ b/basicsuite/ebike-ui/images/bike-rearwheel.png diff --git a/basicsuite/ebike-ui/images/blue_circle_gps_area.png b/basicsuite/ebike-ui/images/blue_circle_gps_area.png Binary files differnew file mode 100644 index 0000000..06e1b6e --- /dev/null +++ b/basicsuite/ebike-ui/images/blue_circle_gps_area.png diff --git a/basicsuite/ebike-ui/images/calories.png b/basicsuite/ebike-ui/images/calories.png Binary files differnew file mode 100644 index 0000000..fe3cbb1 --- /dev/null +++ b/basicsuite/ebike-ui/images/calories.png diff --git a/basicsuite/ebike-ui/images/checkmark.png b/basicsuite/ebike-ui/images/checkmark.png Binary files differnew file mode 100644 index 0000000..0e1b387 --- /dev/null +++ b/basicsuite/ebike-ui/images/checkmark.png diff --git a/basicsuite/ebike-ui/images/curtain_shadow_handle.png b/basicsuite/ebike-ui/images/curtain_shadow_handle.png Binary files differnew file mode 100644 index 0000000..afaf3ec --- /dev/null +++ b/basicsuite/ebike-ui/images/curtain_shadow_handle.png diff --git a/basicsuite/ebike-ui/images/curtain_up_arrow.png b/basicsuite/ebike-ui/images/curtain_up_arrow.png Binary files differnew file mode 100644 index 0000000..97095d0 --- /dev/null +++ b/basicsuite/ebike-ui/images/curtain_up_arrow.png diff --git a/basicsuite/ebike-ui/images/fps_icon.png b/basicsuite/ebike-ui/images/fps_icon.png Binary files differnew file mode 100644 index 0000000..4cecab7 --- /dev/null +++ b/basicsuite/ebike-ui/images/fps_icon.png diff --git a/basicsuite/ebike-ui/images/info.png b/basicsuite/ebike-ui/images/info.png Binary files differnew file mode 100644 index 0000000..6ad9192 --- /dev/null +++ b/basicsuite/ebike-ui/images/info.png diff --git a/basicsuite/ebike-ui/images/info_selected.png b/basicsuite/ebike-ui/images/info_selected.png Binary files differnew file mode 100644 index 0000000..25056ff --- /dev/null +++ b/basicsuite/ebike-ui/images/info_selected.png diff --git a/basicsuite/ebike-ui/images/lights_off.png b/basicsuite/ebike-ui/images/lights_off.png Binary files differnew file mode 100644 index 0000000..4ad0abd --- /dev/null +++ b/basicsuite/ebike-ui/images/lights_off.png diff --git a/basicsuite/ebike-ui/images/lights_on.png b/basicsuite/ebike-ui/images/lights_on.png Binary files differnew file mode 100644 index 0000000..6da9893 --- /dev/null +++ b/basicsuite/ebike-ui/images/lights_on.png diff --git a/basicsuite/ebike-ui/images/list.png b/basicsuite/ebike-ui/images/list.png Binary files differnew file mode 100644 index 0000000..2e1633d --- /dev/null +++ b/basicsuite/ebike-ui/images/list.png diff --git a/basicsuite/ebike-ui/images/list_selected.png b/basicsuite/ebike-ui/images/list_selected.png Binary files differnew file mode 100644 index 0000000..1c61a5e --- /dev/null +++ b/basicsuite/ebike-ui/images/list_selected.png diff --git a/basicsuite/ebike-ui/images/map-marker.png b/basicsuite/ebike-ui/images/map-marker.png Binary files differnew file mode 100644 index 0000000..e2dd669 --- /dev/null +++ b/basicsuite/ebike-ui/images/map-marker.png diff --git a/basicsuite/ebike-ui/images/map_btn_shadow.png b/basicsuite/ebike-ui/images/map_btn_shadow.png Binary files differnew file mode 100644 index 0000000..c40b9b1 --- /dev/null +++ b/basicsuite/ebike-ui/images/map_btn_shadow.png diff --git a/basicsuite/ebike-ui/images/map_destination.png b/basicsuite/ebike-ui/images/map_destination.png Binary files differnew file mode 100644 index 0000000..a41b134 --- /dev/null +++ b/basicsuite/ebike-ui/images/map_destination.png diff --git a/basicsuite/ebike-ui/images/map_locate.png b/basicsuite/ebike-ui/images/map_locate.png Binary files differnew file mode 100644 index 0000000..2579c94 --- /dev/null +++ b/basicsuite/ebike-ui/images/map_locate.png diff --git a/basicsuite/ebike-ui/images/map_location_arrow.png b/basicsuite/ebike-ui/images/map_location_arrow.png Binary files differnew file mode 100644 index 0000000..9ec4d44 --- /dev/null +++ b/basicsuite/ebike-ui/images/map_location_arrow.png diff --git a/basicsuite/ebike-ui/images/map_zoomin.png b/basicsuite/ebike-ui/images/map_zoomin.png Binary files differnew file mode 100644 index 0000000..aea24fe --- /dev/null +++ b/basicsuite/ebike-ui/images/map_zoomin.png diff --git a/basicsuite/ebike-ui/images/map_zoomout.png b/basicsuite/ebike-ui/images/map_zoomout.png Binary files differnew file mode 100644 index 0000000..e3f90da --- /dev/null +++ b/basicsuite/ebike-ui/images/map_zoomout.png diff --git a/basicsuite/ebike-ui/images/nav_bear_l.png b/basicsuite/ebike-ui/images/nav_bear_l.png Binary files differnew file mode 100644 index 0000000..b078fbf --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_bear_l.png diff --git a/basicsuite/ebike-ui/images/nav_bear_r.png b/basicsuite/ebike-ui/images/nav_bear_r.png Binary files differnew file mode 100644 index 0000000..84b48c1 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_bear_r.png diff --git a/basicsuite/ebike-ui/images/nav_hard_l.png b/basicsuite/ebike-ui/images/nav_hard_l.png Binary files differnew file mode 100644 index 0000000..7235cc3 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_hard_l.png diff --git a/basicsuite/ebike-ui/images/nav_hard_r.png b/basicsuite/ebike-ui/images/nav_hard_r.png Binary files differnew file mode 100644 index 0000000..4a19b65 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_hard_r.png diff --git a/basicsuite/ebike-ui/images/nav_left.png b/basicsuite/ebike-ui/images/nav_left.png Binary files differnew file mode 100644 index 0000000..5a5aa26 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_left.png diff --git a/basicsuite/ebike-ui/images/nav_light_left.png b/basicsuite/ebike-ui/images/nav_light_left.png Binary files differnew file mode 100644 index 0000000..7c88914 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_light_left.png diff --git a/basicsuite/ebike-ui/images/nav_light_right.png b/basicsuite/ebike-ui/images/nav_light_right.png Binary files differnew file mode 100644 index 0000000..3bb07ff --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_light_right.png diff --git a/basicsuite/ebike-ui/images/nav_nodir.png b/basicsuite/ebike-ui/images/nav_nodir.png Binary files differnew file mode 100644 index 0000000..7fc92a7 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_nodir.png diff --git a/basicsuite/ebike-ui/images/nav_right.png b/basicsuite/ebike-ui/images/nav_right.png Binary files differnew file mode 100644 index 0000000..8eecf13 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_right.png diff --git a/basicsuite/ebike-ui/images/nav_straight.png b/basicsuite/ebike-ui/images/nav_straight.png Binary files differnew file mode 100644 index 0000000..44a58c8 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_straight.png diff --git a/basicsuite/ebike-ui/images/nav_uturn_l.png b/basicsuite/ebike-ui/images/nav_uturn_l.png Binary files differnew file mode 100644 index 0000000..c9714d3 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_uturn_l.png diff --git a/basicsuite/ebike-ui/images/nav_uturn_r.png b/basicsuite/ebike-ui/images/nav_uturn_r.png Binary files differnew file mode 100644 index 0000000..a4aee58 --- /dev/null +++ b/basicsuite/ebike-ui/images/nav_uturn_r.png diff --git a/basicsuite/ebike-ui/images/navigation_widget_shadow.png b/basicsuite/ebike-ui/images/navigation_widget_shadow.png Binary files differnew file mode 100644 index 0000000..4de81eb --- /dev/null +++ b/basicsuite/ebike-ui/images/navigation_widget_shadow.png diff --git a/basicsuite/ebike-ui/images/nextsong.png b/basicsuite/ebike-ui/images/nextsong.png Binary files differnew file mode 100644 index 0000000..90d6520 --- /dev/null +++ b/basicsuite/ebike-ui/images/nextsong.png diff --git a/basicsuite/ebike-ui/images/nextsong_pressed.png b/basicsuite/ebike-ui/images/nextsong_pressed.png Binary files differnew file mode 100644 index 0000000..b764100 --- /dev/null +++ b/basicsuite/ebike-ui/images/nextsong_pressed.png diff --git a/basicsuite/ebike-ui/images/ok.png b/basicsuite/ebike-ui/images/ok.png Binary files differnew file mode 100644 index 0000000..6d862a4 --- /dev/null +++ b/basicsuite/ebike-ui/images/ok.png diff --git a/basicsuite/ebike-ui/images/pause.png b/basicsuite/ebike-ui/images/pause.png Binary files differnew file mode 100644 index 0000000..9425527 --- /dev/null +++ b/basicsuite/ebike-ui/images/pause.png diff --git a/basicsuite/ebike-ui/images/pause_pressed.png b/basicsuite/ebike-ui/images/pause_pressed.png Binary files differnew file mode 100644 index 0000000..b90e029 --- /dev/null +++ b/basicsuite/ebike-ui/images/pause_pressed.png diff --git a/basicsuite/ebike-ui/images/placeholder.png b/basicsuite/ebike-ui/images/placeholder.png Binary files differnew file mode 100644 index 0000000..a0e647e --- /dev/null +++ b/basicsuite/ebike-ui/images/placeholder.png diff --git a/basicsuite/ebike-ui/images/play.png b/basicsuite/ebike-ui/images/play.png Binary files differnew file mode 100644 index 0000000..d31ae43 --- /dev/null +++ b/basicsuite/ebike-ui/images/play.png diff --git a/basicsuite/ebike-ui/images/play_pressed.png b/basicsuite/ebike-ui/images/play_pressed.png Binary files differnew file mode 100644 index 0000000..3cd498b --- /dev/null +++ b/basicsuite/ebike-ui/images/play_pressed.png diff --git a/basicsuite/ebike-ui/images/prevsong.png b/basicsuite/ebike-ui/images/prevsong.png Binary files differnew file mode 100644 index 0000000..63d2619 --- /dev/null +++ b/basicsuite/ebike-ui/images/prevsong.png diff --git a/basicsuite/ebike-ui/images/prevsong_pressed.png b/basicsuite/ebike-ui/images/prevsong_pressed.png Binary files differnew file mode 100644 index 0000000..dbe5690 --- /dev/null +++ b/basicsuite/ebike-ui/images/prevsong_pressed.png diff --git a/basicsuite/ebike-ui/images/search.png b/basicsuite/ebike-ui/images/search.png Binary files differnew file mode 100644 index 0000000..f840e72 --- /dev/null +++ b/basicsuite/ebike-ui/images/search.png diff --git a/basicsuite/ebike-ui/images/search_cancel.png b/basicsuite/ebike-ui/images/search_cancel.png Binary files differnew file mode 100644 index 0000000..679ce32 --- /dev/null +++ b/basicsuite/ebike-ui/images/search_cancel.png diff --git a/basicsuite/ebike-ui/images/settings.png b/basicsuite/ebike-ui/images/settings.png Binary files differnew file mode 100644 index 0000000..9651de6 --- /dev/null +++ b/basicsuite/ebike-ui/images/settings.png diff --git a/basicsuite/ebike-ui/images/settings_selected.png b/basicsuite/ebike-ui/images/settings_selected.png Binary files differnew file mode 100644 index 0000000..2e8cd7f --- /dev/null +++ b/basicsuite/ebike-ui/images/settings_selected.png diff --git a/basicsuite/ebike-ui/images/small_input_box_shadow.png b/basicsuite/ebike-ui/images/small_input_box_shadow.png Binary files differnew file mode 100644 index 0000000..401c0f2 --- /dev/null +++ b/basicsuite/ebike-ui/images/small_input_box_shadow.png diff --git a/basicsuite/ebike-ui/images/small_speedometer_arrow.png b/basicsuite/ebike-ui/images/small_speedometer_arrow.png Binary files differnew file mode 100644 index 0000000..4959e8d --- /dev/null +++ b/basicsuite/ebike-ui/images/small_speedometer_arrow.png diff --git a/basicsuite/ebike-ui/images/small_speedometer_shadow.png b/basicsuite/ebike-ui/images/small_speedometer_shadow.png Binary files differnew file mode 100644 index 0000000..5b37857 --- /dev/null +++ b/basicsuite/ebike-ui/images/small_speedometer_shadow.png diff --git a/basicsuite/ebike-ui/images/speed.png b/basicsuite/ebike-ui/images/speed.png Binary files differnew file mode 100644 index 0000000..17d2e6d --- /dev/null +++ b/basicsuite/ebike-ui/images/speed.png diff --git a/basicsuite/ebike-ui/images/spinner.png b/basicsuite/ebike-ui/images/spinner.png Binary files differnew file mode 100644 index 0000000..e59efb2 --- /dev/null +++ b/basicsuite/ebike-ui/images/spinner.png diff --git a/basicsuite/ebike-ui/images/top_curtain_drag.png b/basicsuite/ebike-ui/images/top_curtain_drag.png Binary files differnew file mode 100644 index 0000000..28388c6 --- /dev/null +++ b/basicsuite/ebike-ui/images/top_curtain_drag.png diff --git a/basicsuite/ebike-ui/images/trip.png b/basicsuite/ebike-ui/images/trip.png Binary files differnew file mode 100644 index 0000000..5a1761c --- /dev/null +++ b/basicsuite/ebike-ui/images/trip.png diff --git a/basicsuite/ebike-ui/images/warning.png b/basicsuite/ebike-ui/images/warning.png Binary files differnew file mode 100644 index 0000000..0c37552 --- /dev/null +++ b/basicsuite/ebike-ui/images/warning.png 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 ¢er) +{ + 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 ¢er); + 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 Binary files differnew file mode 100644 index 0000000..5e9edea --- /dev/null +++ b/basicsuite/ebike-ui/mostrecent.bson 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 Binary files differnew file mode 100644 index 0000000..4cd0851 --- /dev/null +++ b/basicsuite/ebike-ui/preview_l.jpg 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 ¤t) +{ + 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 ¤t); + +private: + DataStore *m_datastore; + QJsonArray m_trips; + QJsonObject m_current; + bool m_refreshing; + bool m_saving; +}; + +#endif // TRIPDATAMODEL_H |