Files
shipheart-wasm/qml/detail_circuit.qml
T

347 lines
15 KiB
QML

import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import qt.koriolan.shipheart 1.0
import "responsive.js" as Responsive
// ============================================================================
// 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: Responsive.police(22, page_detail_circuit.width)
font.bold: true
color: couleur_gris_pale
}
// Sélecteur de variable (propre aux circuits/batteries) - 4 colonnes
// normalement, 2x2 si écran étroit :
GridLayout {
Layout.fillWidth: true
columns: Responsive.colonnesSelonLargeur(page_detail_circuit.width, variablesLibelles.length)
rowSpacing: 8
columnSpacing: 8
Repeater {
model: variablesLibelles
delegate: Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Responsive.HAUTEUR_MIN_TACTILE
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: Responsive.police(13, page_detail_circuit.width)
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: Responsive.police(64, page_detail_circuit.width)
font.bold: true
color: couleur_blanc_casse
}
Text {
text: uniteAffichage
font.pixelSize: Responsive.police(20, page_detail_circuit.width)
color: couleur_gris_pale
Layout.alignment: Qt.AlignTop
}
}
Item { Layout.fillWidth: true }
ColumnLayout {
spacing: 10
RowLayout {
Text { text: qsTr("Max"); color: couleur_gris_pale; font.pixelSize: Responsive.police(13, page_detail_circuit.width); Layout.preferredWidth: 90 }
Text { text: valeurMax.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; font.pixelSize: Responsive.police(13, page_detail_circuit.width); Layout.preferredWidth: 100 }
Text { text: "[" + formater_date(tempsMax) + "]"; color: couleur_gris_moyen; font.pixelSize: Responsive.police(13, page_detail_circuit.width) }
}
RowLayout {
Text { text: qsTr("Moyen"); color: couleur_gris_pale; font.pixelSize: Responsive.police(13, page_detail_circuit.width); Layout.preferredWidth: 90 }
Text { text: valeurMoyenne.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; font.pixelSize: Responsive.police(13, page_detail_circuit.width); Layout.preferredWidth: 100 }
}
RowLayout {
Text { text: qsTr("Min"); color: couleur_gris_pale; font.pixelSize: Responsive.police(13, page_detail_circuit.width); Layout.preferredWidth: 90 }
Text { text: valeurMin.toFixed(precisionAffichage) + " " + uniteAffichage; color: couleur_blanc_casse; font.pixelSize: Responsive.police(13, page_detail_circuit.width); Layout.preferredWidth: 100 }
Text { text: "[" + formater_date(tempsMin) + "]"; color: couleur_gris_moyen; font.pixelSize: Responsive.police(13, page_detail_circuit.width) }
}
RowLayout {
Text { text: qsTr("Delta (Max - Min)"); color: couleur_gris_pale; font.pixelSize: Responsive.police(13, page_detail_circuit.width); Layout.preferredWidth: 160 }
Text { text: (valeurMax - valeurMin).toFixed(precisionAffichage + 1) + " " + uniteAffichage; color: couleur_blanc_casse; font.pixelSize: Responsive.police(13, page_detail_circuit.width) }
}
}
}
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);
}
// Quadrillage vertical ET étiquettes, ensemble, sur les mêmes
// paliers ronds - paliers de 5 min (1h), heure pleine (24h) ou
// minuit (7j/30j). Remplace l'ancien double affichage (quadrillage
// sans étiquette + étiquettes à des positions non alignées).
function prochain_palier_rond(instant, periodeIdx) {
var d = new Date(instant);
if (periodeIdx >= 2) {
d.setHours(0, 0, 0, 0);
if (d.getTime() < instant) d.setDate(d.getDate() + 1);
} else if (periodeIdx === 1) {
d.setMinutes(0, 0, 0);
if (d.getTime() < instant) d.setHours(d.getHours() + 1);
} else {
d.setSeconds(0, 0);
d.setMinutes(Math.ceil(d.getMinutes() / 5) * 5);
if (d.getTime() < instant) d.setMinutes(d.getMinutes() + 5);
}
return d.getTime();
}
var pasGrilleMs = (periodeIndex >= 2) ? 86400000 : (periodeIndex === 1) ? 3600000 : 5 * 60000;
var formatEtiquette = (periodeIndex >= 2) ? "dd/MM" : "HH:mm";
ctx.strokeStyle = "#2a2a2a";
ctx.fillStyle = couleur_gris_pale;
// Une étiquette sur 3 seulement pour 24h/30j (trop chargé sinon,
// 24-25 ou 30 barres) - à partir de la première, quitte à ce que les
// 1-2 dernières graduations restent sans étiquette :
var pasEtiquette = (periodeIndex === 1 || periodeIndex === 3) ? 3 : 1;
var indexGrille = 0;
for (var tGrille = prochain_palier_rond(tMin, periodeIndex); tGrille <= tMax; tGrille += pasGrilleMs, indexGrille++) {
var xGrille = px(tGrille);
ctx.beginPath();
ctx.moveTo(xGrille, marge);
ctx.lineTo(xGrille, marge + hauteurUtile);
ctx.stroke();
if (indexGrille % pasEtiquette === 0) {
ctx.fillText(Qt.formatDateTime(new Date(tGrille), formatEtiquette), xGrille - 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: Math.max(40, Responsive.HAUTEUR_MIN_TACTILE)
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.pixelSize: Responsive.police(14, page_detail_circuit.width)
font.bold: (index === periodeIndex)
}
MouseArea {
anchors.fill: parent
onClicked: {
periodeIndex = index;
charger_courbe();
}
}
}
}
}
}
// Bouton retour :
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(44, Responsive.HAUTEUR_MIN_TACTILE)
radius: 8
color: shipheart_couleur_fond_bouton
Text {
anchors.centerIn: parent
text: "↩"
font.pixelSize: Responsive.police(20, page_detail_circuit.width)
color: couleur_blanc_casse
}
MouseArea {
anchors.fill: parent
onClicked: stackView.pop()
}
}
}
}