4 pages de détail avec courbe (Canvas), navigation depuis home.qml, sélecteurs de variable pour circuits/batteries
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
import qt.koriolan.shipheart 1.0
|
||||
|
||||
// ============================================================================
|
||||
// detail_circuit.qml - page de détail d'un circuit (valeur + historique).
|
||||
// ----------------------------------------------------------------------------
|
||||
// Poussée sur le StackView depuis home.qml (clic court sur un circuit).
|
||||
// Domaine circuits : sélecteur de variable (consigne/tension/intensité/
|
||||
// fréquence) en plus du sélecteur de période - contrairement à capteurs/
|
||||
// réservoirs, à variable unique. Page en LECTURE SEULE : la commande du
|
||||
// circuit (bascule marche/arrêt) reste sur l'appui long depuis home.qml,
|
||||
// pas dupliquée ici.
|
||||
//
|
||||
// Mêmes simplifications volontaires que detail_capteur.qml (Canvas plutôt que
|
||||
// QtCharts, ligne continue plutôt qu'escalier, pas de zoom tactile).
|
||||
// ============================================================================
|
||||
|
||||
Shipheart_page {
|
||||
id: page_detail_circuit
|
||||
|
||||
property int idElement: 0
|
||||
property string nomElement: ""
|
||||
|
||||
property int periodeIndex: 1 // 24 heures par défaut
|
||||
readonly property var periodesLibelles: [qsTr("1 heure"), qsTr("24 heures"), qsTr("7 jours"), qsTr("30 jours")]
|
||||
|
||||
property int variableIndex: 1 // "tension" par défaut
|
||||
readonly property var variablesCodes: ["consigne", "tension", "intensite", "frequence"]
|
||||
readonly property var variablesLibelles: [qsTr("Consigne"), qsTr("Tension"), qsTr("Intensité"), qsTr("Fréquence")]
|
||||
|
||||
property var pointsCourbe: []
|
||||
property real valeurMin: 0
|
||||
property real valeurMax: 0
|
||||
property real valeurMoyenne: 0
|
||||
property double tempsMin: 0
|
||||
property double tempsMax: 0
|
||||
property string uniteAffichage: ""
|
||||
property int precisionAffichage: 1
|
||||
|
||||
property real valeurActuelle: 0
|
||||
|
||||
property DataProvider dataProvider: DataProvider {}
|
||||
|
||||
function charger_courbe() {
|
||||
dataProvider.get_mesures("circuits", idElement, periodeIndex, variablesCodes[variableIndex]);
|
||||
}
|
||||
|
||||
function formater_date(timestampMs) {
|
||||
return Qt.formatDateTime(new Date(timestampMs), "dd/MM HH:mm");
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: dataProvider
|
||||
|
||||
function onMesuresRecues(type, id, periode, variable, donnees) {
|
||||
if (id !== idElement || periode !== periodeIndex || variable !== variablesCodes[variableIndex]) return; // Réponse obsolète
|
||||
|
||||
pointsCourbe = donnees.points;
|
||||
uniteAffichage = donnees.unite;
|
||||
precisionAffichage = donnees.precision;
|
||||
valeurActuelle = donnees.valeur_actuelle;
|
||||
if (donnees.points.length > 0) {
|
||||
valeurMin = donnees.min.v;
|
||||
tempsMin = donnees.min.t;
|
||||
valeurMax = donnees.max.v;
|
||||
tempsMax = donnees.max.t;
|
||||
valeurMoyenne = donnees.moyenne;
|
||||
}
|
||||
graphique.requestPaint();
|
||||
}
|
||||
|
||||
function onErreur(message) {
|
||||
console.log("DataProvider erreur (détail circuit) : " + message);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: charger_courbe()
|
||||
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: charger_courbe()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
spacing: 15
|
||||
|
||||
Text {
|
||||
text: nomElement
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
color: couleur_gris_pale
|
||||
}
|
||||
|
||||
// Sélecteur de variable (propre aux circuits/batteries) :
|
||||
Row {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
Repeater {
|
||||
model: variablesLibelles
|
||||
delegate: Rectangle {
|
||||
width: (page_detail_circuit.width - 40 - 3 * 8) / 4
|
||||
height: 36
|
||||
radius: 8
|
||||
color: (index === variableIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: couleur_blanc_casse
|
||||
font.pixelSize: 13
|
||||
font.bold: (index === variableIndex)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
variableIndex = index;
|
||||
charger_courbe();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 20
|
||||
opacity: dataProvider.connecte ? 1.0 : 0.45
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
spacing: 4
|
||||
Text {
|
||||
text: valeurActuelle.toFixed(precisionAffichage)
|
||||
font.pixelSize: 64
|
||||
font.bold: true
|
||||
color: couleur_blanc_casse
|
||||
}
|
||||
Text {
|
||||
text: uniteAffichage
|
||||
font.pixelSize: 20
|
||||
color: couleur_gris_pale
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 10
|
||||
RowLayout {
|
||||
Text { text: qsTr("Max"); color: couleur_gris_pale; Layout.preferredWidth: 90 }
|
||||
Text { text: valeurMax.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 }
|
||||
Text { text: "[" + formater_date(tempsMax) + "]"; color: couleur_gris_moyen }
|
||||
}
|
||||
RowLayout {
|
||||
Text { text: qsTr("Moyen"); color: couleur_gris_pale; Layout.preferredWidth: 90 }
|
||||
Text { text: valeurMoyenne.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 }
|
||||
}
|
||||
RowLayout {
|
||||
Text { text: qsTr("Min"); color: couleur_gris_pale; Layout.preferredWidth: 90 }
|
||||
Text { text: valeurMin.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; Layout.preferredWidth: 100 }
|
||||
Text { text: "[" + formater_date(tempsMin) + "]"; color: couleur_gris_moyen }
|
||||
}
|
||||
RowLayout {
|
||||
Text { text: qsTr("Delta (Max - Min)"); color: couleur_gris_pale; Layout.preferredWidth: 160 }
|
||||
Text { text: (valeurMax - valeurMin).toFixed(precisionAffichage + 1) + " " + uniteAffichage; color: couleur_blanc_casse }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 15
|
||||
opacity: dataProvider.connecte ? 1.0 : 0.45
|
||||
|
||||
Canvas {
|
||||
id: graphique
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
if (pointsCourbe.length < 2) {
|
||||
ctx.fillStyle = couleur_rouge_transparent;
|
||||
ctx.font = "14px sans-serif";
|
||||
ctx.fillText(qsTr("Pas assez de données sur cette période."), width / 2 - 110, height / 2);
|
||||
return;
|
||||
}
|
||||
|
||||
var marge = 45;
|
||||
var largeurUtile = width - 2 * marge;
|
||||
var hauteurUtile = height - 2 * marge;
|
||||
|
||||
var tMin = pointsCourbe[0].t;
|
||||
var tMax = pointsCourbe[pointsCourbe.length - 1].t;
|
||||
|
||||
var padding = (valeurMax - valeurMin) * 0.1 || 1;
|
||||
var yMin = valeurMin - padding;
|
||||
var yMax = valeurMax + padding;
|
||||
|
||||
function px(t) { return marge + ((t - tMin) / (tMax - tMin)) * largeurUtile; }
|
||||
function py(v) { return marge + hauteurUtile - ((v - yMin) / (yMax - yMin)) * hauteurUtile; }
|
||||
|
||||
ctx.strokeStyle = "#333333";
|
||||
ctx.fillStyle = couleur_gris_pale;
|
||||
ctx.font = "11px sans-serif";
|
||||
var nbLignesY = 5;
|
||||
for (var i = 0; i <= nbLignesY; i++) {
|
||||
var v = yMin + (i / nbLignesY) * (yMax - yMin);
|
||||
var y = py(v);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(marge, y);
|
||||
ctx.lineTo(width - marge, y);
|
||||
ctx.stroke();
|
||||
ctx.fillText(v.toFixed(precisionAffichage), 2, y + 4);
|
||||
}
|
||||
|
||||
var nbLabelsX = 6;
|
||||
for (var j = 0; j <= nbLabelsX; j++) {
|
||||
var t = tMin + (j / nbLabelsX) * (tMax - tMin);
|
||||
var x = px(t);
|
||||
ctx.fillText(Qt.formatDateTime(new Date(t), "HH:mm"), x - 18, height - 8);
|
||||
}
|
||||
|
||||
ctx.strokeStyle = couleur_bleu_koriolan;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
for (var k = 0; k < pointsCourbe.length; k++) {
|
||||
var xx = px(pointsCourbe[k].t);
|
||||
var yy = py(pointsCourbe[k].v);
|
||||
if (k === 0) ctx.moveTo(xx, yy);
|
||||
else ctx.lineTo(xx, yy);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = "lightgreen";
|
||||
ctx.beginPath();
|
||||
ctx.arc(px(tempsMin), py(valeurMin), 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(px(tempsMax), py(valeurMax), 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 8
|
||||
Repeater {
|
||||
model: periodesLibelles
|
||||
delegate: Rectangle {
|
||||
Layout.preferredWidth: 140
|
||||
Layout.preferredHeight: 40
|
||||
radius: 8
|
||||
color: (index === periodeIndex) ? shipheart_couleur_fond_bouton_actif : shipheart_couleur_fond_bouton
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: couleur_blanc_casse
|
||||
font.bold: (index === periodeIndex)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
periodeIndex = index;
|
||||
charger_courbe();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bouton retour :
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 44
|
||||
radius: 8
|
||||
color: shipheart_couleur_fond_bouton
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "↩"
|
||||
font.pixelSize: 20
|
||||
color: couleur_blanc_casse
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: stackView.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user