Improve energy views

This commit is contained in:
Michael Zanetti 2021-01-04 00:39:51 +01:00
parent 0549a42032
commit 05d1938167
4 changed files with 341 additions and 330 deletions

View File

@ -35,9 +35,9 @@ Item {
"presencesensor": "darkblue",
"closablesensor": "green",
"smartmeterproducer": "lightgreen",
"extendedsmartmeterproducer": "lightgreen",
"smartmeterconsumer": "orange",
"extendedsmartmeterproducer": "blue",
"extendedsmartmeterconsumer": "blue",
"extendedsmartmeterconsumer": "orange",
"heating" : "gainsboro",
"thermostat": "dodgerblue",
"irrigation": "lightblue",

View File

@ -36,8 +36,8 @@ ToolButton {
property alias color: image.color
contentItem: Item {
height: 20
width: 20
height: app.iconSize
width: app.iconSize
ColorIcon {
id: image
anchors.fill: parent

View File

@ -72,11 +72,27 @@ Item {
viewStartTime: xAxis.min
}
ColumnLayout {
ChartView {
id: chartView
anchors.fill: parent
spacing: 0
margins.top: app.iconSize + app.margins
margins.bottom: app.margins / 2
margins.left: 0
margins.right: 0
backgroundColor: Style.tileBackgroundColor
backgroundRoundness: Style.tileRadius
legend.visible: false
legend.labelColor: Style.foregroundColor
titleColor: Style.foregroundColor
titleFont.pixelSize: app.largeFont
animationDuration: 300
animationOptions: ChartView.SeriesAnimations
RowLayout {
Layout.leftMargin: app.margins; Layout.rightMargin: app.margins
anchors { left: parent.left; top: parent.top; right: parent.right; topMargin: app.margins / 2; leftMargin: app.margins; rightMargin: app.margins }
ColorIcon {
Layout.preferredHeight: app.iconSize
Layout.preferredWidth: app.iconSize
@ -84,21 +100,13 @@ Item {
visible: root.iconSource.length > 0
color: root.color
}
Led {
visible: root.stateType.type.toLowerCase() === "bool"
state: root.valueState.value === true ? "on" : "off"
}
Label {
Layout.fillWidth: true
text: root.stateType.type.toLowerCase() === "bool"
? root.stateType.displayName
: 1.0 * Math.round(Types.toUiValue(root.valueState.value, root.stateType.unit) * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + Types.toUiUnit(root.stateType.unit)
? root.stateType.displayName
: 1.0 * Math.round(Types.toUiValue(root.valueState.value, root.stateType.unit) * Math.pow(10, root.roundTo)) / Math.pow(10, root.roundTo) + " " + Types.toUiUnit(root.stateType.unit)
font.pixelSize: app.largeFont
}
HeaderButton {
imageSource: "../images/zoom-out.svg"
onClicked: {
@ -106,7 +114,6 @@ Item {
xAxis.min = newTime;
}
}
HeaderButton {
imageSource: "../images/zoom-in.svg"
enabled: xAxis.timeDiff > (60 * 30)
@ -117,333 +124,318 @@ Item {
}
}
ChartView {
id: chartView
Layout.fillWidth: true
Layout.fillHeight: true
margins.top: 0
margins.bottom: 0
margins.left: 0
margins.right: 0
backgroundColor: Material.background
legend.visible: false
legend.labelColor: Style.foregroundColor
animationDuration: 300
animationOptions: ChartView.SeriesAnimations
ValueAxis {
id: yAxis
max: {
switch (root.stateType.type.toLowerCase()) {
case "bool":
return 1;
default:
Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05))
}
}
min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05))
// onMinChanged: applyNiceNumbers();
// onMaxChanged: applyNiceNumbers();
labelsFont.pixelSize: app.smallFont
labelFormat: {
switch (root.stateType.type.toLowerCase()) {
case "bool":
return "x";
default:
return "%d";
}
}
labelsColor: Style.foregroundColor
tickCount: root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40
color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2)
gridLineColor: color
}
ValueAxis {
id: connectedAxis
min: 0
max: 1
visible: false
}
DateTimeAxis {
id: xAxis
gridVisible: false
color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2)
tickCount: chartView.width / 70
labelsFont.pixelSize: app.smallFont
labelsColor: Style.foregroundColor
property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000
function getTimeSpanString() {
var td = Math.round(timeDiff)
if (td < 60) {
return qsTr("%n seconds", "", td);
}
td = Math.round(td / 60)
if (td < 60) {
return qsTr("%n minutes", "", td);
}
td = Math.round(td / 60)
if (td < 48) {
return qsTr("%n hours", "", td);
}
td = Math.round(td / 24);
if (td < 14) {
return qsTr("%n days", "", td);
}
td = Math.round(td / 7)
if (td < 9) {
return qsTr("%n weeks", "", td);
}
td = Math.round(td * 7 / 30)
if (td < 24) {
return qsTr("%n months", "", td);
}
td = Math.round(td * 30 / 356)
return qsTr("%n years", "", td)
}
titleText: {
if (xAxis.min.getYear() === xAxis.max.getYear()
&& xAxis.min.getMonth() === xAxis.max.getMonth()
&& xAxis.min.getDate() === xAxis.max.getDate()) {
return Qt.formatDate(xAxis.min) + " (" + getTimeSpanString() + ")"
}
return Qt.formatDate(xAxis.min) + " - " + Qt.formatDate(xAxis.max) + " (" + getTimeSpanString() + ")"
}
titleBrush: Style.foregroundColor
format: {
if (timeDiff < 60) { // one minute
return "mm:ss"
}
if (timeDiff < 60 * 60) { // one hour
return "hh:mm"
}
if (timeDiff < 60 * 60 * 24 * 2) { // two day
return "hh:mm"
}
if (timeDiff < 60 * 60 * 24 * 7) { // one week
return "ddd hh:mm"
}
if (timeDiff < 60 * 60 * 24 * 7 * 30) { // one month
return "dd.MM."
}
return "MMM yy"
}
min: {
var date = new Date();
date.setTime(date.getTime() - (1000 * 60 * 60 * 6) + 2000);
return date;
}
max: {
var date = new Date();
date.setTime(date.getTime() + 2000)
return date;
ValueAxis {
id: yAxis
max: {
switch (root.stateType.type.toLowerCase()) {
case "bool":
return 1;
default:
Math.ceil(logsModelNg.maxValue + Math.abs(logsModelNg.maxValue * .05))
}
}
AreaSeries {
axisX: xAxis
axisY: connectedAxis
name: qsTr("Not connected")
visible: root.hasConnectable
upperSeries: LineSeries {
XYPoint {x: xAxis.min.getTime(); y: 1}
XYPoint {x: xAxis.max.getTime(); y: 1}
min: Math.floor(logsModelNg.minValue - Math.abs(logsModelNg.minValue * .05))
// onMinChanged: applyNiceNumbers();
// onMaxChanged: applyNiceNumbers();
labelsFont.pixelSize: app.smallFont
labelFormat: {
switch (root.stateType.type.toLowerCase()) {
case "bool":
return "x";
default:
return "%d";
}
}
labelsColor: Style.foregroundColor
tickCount: root.stateType.type.toLowerCase() === "bool" ? 2 : chartView.height / 40
color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2)
gridLineColor: color
}
lowerSeries: LineSeries {
id: connectedLineSeries
onPointAdded: {
var newPoint = connectedLineSeries.at(index)
// print("pointadded", newPoint.x, newPoint.y)
}
ValueAxis {
id: connectedAxis
min: 0
max: 1
visible: false
}
DateTimeAxis {
id: xAxis
gridVisible: false
color: Qt.rgba(Style.foregroundColor.r, Style.foregroundColor.g, Style.foregroundColor.b, .2)
tickCount: chartView.width / 70
labelsFont.pixelSize: app.smallFont
labelsColor: Style.foregroundColor
property int timeDiff: (xAxis.max.getTime() - xAxis.min.getTime()) / 1000
function getTimeSpanString() {
var td = Math.round(timeDiff)
if (td < 60) {
return qsTr("%n seconds", "", td);
}
color: "#55ff0000"
borderWidth: 0
td = Math.round(td / 60)
if (td < 60) {
return qsTr("%n minutes", "", td);
}
td = Math.round(td / 60)
if (td < 48) {
return qsTr("%n hours", "", td);
}
td = Math.round(td / 24);
if (td < 14) {
return qsTr("%n days", "", td);
}
td = Math.round(td / 7)
if (td < 9) {
return qsTr("%n weeks", "", td);
}
td = Math.round(td * 7 / 30)
if (td < 24) {
return qsTr("%n months", "", td);
}
td = Math.round(td * 30 / 356)
return qsTr("%n years", "", td)
}
AreaSeries {
id: mainSeries
axisX: xAxis
axisY: yAxis
name: root.stateType.displayName
borderColor: root.color
borderWidth: 4
lowerSeries: LineSeries {
id: lineSeries0
XYPoint { x: xAxis.max.getTime(); y: 0 }
XYPoint { x: xAxis.min.getTime(); y: 0 }
titleText: {
if (xAxis.min.getYear() === xAxis.max.getYear()
&& xAxis.min.getMonth() === xAxis.max.getMonth()
&& xAxis.min.getDate() === xAxis.max.getDate()) {
return Qt.formatDate(xAxis.min) + " (" + getTimeSpanString() + ")"
}
return Qt.formatDate(xAxis.min) + " - " + Qt.formatDate(xAxis.max) + " (" + getTimeSpanString() + ")"
}
titleBrush: Style.foregroundColor
format: {
if (timeDiff < 60) { // one minute
return "mm:ss"
}
if (timeDiff < 60 * 60) { // one hour
return "hh:mm"
}
if (timeDiff < 60 * 60 * 24 * 2) { // two day
return "hh:mm"
}
if (timeDiff < 60 * 60 * 24 * 7) { // one week
return "ddd hh:mm"
}
if (timeDiff < 60 * 60 * 24 * 7 * 30) { // one month
return "dd.MM."
}
return "MMM yy"
}
min: {
var date = new Date();
date.setTime(date.getTime() - (1000 * 60 * 60 * 6) + 2000);
return date;
}
max: {
var date = new Date();
date.setTime(date.getTime() + 2000)
return date;
}
}
AreaSeries {
axisX: xAxis
axisY: connectedAxis
name: qsTr("Not connected")
visible: root.hasConnectable
upperSeries: LineSeries {
XYPoint {x: xAxis.min.getTime(); y: 1}
XYPoint {x: xAxis.max.getTime(); y: 1}
}
lowerSeries: LineSeries {
id: connectedLineSeries
onPointAdded: {
var newPoint = connectedLineSeries.at(index)
// print("pointadded", newPoint.x, newPoint.y)
}
upperSeries: LineSeries {
id: lineSeries1
onPointAdded: {
var newPoint = lineSeries1.at(index)
// print("pointadded", newPoint.x, newPoint.y)
}
color: "#55ff0000"
borderWidth: 0
}
if (newPoint.x > lineSeries0.at(0).x) {
lineSeries0.replace(0, newPoint.x, 0)
}
if (newPoint.x < lineSeries0.at(1).x) {
lineSeries0.replace(1, newPoint.x, 0)
}
AreaSeries {
id: mainSeries
axisX: xAxis
axisY: yAxis
name: root.stateType.displayName
borderColor: root.color
borderWidth: 4
lowerSeries: LineSeries {
id: lineSeries0
XYPoint { x: xAxis.max.getTime(); y: 0 }
XYPoint { x: xAxis.min.getTime(); y: 0 }
}
if (newPoint.x <= xAxis.max.getTime() || logsModelNg.busy) {
return;
}
var diffMaxToNew = newPoint.x - xAxis.max.getTime();
if (diffMaxToNew < 1000 * 60 * 5) {
chartView.animationOptions = ChartView.NoAnimation
var newMin = xAxis.min.getTime() + diffMaxToNew;
xAxis.max = new Date(newPoint.x);
xAxis.min = new Date(newMin)
chartView.animationOptions = ChartView.SeriesAnimations
}
upperSeries: LineSeries {
id: lineSeries1
onPointAdded: {
var newPoint = lineSeries1.at(index)
// print("pointadded", newPoint.x, newPoint.y)
if (newPoint.x > lineSeries0.at(0).x) {
lineSeries0.replace(0, newPoint.x, 0)
}
if (newPoint.x < lineSeries0.at(1).x) {
lineSeries0.replace(1, newPoint.x, 0)
}
}
color: Qt.rgba(root.color.r, root.color.g, root.color.b, .3)
onHovered: {
markClosestPoint(point)
}
function markClosestPoint(point) {
if (lineSeries1.count == 0) {
if (newPoint.x <= xAxis.max.getTime() || logsModelNg.busy) {
return;
}
if (lineSeries1.count == 1) {
selectedHighlights.removePoints(0, selectedHighlights.count)
selectedHighlights.append(lineSeries1.at(0).x, lineSeries1.at(1).y)
return;
var diffMaxToNew = newPoint.x - xAxis.max.getTime();
if (diffMaxToNew < 1000 * 60 * 5) {
chartView.animationOptions = ChartView.NoAnimation
var newMin = xAxis.min.getTime() + diffMaxToNew;
xAxis.max = new Date(newPoint.x);
xAxis.min = new Date(newMin)
chartView.animationOptions = ChartView.SeriesAnimations
}
var searchIndex = Math.floor(lineSeries1.count / 2)
var previousIndex = 0;
var nextIndex = lineSeries1.count - 1;
}
}
color: Qt.rgba(root.color.r, root.color.g, root.color.b, .3)
onHovered: {
markClosestPoint(point)
}
while (previousIndex + 1 != nextIndex) {
if (point.x < lineSeries1.at(searchIndex).x) {
previousIndex = searchIndex;
} else if (point.x > lineSeries1.at(searchIndex).x) {
nextIndex = searchIndex;
}
searchIndex = previousIndex + Math.floor((nextIndex - previousIndex) / 2);
}
var diffToPrevious = Math.abs(point.x - lineSeries1.at(previousIndex).x)
var diffToNext = Math.abs(point.x - lineSeries1.at(nextIndex).x)
var closestPoint = diffToPrevious < diffToNext ? lineSeries1.at(previousIndex) : lineSeries1.at(nextIndex);
function markClosestPoint(point) {
if (lineSeries1.count == 0) {
return;
}
if (lineSeries1.count == 1) {
selectedHighlights.removePoints(0, selectedHighlights.count)
selectedHighlights.append(closestPoint.x, closestPoint.y)
selectedHighlights.append(lineSeries1.at(0).x, lineSeries1.at(1).y)
return;
}
}
ScatterSeries {
id: selectedHighlights
color: root.color
markerSize: 10
borderWidth: 2
borderColor: root.color
axisX: xAxis
axisY: yAxis
pointLabelsVisible: root.stateType.type.toLowerCase() !== "bool"
pointLabelsColor: Style.foregroundColor
pointLabelsFont.pixelSize: app.smallFont
pointLabelsFormat: "@yPoint"
pointLabelsClipping: false
}
var searchIndex = Math.floor(lineSeries1.count / 2)
var previousIndex = 0;
var nextIndex = lineSeries1.count - 1;
BusyIndicator {
anchors.centerIn: parent
visible: logsModelNg.busy
}
MouseArea {
id: scrollMouseArea
x: chartView.plotArea.x
y: chartView.plotArea.y
width: chartView.plotArea.width
height: chartView.plotArea.height
property int lastX: 0
property int startX: 0
preventStealing: false
property bool autoScroll: true
function scrollRightLimited(dx) {
chartView.animationOptions = ChartView.NoAnimation
var now = new Date()
// if we're already at the limit, don't even start scrolling
if (dx < 0 || xAxis.max < now) {
chartView.scrollRight(dx)
while (previousIndex + 1 != nextIndex) {
if (point.x < lineSeries1.at(searchIndex).x) {
previousIndex = searchIndex;
} else if (point.x > lineSeries1.at(searchIndex).x) {
nextIndex = searchIndex;
}
// figure out if we scrolled too far
var overshoot = xAxis.max.getTime() - now.getTime()
// print("overshoot is:", overshoot, "oldMax", xAxis.max, "newMax", now, "oldMin", xAxis.min, "newMin", new Date(xAxis.min.getTime() - overshoot))
if (overshoot > 0) {
var range = xAxis.max - xAxis.min
xAxis.max = now
xAxis.min = new Date(xAxis.max.getTime() - range)
}
// If the user scrolled closer than 5 pixels to the right edge, enable autoscroll
autoScroll = overshoot > -5;
chartView.animationOptions = ChartView.SeriesAnimations
searchIndex = previousIndex + Math.floor((nextIndex - previousIndex) / 2);
}
var diffToPrevious = Math.abs(point.x - lineSeries1.at(previousIndex).x)
var diffToNext = Math.abs(point.x - lineSeries1.at(nextIndex).x)
var closestPoint = diffToPrevious < diffToNext ? lineSeries1.at(previousIndex) : lineSeries1.at(nextIndex);
function zoomInLimited(dy) {
chartView.animationOptions = ChartView.NoAnimation
var oldMax = xAxis.max;
chartView.scrollRight(dy);
xAxis.min = new Date(xAxis.min.getTime() - xAxis.timeDiff * 1000 * 2)
chartView.animationOptions = ChartView.SeriesAnimations
selectedHighlights.removePoints(0, selectedHighlights.count)
selectedHighlights.append(closestPoint.x, closestPoint.y)
}
}
ScatterSeries {
id: selectedHighlights
color: root.color
markerSize: 10
borderWidth: 2
borderColor: root.color
axisX: xAxis
axisY: yAxis
pointLabelsVisible: root.stateType.type.toLowerCase() !== "bool"
pointLabelsColor: Style.foregroundColor
pointLabelsFont.pixelSize: app.smallFont
pointLabelsFormat: "@yPoint"
pointLabelsClipping: false
}
BusyIndicator {
anchors.centerIn: parent
visible: logsModelNg.busy
}
MouseArea {
id: scrollMouseArea
x: chartView.plotArea.x
y: chartView.plotArea.y
width: chartView.plotArea.width
height: chartView.plotArea.height
property int lastX: 0
property int startX: 0
preventStealing: false
property bool autoScroll: true
function scrollRightLimited(dx) {
chartView.animationOptions = ChartView.NoAnimation
var now = new Date()
// if we're already at the limit, don't even start scrolling
if (dx < 0 || xAxis.max < now) {
chartView.scrollRight(dx)
}
// figure out if we scrolled too far
var overshoot = xAxis.max.getTime() - now.getTime()
// print("overshoot is:", overshoot, "oldMax", xAxis.max, "newMax", now, "oldMin", xAxis.min, "newMin", new Date(xAxis.min.getTime() - overshoot))
if (overshoot > 0) {
var range = xAxis.max - xAxis.min
xAxis.max = now
xAxis.min = new Date(xAxis.max.getTime() - range)
}
// If the user scrolled closer than 5 pixels to the right edge, enable autoscroll
autoScroll = overshoot > -5;
onPressed: {
chartView.animationOptions = ChartView.SeriesAnimations
}
function zoomInLimited(dy) {
chartView.animationOptions = ChartView.NoAnimation
var oldMax = xAxis.max;
chartView.scrollRight(dy);
xAxis.min = new Date(xAxis.min.getTime() - xAxis.timeDiff * 1000 * 2)
chartView.animationOptions = ChartView.SeriesAnimations
}
onPressed: {
lastX = mouse.x
startX = mouse.x
}
onClicked: {
var pt = chartView.mapToValue(Qt.point(mouse.x + chartView.plotArea.x, mouse.y + chartView.plotArea.y), mainSeries)
mainSeries.markClosestPoint(pt)
}
onWheel: {
scrollRightLimited(-wheel.pixelDelta.x)
// zoomInLimited(wheel.pixelDelta.y)
}
onPositionChanged: {
if (lastX !== mouse.x) {
scrollRightLimited(lastX - mouseX)
lastX = mouse.x
startX = mouse.x
}
onClicked: {
var pt = chartView.mapToValue(Qt.point(mouse.x + chartView.plotArea.x, mouse.y + chartView.plotArea.y), mainSeries)
mainSeries.markClosestPoint(pt)
}
onWheel: {
scrollRightLimited(-wheel.pixelDelta.x)
// zoomInLimited(wheel.pixelDelta.y)
if (Math.abs(startX - mouse.x) > 10) {
preventStealing = true;
}
}
onPositionChanged: {
if (lastX !== mouse.x) {
scrollRightLimited(lastX - mouseX)
lastX = mouse.x
}
if (Math.abs(startX - mouse.x) > 10) {
preventStealing = true;
}
}
onReleased: preventStealing = false;
onReleased: preventStealing = false;
Timer {
running: scrollMouseArea.autoScroll
interval: 1000
repeat: true
onTriggered: {
scrollMouseArea.scrollRightLimited(10)
}
Timer {
running: scrollMouseArea.autoScroll
interval: 1000
repeat: true
onTriggered: {
scrollMouseArea.scrollRightLimited(10)
}
}
}
}
}

View File

@ -38,6 +38,11 @@ import "../customviews"
DevicePageBase {
id: root
readonly property State totalEnergyConsumedState: root.thing.stateByName("totalEnergyConsumed")
readonly property StateType totalEnergyConsumedStateType: root.thing.thingClass.stateTypes.findByName("totalEnergyConsumed")
readonly property State totalEnergyProducedState: root.thing.stateByName("totalEnergyProduced")
readonly property StateType totalEnergyProducedStateType: root.thing.thingClass.stateTypes.findByName("totalEnergyProduced")
Flickable {
anchors.fill: parent
contentHeight: contentGrid.height
@ -45,34 +50,48 @@ DevicePageBase {
GridLayout {
id: contentGrid
width: parent.width
columns: Math.min(width / 300, contentModel.count)
width: parent.width - app.margins
anchors.horizontalCenter: parent.horizontalCenter
columns: 1
Repeater {
model: ListModel {
id: contentModel
Component.onCompleted: {
if (root.deviceClass.interfaces.indexOf("extendedsmartmeterproducer") >= 0
|| root.deviceClass.interfaces.indexOf("extendedsmartmeterconsumer") >= 0) {
append( {interface: "extendedsmartmeterproducer", stateTypeName: "currentPower" })
}
if (root.deviceClass.interfaces.indexOf("smartmeterproducer") >= 0) {
append( {interface: "smartmeterproducer", stateTypeName: "totalEnergyProduced" })
}
if (root.deviceClass.interfaces.indexOf("smartmeterconsumer") >= 0) {
append( {interface: "smartmeterconsumer", stateTypeName: "totalEnergyConsumed" })
}
print("shown graphs are", count)
BigTile {
Layout.preferredWidth: contentGrid.width / contentGrid.columns
showHeader: false
contentItem: RowLayout {
ColorIcon {
Layout.preferredHeight: app.iconSize
Layout.preferredWidth: app.iconSize
name: app.interfaceToIcon("smartmeterconsumer")
color: app.interfaceToColor("smartmeterconsumer")
}
Label {
Layout.fillWidth: true
text: root.totalEnergyConsumedState.value.toFixed(2) + " " + root.totalEnergyConsumedStateType.unitString
font.pixelSize: app.largeFont
}
ColorIcon {
Layout.preferredHeight: app.iconSize
Layout.preferredWidth: app.iconSize
name: app.interfaceToIcon("smartmeterproducer")
color: app.interfaceToColor("smartmeterproducer")
}
Label {
Layout.fillWidth: true
text: root.totalEnergyProducedState.value.toFixed(2) + " " + root.totalEnergyProducedStateType.unitString
font.pixelSize: app.largeFont
}
}
delegate: GenericTypeGraph {
Layout.preferredWidth: contentGrid.width / contentGrid.columns
device: root.device
stateType: root.deviceClass.stateTypes.findByName(model.stateTypeName)
color: app.interfaceToColor(model.interface)
iconSource: app.interfaceToIcon(model.interface)
roundTo: 5
}
}
GenericTypeGraph {
Layout.preferredWidth: contentGrid.width / contentGrid.columns
device: root.device
stateType: root.deviceClass.stateTypes.findByName("currentPower")
color: app.interfaceToColor("smartmeterconsumer")
iconSource: app.interfaceToIcon("smartmeterconsumer")
roundTo: 5
}
}
}