@sailingrotevista/rotevista-dash 6.2.8 → 7.0.2
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/app.js +195 -592
- package/charts.js +275 -0
- package/gauge.js +82 -0
- package/index.html +142 -99
- package/index.js +8 -4
- package/package.json +1 -1
- package/{radar.html → sample_radar.html} +183 -74
- package/style.css +37 -0
- package/utils.js +140 -0
- package/weather-radar.js +281 -0
- /package/{test.html → debug_signalk_connection.html} +0 -0
- /package/{radaar debug.html → debug_weather_radar.html} +0 -0
package/charts.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
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
|
+
* file charts.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// --- 1. MOTORE DI CALCOLO DELLE SCALE (Snap a griglia & Protezione Profondità) ---
|
|
12
|
+
function calculateScale(type, data, mode) {
|
|
13
|
+
const s = CONFIG.scales[type];
|
|
14
|
+
const currentVal = data[data.length - 1];
|
|
15
|
+
|
|
16
|
+
if (currentVal === undefined || currentVal === null) {
|
|
17
|
+
return { min: 0, max: s ? s.stdMax : 10 };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!store.herculesScales) store.herculesScales = {};
|
|
21
|
+
if (!store.herculesScales[type]) {
|
|
22
|
+
store.herculesScales[type] = { min: 0, max: s ? s.stdMax : 10 };
|
|
23
|
+
}
|
|
24
|
+
let currentScale = store.herculesScales[type];
|
|
25
|
+
|
|
26
|
+
// --- SEZIONE PROFONDITÀ ---
|
|
27
|
+
if (type === 'depth') {
|
|
28
|
+
const shallowThreshold = Math.max(s.stdMax, 10);
|
|
29
|
+
if (store.depthProtectedActive === undefined) store.depthProtectedActive = false;
|
|
30
|
+
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const depthSafetyWindowMs = 120000; // 2 minuti
|
|
33
|
+
|
|
34
|
+
const recentPoints = store.histories.depth.filter(p => (now - p.time) <= depthSafetyWindowMs);
|
|
35
|
+
const recentVals = recentPoints.map(p => p.val);
|
|
36
|
+
|
|
37
|
+
const localMax = recentVals.length > 0 ? Math.max(...recentVals) : currentVal;
|
|
38
|
+
const localMin = recentVals.length > 0 ? Math.min(...recentVals) : currentVal;
|
|
39
|
+
|
|
40
|
+
if (mode !== 'hercules') {
|
|
41
|
+
if (!store.depthProtectedActive && currentVal <= shallowThreshold) {
|
|
42
|
+
store.depthProtectedActive = true;
|
|
43
|
+
}
|
|
44
|
+
if (store.depthProtectedActive) {
|
|
45
|
+
if (localMin > shallowThreshold) {
|
|
46
|
+
store.depthProtectedActive = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (store.depthProtectedActive) {
|
|
50
|
+
return { min: 0, max: shallowThreshold };
|
|
51
|
+
}
|
|
52
|
+
const maxHistorico = Math.max(...data);
|
|
53
|
+
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (mode === 'hercules') {
|
|
57
|
+
const roundStep = s.hercSpan;
|
|
58
|
+
let targetMin = 0;
|
|
59
|
+
if (localMin > shallowThreshold) {
|
|
60
|
+
targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
|
|
61
|
+
}
|
|
62
|
+
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
63
|
+
|
|
64
|
+
if (targetMax - targetMin < 4) {
|
|
65
|
+
targetMax = targetMin + 4;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
69
|
+
currentScale.min = Math.min(currentScale.min, targetMin);
|
|
70
|
+
currentScale.max = Math.max(currentScale.max, targetMax);
|
|
71
|
+
} else {
|
|
72
|
+
const allStableInTarget = recentVals.every(val => val >= targetMin && val <= targetMax);
|
|
73
|
+
if (allStableInTarget) {
|
|
74
|
+
currentScale.min = targetMin;
|
|
75
|
+
currentScale.max = targetMax;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { min: currentScale.min, max: currentScale.max };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- ALTRI GRAFICI (STW, SOG, TWS) ---
|
|
83
|
+
if (mode !== 'hercules') {
|
|
84
|
+
const maxHistorico = Math.max(...data);
|
|
85
|
+
return { min: 0, max: Math.max(s.stdMax, Math.ceil(maxHistorico / s.step) * s.step) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (mode === 'hercules') {
|
|
89
|
+
const roundStep = s.hercSpan;
|
|
90
|
+
const oneThirdCount = Math.max(1, Math.floor(data.length / 3));
|
|
91
|
+
const recentThirdData = data.slice(-oneThirdCount);
|
|
92
|
+
|
|
93
|
+
const localMin = Math.min(...recentThirdData);
|
|
94
|
+
const localMax = Math.max(...recentThirdData);
|
|
95
|
+
|
|
96
|
+
let targetMin = Math.max(0, Math.floor(localMin / roundStep) * roundStep);
|
|
97
|
+
let targetMax = Math.ceil(localMax / roundStep) * roundStep;
|
|
98
|
+
|
|
99
|
+
if (targetMax - targetMin === 0) {
|
|
100
|
+
targetMax = targetMin + roundStep;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (currentVal < currentScale.min || currentVal > currentScale.max) {
|
|
104
|
+
currentScale.min = Math.min(currentScale.min, targetMin);
|
|
105
|
+
currentScale.max = Math.max(currentScale.max, targetMax);
|
|
106
|
+
} else {
|
|
107
|
+
const allStableInTarget = recentThirdData.every(val => val >= targetMin && val <= targetMax);
|
|
108
|
+
if (allStableInTarget) {
|
|
109
|
+
currentScale.min = targetMin;
|
|
110
|
+
currentScale.max = targetMax;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { min: currentScale.min, max: currentScale.max };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Scrive le etichette delle scale numeriche nei box
|
|
118
|
+
function updateScaleLabels(t, min, max) {
|
|
119
|
+
const el = document.getElementById(t + '-scale');
|
|
120
|
+
if (el) el.innerHTML = `<span>${Math.round(max)}</span><span>${Math.round((min+max)/2)}</span><span>${Math.round(min)}</span>`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Intercetta e instrada il rinfresco dei grafici
|
|
124
|
+
function refreshGraph(t) {
|
|
125
|
+
if (t === 'aws') {
|
|
126
|
+
if (displayModeTws !== 'AWS') return;
|
|
127
|
+
t = 'tws';
|
|
128
|
+
}
|
|
129
|
+
if (t === 'vmg') {
|
|
130
|
+
if (displayModeSog !== 'VMG') return;
|
|
131
|
+
t = 'sog';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const boxType = t;
|
|
135
|
+
let rawData;
|
|
136
|
+
|
|
137
|
+
if (t === 'tws' && displayModeTws === 'AWS') {
|
|
138
|
+
rawData = store.histories['aws'];
|
|
139
|
+
} else {
|
|
140
|
+
rawData = store.histories[t];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!rawData || rawData.length < 2) return;
|
|
144
|
+
|
|
145
|
+
const values = rawData.map(p => p.val);
|
|
146
|
+
const mode = graphModes[boxType];
|
|
147
|
+
const cfg = calculateScale(boxType, values, mode);
|
|
148
|
+
|
|
149
|
+
const box = document.querySelector(`.box-${boxType}`);
|
|
150
|
+
if (box) box.classList.toggle('box-hercules', mode === 'hercules');
|
|
151
|
+
|
|
152
|
+
updateScaleLabels(boxType, cfg.min, cfg.max);
|
|
153
|
+
drawGraph(rawData, boxType + '-graph', cfg.min, cfg.max, t === 'tws', mode === 'hercules');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Genera fisicamente le curve e le aree SVG
|
|
157
|
+
function drawGraph(d, id, min, max, isTws, isHercules) {
|
|
158
|
+
const svg = document.getElementById(id);
|
|
159
|
+
if (!svg || d.length < 2) return;
|
|
160
|
+
|
|
161
|
+
const w = 200, h = 40;
|
|
162
|
+
const range = max - min || 1;
|
|
163
|
+
const isDepth = (id === 'depth-graph');
|
|
164
|
+
|
|
165
|
+
const latestPoint = d[d.length - 1];
|
|
166
|
+
const now = latestPoint ? latestPoint.time : Date.now();
|
|
167
|
+
|
|
168
|
+
const box = svg.closest('.data-box');
|
|
169
|
+
const isFocused = isFocusActive && box && box.classList.contains('is-focused');
|
|
170
|
+
|
|
171
|
+
const visibleMinutes = CONFIG.graphs.historyMinutes * (isNavigating ? 1 : 2);
|
|
172
|
+
const viewportMs = visibleMinutes * 60000;
|
|
173
|
+
const viewportStart = now - viewportMs;
|
|
174
|
+
|
|
175
|
+
const visibleData = d.filter(p => p.time >= viewportStart);
|
|
176
|
+
if (visibleData.length < 2) return;
|
|
177
|
+
|
|
178
|
+
const colDanger = "#ff3b30", colWarning = "#ff9800", colTws = "#2c3e50", colAws = "#5c6bc0";
|
|
179
|
+
const colDepth = "#0088cc", colStw = "#00C851", colSog = "#ffbb33", colVmg = "#00b8d4";
|
|
180
|
+
|
|
181
|
+
const getColorProps = (val) => {
|
|
182
|
+
const baseStroke = isFocused ? "4.2" : "1.6";
|
|
183
|
+
const alertStroke = isFocused ? "4.8" : "2.2";
|
|
184
|
+
const warnStroke = isFocused ? "4.4" : "1.8";
|
|
185
|
+
|
|
186
|
+
let color = colTws, opacity = "0.15", stroke = baseStroke;
|
|
187
|
+
if (isTws) {
|
|
188
|
+
const baseWind = (displayModeTws === 'AWS') ? colAws : colTws;
|
|
189
|
+
if (val >= CONFIG.graphs.reef2) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
|
|
190
|
+
else if (val >= CONFIG.graphs.reef1) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
|
|
191
|
+
else color = baseWind;
|
|
192
|
+
} else if (isDepth) {
|
|
193
|
+
if (val < CONFIG.alarms.depthDanger) { color = colDanger; opacity = "0.55"; stroke = alertStroke; }
|
|
194
|
+
else if (val < CONFIG.alarms.depthWarning) { color = colWarning; opacity = "0.45"; stroke = warnStroke; }
|
|
195
|
+
else color = colDepth;
|
|
196
|
+
} else {
|
|
197
|
+
if (id === 'stw-graph') color = colStw;
|
|
198
|
+
else if (id === 'sog-graph') color = (displayModeSog === 'VMG') ? colVmg : colSog;
|
|
199
|
+
}
|
|
200
|
+
return { color, opacity, stroke };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
let grids = "";
|
|
204
|
+
[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" />`);
|
|
205
|
+
|
|
206
|
+
const gridInterval = (visibleMinutes <= 15) ? 1 : 5;
|
|
207
|
+
for (let m = gridInterval; m < visibleMinutes; m += gridInterval) {
|
|
208
|
+
const x = w - ((m / visibleMinutes) * w);
|
|
209
|
+
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" />`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isDepth) {
|
|
213
|
+
const dangerVal = CONFIG.alarms.depthDanger;
|
|
214
|
+
const warningVal = CONFIG.alarms.depthWarning;
|
|
215
|
+
const marginX = 4;
|
|
216
|
+
const alarmStrokeWidth = isFocused ? "4.8" : "1.8";
|
|
217
|
+
|
|
218
|
+
if (dangerVal >= min && dangerVal <= max) {
|
|
219
|
+
const p = (dangerVal - min) / range;
|
|
220
|
+
const y = h - (p * h);
|
|
221
|
+
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" />`;
|
|
222
|
+
}
|
|
223
|
+
if (warningVal >= min && warningVal <= max) {
|
|
224
|
+
const p = (warningVal - min) / range;
|
|
225
|
+
const y = h - (p * h);
|
|
226
|
+
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" />`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let gradientStops = "", lines = "", areaPath = "";
|
|
231
|
+
let started = false;
|
|
232
|
+
|
|
233
|
+
for (let i = 1; i < visibleData.length; i++) {
|
|
234
|
+
const pA = visibleData[i - 1];
|
|
235
|
+
const pB = visibleData[i];
|
|
236
|
+
|
|
237
|
+
const x1 = ((pA.time - viewportStart) / viewportMs) * w;
|
|
238
|
+
const x2 = ((pB.time - viewportStart) / viewportMs) * w;
|
|
239
|
+
const y1 = h - (Math.max(0, Math.min(1, (pA.val - min) / range)) * h);
|
|
240
|
+
const y2 = h - (Math.max(0, Math.min(1, (pB.val - min) / range)) * h);
|
|
241
|
+
|
|
242
|
+
const props = getColorProps(pB.val);
|
|
243
|
+
const deltaTime = pB.time - pA.time;
|
|
244
|
+
const expectedInterval = viewportMs / CONFIG.graphs.samples;
|
|
245
|
+
const isGap = deltaTime > (expectedInterval * 2.5);
|
|
246
|
+
|
|
247
|
+
const offset1 = (x1 / w) * 100, offset2 = (x2 / w) * 100;
|
|
248
|
+
gradientStops += `<stop offset="${offset1}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
249
|
+
gradientStops += `<stop offset="${offset2}%" stop-color="${props.color}" stop-opacity="${props.opacity}" />`;
|
|
250
|
+
|
|
251
|
+
if (isGap) {
|
|
252
|
+
if (started) {
|
|
253
|
+
areaPath += `L ${x1} ${h} `;
|
|
254
|
+
started = false;
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
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" />`;
|
|
258
|
+
if (!started) {
|
|
259
|
+
areaPath += `M ${Math.max(0, x1)} ${h} L ${Math.max(0, x1)} ${y1} `;
|
|
260
|
+
started = true;
|
|
261
|
+
}
|
|
262
|
+
areaPath += `L ${x2} ${y2} `;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (started) {
|
|
267
|
+
const last = visibleData[visibleData.length - 1];
|
|
268
|
+
const lastX = ((last.time - viewportStart) / viewportMs) * w;
|
|
269
|
+
areaPath += `L ${lastX} ${h} Z`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const gradId = `grad-${id}`;
|
|
273
|
+
const defs = `<defs><linearGradient id="${gradId}" x1="0" y1="0" x2="${w}" y2="0" gradientUnits="userSpaceOnUse">${gradientStops}</linearGradient></defs>`;
|
|
274
|
+
svg.innerHTML = `${defs}${grids}<path d="${areaPath}" fill="url(#${gradId})" stroke="none" />${lines}`;
|
|
275
|
+
}
|
package/gauge.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
* file gauge,js
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// 1. VARIABILI DI STATO DELLE ROTAZIONI (Estratte da app.js)
|
|
11
|
+
let curAwaRot = 0, curTwaRot = 0, curTrackRot = 0, curTwdRoseRot = 0;
|
|
12
|
+
let curBoatCompassRot = 0, curWindCompassRot = 0;
|
|
13
|
+
let smoothedLeeway = 0;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Aggiorna la visualizzazione grafica dello scarroccio (Leeway Slider)
|
|
17
|
+
*/
|
|
18
|
+
function updateLeewayDisplay(deg) {
|
|
19
|
+
const c = 125, px = 125/20; let w = Math.min(Math.abs(deg)*px, 125);
|
|
20
|
+
ui.leewayMask.setAttribute('x', deg >= 0 ? c : c - w); ui.leewayMask.setAttribute('width', w);
|
|
21
|
+
ui.leewayVal.textContent = `LEEWAY: ${deg.toFixed(1)}°`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Genera dinamicamente i ticks sul quadrante della bussola centrale (ex genTicks)
|
|
26
|
+
*/
|
|
27
|
+
function initCompassTicks() {
|
|
28
|
+
const c = document.getElementById('ticks');
|
|
29
|
+
if (c) {
|
|
30
|
+
c.innerHTML = ""; // Pulisce i tick esistenti prima del disegno
|
|
31
|
+
for (let i = 0; i < 360; i += 10) {
|
|
32
|
+
const l = document.createElementNS("http://www.w3.org/2000/svg", "line");
|
|
33
|
+
const m = i % 30 === 0;
|
|
34
|
+
l.setAttribute("x1", "200"); l.setAttribute("y1", "40"); l.setAttribute("x2", "200"); l.setAttribute("y2", (m ? 60 : 50));
|
|
35
|
+
l.setAttribute("stroke", m ? "#000" : "#bbb"); l.setAttribute("stroke-width", m ? "2" : "1");
|
|
36
|
+
l.setAttribute("transform", `rotate(${i}, 200, 200)`); c.appendChild(l);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Aggiorna tutti gli elementi analogici della Bussola Centrale e del Mini Compass
|
|
43
|
+
*/
|
|
44
|
+
function updateCentralGauge(store, ui, now, isNavigating, sogKts, stwKts, rawAws, awsVal) {
|
|
45
|
+
|
|
46
|
+
// A. Visualizzazione Testuale dell'Apparent Wind (AWS) al centro della bussola
|
|
47
|
+
if (rawAws !== undefined && rawAws !== null && !isNaN(awsVal)) {
|
|
48
|
+
const strVal = awsVal.toFixed(1);
|
|
49
|
+
let awsSvgEl = document.getElementById('aws-val-svg');
|
|
50
|
+
if (awsSvgEl) {
|
|
51
|
+
if (awsSvgEl.textContent !== strVal) {
|
|
52
|
+
awsSvgEl.textContent = strVal;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// B. Rotazione dei puntatori analogici di AWA (Apparent) e TWA (True)
|
|
58
|
+
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true);
|
|
59
|
+
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true);
|
|
60
|
+
if (smAwa) ui.awa.setAttribute('transform', `rotate(${curAwaRot = getShortestRotation(curAwaRot, radToDeg(smAwa.val))}, 200, 200)`);
|
|
61
|
+
if (smTwa) ui.twa.setAttribute('transform', `rotate(${curTwaRot = getShortestRotation(curTwaRot, radToDeg(smTwa.val))}, 200, 200)`);
|
|
62
|
+
|
|
63
|
+
// C. Calcolo dello Scarroccio (Leeway) e orientamento del vettore Track
|
|
64
|
+
if (store.raw["navigation.courseOverGroundTrue"] !== undefined && store.raw["navigation.headingTrue"] !== undefined) {
|
|
65
|
+
let driftDeg = radToDeg((store.raw["navigation.courseOverGroundTrue"] - store.raw["navigation.headingTrue"] + Math.PI * 3) % (2 * Math.PI) - Math.PI);
|
|
66
|
+
smoothedLeeway = (sogKts < CONFIG.averaging.minSpeed) ? 0 : (smoothedLeeway * 0.9) + (driftDeg * 0.1);
|
|
67
|
+
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
68
|
+
ui.track.setAttribute('transform', `rotate(${curTrackRot}, 200, 200)`);
|
|
69
|
+
ui.leewayVal.style.color = (Math.abs(sogKts - stwKts) > 0.5 && Math.abs(smoothedLeeway) > 7) ? "#ff9800" : "";
|
|
70
|
+
updateLeewayDisplay(Math.max(-20, Math.min(20, smoothedLeeway)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// D. Orientamento delle icone della barca e del vento nel Mini-Compass (TWD)
|
|
74
|
+
const smHdgIcons = getCircularAverageFromBuffer(store.smoothBuf.hdg, 2000, false);
|
|
75
|
+
const smTwdIcons = getCircularAverageFromBuffer(store.smoothBuf.twd, 2000, false);
|
|
76
|
+
if (smHdgIcons && smTwdIcons) {
|
|
77
|
+
curWindCompassRot = getShortestRotation(curWindCompassRot, radToDeg(smTwdIcons.val));
|
|
78
|
+
ui.twdArrow.setAttribute('transform', `rotate(${curWindCompassRot}, 20, 20)`);
|
|
79
|
+
curBoatCompassRot = getShortestRotation(curBoatCompassRot, radToDeg(smHdgIcons.val));
|
|
80
|
+
ui.twdBoat.setAttribute('transform', `rotate(${curBoatCompassRot}, 20, 20)`);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/index.html
CHANGED
|
@@ -63,104 +63,144 @@
|
|
|
63
63
|
</div>
|
|
64
64
|
|
|
65
65
|
<!-- BUSSOLA CENTRALE -->
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
66
|
+
<div class="box-gauge">
|
|
67
|
+
<!-- STATO CONNESSIONE -->
|
|
68
|
+
<div id="status" class="offline">OFFLINE</div>
|
|
69
|
+
|
|
70
|
+
<!-- BUSSOLA 1: ANALOGICA STANDARD (Visibile all'avvio) -->
|
|
71
|
+
<svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
|
|
72
|
+
<defs>
|
|
73
|
+
<clipPath id="boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
74
|
+
<linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
75
|
+
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
|
76
|
+
<stop offset="100%" style="stop-color:#888888;stop-opacity:1" />
|
|
77
|
+
</linearGradient>
|
|
78
|
+
<linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
79
|
+
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
80
|
+
<stop offset="25%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
81
|
+
<stop offset="50%" style="stop-color:#00ff00;stop-opacity:1" />
|
|
82
|
+
<stop offset="75%" style="stop-color:#ff8800;stop-opacity:1" />
|
|
83
|
+
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
84
|
+
</linearGradient>
|
|
85
|
+
<linearGradient id="leeway-night-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
86
|
+
<stop offset="0%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
87
|
+
<stop offset="50%" style="stop-color:#330000;stop-opacity:1" />
|
|
88
|
+
<stop offset="100%" style="stop-color:#ff0000;stop-opacity:1" />
|
|
89
|
+
</linearGradient>
|
|
90
|
+
<clipPath id="leeway-clip">
|
|
91
|
+
<rect id="leeway-mask-rect" x="125" y="0" width="0" height="12" rx="2" />
|
|
92
|
+
</clipPath>
|
|
93
|
+
<filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
94
|
+
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
95
|
+
</filter>
|
|
96
|
+
</defs>
|
|
97
|
+
|
|
98
|
+
<circle cx="200" cy="200" r="160" fill="#050505" />
|
|
99
|
+
<circle cx="200" cy="200" r="125" fill="#121212" />
|
|
100
|
+
|
|
101
|
+
<path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" stroke-width="12" />
|
|
102
|
+
<path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" stroke-width="12" />
|
|
103
|
+
<path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" stroke-width="12" />
|
|
104
|
+
|
|
105
|
+
<g id="ticks"></g>
|
|
106
|
+
|
|
107
|
+
<g id="tick-labels" fill="#bbb" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
|
|
108
|
+
<text font-size="16" transform="translate(200, 74)">0</text>
|
|
109
|
+
<text font-size="16" transform="translate(326, 200) rotate(90)">90</text>
|
|
110
|
+
<text font-size="16" transform="translate(74, 200) rotate(-90)">90</text>
|
|
111
|
+
<text font-size="16" transform="translate(200, 326) rotate(180)">180</text>
|
|
112
|
+
<text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
|
|
113
|
+
<text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
|
|
114
|
+
<text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
|
|
115
|
+
<text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
|
|
116
|
+
<text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
|
|
117
|
+
<text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
|
|
118
|
+
<text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
|
|
119
|
+
<text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
|
|
120
|
+
</g>
|
|
121
|
+
|
|
122
|
+
<g id="track-pointer" transform="rotate(0, 200, 200)">
|
|
123
|
+
<path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" stroke-width="0.5" />
|
|
124
|
+
</g>
|
|
125
|
+
|
|
126
|
+
<circle id="fullscreen-hotspot" cx="200" cy="200" r="55" fill="#181818" stroke="#333" stroke-width="1" filter="url(#center-glow)" cursor="pointer" />
|
|
127
|
+
|
|
128
|
+
<path id="boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
|
|
129
|
+
fill="url(#axiom-grad)" transform="translate(0, 5)" clip-path="url(#boat-clip)" style="pointer-events: none;" />
|
|
130
|
+
|
|
131
|
+
<g id="aws-display-group" transform="translate(200, 265)">
|
|
132
|
+
<text x="0" y="0" fill="#777" font-size="10" font-weight="bold" text-anchor="middle" text-transform="uppercase">Apparent Wind kts</text>
|
|
133
|
+
<text id="aws-val-svg" x="0" y="42" fill="#fff" font-size="52" font-weight="bold" text-anchor="middle">0.0</text>
|
|
134
|
+
</g>
|
|
135
|
+
|
|
136
|
+
<g id="awa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
|
|
137
|
+
<path d="M 200,70 L 213,95 L 200,145 L 187,95 Z" fill="#ff8c00" stroke="#000" stroke-width="1" />
|
|
138
|
+
<text x="200" y="90" fill="#000" font-size="10" font-weight="900" text-anchor="middle" font-family="Arial Black">A</text>
|
|
139
|
+
</g>
|
|
140
|
+
|
|
141
|
+
<g id="twa-pointer" transform="rotate(0, 200, 200)" opacity="0.9">
|
|
142
|
+
<path d="M 200,92 A 8,8 0 0 1 208,100 C 208,108 200,128 200,128 C 200,128 192,108 192,100 A 8,8 0 0 1 200,92 Z"
|
|
143
|
+
fill="#ffff00" stroke="#000" stroke-width="0.8" />
|
|
144
|
+
<text x="200" y="106" fill="#000" font-size="9" font-weight="900" text-anchor="middle" font-family="Arial Black">T</text>
|
|
145
|
+
<circle id="trend-gauge-cw" cx="215" cy="110" r="4" fill="#bbb" />
|
|
146
|
+
<circle id="trend-gauge-ccw" cx="185" cy="110" r="4" fill="#bbb" />
|
|
147
|
+
</g>
|
|
148
|
+
|
|
149
|
+
<g transform="translate(75, 395)">
|
|
150
|
+
<text x="125" y="-12" id="leeway-val" fill="#aaa" font-size="11" text-anchor="middle" font-weight="bold">LEEWAY: 0.0°</text>
|
|
151
|
+
<rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
|
|
152
|
+
<rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clip-path="url(#leeway-clip)" rx="3" />
|
|
153
|
+
<g stroke="#555" stroke-width="1">
|
|
154
|
+
<line x1="0" y1="-2" x2="0" y2="14" /><line x1="62.5" y1="2" x2="62.5" y2="10" />
|
|
155
|
+
<line x1="125" y1="-2" x2="125" y2="14" /><line x1="187.5" y1="2" x2="187.5" y2="10" />
|
|
156
|
+
<line x1="250" y1="-2" x2="250" y2="14" />
|
|
157
|
+
</g>
|
|
158
|
+
<g fill="#555" font-size="8" text-anchor="middle" font-weight="bold">
|
|
159
|
+
<text x="0" y="24">-20°</text><text x="62.5" y="24">-10</text>
|
|
160
|
+
<text x="125" y="24">0°</text><text x="187.5" y="24">10</text>
|
|
161
|
+
<text x="250" y="24">20°</text>
|
|
162
|
+
</g>
|
|
163
|
+
</g>
|
|
164
|
+
</svg>
|
|
165
|
+
|
|
166
|
+
<!-- BUSSOLA 2: RADAR HISTORICAL (Nascosto all'avvio) -->
|
|
167
|
+
<svg id="wind-radar" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet" style="display: none;">
|
|
168
|
+
<defs id="radar-gradients">
|
|
169
|
+
<clipPath id="radar-boat-clip"><circle cx="200" cy="200" r="50" /></clipPath>
|
|
170
|
+
<filter id="radar-center-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
171
|
+
<feDropShadow dx="0" dy="0" stdDeviation="8" flood-color="#aaaaaa" flood-opacity="0.5" />
|
|
172
|
+
</filter>
|
|
173
|
+
</defs>
|
|
174
|
+
|
|
175
|
+
<circle cx="200" cy="200" r="160" fill="rgb(252, 252, 252)" />
|
|
176
|
+
<circle cx="200" cy="200" r="125" fill="rgb(240, 240, 240)" />
|
|
177
|
+
|
|
178
|
+
<g id="radar-ticks"></g>
|
|
179
|
+
|
|
180
|
+
<g id="radar-tick-labels" fill="#000000" text-anchor="middle" dominant-baseline="middle" font-family="Arial" font-weight="bold">
|
|
181
|
+
<text font-size="16" transform="translate(200, 74)">N</text>
|
|
182
|
+
<text font-size="16" transform="translate(326, 200) rotate(90)">E</text>
|
|
183
|
+
<text font-size="16" transform="translate(74, 200) rotate(-90)">W</text>
|
|
184
|
+
<text font-size="16" transform="translate(200, 326) rotate(180)">S</text>
|
|
185
|
+
|
|
186
|
+
<text font-size="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
|
|
187
|
+
<text font-size="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
|
|
188
|
+
<text font-size="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
|
|
189
|
+
<text font-size="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
|
|
190
|
+
<text font-size="11" transform="translate(137.5, 91.7) rotate(-30)">330</text>
|
|
191
|
+
<text font-size="11" transform="translate(91.7, 137.5) rotate(-60)">300</text>
|
|
192
|
+
<text font-size="11" transform="translate(91.7, 262.5) rotate(-120)">240</text>
|
|
193
|
+
<text font-size="11" transform="translate(137.5, 308.3) rotate(-150)">210</text>
|
|
194
|
+
</g>
|
|
195
|
+
|
|
196
|
+
<circle id="ora-orbit" cx="200" cy="200" r="67.2" fill="none" stroke="#cfd8dc" stroke-width="1.2" stroke-dasharray="4, 4" />
|
|
197
|
+
<g id="radar-rings"></g>
|
|
198
|
+
|
|
199
|
+
<circle id="radar-hotspot" cx="200" cy="200" r="55" fill="rgb(238, 238, 238)" stroke="#e0e0e0" stroke-width="1" filter="url(#radar-center-glow)" cursor="pointer" />
|
|
200
|
+
<path id="radar-boat-icon" d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
|
|
201
|
+
fill="#000000" transform="translate(0, 5)" clip-path="url(#radar-boat-clip)" style="pointer-events: none;" />
|
|
202
|
+
</svg>
|
|
203
|
+
</div>
|
|
164
204
|
|
|
165
205
|
<!-- COLONNA DESTRA -->
|
|
166
206
|
<!-- NOTA: Etichetta a SX e Unità a DX nel codice, il CSS usa row-reverse per specchiarli sul bordo -->
|
|
@@ -215,7 +255,10 @@
|
|
|
215
255
|
</div>
|
|
216
256
|
</div>
|
|
217
257
|
</div>
|
|
218
|
-
|
|
258
|
+
<script src="utils.js"></script>
|
|
259
|
+
<script src="charts.js"></script>
|
|
260
|
+
<script src="gauge.js"></script>
|
|
261
|
+
<script src="weather-radar.js"></script>
|
|
219
262
|
<script src="app.js?v=6.0"></script>
|
|
220
263
|
</body>
|
|
221
264
|
</html>
|