From 8825bca5587ba07e7462622ba37d125ba17e4969 Mon Sep 17 00:00:00 2001 From: dodoradio Date: Mon, 7 Aug 2023 21:25:44 +0100 Subject: Add barometer graph This is based mostly on my work from asteroid-health (https://git.dodorad.io/dodoradio/asteroid-health). Some tweaks have been applied on top of this code, which might prove useful in asteroid-health as well. --- src/Barometer.qml | 18 ++++- src/CMakeLists.txt | 2 +- src/graphs/BarometerGraph.qml | 81 +++++++++++++++++++++ src/graphs/TimeLabels.qml | 166 ++++++++++++++++++++++++++++++++++++++++++ src/graphs/VerticalLabels.qml | 82 +++++++++++++++++++++ src/graphs/lineGraph.cpp | 140 +++++++++++++++++++++++++++++++++++ src/graphs/lineGraph.h | 86 ++++++++++++++++++++++ src/main.cpp | 2 + src/resources.qrc | 3 + 9 files changed, 575 insertions(+), 5 deletions(-) create mode 100644 src/graphs/BarometerGraph.qml create mode 100644 src/graphs/TimeLabels.qml create mode 100644 src/graphs/VerticalLabels.qml create mode 100644 src/graphs/lineGraph.cpp create mode 100644 src/graphs/lineGraph.h diff --git a/src/Barometer.qml b/src/Barometer.qml index c34f51d..b494fc0 100644 --- a/src/Barometer.qml +++ b/src/Barometer.qml @@ -23,6 +23,7 @@ import QtSensors 5.11 import org.asteroid.controls 1.0 import org.asteroid.utils 1.0 import Nemo.Configuration 1.0 +import "graphs" Item { id: barometerRoot @@ -38,14 +39,23 @@ Item { width: parent.width*0.2 height: width anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: pressureText.top + anchors.bottom: baroGraph.top + } + BarometerGraph { + id: baroGraph + height: parent.height/3 + width: parent.width*7/8 + anchors.centerIn: parent } Label { id: pressureText - anchors.centerIn: parent + anchors.top:baroGraph.bottom + anchors.topMargin: parent.height*0.05 + anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter text: (pressureSensor.reading.pressure + barometerOffset.value)/100 - font.pixelSize: parent.height / 4 + font.pixelSize: parent.height / 12 + font.bold: true } ConfigurationValue { id: barometerOffset @@ -57,6 +67,6 @@ Item { anchors.top: pressureText.bottom horizontalAlignment: Text.AlignHCenter text: "hPa" - font.pixelSize: parent.height / 6 + font.pixelSize: parent.height / 18 } } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5c71d10..22a6104 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(asteroid-toolwatch main.cpp resources.qrc) +add_library(asteroid-toolwatch main.cpp graphs/lineGraph.cpp graphs/lineGraph.h resources.qrc) set_target_properties(asteroid-toolwatch PROPERTIES PREFIX "") target_link_libraries(asteroid-toolwatch PUBLIC diff --git a/src/graphs/BarometerGraph.qml b/src/graphs/BarometerGraph.qml new file mode 100644 index 0000000..42f378d --- /dev/null +++ b/src/graphs/BarometerGraph.qml @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 Arseniy Movshev + * 2019 Florent Revest + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.15 +import org.asteroid.controls 1.0 + +import org.asteroid.health 1.0 +import org.asteroid.sensorlogd 1.0 + +Item { + id: graph + property date startTime: new Date() + property date endTime: new Date() + + onStartTimeChanged: baroGraph.loadGraphData(baroDataLoader.getDataFromTo(startTime,endTime)) + onEndTimeChanged: baroGraph.loadGraphData(baroDataLoader.getDataFromTo(startTime,endTime)) + + Component.onCompleted: { + baroGraph.loadGraphData(baroDataLoader.getDataFromTo(startTime,endTime)) + } + BarometerDataLoader { id: baroDataLoader + onDataChanged: baroGraph.loadGraphData(getDataFromTo(startTime,endTime)) + } + VerticalLabels { // labels column + id: markerParent + width: parent.width/8 + startValue: baroGraph.minValue + endValue: baroGraph.maxValue + maxLabels: 5 + minLabels: 2 + scaleFactor: 0.01 + labelK: false + anchors { + left: parent.left + top: baroGraph.top + bottom: baroGraph.bottom + topMargin: baroGraph.lineWidth/2 + bottomMargin: anchors.topMargin + } + } + LineGraph { + id: baroGraph + anchors { + left: markerParent.right + right: parent.right + top: parent.top + bottom: labelsRow.top + } + relativeMode: true + lineWidth: 4 + } + TimeLabels { + id: labelsRow + height: Dims.w(5) + startTime: baroGraph.minTime / 1000 + endTime: baroGraph.maxTime / 1000 + maxLabels: 6 //try to get the graph to look a little bit better + anchors { + bottom: parent.bottom + left: baroGraph.left + right: baroGraph.right + rightMargin: baroGraph.lineWidth/2 + leftMargin: anchors.rightMargin + } + } +} diff --git a/src/graphs/TimeLabels.qml b/src/graphs/TimeLabels.qml new file mode 100644 index 0000000..514ce2a --- /dev/null +++ b/src/graphs/TimeLabels.qml @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2023 Arseniy Movshev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.15 +import org.asteroid.controls 1.0 + +Item { + id: root + property real startTime: 0 + property real endTime: 0 + property int minLabels: 3 //these min and max numbers of labels are only guidelines for the algorithm, and don't actually set hard limits. + property int maxLabels: 8 + property int startValueDivision + onStartTimeChanged: update() + onEndTimeChanged: update() + onMinLabelsChanged: update() + onMaxLabelsChanged: update() + Component.onCompleted: update() + function update() { + listModel.clear() + var delta = endTime - startTime + var interval = 0 + if (delta < 60 * maxLabels) { // check 1 minute + if (delta < 60 * minLabels) { + interval = 30 //label every 30s otherwise + } else { + interval = 60 + } + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value + if (date.getMinutes() == 0 | i == 0) { + value = date.getHours() + (date.getMinutes() < 10 ? ":0" + date.getMinutes().toString() : "0" + date.getMinutes().toString()) + } else { + value = date.getMinutes() < 10 ? ":0" + date.getMinutes().toString() : ":" + date.getMinutes().toString() + } + var x = (currentTime - startTime) / delta + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + + } else if (delta < 600 * maxLabels) { // check 10 minutes + if (delta < 600 * minLabels) { + interval = 300 //label every 5m otherwise + } else { + interval = 600 + } + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value + if (date.getMinutes() == 0 | i == 0) { + value = date.getHours() + (date.getMinutes() < 10 ? ":0" + date.getMinutes().toString() : ":" + date.getMinutes().toString()) + } else { + value = date.getMinutes() < 10 ? ":0" + date.getMinutes().toString() : ":" + date.getMinutes().toString() + } + var x = (currentTime - startTime) / delta + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + + } else if (delta < 14400 * maxLabels) { // check every 4 hours - this is an ugly workaround so that a full day still gets some sort of divisions + if (delta > 7200 * maxLabels) { + interval = 14400 //label every 4h if 1h doesn't work + } else if (delta > 3600 * maxLabels) { + interval = 7200 //label every 2h if 1h doesn't work + } else if (delta < 3600 * minLabels) { + interval = 1800 //label every 30m otherwise + } else { + interval = 3600 + } + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value + if (date.getMinutes() == 0) { + value = date.getHours().toString() + "h" + } else { + value = ":" + date.getMinutes() + } + var x = (currentTime - startTime) / delta + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + + } else if (delta < 86400 * maxLabels) { // check days + if (delta < 86400 * minLabels) { + interval = 43200 //label every 12h otherwise + } else { + interval = 86400 + } + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value = date.getDate().toString() /* we should add am and pm here for 12h mode*/ + var x = ((currentTime - startTime) / delta) + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + + } else if (delta < 604800 * maxLabels) { // check weeks + interval = 604800 + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value = date.getDate().toString() + var x = ((currentTime - startTime) / delta) + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + } else { // handle months + console.log("viewing more than several days of data isn't implemented yet. please pester dodoradio about this.") + } + // this is slightly crude - we assume that the min and max label numbers will be set in such a way that halving gets us a reasonable interval. + // we also ignore everything over a number of weeks, as this needs a bit more thought that I don't want to deal with right now. this graph code is stalling too much other development. + } + Repeater { + model: ListModel { id: listModel } + delegate: Label { + text: model.value + font.pixelSize: Dims.w(5) + x: model.x*root.width - width/2 + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/src/graphs/VerticalLabels.qml b/src/graphs/VerticalLabels.qml new file mode 100644 index 0000000..1273207 --- /dev/null +++ b/src/graphs/VerticalLabels.qml @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 Arseniy Movshev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.15 +import org.asteroid.controls 1.0 + +Item { + id: root + property real startValue: 0 + property real endValue: 0 + property real scaleFactor: 1 // to avoid float issues, set the range using ints and then apply this scale factor to the output + property int minLabels: 3 + property int maxLabels: 8 + property real valueDivisionsInterval: 0 + property real startValueDivision: 0 + property real valuesDelta + property bool labelK: true + onStartValueChanged: update() + onEndValueChanged: update() + onMinLabelsChanged: update() + onMaxLabelsChanged: update() + implicitWidth: childrenRect.width + + function update() { // this tries to guess how to generate nice looking labels + valuesDelta = endValue - startValue + var powTen = Math.pow(10,Math.trunc(Math.log10(valuesDelta))) + var interval = powTen + var numLabel = Math.floor(valuesDelta/interval) + if (numLabel > maxLabels) { + interval = powTen*2 + numLabel = valuesDelta/interval + if (numLabel > maxLabels) { + interval = powTen*5 + numLabel = valuesDelta/interval + if (numLabel > maxLabels) { + interval = powTen*10 + numLabel = valuesDelta/interval + } + } + } + if (numLabel < minLabels) { + interval = powTen/2 + numLabel = valuesDelta/interval + if (numLabel < minLabels) { + interval = powTen/5 + numLabel = valuesDelta/interval + if (numLabel < minLabels) { + interval = powTen/10 + numLabel = valuesDelta/interval + } + } + } + valueDivisionsInterval = interval + labelsRepeater.model = Math.round(numLabel) + 1 + startValueDivision = interval*Math.trunc(startValue/interval) + } + Repeater { + id: labelsRepeater + delegate: Label { + anchors.right: parent.right + property real value: root.startValueDivision + root.valueDivisionsInterval*index + text: (value*root.scaleFactor > 1000 & root.labelK) ? value*root.scaleFactor/1000 + "k" : value*root.scaleFactor //maybe this spaghetti could be better written but it's only run once on item load + font.pixelSize: Dims.w(5) + y: parent.height - (parent.height)*((value-root.startValue)/root.valuesDelta) - height/2 + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/src/graphs/lineGraph.cpp b/src/graphs/lineGraph.cpp new file mode 100644 index 0000000..36171b9 --- /dev/null +++ b/src/graphs/lineGraph.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2023 Arseniy Movshev + * 2017 Florent Revest + * All rights reserved. + * + * You may use this file under the terms of 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 author 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 HOLDERS 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. + */ + +#include "lineGraph.h" + +#include +#include +#include +#include +#include +#include +#include + +LineGraph::LineGraph() +{ + setFlag(ItemHasContents, true); + setAntialiasing(true); + setRenderTarget(QQuickPaintedItem::FramebufferObject); +} + +void LineGraph::paint(QPainter *painter) +{ + if (m_filedata.count() < 2) { + qDebug() << "not rendering, not enough data"; + return; + } + int j = m_filedata.count(); + QPointF points[j]; + if (!graphRelative) { + minValue = 0; + } + float valueDelta = maxValue - minValue; + float timeDelta = maxTime - minTime; + float calculatedValue = 0; + float calculatedTimeSeconds = 0; + for(int i = 0; i < j; i++) { + calculatedTimeSeconds = (m_filedata[i].x() - minTime)/timeDelta; + calculatedValue = 1 - (m_filedata[i].y() - minValue)/valueDelta; + points[i] = QPointF(m_lineWidth + calculatedTimeSeconds*(width()-2*m_lineWidth), m_lineWidth + calculatedValue*(height()-2*m_lineWidth)); //these +2 -1 are here to make sure that the graph fits within the drawn area, as it will be clipped by qt if it doesn't. + } + QPen pen; + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + pen.setWidthF(m_lineWidth); + pen.setColor(m_color); + painter->setRenderHints(QPainter::Antialiasing); + painter->setPen(pen); + painter->drawPolyline(points,j); +} + +void LineGraph::loadGraphData(QVariant fileDataInput) { + qDebug() << "loadGraphData called"; + QList fileDataAsList = fileDataInput.toList(); + if (fileDataAsList.count() < 1) { + qDebug() << "no heartrate data to show, failing load"; + return; + } + int j = fileDataAsList.count(); + minTime = fileDataAsList[0].toPointF().x(); + maxTime = fileDataAsList[j-1].toPointF().x(); + minValue = fileDataAsList[0].toPointF().y(); + maxValue = minValue; + m_filedata.clear(); + for(int i = 0; i < j; i++) { + m_filedata.append(fileDataAsList[i].toPointF()); + if (minValue > m_filedata[i].y()) minValue = m_filedata[i].y(); + if (maxValue < m_filedata[i].y()) maxValue = m_filedata[i].y(); + } + emit loadingDone(); + update(); +} + +void LineGraph::setLineColor(QColor color) { + m_color = color; + update(); +} + +QColor LineGraph::lineColor() { + return m_color; +} + +void LineGraph::setLineWidth(float width) { + m_lineWidth = width; + update(); +} + +float LineGraph::lineWidth() { + return m_lineWidth; +} + +int LineGraph::getMaxValue() { + return maxValue; +} + +int LineGraph::getMinValue() { + return minValue; +} + +QDateTime LineGraph::getMaxTime() { + return QDateTime::fromSecsSinceEpoch(maxTime); +} + +QDateTime LineGraph::getMinTime() { + return QDateTime::fromSecsSinceEpoch(minTime); +} + +bool LineGraph::relative() { + return graphRelative; +} + +void LineGraph::setRelative(bool newRelative) { + graphRelative = newRelative; +} diff --git a/src/graphs/lineGraph.h b/src/graphs/lineGraph.h new file mode 100644 index 0000000..f6a592c --- /dev/null +++ b/src/graphs/lineGraph.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 Florent Revest + * All rights reserved. + * + * You may use this file under the terms of 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 author 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 HOLDERS 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. + */ + +#ifndef LINEGRAPH_H +#define LINEGRAPH_H + +#include +#include +#include +#include +#include + +class LineGraph : public QQuickPaintedItem +{ + Q_OBJECT + + Q_PROPERTY(float lineWidth READ lineWidth WRITE setLineWidth) + Q_PROPERTY(QColor lineColor READ lineColor WRITE setLineColor NOTIFY lineColorChanged) + Q_PROPERTY(int maxValue READ getMaxValue NOTIFY loadingDone) + Q_PROPERTY(int minValue READ getMinValue NOTIFY loadingDone) + Q_PROPERTY(QDateTime maxTime READ getMaxTime NOTIFY loadingDone) + Q_PROPERTY(QDateTime minTime READ getMinTime NOTIFY loadingDone) + Q_PROPERTY(bool relativeMode READ relative WRITE setRelative) + +public: + Q_INVOKABLE void loadGraphData(QVariant fileDataInput); + LineGraph(); + void paint(QPainter *painter) override; + +signals: + void loadingDone(); + void lineColorChanged(); + +public slots: + float lineWidth(); + void setLineWidth(float width); + QColor lineColor(); + void setLineColor(QColor color); + int getMaxValue(); + int getMinValue(); + QDateTime getMaxTime(); + QDateTime getMinTime(); + bool relative(); + void setRelative(bool newRelative); + +private: + void updateBasePixmap(); + + float m_lineWidth = 0; + QColor m_color = QColor(255,255,255); + QPixmap m_pixmap; + QList m_filedata; + float minValue = 0; + float maxValue = 0; + int minTime; + int maxTime; + bool graphRelative; +}; + +#endif // LINEGRAPH_H diff --git a/src/main.cpp b/src/main.cpp index a44544c..891274f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,8 +16,10 @@ */ #include +#include "graphs/lineGraph.h" int main(int argc, char *argv[]) { + qmlRegisterType("org.asteroid.health", 1, 0, "LineGraph"); return AsteroidApp::main(argc, argv); } diff --git a/src/resources.qrc b/src/resources.qrc index 6b7d9fa..a00b892 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -6,5 +6,8 @@ Barometer.qml compass.svg SettingsPage.qml + graphs/TimeLabels.qml + graphs/VerticalLabels.qml + graphs/BarometerGraph.qml -- cgit v1.2.3-54-g00ecf