@sailingrotevista/rotevista-dash 6.2.7 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/charts.js ADDED
@@ -0,0 +1,274 @@
1
+
2
+ /**
3
+ * ==========================================================================
4
+ * Sailing Dashboard Pro - Sparkline Graphics Engine
5
+ * ==========================================================================
6
+ * Gestisce l'adattamento delle scale dei grafici, l'arrotondamento dei limiti
7
+ * (Snap a griglia) e la generazione dinamica delle curve SVG.
8
+ */
9
+
10
+ // --- 1. MOTORE DI CALCOLO DELLE SCALE (Snap a griglia & Protezione Profondità) ---
11
+ function calculateScale(type, data, mode) {
12
+ const s = CONFIG.scales[type];
13
+ const currentVal = data[data.length - 1];
14
+
15
+ if (currentVal === undefined || currentVal === null) {
16
+ return { min: 0, max: s ? s.stdMax : 10 };
17
+ }
18
+
19
+ if (!store.herculesScales) store.herculesScales = {};
20
+ if (!store.herculesScales[type]) {
21
+ store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
22
+ }
23
+ let currentScale = store.herculesScales[type];
24
+
25
+ // --- SEZIONE PROFONDITÀ ---
26
+ if (type === 'depth') {
27
+ const shallowThreshold = Math.max(s.stdMax, 10);
28
+ if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
29
+
30
+ const now = Date.now();
31
+ const depthSafetyWindowMs = 120000; // 2 minuti
32
+
33
+ const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
34
+ const recentVals = recentPoints.map(p => p.val);
35
+
36
+ const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
37
+ const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
38
+
39
+ if (mode !== 'hercules') {
40
+ if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
41
+ store.depthProtectedActive = true;
42
+ }
43
+ if (store.depthProtectedActive) {
44
+ if (localMin > shallowThreshold) {
45
+ store.depthProtectedActive = false;
46
+ }
47
+ }
48
+ if (store.depthProtectedActive) {
49
+ return { min: 0, max: shallowThreshold };
50
+ }
51
+ const maxHistorico = Math.max(...data);
52
+ return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
53
+ }
54
+
55
+ if (mode === 'hercules') {
56
+ const roundStep = s.hercSpan;
57
+ let targetMin = 0;
58
+ if (localMin > shallowThreshold) {
59
+ targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
60
+ }
61
+ let targetMax = Math.ceil(localMax / roundStep) * roundStep;
62
+
63
+ if (targetMax - targetMin < 4) {
64
+ targetMax = targetMin + 4;
65
+ }
66
+
67
+ if (currentVal < currentScale.min || currentVal > currentScale.max) {
68
+ currentScale.min = Math.min(currentScale.min, targetMin);
69
+ currentScale.max = Math.max(currentScale.max, targetMax);
70
+ } else {
71
+ const allStableInTarget = recentVals.every(val => val >= targetMin && val <= targetMax);
72
+ if (allStableInTarget) {
73
+ currentScale.min = targetMin;
74
+ currentScale.max = targetMax;
75
+ }
76
+ }
77
+ return { min: currentScale.min, max: currentScale.max };
78
+ }
79
+ }
80
+
81
+ // --- ALTRI GRAFICI (STW, SOG, TWS) ---
82
+ if (mode !== 'hercules') {
83
+ const maxHistorico = Math.max(...data);
84
+ return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
85
+ }
86
+
87
+ if (mode === 'hercules') {
88
+ const roundStep = s.hercSpan;
89
+ const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
90
+ const recentThirdData = data.slice(-oneThirdCount);
91
+
92
+ const localMin = Math.min(...recentThirdData);
93
+ const localMax = Math.max(...recentThirdData);
94
+
95
+ let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
96
+ let targetMax = Math.ceil(localMax / roundStep) * roundStep;
97
+
98
+ if (targetMax - targetMin === 0) {
99
+ targetMax = targetMin + roundStep;
100
+ }
101
+
102
+ if (currentVal < currentScale.min || currentVal > currentScale.max) {
103
+ currentScale.min = Math.min(currentScale.min, targetMin);
104
+ currentScale.max = Math.max(currentScale.max, targetMax);
105
+ } else {
106
+ const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
107
+ if (allStableInTarget) {
108
+ currentScale.min = targetMin;
109
+ currentScale.max = targetMax;
110
+ }
111
+ }
112
+ return { min: currentScale.min, max: currentScale.max };
113
+ }
114
+ }
115
+
116
+ // Scrive le etichette delle scale numeriche nei box
117
+ function updateScaleLabels(t, min, max) {
118
+ const el = document.getElementById(t + '-scale');
119
+ if (el) el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`;
120
+ }
121
+
122
+ // Intercetta e instrada il rinfresco dei grafici
123
+ function refreshGraph(t) {
124
+ if (t === 'aws') {
125
+ if (displayModeTws !== 'AWS') return;
126
+ t = 'tws';
127
+ }
128
+ if (t === 'vmg') {
129
+ if (displayModeSog !== 'VMG') return;
130
+ t = 'sog';
131
+ }
132
+
133
+ const boxType = t;
134
+ let rawData;
135
+
136
+ if (t === 'tws' && displayModeTws === 'AWS') {
137
+ rawData = store.histories['aws'];
138
+ } else {
139
+ rawData = store.histories[t];
140
+ }
141
+
142
+ if (!rawData || rawData.length < 2) return;
143
+
144
+ const values = rawData.map(p => p.val);
145
+ const mode = graphModes[boxType];
146
+ const cfg = calculateScale(boxType, values, mode);
147
+
148
+ const box = document.querySelector(`.box-${boxType}`);
149
+ if (box) box.classList.toggle('box-hercules', mode === 'hercules');
150
+
151
+ updateScaleLabels(boxType, cfg.min, cfg.max);
152
+ drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
153
+ }
154
+
155
+ // Genera fisicamente le curve e le aree SVG
156
+ function drawGraph(d, id, min, max, isTws, isHercules) {
157
+ const svg = document.getElementById(id);
158
+ if (!svg || d.length < 2) return;
159
+
160
+ const w = 200, h = 40;
161
+ const range = max - min || 1;
162
+ const isDepth = (id === 'depth-graph');
163
+
164
+ const latestPoint = d[d.length - 1];
165
+ const now = latestPoint ? latestPoint.time : Date.now();
166
+
167
+ const box = svg.closest('.data-box');
168
+ const isFocused = isFocusActive && box && box.classList.contains('is-focused');
169
+
170
+ const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
171
+ const viewportMs = visibleMinutes * 60000;
172
+ const viewportStart = now - viewportMs;
173
+
174
+ const visibleData = d.filter(p => p.time >= viewportStart);
175
+ if (visibleData.length < 2) return;
176
+
177
+ const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
178
+ const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
179
+
180
+ const getColorProps = (val) => {
181
+ const baseStroke = isFocused ? "4.2" : "1.6";
182
+ const alertStroke = isFocused ? "4.8" : "2.2";
183
+ const warnStroke = isFocused ? "4.4" : "1.8";
184
+
185
+ let color = colTws, opacity = "0.15", stroke = baseStroke;
186
+ if (isTws) {
187
+ const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
188
+ if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
189
+ else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
190
+ else color = baseWind;
191
+ } else if (isDepth) {
192
+ if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
193
+ else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
194
+ else color = colDepth;
195
+ } else {
196
+ if (id === 'stw-graph') color = colStw;
197
+ else if (id === 'sog-graph') color = (displayModeSog === 'VMG') ? colVmg : colSog;
198
+ }
199
+ return { color, opacity, stroke };
200
+ };
201
+
202
+ let grids = "";
203
+ [0.25, 0.5, 0.75].forEach(p => grids += `<line x1="0" y1="${h-(p*h)}" x2="${w}" y2="${h-(p*h)}" stroke="rgba(0,0,0,0.12)" stroke-width="0.5" vector-effect="non-scaling-stroke" />`);
204
+
205
+ const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
206
+ for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
207
+ const x = w - ((m / visibleMinutes) * w);
208
+ grids += `<line x1="${x}" y1="0" x2="${x}" y2="${h}" stroke="rgba(0,0,0,0.08)" stroke-width="0.4" vector-effect="non-scaling-stroke" />`;
209
+ }
210
+
211
+ if (isDepth) {
212
+ const dangerVal = CONFIG.alarms.depthDanger;
213
+ const warningVal = CONFIG.alarms.depthWarning;
214
+ const marginX = 4;
215
+ const alarmStrokeWidth = isFocused ? "4.8" : "1.8";
216
+
217
+ if (dangerVal >= min && dangerVal <= max) {
218
+ const p = (dangerVal - min) / range;
219
+ const y = h - (p * h);
220
+ grids += `<line x1="${marginX}" y1="${y}" x2="${w - marginX}" y2="${y}" stroke="rgba(255, 59, 48, 0.95)" stroke-width="${alarmStrokeWidth}" stroke-dasharray="12, 6, 2, 6" vector-effect="non-scaling-stroke" />`;
221
+ }
222
+ if (warningVal >= min && warningVal <= max) {
223
+ const p = (warningVal - min) / range;
224
+ const y = h - (p * h);
225
+ grids += `<line x1="${marginX}" y1="${y}" x2="${w - marginX}" y2="${y}" stroke="rgba(255, 204, 0, 0.95)" stroke-width="${alarmStrokeWidth}" stroke-dasharray="6 , 2, 6, 12" vector-effect="non-scaling-stroke" />`;
226
+ }
227
+ }
228
+
229
+ let gradientStops = "", lines = "", areaPath = "";
230
+ let started = false;
231
+
232
+ for (let i = 1; i < visibleData.length; i++) {
233
+ const pA = visibleData[i - 1];
234
+ const pB = visibleData[i];
235
+
236
+ const x1 = ((pA.time - viewportStart) / viewportMs) * w;
237
+ const x2 = ((pB.time - viewportStart) / viewportMs) * w;
238
+ const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
239
+ const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
240
+
241
+ const props = getColorProps(pB.val);
242
+ const deltaTime = pB.time - pA.time;
243
+ const expectedInterval = viewportMs / CONFIG.graphs.samples;
244
+ const isGap = deltaTime > (expectedInterval * 2.5);
245
+
246
+ const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
247
+ gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
248
+ gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
249
+
250
+ if (isGap) {
251
+ if (started) {
252
+ areaPath += `L ${x1} ${h} `;
253
+ started = false;
254
+ }
255
+ } else {
256
+ lines += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" style="stroke:${props.color}; stroke-width:${props.stroke}; stroke-linecap:round; shape-rendering:geometricPrecision;" vector-effect="non-scaling-stroke" />`;
257
+ if (!started) {
258
+ areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
259
+ started = true;
260
+ }
261
+ areaPath += `L ${x2} ${y2} `;
262
+ }
263
+ }
264
+
265
+ if (started) {
266
+ const last = visibleData[visibleData.length - 1];
267
+ const lastX = ((last.time - viewportStart) / viewportMs) * w;
268
+ areaPath += `L ${lastX} ${h} Z`;
269
+ }
270
+
271
+ const gradId = `grad-${id}`;
272
+ const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
273
+ svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
274
+ }
package/gauge.js ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * ==========================================================================
3
+ * Sailing Dashboard Pro - Central Compass & Wind Gauge Engine
4
+ * ==========================================================================
5
+ * Gestisce l'aggiornamento grafico dei puntatori analogici (AWA, TWA),
6
+ * dello scarroccio (Leeway), della rotta (Track) e dei trend della bussola.
7
+ */
8
+
9
+ // 1. VARIABILI DI STATO DELLE ROTAZIONI (Estratte da app.js)
10
+ let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
11
+ let curBoatCompassRot = 0, curWindCompassRot = 0;
12
+ let smoothedLeeway = 0;
13
+
14
+ /**
15
+ * Aggiorna la visualizzazione grafica dello scarroccio (Leeway Slider)
16
+ */
17
+ function updateLeewayDisplay(deg) {
18
+ const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125);
19
+ ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w);
20
+ ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`;
21
+ }
22
+
23
+ /**
24
+ * Genera dinamicamente i ticks sul quadrante della bussola centrale (ex genTicks)
25
+ */
26
+ function initCompassTicks() {
27
+ const c = document.getElementById('ticks');
28
+ if (c) {
29
+ c.innerHTML = ""; // Pulisce i tick esistenti prima del disegno
30
+ for (let i = 0; i < 360; i += 10) {
31
+ const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
32
+ const m = i % 30 === 0;
33
+ l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
34
+ l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1");
35
+ l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l);
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Aggiorna tutti gli elementi analogici della Bussola Centrale e del Mini Compass
42
+ */
43
+ function updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal) {
44
+
45
+ // A. Visualizzazione Testuale dell'Apparent Wind (AWS) al centro della bussola
46
+ if (rawAws !== undefined && rawAws !== null && !isNaN(awsVal)) {
47
+ const strVal = awsVal.toFixed(1);
48
+ let awsSvgEl = document.getElementById('aws-val-svg');
49
+ if (awsSvgEl) {
50
+ if (awsSvgEl.textContent !== strVal) {
51
+ awsSvgEl.textContent = strVal;
52
+ }
53
+ }
54
+ }
55
+
56
+ // B. Rotazione dei puntatori analogici di AWA (Apparent) e TWA (True)
57
+ const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
58
+ const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
59
+ if (smAwa) ui.awa.setAttribute('transform', `rotate(${curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val))}, 200, 200)`);
60
+ if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
61
+
62
+ // C. Calcolo dello Scarroccio (Leeway) e orientamento del vettore Track
63
+ if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
64
+ let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (2 * Math.PI) - Math.PI);
65
+ smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
66
+ curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
67
+ ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
68
+ ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#ff9800" : "";
69
+ updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
70
+ }
71
+
72
+ // D. Orientamento delle icone della barca e del vento nel Mini-Compass (TWD)
73
+ const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
74
+ const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
75
+ if (smHdgIcons && smTwdIcons) {
76
+ curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
77
+ ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
78
+ curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val));
79
+ ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
80
+ }
81
+ }
package/index.html CHANGED
@@ -215,7 +215,9 @@
215
215
  </div>
216
216
  </div>
217
217
  </div>
218
-
218
+ <script src="utils.js"></script>
219
+ <script src="charts.js"></script>
220
+ <script src="gauge.js"></script>
219
221
  <script src="app.js?v=6.0"></script>
220
222
  </body>
221
223
  </html>
package/index.js CHANGED
@@ -559,21 +559,22 @@ module.exports = function (app) {
559
559
  * Raccoglie i 30 record da 1 minuto, estrae i picchi, unisce gli angoli
560
560
  * e applica la scrematura del percentile al 5% prima di salvare lo slot.
561
561
  */
562
- function freeze30mSlot(slotTimestamp) {
563
- const startTime = slotTimestamp;
564
- const endTime = slotTimestamp + 1800000; // 30 minuti in millisecondi
562
+ function freeze30mSlot(slotTimestamp) {
563
+ const startTime = slotTimestamp;
564
+ const endTime = slotTimestamp + 1800000; // 30 minuti in millisecondi
565
565
 
566
- // 1. Estrae i record storici del TWD e del TWS che ricadono in quella mezz'ora
567
- const twdPoints = histories['twd'].filter(p => p.time >= startTime && p.time < endTime);
568
- const twsPoints = histories['tws'].filter(p => p.time >= startTime && p.time < endTime);
566
+ // 1. Estrae i record storici del TWD e del TWS che ricadono in quella mezz'ora
567
+ const twdPoints = histories['twd'].filter(p => p.time >= startTime && p.time < endTime);
568
+ const twsPoints = histories['tws'].filter(p => p.time >= startTime && p.time < endTime);
569
569
 
570
- if (twdPoints.length === 0) return; // Se non ci sono dati, salta lo slot
570
+ if (twdPoints.length === 0) return; // Se non ci sono dati, salta lo slot
571
571
 
572
- // 2. Calcola il vento massimo sostenuto (in nodi) registrato nel periodo
573
- const twsVals = twsPoints.map(p => p.val).filter(v => isFinite(v));
574
- const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
572
+ // 2. Calcola il vento massimo sostenuto (in nodi) registrato nel periodo
573
+ const twsVals = twsPoints.map(p => p.val).filter(v => isFinite(v));
574
+ const maxTws = twsVals.length > 0 ? Math.max(...twsVals) : 0;
575
+ const minTws = twsVals.length > 0 ? Math.min(...twsVals) : 0; // Chirurgico: Calcoliamo il vento minimo del periodo
575
576
 
576
- // 3. Estrae tutti gli estremi angolari catturati minuto per minuto
577
+ // 3. Estrae tutti gli estremi angolari catturati minuto per minuto
577
578
  let allAngles = [];
578
579
  twdPoints.forEach(p => {
579
580
  allAngles.push(p.val);
@@ -611,15 +612,16 @@ module.exports = function (app) {
611
612
  const finalMin = (finalAvg + minDiff + Math.PI * 2) % (Math.PI * 2);
612
613
  const finalMax = (finalAvg + maxDiff + Math.PI * 2) % (Math.PI * 2);
613
614
 
614
- // 7. Salva l'arco compresso e pulito nello store dedicato
615
- windRadarSlots.push({
616
- timestamp: startTime,
617
- twdMin: finalMin,
618
- twdMax: finalMax,
619
- twsPeak: maxTws
620
- });
615
+ // 7. Salva l'arco compresso e pulito nello store dedicato
616
+ windRadarSlots.push({
617
+ timestamp: startTime,
618
+ twdMin: finalMin,
619
+ twdMax: finalMax,
620
+ twsPeak: maxTws,
621
+ twsMin: minTws // Chirurgico: Salviamo il vento minimo per poter tracciare la variabilità (Gust Factor)
622
+ });
621
623
 
622
- // Pruning: manteniamo in RAM solo le ultime 6 ore (12 slot)
624
+ // Pruning: manteniamo in RAM solo le ultime 6 ore (12 slot)
623
625
  while (windRadarSlots.length > 12) {
624
626
  windRadarSlots.shift();
625
627
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailingrotevista/rotevista-dash",
3
- "version": "6.2.7",
3
+ "version": "7.0.1",
4
4
  "description": "Wind Dashboard with navigation and course aids",
5
5
  "main": "index.js",
6
6
  "publishConfig": {