@sailingrotevista/rotevista-dash 8.0.1 → 8.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.
Potentially problematic release.
This version of @sailingrotevista/rotevista-dash might be problematic. Click here for more details.
- package/package.json +9 -1
- package/public/assets/{index-C4co33rh.css → index-BFSy7dOW.css} +1 -1
- package/public/assets/index-BHhAwNqq.js +9 -0
- package/public/index.html +2 -2
- package/bun.lock +0 -157
- package/index.html +0 -22
- package/metadata.json +0 -7
- package/public/assets/index-DvzZHuhg.js +0 -9
- package/src/App.tsx +0 -346
- package/src/components/AnalogGauge.tsx +0 -243
- package/src/components/DataBox.tsx +0 -116
- package/src/components/MiniCompass.tsx +0 -120
- package/src/components/Sparkline.tsx +0 -343
- package/src/components/WeatherRadar.tsx +0 -280
- package/src/hooks/useDashboard.ts +0 -1161
- package/src/main.tsx +0 -10
- package/src/style.css +0 -705
- package/src/types.ts +0 -88
- package/src/utils/math.ts +0 -153
- package/src/utils/radarMath.ts +0 -89
- package/src/vite-env.d.ts +0 -1
- package/static/icons/dash.png +0 -0
- package/static/manifest.json +0 -17
- package/tsconfig.json +0 -20
- package/vite.config.ts +0 -22
|
@@ -1,1161 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
AppConfig,
|
|
4
|
-
HistoryPoint,
|
|
5
|
-
AngleBufferItem,
|
|
6
|
-
WindRadarSlot,
|
|
7
|
-
FutureForecast,
|
|
8
|
-
DisplayModeSog,
|
|
9
|
-
DisplayModeTws,
|
|
10
|
-
ActiveInstrument,
|
|
11
|
-
GraphMode,
|
|
12
|
-
BoxType,
|
|
13
|
-
} from '../types';
|
|
14
|
-
import {
|
|
15
|
-
radToDeg,
|
|
16
|
-
degToRad,
|
|
17
|
-
msToKts,
|
|
18
|
-
ktsToMs,
|
|
19
|
-
getShortestRotation,
|
|
20
|
-
getCircularAverageFromBuffer,
|
|
21
|
-
playBingBing,
|
|
22
|
-
playGybeAlarm,
|
|
23
|
-
} from '../utils/math';
|
|
24
|
-
|
|
25
|
-
const DEFAULT_CONFIG: AppConfig = {
|
|
26
|
-
alarms: { depthDanger: 2.5, depthWarning: 3.5 },
|
|
27
|
-
averaging: {
|
|
28
|
-
smoothWindow: 2000,
|
|
29
|
-
longWindow: 30000,
|
|
30
|
-
stabilityTolerance: 2000,
|
|
31
|
-
stabilityThreshold: 0.95,
|
|
32
|
-
minSpeed: 0.5,
|
|
33
|
-
stabilityBreakout: 15,
|
|
34
|
-
},
|
|
35
|
-
graphs: { reef1: 15.0, reef2: 20.0, historyMinutes: 30, samples: 60 },
|
|
36
|
-
scales: {
|
|
37
|
-
stw: { stdMax: 12, step: 2, hercSpan: 4 },
|
|
38
|
-
sog: { stdMax: 12, step: 2, hercSpan: 4 },
|
|
39
|
-
tws: { stdMax: 25, step: 5, hercSpan: 10 },
|
|
40
|
-
depth: { stdMax: 20, step: 10, hercSpan: 10 },
|
|
41
|
-
},
|
|
42
|
-
server: { fallbackIp: 'venus.local:3000' },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function safePush(buffer: AngleBufferItem[], val: number, time: number, isTwd = false, historyMinutes = 30) {
|
|
46
|
-
if (val === null || val === undefined || isNaN(val)) return;
|
|
47
|
-
|
|
48
|
-
buffer.push({
|
|
49
|
-
val,
|
|
50
|
-
time,
|
|
51
|
-
sin: Math.sin(val),
|
|
52
|
-
cos: Math.cos(val),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const limitMinutes = isTwd ? 60 : historyMinutes;
|
|
56
|
-
const maxHistoryMs = limitMinutes * 60000 * 2 + 60000;
|
|
57
|
-
|
|
58
|
-
while (buffer.length > 0 && time - buffer[0].time > maxHistoryMs) {
|
|
59
|
-
buffer.shift();
|
|
60
|
-
}
|
|
61
|
-
if (buffer.length > 36000) buffer.shift();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function useDashboard() {
|
|
65
|
-
const [config, setConfig] = useState<AppConfig>(DEFAULT_CONFIG);
|
|
66
|
-
const [displayModeSog, setDisplayModeSog] = useState<DisplayModeSog>('SOG');
|
|
67
|
-
const [displayModeTws, setDisplayModeTws] = useState<DisplayModeTws>('TWS');
|
|
68
|
-
const [activeInstrument, setActiveInstrument] = useState<ActiveInstrument>('gauge');
|
|
69
|
-
const [isNightMode, setIsNightMode] = useState<boolean>(false);
|
|
70
|
-
const [isFocusActive, setIsFocusActive] = useState<boolean>(false);
|
|
71
|
-
const [focusedBox, setFocusedBox] = useState<BoxType | null>(null);
|
|
72
|
-
const [graphModes, setGraphModes] = useState<Record<string, GraphMode>>({
|
|
73
|
-
stw: 'standard',
|
|
74
|
-
sog: 'standard',
|
|
75
|
-
tws: 'standard',
|
|
76
|
-
depth: 'standard',
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const [connectionStatus, setConnectionStatus] = useState<{ text: string; className: string }>({
|
|
80
|
-
text: 'OFFLINE',
|
|
81
|
-
className: 'offline',
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Display values for UI
|
|
85
|
-
const [displayValues, setDisplayValues] = useState({
|
|
86
|
-
stw: '0.0',
|
|
87
|
-
sog: '0.0',
|
|
88
|
-
sogColor: '',
|
|
89
|
-
hdg: '000°',
|
|
90
|
-
hdgUnstable: false,
|
|
91
|
-
cog: '000°',
|
|
92
|
-
cogUnstable: false,
|
|
93
|
-
depth: '--.-',
|
|
94
|
-
depthAlarmClass: '',
|
|
95
|
-
tws: '0.0',
|
|
96
|
-
twsColor: '',
|
|
97
|
-
awsSvg: '0.0',
|
|
98
|
-
twaAvg: '---°',
|
|
99
|
-
twaUnstable: false,
|
|
100
|
-
awaAvg: '---°',
|
|
101
|
-
awaUnstable: false,
|
|
102
|
-
twdAvg: '---°',
|
|
103
|
-
twdUnstable: false,
|
|
104
|
-
tackHdg: '---°',
|
|
105
|
-
tackHdgUnstable: false,
|
|
106
|
-
tackCog: '---°',
|
|
107
|
-
tackCogUnstable: false,
|
|
108
|
-
tackLabel: 'TACK',
|
|
109
|
-
leewayDeg: 0,
|
|
110
|
-
trackRotDeg: 0,
|
|
111
|
-
awaPointerDeg: 0,
|
|
112
|
-
twaPointerDeg: 0,
|
|
113
|
-
windCompassRotDeg: 0,
|
|
114
|
-
boatCompassRotDeg: 0,
|
|
115
|
-
tacticCwDot: { active: false, color: '#bbb', gybing: false },
|
|
116
|
-
tacticCcwDot: { active: false, color: '#bbb', gybing: false },
|
|
117
|
-
compassCwDot: { active: false, color: '#bbb' },
|
|
118
|
-
compassCcwDot: { active: false, color: '#bbb' },
|
|
119
|
-
meteoLed: null as { active: boolean; color: string; angleTarget: number } | null,
|
|
120
|
-
isNavigating: false,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Histories for sparklines & radar
|
|
124
|
-
const [historiesState, setHistoriesState] = useState<{
|
|
125
|
-
stw: HistoryPoint[];
|
|
126
|
-
sog: HistoryPoint[];
|
|
127
|
-
vmg: HistoryPoint[];
|
|
128
|
-
depth: HistoryPoint[];
|
|
129
|
-
tws: HistoryPoint[];
|
|
130
|
-
aws: HistoryPoint[];
|
|
131
|
-
twd: HistoryPoint[];
|
|
132
|
-
}>({
|
|
133
|
-
stw: [],
|
|
134
|
-
sog: [],
|
|
135
|
-
vmg: [],
|
|
136
|
-
depth: [],
|
|
137
|
-
tws: [],
|
|
138
|
-
aws: [],
|
|
139
|
-
twd: [],
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const [windRadarSlots, setWindRadarSlots] = useState<WindRadarSlot[]>([]);
|
|
143
|
-
const [futureForecast, setFutureForecast] = useState<FutureForecast | null>(null);
|
|
144
|
-
|
|
145
|
-
// Internal mutable store
|
|
146
|
-
const storeRef = useRef({
|
|
147
|
-
raw: {} as Record<string, any>,
|
|
148
|
-
timestamps: {} as Record<string, number>,
|
|
149
|
-
smoothBuf: {
|
|
150
|
-
hdg: [] as AngleBufferItem[],
|
|
151
|
-
cog: [] as AngleBufferItem[],
|
|
152
|
-
awa: [] as AngleBufferItem[],
|
|
153
|
-
twa: [] as AngleBufferItem[],
|
|
154
|
-
twd: [] as AngleBufferItem[],
|
|
155
|
-
},
|
|
156
|
-
longBuf: {
|
|
157
|
-
hdg: [] as AngleBufferItem[],
|
|
158
|
-
cog: [] as AngleBufferItem[],
|
|
159
|
-
awa: [] as AngleBufferItem[],
|
|
160
|
-
twa: [] as AngleBufferItem[],
|
|
161
|
-
twd: [] as AngleBufferItem[],
|
|
162
|
-
},
|
|
163
|
-
histories: {
|
|
164
|
-
stw: [] as HistoryPoint[],
|
|
165
|
-
sog: [] as HistoryPoint[],
|
|
166
|
-
vmg: [] as HistoryPoint[],
|
|
167
|
-
depth: [] as HistoryPoint[],
|
|
168
|
-
tws: [] as HistoryPoint[],
|
|
169
|
-
aws: [] as HistoryPoint[],
|
|
170
|
-
twd: [] as HistoryPoint[],
|
|
171
|
-
},
|
|
172
|
-
graphTempBuf: {
|
|
173
|
-
stw: [] as Array<{ val: number; time: number }>,
|
|
174
|
-
sog: [] as Array<{ val: number; time: number }>,
|
|
175
|
-
vmg: [] as Array<{ val: number; time: number }>,
|
|
176
|
-
depth: [] as Array<{ val: number; time: number }>,
|
|
177
|
-
tws: [] as Array<{ val: number; time: number }>,
|
|
178
|
-
aws: [] as Array<{ val: number; time: number }>,
|
|
179
|
-
twd: [] as Array<{ val: number; time: number }>,
|
|
180
|
-
},
|
|
181
|
-
lastUpdates: { stw: 0, sog: 0, depth: 0, tws: 0, vmg: 0, aws: 0, twd: 0 },
|
|
182
|
-
lastMessageReceivedTime: Date.now(),
|
|
183
|
-
curAwaRot: 0,
|
|
184
|
-
curTwaRot: 0,
|
|
185
|
-
curTrackRot: 0,
|
|
186
|
-
curWindCompassRot: 0,
|
|
187
|
-
curBoatCompassRot: 0,
|
|
188
|
-
smoothedLeeway: 0,
|
|
189
|
-
lastGybeAlarmTime: 0,
|
|
190
|
-
lastGybeSide: null as 'starboard' | 'port' | null,
|
|
191
|
-
emaTwaSin: 0,
|
|
192
|
-
emaTwaCos: 0,
|
|
193
|
-
firstEmaRun: true,
|
|
194
|
-
lastTWCompute: 0,
|
|
195
|
-
currentTackLabelMode: 'TACK',
|
|
196
|
-
isSocketOpen: false,
|
|
197
|
-
isSimulating: false,
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Manage history insertion
|
|
201
|
-
const manageHistory = useCallback((type: keyof typeof storeRef.current.histories, value: number) => {
|
|
202
|
-
if (value === undefined || value === null || !isFinite(value)) return;
|
|
203
|
-
const store = storeRef.current;
|
|
204
|
-
const now = Date.now();
|
|
205
|
-
const historyMinutes = Math.max(1, config.graphs.historyMinutes || 10);
|
|
206
|
-
const samples = Math.max(2, config.graphs.samples || 60);
|
|
207
|
-
const bucketIntervalMs = (historyMinutes * 60000) / samples;
|
|
208
|
-
|
|
209
|
-
if (!store.graphTempBuf[type]) store.graphTempBuf[type] = [];
|
|
210
|
-
if (!store.histories[type]) store.histories[type] = [];
|
|
211
|
-
|
|
212
|
-
if (store.lastUpdates[type] === undefined || store.lastUpdates[type] === 0) {
|
|
213
|
-
store.lastUpdates[type] = Math.floor(now / bucketIntervalMs) * bucketIntervalMs;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const tempBuf = store.graphTempBuf[type];
|
|
217
|
-
tempBuf.push({ val: value, time: now });
|
|
218
|
-
|
|
219
|
-
const bucketReady = (now - store.lastUpdates[type] > bucketIntervalMs) || store.histories[type].length === 0;
|
|
220
|
-
if (!bucketReady) return;
|
|
221
|
-
|
|
222
|
-
let finalValue = value;
|
|
223
|
-
if (tempBuf.length > 0) {
|
|
224
|
-
if (type === 'tws' || type === 'aws') {
|
|
225
|
-
const tauMs = 2500;
|
|
226
|
-
let ema = tempBuf[0].val;
|
|
227
|
-
let maxSustained = ema;
|
|
228
|
-
for (let i = 1; i < tempBuf.length; i++) {
|
|
229
|
-
const dt = Math.max(1, tempBuf[i].time - tempBuf[i - 1].time);
|
|
230
|
-
const alpha = 1 - Math.exp(-dt / tauMs);
|
|
231
|
-
ema = tempBuf[i].val * alpha + ema * (1 - alpha);
|
|
232
|
-
if (isFinite(ema) && ema > maxSustained) maxSustained = ema;
|
|
233
|
-
}
|
|
234
|
-
finalValue = maxSustained;
|
|
235
|
-
} else if (type === 'depth') {
|
|
236
|
-
let minVal = Infinity;
|
|
237
|
-
for (let i = 0; i < tempBuf.length; i++) {
|
|
238
|
-
if (isFinite(tempBuf[i].val) && tempBuf[i].val < minVal) {
|
|
239
|
-
minVal = tempBuf[i].val;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (minVal !== Infinity) finalValue = minVal;
|
|
243
|
-
} else {
|
|
244
|
-
let sum = 0;
|
|
245
|
-
let count = 0;
|
|
246
|
-
for (let i = 0; i < tempBuf.length; i++) {
|
|
247
|
-
if (isFinite(tempBuf[i].val)) {
|
|
248
|
-
sum += tempBuf[i].val;
|
|
249
|
-
count++;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (count > 0) finalValue = sum / count;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (!isFinite(finalValue)) return;
|
|
257
|
-
finalValue = Math.max(0, finalValue);
|
|
258
|
-
|
|
259
|
-
store.histories[type].push({ val: finalValue, time: now });
|
|
260
|
-
|
|
261
|
-
const maxViewportMinutes = historyMinutes * 2;
|
|
262
|
-
const maxHistoryMs = maxViewportMinutes * 60000 + 60000;
|
|
263
|
-
while (store.histories[type].length > 0 && now - store.histories[type][0].time > maxHistoryMs) {
|
|
264
|
-
store.histories[type].shift();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
store.graphTempBuf[type] = [];
|
|
268
|
-
store.lastUpdates[type] = Math.floor(now / bucketIntervalMs) * bucketIntervalMs;
|
|
269
|
-
}, [config.graphs]);
|
|
270
|
-
|
|
271
|
-
// Process incoming data
|
|
272
|
-
const processIncomingData = useCallback((path: string, val: any, timeMs?: number) => {
|
|
273
|
-
if (val === null || val === undefined || (typeof val === 'number' && isNaN(val))) return;
|
|
274
|
-
|
|
275
|
-
if ((path === 'environment.wind.speedApparent' || path === 'environment.wind.speedTrue') && val > 51.44) return;
|
|
276
|
-
if ((path === 'navigation.speedThroughWater' || path === 'navigation.speedOverGround') && val > 25.72) return;
|
|
277
|
-
if (path === 'environment.depth.belowTransducer' && (val < -2.0 || val > 500)) return;
|
|
278
|
-
|
|
279
|
-
const store = storeRef.current;
|
|
280
|
-
const now = timeMs || Date.now();
|
|
281
|
-
const localNow = Date.now();
|
|
282
|
-
|
|
283
|
-
store.timestamps[path] = localNow;
|
|
284
|
-
store.raw[path] = val;
|
|
285
|
-
store.lastMessageReceivedTime = localNow;
|
|
286
|
-
|
|
287
|
-
if (path === 'environment.wind.directionTrue') {
|
|
288
|
-
const directionVal = typeof val === 'object' && val.val !== undefined ? val.val : val;
|
|
289
|
-
safePush(store.smoothBuf.twd, directionVal, now, true, config.graphs.historyMinutes);
|
|
290
|
-
safePush(store.longBuf.twd, directionVal, now, true, config.graphs.historyMinutes);
|
|
291
|
-
// Also push to twd history for radar
|
|
292
|
-
store.histories.twd.push({ val: directionVal, time: now });
|
|
293
|
-
while (store.histories.twd.length > 0 && now - store.histories.twd[0].time > 120 * 60000) {
|
|
294
|
-
store.histories.twd.shift();
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (path === 'navigation.headingTrue') {
|
|
299
|
-
store.timestamps['navigation.headingTrueNative'] = now;
|
|
300
|
-
safePush(store.smoothBuf.hdg, val, now, false, config.graphs.historyMinutes);
|
|
301
|
-
safePush(store.longBuf.hdg, val, now, false, config.graphs.historyMinutes);
|
|
302
|
-
} else if (path === 'navigation.headingMagnetic') {
|
|
303
|
-
const hasNativeTrueHdg = store.timestamps['navigation.headingTrueNative'] && (now - store.timestamps['navigation.headingTrueNative'] < 5000);
|
|
304
|
-
if (!hasNativeTrueHdg) {
|
|
305
|
-
const variation = store.raw['navigation.magneticVariation'] || 0;
|
|
306
|
-
const calculatedTrueHdg = (val + variation + 2 * Math.PI) % (2 * Math.PI);
|
|
307
|
-
store.raw['navigation.headingTrue'] = calculatedTrueHdg;
|
|
308
|
-
store.timestamps['navigation.headingTrue'] = localNow;
|
|
309
|
-
safePush(store.smoothBuf.hdg, calculatedTrueHdg, now, false, config.graphs.historyMinutes);
|
|
310
|
-
safePush(store.longBuf.hdg, calculatedTrueHdg, now, false, config.graphs.historyMinutes);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (path === 'navigation.courseOverGroundTrue') {
|
|
315
|
-
safePush(store.smoothBuf.cog, val, now, false, config.graphs.historyMinutes);
|
|
316
|
-
safePush(store.longBuf.cog, val, now, false, config.graphs.historyMinutes);
|
|
317
|
-
|
|
318
|
-
const hasCompass = store.raw['navigation.headingTrue'] !== undefined || store.raw['navigation.headingMagnetic'] !== undefined;
|
|
319
|
-
const sog = store.raw['navigation.speedOverGround'] || 0;
|
|
320
|
-
if (!hasCompass && sog > 0.77) {
|
|
321
|
-
store.raw['navigation.headingTrue'] = val;
|
|
322
|
-
store.timestamps['navigation.headingTrue'] = localNow;
|
|
323
|
-
safePush(store.smoothBuf.hdg, val, now, false, config.graphs.historyMinutes);
|
|
324
|
-
safePush(store.longBuf.hdg, val, now, false, config.graphs.historyMinutes);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Compute true wind if necessary
|
|
329
|
-
const twPaths = [
|
|
330
|
-
'environment.wind.speedApparent',
|
|
331
|
-
'environment.wind.angleApparent',
|
|
332
|
-
'navigation.speedThroughWater',
|
|
333
|
-
'navigation.speedOverGround',
|
|
334
|
-
'navigation.headingTrue',
|
|
335
|
-
'navigation.courseOverGroundTrue',
|
|
336
|
-
];
|
|
337
|
-
|
|
338
|
-
if (twPaths.includes(path)) {
|
|
339
|
-
if (now - store.lastTWCompute > 100) {
|
|
340
|
-
const aws = store.raw['environment.wind.speedApparent'];
|
|
341
|
-
const awa = store.raw['environment.wind.angleApparent'];
|
|
342
|
-
const stw = store.raw['navigation.speedThroughWater'];
|
|
343
|
-
const sog = store.raw['navigation.speedOverGround'] || 0;
|
|
344
|
-
const hdg = store.raw['navigation.headingTrue'];
|
|
345
|
-
|
|
346
|
-
if (aws !== undefined && awa !== undefined) {
|
|
347
|
-
const hasFreshStw = store.timestamps['navigation.speedThroughWater'] && (now - store.timestamps['navigation.speedThroughWater'] < 15000);
|
|
348
|
-
let speedRef = sog > 0.77 && (stw || 0) < 0.25 ? sog : hasFreshStw && stw !== undefined ? stw : sog;
|
|
349
|
-
|
|
350
|
-
const hasNativeTws = store.timestamps['environment.wind.speedTrue'] && (Date.now() - store.timestamps['environment.wind.speedTrue'] < 5000);
|
|
351
|
-
let tws_water = 0;
|
|
352
|
-
if (hasNativeTws) {
|
|
353
|
-
tws_water = store.raw['environment.wind.speedTrue'] ? msToKts(store.raw['environment.wind.speedTrue']) : 0;
|
|
354
|
-
} else {
|
|
355
|
-
tws_water = Math.sqrt(aws * aws + speedRef * speedRef - 2 * aws * speedRef * Math.cos(awa));
|
|
356
|
-
store.raw['environment.wind.speedTrue'] = tws_water;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
let twa = 0;
|
|
360
|
-
const hasNativeTwa = store.timestamps['environment.wind.angleTrueWater'] && (Date.now() - store.timestamps['environment.wind.angleTrueWater'] < 5000);
|
|
361
|
-
if (hasNativeTwa && store.raw['environment.wind.angleTrueWater'] !== undefined) {
|
|
362
|
-
twa = store.raw['environment.wind.angleTrueWater'];
|
|
363
|
-
} else if (tws_water > 0.05) {
|
|
364
|
-
twa = Math.atan2(aws * Math.sin(awa), aws * Math.cos(awa) - speedRef);
|
|
365
|
-
store.raw['environment.wind.angleTrueWater'] = twa;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (tws_water > 0.05 || hasNativeTwa) {
|
|
369
|
-
safePush(store.smoothBuf.twa, twa, now, true, config.graphs.historyMinutes);
|
|
370
|
-
safePush(store.longBuf.twa, twa, now, true, config.graphs.historyMinutes);
|
|
371
|
-
if (awa !== undefined) {
|
|
372
|
-
safePush(store.smoothBuf.awa, awa, now, true, config.graphs.historyMinutes);
|
|
373
|
-
safePush(store.longBuf.awa, awa, now, true, config.graphs.historyMinutes);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const hasNativeTwd = store.timestamps['environment.wind.directionTrue'] && (Date.now() - store.timestamps['environment.wind.directionTrue'] < 5000);
|
|
378
|
-
if (!hasNativeTwd && tws_water > 0.05 && hdg !== undefined) {
|
|
379
|
-
const twd = (hdg + twa + 2 * Math.PI) % (2 * Math.PI);
|
|
380
|
-
store.raw['environment.wind.directionTrue'] = twd;
|
|
381
|
-
safePush(store.smoothBuf.twd, twd, now, true, config.graphs.historyMinutes);
|
|
382
|
-
safePush(store.longBuf.twd, twd, now, true, config.graphs.historyMinutes);
|
|
383
|
-
store.histories.twd.push({ val: twd, time: now });
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
store.lastTWCompute = now;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}, [config.graphs.historyMinutes]);
|
|
390
|
-
|
|
391
|
-
// Load configuration from settings.json
|
|
392
|
-
useEffect(() => {
|
|
393
|
-
fetch('/settings.json')
|
|
394
|
-
.then(res => res.json())
|
|
395
|
-
.then(data => {
|
|
396
|
-
if (data) {
|
|
397
|
-
setConfig(prev => ({
|
|
398
|
-
...prev,
|
|
399
|
-
...data,
|
|
400
|
-
alarms: { ...prev.alarms, ...(data.alarms || {}) },
|
|
401
|
-
graphs: { ...prev.graphs, ...(data.graphs || {}) },
|
|
402
|
-
averaging: { ...prev.averaging, ...(data.averaging || {}) },
|
|
403
|
-
scales: { ...prev.scales, ...(data.scales || {}) },
|
|
404
|
-
}));
|
|
405
|
-
}
|
|
406
|
-
})
|
|
407
|
-
.catch(() => {
|
|
408
|
-
// Use defaults
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
// Load saved dashboard state
|
|
412
|
-
const saved = localStorage.getItem('rotevista_dash_state');
|
|
413
|
-
if (saved) {
|
|
414
|
-
try {
|
|
415
|
-
const state = JSON.parse(saved);
|
|
416
|
-
if (state.displayModeSog) setDisplayModeSog(state.displayModeSog);
|
|
417
|
-
if (state.displayModeTws) setDisplayModeTws(state.displayModeTws);
|
|
418
|
-
if (state.graphModes) setGraphModes(state.graphModes);
|
|
419
|
-
if (state.isNightMode) setIsNightMode(state.isNightMode);
|
|
420
|
-
} catch (e) {
|
|
421
|
-
// Ignore
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}, []);
|
|
425
|
-
|
|
426
|
-
// Sync Night Mode class with body
|
|
427
|
-
useEffect(() => {
|
|
428
|
-
if (isNightMode) {
|
|
429
|
-
document.body.classList.add('night-mode');
|
|
430
|
-
} else {
|
|
431
|
-
document.body.classList.remove('night-mode');
|
|
432
|
-
}
|
|
433
|
-
}, [isNightMode]);
|
|
434
|
-
|
|
435
|
-
const historyInitializedRef = useRef(false);
|
|
436
|
-
|
|
437
|
-
// Pre-seed mock history for rich initial display (strictly once)
|
|
438
|
-
useEffect(() => {
|
|
439
|
-
if (historyInitializedRef.current) return;
|
|
440
|
-
historyInitializedRef.current = true;
|
|
441
|
-
|
|
442
|
-
const store = storeRef.current;
|
|
443
|
-
const now = Date.now();
|
|
444
|
-
const historyMinutes = config.graphs.historyMinutes || 30;
|
|
445
|
-
const stepMs = (historyMinutes * 60000) / 60;
|
|
446
|
-
|
|
447
|
-
const baseHdg = 45;
|
|
448
|
-
const baseTws = 12;
|
|
449
|
-
const baseTwd = 90;
|
|
450
|
-
const baseDepth = 12;
|
|
451
|
-
const baseStw = 5.2;
|
|
452
|
-
|
|
453
|
-
// Reset histories to prevent duplicates
|
|
454
|
-
store.histories.stw = [];
|
|
455
|
-
store.histories.sog = [];
|
|
456
|
-
store.histories.vmg = [];
|
|
457
|
-
store.histories.tws = [];
|
|
458
|
-
store.histories.aws = [];
|
|
459
|
-
store.histories.depth = [];
|
|
460
|
-
store.histories.twd = [];
|
|
461
|
-
|
|
462
|
-
// Ascending order: from oldest (now - 60*stepMs) to newest (now)
|
|
463
|
-
// Realistic marine telemetry with wave chop, gustiness, and steering response
|
|
464
|
-
for (let i = 60; i >= 0; i--) {
|
|
465
|
-
const t = now - i * stepMs;
|
|
466
|
-
// Wind speed has natural gust cycles (base 12.5 kts, oscillating with realistic gust peaks)
|
|
467
|
-
const twsNoise = Math.sin(i * 0.45) * 1.8 + Math.cos(i * 0.9) * 0.9 + Math.sin(i * 1.7) * 0.6;
|
|
468
|
-
const twsVal = Math.max(7.0, baseTws + twsNoise);
|
|
469
|
-
|
|
470
|
-
// Wind direction shifts with atmospheric oscillations (shifts ±6°)
|
|
471
|
-
const twdShift = Math.sin(i * 0.3) * 5.5 + Math.cos(i * 0.75) * 2.5;
|
|
472
|
-
const twdDegVal = (baseTwd + twdShift + 360) % 360;
|
|
473
|
-
const twdVal = degToRad(twdDegVal);
|
|
474
|
-
|
|
475
|
-
// Helmsman tracks wind shifts with slight yaw (±2°)
|
|
476
|
-
const hdgShift = twdShift * 0.7 + Math.sin(i * 1.2) * 1.8;
|
|
477
|
-
const hdgDegVal = (baseHdg + hdgShift + 360) % 360;
|
|
478
|
-
const hdgVal = degToRad(hdgDegVal);
|
|
479
|
-
|
|
480
|
-
// True Wind Angle fluctuates naturally between 41° and 49°
|
|
481
|
-
const twaDegVal = Math.max(38, Math.min(52, (twdDegVal - hdgDegVal + 360) % 360));
|
|
482
|
-
const twaRad = degToRad(twaDegVal);
|
|
483
|
-
|
|
484
|
-
// Hull speed in waves: boat surges when wind increases, with natural wave chop
|
|
485
|
-
const waveChop = Math.sin(i * 1.5) * 0.35 + Math.cos(i * 2.8) * 0.25;
|
|
486
|
-
const windDrive = (twsVal - 12.0) * 0.22;
|
|
487
|
-
const stwVal = Math.max(3.5, baseStw + windDrive + waveChop);
|
|
488
|
-
|
|
489
|
-
// SOG: speed over ground with tidal current drift (+0.25 kts) and GPS micro-jitter
|
|
490
|
-
const sogVal = Math.max(3.8, stwVal + 0.25 + Math.sin(i * 0.8) * 0.15);
|
|
491
|
-
|
|
492
|
-
// VMG: authentic Velocity Made Good = SOG * cos(TWA)
|
|
493
|
-
const vmgVal = Math.max(2.2, sogVal * Math.cos(twaRad));
|
|
494
|
-
|
|
495
|
-
// Apparent wind vector
|
|
496
|
-
const tw_x = twsVal * Math.cos(twaRad);
|
|
497
|
-
const tw_y = twsVal * Math.sin(twaRad);
|
|
498
|
-
const aw_x = tw_x + stwVal;
|
|
499
|
-
const aw_y = tw_y;
|
|
500
|
-
const awsVal = Math.sqrt(aw_x * aw_x + aw_y * aw_y);
|
|
501
|
-
const awaVal = Math.atan2(aw_y, aw_x);
|
|
502
|
-
|
|
503
|
-
// Seabed depth with natural bathymetric contours
|
|
504
|
-
const depthVal = Math.max(4.0, baseDepth + Math.sin(i * 0.25) * 3.5 + Math.cos(i * 0.6) * 1.2);
|
|
505
|
-
|
|
506
|
-
store.histories.stw.push({ val: parseFloat(stwVal.toFixed(2)), time: t });
|
|
507
|
-
store.histories.sog.push({ val: parseFloat(sogVal.toFixed(2)), time: t });
|
|
508
|
-
store.histories.vmg.push({ val: parseFloat(vmgVal.toFixed(2)), time: t });
|
|
509
|
-
store.histories.tws.push({ val: parseFloat(twsVal.toFixed(2)), time: t });
|
|
510
|
-
store.histories.aws.push({ val: parseFloat(awsVal.toFixed(2)), time: t });
|
|
511
|
-
store.histories.depth.push({ val: parseFloat(depthVal.toFixed(2)), time: t });
|
|
512
|
-
store.histories.twd.push({ val: twdVal, time: t });
|
|
513
|
-
|
|
514
|
-
safePush(store.longBuf.twd, twdVal, t, true, historyMinutes);
|
|
515
|
-
safePush(store.smoothBuf.twd, twdVal, t, true, historyMinutes);
|
|
516
|
-
safePush(store.longBuf.hdg, hdgVal, t, false, historyMinutes);
|
|
517
|
-
// With wind from the right (starboard tack: TWA > 0), aerodynamic and hydrodynamic leeway
|
|
518
|
-
// pushes the vessel to port (left/sottovento). Therefore COG drifts to the left of HDG (COG = HDG - leeway).
|
|
519
|
-
const leewayDegSeed = Math.sin(twaRad) * 2.8;
|
|
520
|
-
const cogDegVal = (hdgDegVal - leewayDegSeed + 360) % 360;
|
|
521
|
-
safePush(store.longBuf.cog, degToRad(cogDegVal), t, false, historyMinutes);
|
|
522
|
-
safePush(store.smoothBuf.cog, degToRad(cogDegVal), t, false, historyMinutes);
|
|
523
|
-
safePush(store.longBuf.twa, twaRad, t, true, historyMinutes);
|
|
524
|
-
safePush(store.smoothBuf.twa, twaRad, t, true, historyMinutes);
|
|
525
|
-
safePush(store.longBuf.awa, awaVal, t, true, historyMinutes);
|
|
526
|
-
safePush(store.smoothBuf.awa, awaVal, t, true, historyMinutes);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
setHistoriesState({
|
|
530
|
-
stw: [...store.histories.stw],
|
|
531
|
-
sog: [...store.histories.sog],
|
|
532
|
-
vmg: [...store.histories.vmg],
|
|
533
|
-
depth: [...store.histories.depth],
|
|
534
|
-
tws: [...store.histories.tws],
|
|
535
|
-
aws: [...store.histories.aws],
|
|
536
|
-
twd: [...store.histories.twd],
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Pre-populate sample historical radar slots for the past 3 hours
|
|
540
|
-
const current30mSlot = Math.floor(now / 1800000) * 1800000;
|
|
541
|
-
const sampleSlots: WindRadarSlot[] = [
|
|
542
|
-
{
|
|
543
|
-
timestamp: current30mSlot - 1800000,
|
|
544
|
-
twdMin: degToRad(75),
|
|
545
|
-
twdMax: degToRad(105),
|
|
546
|
-
twsPeak: 14.5,
|
|
547
|
-
twsMin: 9.8,
|
|
548
|
-
isCalm: false,
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
timestamp: current30mSlot - 3600000,
|
|
552
|
-
twdMin: degToRad(60),
|
|
553
|
-
twdMax: degToRad(95),
|
|
554
|
-
twsPeak: 16.2,
|
|
555
|
-
twsMin: 11.0,
|
|
556
|
-
isCalm: false,
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
timestamp: current30mSlot - 5400000,
|
|
560
|
-
twdMin: degToRad(80),
|
|
561
|
-
twdMax: degToRad(110),
|
|
562
|
-
twsPeak: 12.0,
|
|
563
|
-
twsMin: 8.5,
|
|
564
|
-
isCalm: false,
|
|
565
|
-
},
|
|
566
|
-
];
|
|
567
|
-
setWindRadarSlots(sampleSlots);
|
|
568
|
-
|
|
569
|
-
setFutureForecast({
|
|
570
|
-
timestamp: now + 1800000,
|
|
571
|
-
tws: 13.5,
|
|
572
|
-
twd: degToRad(95),
|
|
573
|
-
gust: 18.0,
|
|
574
|
-
});
|
|
575
|
-
}, [config.graphs.historyMinutes]);
|
|
576
|
-
|
|
577
|
-
// WebSocket or Simulation connection
|
|
578
|
-
useEffect(() => {
|
|
579
|
-
let ws: WebSocket | null = null;
|
|
580
|
-
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
581
|
-
let simTimer: ReturnType<typeof setInterval> | null = null;
|
|
582
|
-
|
|
583
|
-
const startSimulation = () => {
|
|
584
|
-
storeRef.current.isSimulating = true;
|
|
585
|
-
let sim = {
|
|
586
|
-
hdg: 45,
|
|
587
|
-
tws: 12.5,
|
|
588
|
-
twd: 90,
|
|
589
|
-
depth: 12,
|
|
590
|
-
stw: 5.2,
|
|
591
|
-
startTime: Date.now(),
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
simTimer = setInterval(() => {
|
|
595
|
-
const elapsed = (Date.now() - sim.startTime) / 1000;
|
|
596
|
-
|
|
597
|
-
// Wave chop & swell surge (period ~ 3-5 seconds)
|
|
598
|
-
const waveChop = Math.sin(elapsed * 1.5) * 0.35 + Math.cos(elapsed * 2.7) * 0.2 + (Math.random() - 0.5) * 0.15;
|
|
599
|
-
|
|
600
|
-
// Realistic atmospheric gustiness & lulls (base ~ 12.5 kts, gusts up to 14.5 kts, lulls to 10.5 kts)
|
|
601
|
-
const windGust = Math.sin(elapsed * 0.35) * 1.5 + Math.sin(elapsed * 0.12) * 1.2 + (Math.random() - 0.5) * 0.3;
|
|
602
|
-
sim.tws = Math.max(8.0, 12.5 + windGust);
|
|
603
|
-
|
|
604
|
-
// Wind shifts: macro oscillations (±6°) with micro-turbulence (±1.5°)
|
|
605
|
-
const windShift = Math.sin(elapsed * 0.15) * 5.5 + Math.sin(elapsed * 0.5) * 1.8;
|
|
606
|
-
sim.twd = (90 + windShift + 360) % 360;
|
|
607
|
-
|
|
608
|
-
// Helmsman yaw oscillation keeping close-hauled
|
|
609
|
-
const yaw = windShift * 0.7 + Math.sin(elapsed * 0.8) * 1.4 + (Math.random() - 0.5) * 0.4;
|
|
610
|
-
sim.hdg = (45 + yaw + 360) % 360;
|
|
611
|
-
|
|
612
|
-
let twaRel = (sim.twd - sim.hdg + 360) % 360;
|
|
613
|
-
if (twaRel > 180) twaRel -= 360;
|
|
614
|
-
const twaRad = degToRad(twaRel);
|
|
615
|
-
|
|
616
|
-
// Speed through water: boat accelerates on gusts and surges on waves
|
|
617
|
-
const windSurge = (sim.tws - 12.5) * 0.2;
|
|
618
|
-
sim.stw = Math.max(3.8, 5.2 + windSurge + waveChop);
|
|
619
|
-
const sogKts = sim.stw + 0.25 + (Math.random() - 0.5) * 0.08;
|
|
620
|
-
|
|
621
|
-
// Apparent wind vector
|
|
622
|
-
const tw_x = sim.tws * Math.cos(twaRad);
|
|
623
|
-
const tw_y = sim.tws * Math.sin(twaRad);
|
|
624
|
-
const aw_x = tw_x + sim.stw;
|
|
625
|
-
const aw_y = tw_y;
|
|
626
|
-
const awsKts = Math.sqrt(aw_x * aw_x + aw_y * aw_y);
|
|
627
|
-
const awaRad = Math.atan2(aw_y, aw_x);
|
|
628
|
-
|
|
629
|
-
const currentDepth = Math.max(4.0, sim.depth + Math.sin(elapsed * 0.08) * 2.5 + (Math.random() - 0.5) * 0.1);
|
|
630
|
-
|
|
631
|
-
processIncomingData('navigation.headingTrue', degToRad(sim.hdg));
|
|
632
|
-
processIncomingData('environment.wind.speedTrue', ktsToMs(sim.tws));
|
|
633
|
-
processIncomingData('environment.wind.angleTrueWater', twaRad);
|
|
634
|
-
processIncomingData('environment.wind.directionTrue', degToRad(sim.twd));
|
|
635
|
-
processIncomingData('environment.wind.speedApparent', ktsToMs(awsKts));
|
|
636
|
-
processIncomingData('environment.wind.angleApparent', awaRad);
|
|
637
|
-
processIncomingData('environment.depth.belowTransducer', currentDepth);
|
|
638
|
-
processIncomingData('navigation.speedThroughWater', ktsToMs(sim.stw));
|
|
639
|
-
processIncomingData('navigation.speedOverGround', ktsToMs(sogKts));
|
|
640
|
-
// Authentic leeway physics: with wind from starboard (twaRad > 0, right),
|
|
641
|
-
// the hull drifts to port (left/sottovento). COG = HDG - leeway.
|
|
642
|
-
const leewaySimDeg = Math.sin(twaRad) * 2.8 + (Math.random() - 0.5) * 0.15;
|
|
643
|
-
const simCog = (sim.hdg - leewaySimDeg + 360) % 360;
|
|
644
|
-
processIncomingData('navigation.courseOverGroundTrue', degToRad(simCog));
|
|
645
|
-
}, 1000);
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
const connectWs = () => {
|
|
649
|
-
const addr = window.location.host;
|
|
650
|
-
// Only connect to WebSocket if not on localhost / cloud preview without SignalK
|
|
651
|
-
try {
|
|
652
|
-
ws = new WebSocket(`ws://${addr}/signalk/v1/stream?subscribe=none`);
|
|
653
|
-
|
|
654
|
-
ws.onopen = () => {
|
|
655
|
-
storeRef.current.isSocketOpen = true;
|
|
656
|
-
storeRef.current.isSimulating = false;
|
|
657
|
-
if (simTimer) clearInterval(simTimer);
|
|
658
|
-
|
|
659
|
-
const subscriptionPayload = {
|
|
660
|
-
context: 'vessels.self',
|
|
661
|
-
subscribe: [
|
|
662
|
-
{ path: 'navigation.position', minPeriod: 60000 },
|
|
663
|
-
{ path: 'navigation.magneticVariation', minPeriod: 60000 },
|
|
664
|
-
{ path: 'environment.depth.belowTransducer', minPeriod: 1000 },
|
|
665
|
-
{ path: 'navigation.speedThroughWater', minPeriod: 333 },
|
|
666
|
-
{ path: 'navigation.speedOverGround', minPeriod: 333 },
|
|
667
|
-
{ path: 'navigation.courseOverGroundTrue', minPeriod: 333 },
|
|
668
|
-
{ path: 'navigation.headingTrue', minPeriod: 333 },
|
|
669
|
-
{ path: 'navigation.headingMagnetic', minPeriod: 333 },
|
|
670
|
-
{ path: 'environment.wind.speedApparent', minPeriod: 333 },
|
|
671
|
-
{ path: 'environment.wind.angleApparent', minPeriod: 333 },
|
|
672
|
-
{ path: 'environment.wind.speedTrue', minPeriod: 333 },
|
|
673
|
-
{ path: 'environment.wind.angleTrueWater', minPeriod: 333 },
|
|
674
|
-
{ path: 'environment.wind.directionTrue', minPeriod: 333 },
|
|
675
|
-
],
|
|
676
|
-
};
|
|
677
|
-
try {
|
|
678
|
-
ws?.send(JSON.stringify(subscriptionPayload));
|
|
679
|
-
} catch (e) {}
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
ws.onmessage = (e) => {
|
|
683
|
-
try {
|
|
684
|
-
const d = JSON.parse(e.data);
|
|
685
|
-
if (d.updates) {
|
|
686
|
-
d.updates.forEach((u: any) => {
|
|
687
|
-
const timeMs = u.timestamp ? new Date(u.timestamp).getTime() : Date.now();
|
|
688
|
-
if (u.values) {
|
|
689
|
-
u.values.forEach((v: any) => processIncomingData(v.path, v.value, timeMs));
|
|
690
|
-
}
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
} catch (err) {}
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
ws.onclose = () => {
|
|
697
|
-
storeRef.current.isSocketOpen = false;
|
|
698
|
-
// Fall back to simulation if real socket is closed
|
|
699
|
-
if (!storeRef.current.isSimulating) {
|
|
700
|
-
startSimulation();
|
|
701
|
-
}
|
|
702
|
-
reconnectTimeout = setTimeout(connectWs, 30000);
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
ws.onerror = () => {
|
|
706
|
-
if (!storeRef.current.isSimulating) {
|
|
707
|
-
startSimulation();
|
|
708
|
-
}
|
|
709
|
-
};
|
|
710
|
-
} catch (err) {
|
|
711
|
-
startSimulation();
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
|
|
715
|
-
connectWs();
|
|
716
|
-
|
|
717
|
-
return () => {
|
|
718
|
-
if (ws) ws.close();
|
|
719
|
-
if (reconnectTimeout) clearTimeout(reconnectTimeout);
|
|
720
|
-
if (simTimer) clearInterval(simTimer);
|
|
721
|
-
};
|
|
722
|
-
}, [processIncomingData]);
|
|
723
|
-
|
|
724
|
-
// Main 1Hz Tactical Display Loop
|
|
725
|
-
useEffect(() => {
|
|
726
|
-
const interval = setInterval(() => {
|
|
727
|
-
const store = storeRef.current;
|
|
728
|
-
const now = Date.now();
|
|
729
|
-
|
|
730
|
-
const stwKts = msToKts(store.raw['navigation.speedThroughWater'] || 0);
|
|
731
|
-
const sogKts = msToKts(store.raw['navigation.speedOverGround'] || 0);
|
|
732
|
-
const isNav = stwKts > config.averaging.minSpeed || sogKts > config.averaging.minSpeed;
|
|
733
|
-
|
|
734
|
-
// Depth Alarm
|
|
735
|
-
const rawDepth = store.raw['environment.depth.belowTransducer'];
|
|
736
|
-
let depthAlarm = '';
|
|
737
|
-
if (rawDepth !== undefined) {
|
|
738
|
-
manageHistory('depth', rawDepth);
|
|
739
|
-
if (rawDepth < config.alarms.depthDanger) {
|
|
740
|
-
depthAlarm = 'alarm-danger blink-alarm';
|
|
741
|
-
if (isNav) playBingBing();
|
|
742
|
-
} else if (rawDepth < config.alarms.depthWarning) {
|
|
743
|
-
depthAlarm = 'alarm-warning';
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// STW & SOG / VMG histories
|
|
748
|
-
if (store.raw['navigation.speedThroughWater'] !== undefined) {
|
|
749
|
-
manageHistory('stw', stwKts);
|
|
750
|
-
}
|
|
751
|
-
if (store.raw['navigation.speedOverGround'] !== undefined) {
|
|
752
|
-
manageHistory('sog', sogKts);
|
|
753
|
-
const hasFreshStw = store.timestamps['navigation.speedThroughWater'] && (now - store.timestamps['navigation.speedThroughWater'] < 15000);
|
|
754
|
-
const speedRefKts = hasFreshStw && !(sogKts > 1.5 && stwKts < 0.5) ? stwKts : sogKts;
|
|
755
|
-
const vmgVal = Math.abs(speedRefKts * Math.cos(store.raw['environment.wind.angleTrueWater'] || 0));
|
|
756
|
-
manageHistory('vmg', vmgVal);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// TWS & AWS histories
|
|
760
|
-
const rawTws = store.raw['environment.wind.speedTrue'];
|
|
761
|
-
const rawAws = store.raw['environment.wind.speedApparent'];
|
|
762
|
-
const twsVal = rawTws !== undefined && rawTws !== null ? msToKts(rawTws) : 0;
|
|
763
|
-
const awsVal = rawAws !== undefined && rawAws !== null ? msToKts(rawAws) : 0;
|
|
764
|
-
if (rawTws !== undefined) manageHistory('tws', twsVal);
|
|
765
|
-
if (rawAws !== undefined) manageHistory('aws', awsVal);
|
|
766
|
-
|
|
767
|
-
// Leeway & Track Pointer calculation
|
|
768
|
-
let smoothedLeeway = store.smoothedLeeway;
|
|
769
|
-
let curTrackRot = store.curTrackRot;
|
|
770
|
-
if (store.raw['navigation.courseOverGroundTrue'] !== undefined && store.raw['navigation.headingTrue'] !== undefined) {
|
|
771
|
-
const driftDeg = radToDeg(
|
|
772
|
-
((store.raw['navigation.courseOverGroundTrue'] - store.raw['navigation.headingTrue'] + Math.PI * 3) % (2 * Math.PI)) - Math.PI
|
|
773
|
-
);
|
|
774
|
-
smoothedLeeway = sogKts < config.averaging.minSpeed ? 0 : smoothedLeeway * 0.9 + driftDeg * 0.1;
|
|
775
|
-
curTrackRot = getShortestRotation(curTrackRot, smoothedLeeway);
|
|
776
|
-
store.smoothedLeeway = smoothedLeeway;
|
|
777
|
-
store.curTrackRot = curTrackRot;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Analog Pointers Rotations
|
|
781
|
-
const smAwa = getCircularAverageFromBuffer(store.smoothBuf.awa, 2000, true, now);
|
|
782
|
-
const smTwa = getCircularAverageFromBuffer(store.smoothBuf.twa, 2000, true, now);
|
|
783
|
-
let awaPointerDeg = store.curAwaRot;
|
|
784
|
-
let twaPointerDeg = store.curTwaRot;
|
|
785
|
-
if (smAwa) {
|
|
786
|
-
awaPointerDeg = getShortestRotation(awaPointerDeg, radToDeg(smAwa.val));
|
|
787
|
-
store.curAwaRot = awaPointerDeg;
|
|
788
|
-
}
|
|
789
|
-
if (smTwa) {
|
|
790
|
-
twaPointerDeg = getShortestRotation(twaPointerDeg, radToDeg(smTwa.val));
|
|
791
|
-
store.curTwaRot = twaPointerDeg;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Mini-Compass rotations
|
|
795
|
-
const smHdgIcons = getCircularAverageFromBuffer(store.longBuf.hdg, config.averaging.longWindow * 2, false, now);
|
|
796
|
-
const smTwdIcons = getCircularAverageFromBuffer(store.longBuf.twd, config.averaging.longWindow, false, now);
|
|
797
|
-
const twdAngleRad = smTwdIcons ? smTwdIcons.val : store.raw['environment.wind.directionTrue'];
|
|
798
|
-
const hdgAngleRad = smHdgIcons ? smHdgIcons.val : store.raw['navigation.headingTrue'];
|
|
799
|
-
|
|
800
|
-
let windCompassRotDeg = store.curWindCompassRot;
|
|
801
|
-
let boatCompassRotDeg = store.curBoatCompassRot;
|
|
802
|
-
if (twdAngleRad !== undefined) {
|
|
803
|
-
windCompassRotDeg = getShortestRotation(windCompassRotDeg, radToDeg(twdAngleRad));
|
|
804
|
-
store.curWindCompassRot = windCompassRotDeg;
|
|
805
|
-
}
|
|
806
|
-
if (hdgAngleRad !== undefined) {
|
|
807
|
-
boatCompassRotDeg = getShortestRotation(boatCompassRotDeg, radToDeg(hdgAngleRad));
|
|
808
|
-
store.curBoatCompassRot = boatCompassRotDeg;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Wind Trend & Gybe Alarm state machine
|
|
812
|
-
const instTwaRad = store.raw['environment.wind.angleTrueWater'];
|
|
813
|
-
let smoothedTwaDeg: number | null = null;
|
|
814
|
-
let tacticCw = { active: false, color: '#bbb', gybing: false };
|
|
815
|
-
let tacticCcw = { active: false, color: '#bbb', gybing: false };
|
|
816
|
-
|
|
817
|
-
if (instTwaRad !== undefined) {
|
|
818
|
-
const dynamicAlpha = Math.max(0.05, 1.1 - config.averaging.stabilityThreshold);
|
|
819
|
-
const curSin = Math.sin(instTwaRad);
|
|
820
|
-
const curCos = Math.cos(instTwaRad);
|
|
821
|
-
|
|
822
|
-
if (store.firstEmaRun) {
|
|
823
|
-
store.emaTwaSin = curSin;
|
|
824
|
-
store.emaTwaCos = curCos;
|
|
825
|
-
store.firstEmaRun = false;
|
|
826
|
-
} else {
|
|
827
|
-
store.emaTwaSin = curSin * dynamicAlpha + store.emaTwaSin * (1 - dynamicAlpha);
|
|
828
|
-
store.emaTwaCos = curCos * dynamicAlpha + store.emaTwaCos * (1 - dynamicAlpha);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
smoothedTwaDeg = radToDeg(Math.atan2(store.emaTwaSin, store.emaTwaCos));
|
|
832
|
-
const absTwaDeg = Math.abs(smoothedTwaDeg);
|
|
833
|
-
|
|
834
|
-
if (absTwaDeg > 155) {
|
|
835
|
-
tacticCw = { active: true, color: '#ff3b30', gybing: true };
|
|
836
|
-
tacticCcw = { active: false, color: '#bbb', gybing: false };
|
|
837
|
-
|
|
838
|
-
let currentTack: 'starboard' | 'port' | null = null;
|
|
839
|
-
if (smoothedTwaDeg > 155 && smoothedTwaDeg < 170) {
|
|
840
|
-
currentTack = 'starboard';
|
|
841
|
-
} else if (smoothedTwaDeg < -155 && smoothedTwaDeg > -170) {
|
|
842
|
-
currentTack = 'port';
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (currentTack !== null) {
|
|
846
|
-
if (store.lastGybeSide === null) {
|
|
847
|
-
store.lastGybeSide = currentTack;
|
|
848
|
-
} else if (store.lastGybeSide !== currentTack) {
|
|
849
|
-
store.lastGybeSide = currentTack;
|
|
850
|
-
const isMotoringDownwind = absTwaDeg > 135 && sogKts > twsVal * 0.75;
|
|
851
|
-
const isGybeDangerous = twsVal > 7.0 && sogKts > 1.0 && !isMotoringDownwind;
|
|
852
|
-
if (isGybeDangerous && now - store.lastGybeAlarmTime > 60000) {
|
|
853
|
-
store.lastGybeAlarmTime = now;
|
|
854
|
-
playGybeAlarm();
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
} else {
|
|
859
|
-
store.lastGybeSide = null;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// Tactical trend dots (lifts / headers)
|
|
864
|
-
const twaNow = getCircularAverageFromBuffer(store.longBuf.twa, 2000, true, now);
|
|
865
|
-
const twaRef = getCircularAverageFromBuffer(store.longBuf.twa, 10000, true, now);
|
|
866
|
-
if (twaNow && twaRef && (smoothedTwaDeg === null || Math.abs(smoothedTwaDeg) <= 155)) {
|
|
867
|
-
const deltaTac = radToDeg(((twaNow.val - twaRef.val + Math.PI * 3) % (2 * Math.PI)) - Math.PI);
|
|
868
|
-
const curTwaDegVal = radToDeg(twaNow.val);
|
|
869
|
-
if (Math.abs(deltaTac) > 3.0) {
|
|
870
|
-
const absTwa = Math.abs(curTwaDegVal);
|
|
871
|
-
let tacticColor = '#bbb';
|
|
872
|
-
if (absTwa <= 75 || absTwa >= 105) {
|
|
873
|
-
let isLift = curTwaDegVal > 0 ? deltaTac > 0 : deltaTac < 0;
|
|
874
|
-
if (absTwa >= 90) isLift = !isLift;
|
|
875
|
-
tacticColor = isLift ? '#27ae60' : '#c0392b';
|
|
876
|
-
}
|
|
877
|
-
if (deltaTac > 0) {
|
|
878
|
-
tacticCw = { active: true, color: tacticColor, gybing: false };
|
|
879
|
-
tacticCcw = { active: false, color: '#bbb', gybing: false };
|
|
880
|
-
} else {
|
|
881
|
-
tacticCcw = { active: true, color: tacticColor, gybing: false };
|
|
882
|
-
tacticCw = { active: false, color: '#bbb', gybing: false };
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
// Strategic Weather Trend (TWD 1m vs 15m/60m)
|
|
888
|
-
const twdNow = getCircularAverageFromBuffer(store.longBuf.twd, 60000, false, now);
|
|
889
|
-
const strategicWindowMs = (isNav ? 15 : 60) * 60000;
|
|
890
|
-
const twdRef = getCircularAverageFromBuffer(store.longBuf.twd, strategicWindowMs, false, now);
|
|
891
|
-
let compassCw = { active: false, color: '#bbb' };
|
|
892
|
-
let compassCcw = { active: false, color: '#bbb' };
|
|
893
|
-
let meteoLedState = null;
|
|
894
|
-
|
|
895
|
-
if (twdNow && twdRef) {
|
|
896
|
-
const deltaMeteo = radToDeg(((twdNow.val - twdRef.val + Math.PI * 3) % (2 * Math.PI)) - Math.PI);
|
|
897
|
-
if (Math.abs(deltaMeteo) > 6.0) {
|
|
898
|
-
const isSouth = store.raw['navigation.position']?.latitude < 0;
|
|
899
|
-
const meteoColor = !isSouth ? (deltaMeteo < 0 ? '#27ae60' : '#c0392b') : deltaMeteo > 0 ? '#27ae60' : '#c0392b';
|
|
900
|
-
if (deltaMeteo > 0) {
|
|
901
|
-
compassCw = { active: true, color: meteoColor };
|
|
902
|
-
} else {
|
|
903
|
-
compassCcw = { active: true, color: meteoColor };
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
meteoLedState = {
|
|
907
|
-
active: true,
|
|
908
|
-
color: meteoColor,
|
|
909
|
-
angleTarget: deltaMeteo > 0 ? radToDeg(twdNow.val) + 15 : radToDeg(twdNow.val) - 15,
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
// Circular averages for digital readouts
|
|
915
|
-
const baseThreshold = config.averaging.stabilityThreshold || 0.95;
|
|
916
|
-
const breakout = config.averaging.stabilityBreakout || 15;
|
|
917
|
-
const windThreshold = Math.max(0.6, baseThreshold - 0.13);
|
|
918
|
-
|
|
919
|
-
const hObj = getCircularAverageFromBuffer(store.longBuf.hdg, config.averaging.longWindow * 2, false, now, baseThreshold, breakout);
|
|
920
|
-
const cObj = getCircularAverageFromBuffer(store.longBuf.cog, config.averaging.longWindow, false, now, baseThreshold, breakout);
|
|
921
|
-
const awObj = getCircularAverageFromBuffer(store.longBuf.awa, config.averaging.longWindow, true, now, windThreshold, breakout);
|
|
922
|
-
const twObj = getCircularAverageFromBuffer(store.longBuf.twa, config.averaging.longWindow, true, now, windThreshold, breakout);
|
|
923
|
-
const twdObj = getCircularAverageFromBuffer(store.longBuf.twd, config.averaging.longWindow, false, now, windThreshold, breakout);
|
|
924
|
-
|
|
925
|
-
// Tack vs Gybe prediction with 10 degree hysteresis
|
|
926
|
-
let tackLabelMode = store.currentTackLabelMode;
|
|
927
|
-
const curAbsTwa = Math.abs(radToDeg(store.raw['environment.wind.angleTrueWater'] || 0));
|
|
928
|
-
if (tackLabelMode === 'TACK' && curAbsTwa > 95) {
|
|
929
|
-
tackLabelMode = 'GYBE';
|
|
930
|
-
} else if (tackLabelMode === 'GYBE' && curAbsTwa < 85) {
|
|
931
|
-
tackLabelMode = 'TACK';
|
|
932
|
-
}
|
|
933
|
-
store.currentTackLabelMode = tackLabelMode;
|
|
934
|
-
|
|
935
|
-
// Tack Opposite Course Reflection
|
|
936
|
-
let tackHdgStr = '---°';
|
|
937
|
-
let tackHdgUnstable = false;
|
|
938
|
-
let tackCogStr = '---°';
|
|
939
|
-
let tackCogUnstable = false;
|
|
940
|
-
|
|
941
|
-
if (hObj && twdObj) {
|
|
942
|
-
const reflectAngle = (targetRad: number, axisRad: number) => {
|
|
943
|
-
const dS = Math.sin(axisRad - targetRad);
|
|
944
|
-
const dC = Math.cos(axisRad - targetRad);
|
|
945
|
-
return Math.atan2(Math.sin(axisRad) * dC + Math.cos(axisRad) * dS, Math.cos(axisRad) * dC - Math.sin(axisRad) * dS);
|
|
946
|
-
};
|
|
947
|
-
|
|
948
|
-
const unstableH = !hObj.stable || !twdObj.stable || hObj.dev > config.averaging.stabilityBreakout;
|
|
949
|
-
if (!isNav) {
|
|
950
|
-
tackHdgStr = '---°';
|
|
951
|
-
} else if (unstableH) {
|
|
952
|
-
tackHdgStr = '---°';
|
|
953
|
-
tackHdgUnstable = true;
|
|
954
|
-
} else {
|
|
955
|
-
const rH = (radToDeg(reflectAngle(hObj.val, twdObj.val)) + 360) % 360;
|
|
956
|
-
tackHdgStr = `${Math.round(rH).toString().padStart(3, '0')}°`;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
if (cObj) {
|
|
960
|
-
const unstableC = !cObj.stable || !twdObj.stable || cObj.dev > config.averaging.stabilityBreakout;
|
|
961
|
-
if (!isNav) {
|
|
962
|
-
tackCogStr = '---°';
|
|
963
|
-
} else if (unstableC) {
|
|
964
|
-
tackCogStr = '---°';
|
|
965
|
-
tackCogUnstable = true;
|
|
966
|
-
} else {
|
|
967
|
-
const rC = (radToDeg(reflectAngle(cObj.val, twdObj.val)) + 360) % 360;
|
|
968
|
-
tackCogStr = `${Math.round(rC).toString().padStart(3, '0')}°`;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// Format digital string values
|
|
974
|
-
const formatCompassDeg = (obj: any, rawVal: number | undefined) => {
|
|
975
|
-
const val = obj?.val !== null && obj?.val !== undefined ? obj.val : rawVal;
|
|
976
|
-
if (val === undefined || isNaN(val)) return { str: '---°', unstable: false };
|
|
977
|
-
const deg = Math.round(radToDeg(val));
|
|
978
|
-
const normalized = ((deg % 360) + 360) % 360;
|
|
979
|
-
const main = `${normalized.toString().padStart(3, '0')}°`;
|
|
980
|
-
const diff = obj && rawVal !== undefined ? Math.abs(((radToDeg(rawVal) - radToDeg(obj.val) + 540) % 360) - 180) : 0;
|
|
981
|
-
const unstable = isNav && obj && (!obj.stable || obj.dev > config.averaging.stabilityBreakout || diff > config.averaging.stabilityBreakout);
|
|
982
|
-
return { str: main, unstable };
|
|
983
|
-
};
|
|
984
|
-
|
|
985
|
-
const formatRelativeDeg = (obj: any, rawVal: number | undefined) => {
|
|
986
|
-
const val = obj?.val !== null && obj?.val !== undefined ? obj.val : rawVal;
|
|
987
|
-
if (val === undefined || isNaN(val)) return { str: '---°', unstable: false };
|
|
988
|
-
const deg = Math.round(radToDeg(val));
|
|
989
|
-
const main = `${deg}°`;
|
|
990
|
-
const diff = obj && rawVal !== undefined ? Math.abs(((radToDeg(rawVal) - radToDeg(obj.val) + 540) % 360) - 180) : 0;
|
|
991
|
-
const unstable = isNav && obj && (!obj.stable || obj.dev > config.averaging.stabilityBreakout || diff > config.averaging.stabilityBreakout);
|
|
992
|
-
return { str: main, unstable };
|
|
993
|
-
};
|
|
994
|
-
|
|
995
|
-
const hdgFormatted = formatCompassDeg(hObj, store.raw['navigation.headingTrue']);
|
|
996
|
-
const cogFormatted = formatCompassDeg(cObj, store.raw['navigation.courseOverGroundTrue']);
|
|
997
|
-
const awaFormatted = formatRelativeDeg(awObj, store.raw['environment.wind.angleApparent']);
|
|
998
|
-
const twaFormatted = formatRelativeDeg(twObj, store.raw['environment.wind.angleTrueWater']);
|
|
999
|
-
const twdFormatted = formatCompassDeg(twdObj, store.raw['environment.wind.directionTrue']);
|
|
1000
|
-
|
|
1001
|
-
// SOG Color (cyan if VMG, drift color if SOG)
|
|
1002
|
-
let sogColor = '';
|
|
1003
|
-
if (displayModeSog === 'VMG') {
|
|
1004
|
-
sogColor = '#00b8d4';
|
|
1005
|
-
} else if (isNav) {
|
|
1006
|
-
const drift = sogKts - stwKts;
|
|
1007
|
-
if (drift < -0.3) sogColor = '#ff3b30';
|
|
1008
|
-
else if (drift > 0.3) sogColor = '#00C851';
|
|
1009
|
-
else sogColor = '#ffbb33';
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// TWS Color
|
|
1013
|
-
let twsColor = '';
|
|
1014
|
-
const currentWindVal = displayModeTws === 'AWS' ? awsVal : twsVal;
|
|
1015
|
-
if (currentWindVal >= config.graphs.reef2) {
|
|
1016
|
-
twsColor = '#ff3b30';
|
|
1017
|
-
} else if (currentWindVal >= config.graphs.reef1) {
|
|
1018
|
-
twsColor = '#ff9800';
|
|
1019
|
-
} else if (displayModeTws === 'AWS') {
|
|
1020
|
-
twsColor = '#5c6bc0';
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
setDisplayValues({
|
|
1024
|
-
stw: stwKts.toFixed(1),
|
|
1025
|
-
sog: displayModeSog === 'VMG' ? (store.histories.vmg[store.histories.vmg.length - 1]?.val || 0).toFixed(1) : sogKts.toFixed(1),
|
|
1026
|
-
sogColor,
|
|
1027
|
-
hdg: hdgFormatted.str,
|
|
1028
|
-
hdgUnstable: hdgFormatted.unstable,
|
|
1029
|
-
cog: cogFormatted.str,
|
|
1030
|
-
cogUnstable: cogFormatted.unstable,
|
|
1031
|
-
depth: rawDepth !== undefined ? rawDepth.toFixed(1) : '--.-',
|
|
1032
|
-
depthAlarmClass: depthAlarm,
|
|
1033
|
-
tws: currentWindVal.toFixed(1),
|
|
1034
|
-
twsColor,
|
|
1035
|
-
awsSvg: awsVal.toFixed(1),
|
|
1036
|
-
twaAvg: twaFormatted.str,
|
|
1037
|
-
twaUnstable: twaFormatted.unstable,
|
|
1038
|
-
awaAvg: awaFormatted.str,
|
|
1039
|
-
awaUnstable: awaFormatted.unstable,
|
|
1040
|
-
twdAvg: twdFormatted.str,
|
|
1041
|
-
twdUnstable: twdFormatted.unstable,
|
|
1042
|
-
tackHdg: tackHdgStr,
|
|
1043
|
-
tackHdgUnstable,
|
|
1044
|
-
tackCog: tackCogStr,
|
|
1045
|
-
tackCogUnstable,
|
|
1046
|
-
tackLabel: tackLabelMode,
|
|
1047
|
-
leewayDeg: smoothedLeeway,
|
|
1048
|
-
trackRotDeg: curTrackRot,
|
|
1049
|
-
awaPointerDeg,
|
|
1050
|
-
twaPointerDeg,
|
|
1051
|
-
windCompassRotDeg,
|
|
1052
|
-
boatCompassRotDeg,
|
|
1053
|
-
tacticCwDot: tacticCw,
|
|
1054
|
-
tacticCcwDot: tacticCcw,
|
|
1055
|
-
compassCwDot: compassCw,
|
|
1056
|
-
compassCcwDot: compassCcw,
|
|
1057
|
-
meteoLed: meteoLedState,
|
|
1058
|
-
isNavigating: isNav,
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
// Update exported histories state for charts
|
|
1062
|
-
setHistoriesState({
|
|
1063
|
-
stw: [...store.histories.stw],
|
|
1064
|
-
sog: [...store.histories.sog],
|
|
1065
|
-
vmg: [...store.histories.vmg],
|
|
1066
|
-
depth: [...store.histories.depth],
|
|
1067
|
-
tws: [...store.histories.tws],
|
|
1068
|
-
aws: [...store.histories.aws],
|
|
1069
|
-
twd: [...store.histories.twd],
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
// =========================================================================
|
|
1073
|
-
// ONLINE STATUS TEXT: Explains "online 30/60 minuti" logic!
|
|
1074
|
-
// =========================================================================
|
|
1075
|
-
const viewportMinutes = config.graphs.historyMinutes * (isNav ? 1 : 2);
|
|
1076
|
-
const requiredMs = viewportMinutes * 60000;
|
|
1077
|
-
|
|
1078
|
-
// Find the oldest available history sample across all channels
|
|
1079
|
-
const oldestTimestamps = [
|
|
1080
|
-
store.histories.stw[0]?.time,
|
|
1081
|
-
store.histories.sog[0]?.time,
|
|
1082
|
-
store.histories.tws[0]?.time,
|
|
1083
|
-
store.histories.depth[0]?.time,
|
|
1084
|
-
].filter(Boolean) as number[];
|
|
1085
|
-
|
|
1086
|
-
const oldestTime = oldestTimestamps.length > 0 ? Math.min(...oldestTimestamps) : null;
|
|
1087
|
-
const isSocketOpen = store.isSocketOpen;
|
|
1088
|
-
const isDataFlowing = now - store.lastMessageReceivedTime < 6000;
|
|
1089
|
-
|
|
1090
|
-
if (store.isSimulating) {
|
|
1091
|
-
if (oldestTime) {
|
|
1092
|
-
const availableMs = now - oldestTime;
|
|
1093
|
-
if (availableMs >= requiredMs) {
|
|
1094
|
-
setConnectionStatus({ text: `ONLINE ${viewportMinutes}min`, className: 'online' });
|
|
1095
|
-
} else {
|
|
1096
|
-
const availableMin = Math.max(1, Math.floor(availableMs / 60000));
|
|
1097
|
-
setConnectionStatus({ text: `ONLINE ${availableMin}/${viewportMinutes}min`, className: 'online' });
|
|
1098
|
-
}
|
|
1099
|
-
} else {
|
|
1100
|
-
setConnectionStatus({ text: `ONLINE`, className: 'online' });
|
|
1101
|
-
}
|
|
1102
|
-
} else if (isSocketOpen && isDataFlowing) {
|
|
1103
|
-
if (oldestTime) {
|
|
1104
|
-
const availableMs = now - oldestTime;
|
|
1105
|
-
if (availableMs >= requiredMs) {
|
|
1106
|
-
setConnectionStatus({ text: `ONLINE ${viewportMinutes}min`, className: 'online' });
|
|
1107
|
-
} else {
|
|
1108
|
-
const availableMin = Math.max(1, Math.floor(availableMs / 60000));
|
|
1109
|
-
setConnectionStatus({ text: `ONLINE ${availableMin}/${viewportMinutes}min`, className: 'online' });
|
|
1110
|
-
}
|
|
1111
|
-
} else {
|
|
1112
|
-
setConnectionStatus({ text: `ONLINE`, className: 'online' });
|
|
1113
|
-
}
|
|
1114
|
-
} else {
|
|
1115
|
-
setConnectionStatus({
|
|
1116
|
-
text: isSocketOpen ? 'STALL (RECONNECTING...)' : 'OFFLINE',
|
|
1117
|
-
className: 'offline',
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1120
|
-
}, 1000);
|
|
1121
|
-
|
|
1122
|
-
return () => clearInterval(interval);
|
|
1123
|
-
}, [config, displayModeSog, displayModeTws, manageHistory]);
|
|
1124
|
-
|
|
1125
|
-
const toggleFocusMode = useCallback((type: BoxType) => {
|
|
1126
|
-
setIsFocusActive(prev => {
|
|
1127
|
-
const next = !prev;
|
|
1128
|
-
setFocusedBox(next ? type : null);
|
|
1129
|
-
return next;
|
|
1130
|
-
});
|
|
1131
|
-
}, []);
|
|
1132
|
-
|
|
1133
|
-
const toggleGraphMode = useCallback((type: string) => {
|
|
1134
|
-
setGraphModes(prev => ({
|
|
1135
|
-
...prev,
|
|
1136
|
-
[type]: prev[type] === 'standard' ? 'hercules' : 'standard',
|
|
1137
|
-
}));
|
|
1138
|
-
}, []);
|
|
1139
|
-
|
|
1140
|
-
return {
|
|
1141
|
-
config,
|
|
1142
|
-
displayModeSog,
|
|
1143
|
-
setDisplayModeSog,
|
|
1144
|
-
displayModeTws,
|
|
1145
|
-
setDisplayModeTws,
|
|
1146
|
-
activeInstrument,
|
|
1147
|
-
setActiveInstrument,
|
|
1148
|
-
isNightMode,
|
|
1149
|
-
setIsNightMode,
|
|
1150
|
-
isFocusActive,
|
|
1151
|
-
focusedBox,
|
|
1152
|
-
graphModes,
|
|
1153
|
-
connectionStatus,
|
|
1154
|
-
displayValues,
|
|
1155
|
-
histories: historiesState,
|
|
1156
|
-
windRadarSlots,
|
|
1157
|
-
futureForecast,
|
|
1158
|
-
toggleFocusMode,
|
|
1159
|
-
toggleGraphMode,
|
|
1160
|
-
};
|
|
1161
|
-
}
|