signalk-livedepth-plugin 0.2.1 → 0.3.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/CHANGELOG.md +113 -0
- package/README.md +67 -41
- package/dist/collector.d.ts +1 -1
- package/dist/collector.d.ts.map +1 -1
- package/dist/config.d.ts +52 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -4
- package/dist/config.js.map +1 -1
- package/dist/plotterext/manifest.d.ts +24 -0
- package/dist/plotterext/manifest.d.ts.map +1 -0
- package/dist/plotterext/manifest.js +79 -0
- package/dist/plotterext/manifest.js.map +1 -0
- package/dist/plotterext/provider.d.ts +7 -0
- package/dist/plotterext/provider.d.ts.map +1 -0
- package/dist/plotterext/provider.js +61 -0
- package/dist/plotterext/provider.js.map +1 -0
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +40 -8
- package/dist/plugin.js.map +1 -1
- package/dist/streams/live.d.ts +16 -4
- package/dist/streams/live.d.ts.map +1 -1
- package/dist/streams/live.js +164 -43
- package/dist/streams/live.js.map +1 -1
- package/dist/tiles/api.d.ts +21 -11
- package/dist/tiles/api.d.ts.map +1 -1
- package/dist/tiles/api.js +36 -27
- package/dist/tiles/api.js.map +1 -1
- package/dist/tiles/core/accumulator.d.ts +37 -0
- package/dist/tiles/core/accumulator.d.ts.map +1 -0
- package/dist/tiles/core/accumulator.js +75 -0
- package/dist/tiles/core/accumulator.js.map +1 -0
- package/dist/tiles/core/correction.d.ts +32 -0
- package/dist/tiles/core/correction.d.ts.map +1 -0
- package/dist/tiles/core/correction.js +43 -0
- package/dist/tiles/core/correction.js.map +1 -0
- package/dist/tiles/core/grid.d.ts +64 -0
- package/dist/tiles/core/grid.d.ts.map +1 -0
- package/dist/tiles/core/grid.js +127 -0
- package/dist/tiles/core/grid.js.map +1 -0
- package/dist/tiles/core/index.d.ts +7 -0
- package/dist/tiles/core/index.d.ts.map +1 -0
- package/dist/tiles/core/index.js +7 -0
- package/dist/tiles/core/index.js.map +1 -0
- package/dist/tiles/core/store.d.ts +37 -0
- package/dist/tiles/core/store.d.ts.map +1 -0
- package/dist/tiles/core/store.js +86 -0
- package/dist/tiles/core/store.js.map +1 -0
- package/dist/tiles/core/tile-source.d.ts +50 -0
- package/dist/tiles/core/tile-source.d.ts.map +1 -0
- package/dist/tiles/core/tile-source.js +86 -0
- package/dist/tiles/core/tile-source.js.map +1 -0
- package/dist/tiles/core/types.d.ts +11 -0
- package/dist/tiles/core/types.d.ts.map +1 -0
- package/dist/tiles/core/types.js +2 -0
- package/dist/tiles/core/types.js.map +1 -0
- package/dist/tiles/index.d.ts +1 -1
- package/dist/tiles/index.d.ts.map +1 -1
- package/dist/tiles/index.js +1 -1
- package/dist/tiles/index.js.map +1 -1
- package/dist/tiles/seed.d.ts +1 -1
- package/dist/tiles/seed.d.ts.map +1 -1
- package/dist/tiles/seed.js +1 -1
- package/dist/tiles/seed.js.map +1 -1
- package/dist/tiles/store.d.ts +1 -1
- package/dist/tiles/store.d.ts.map +1 -1
- package/dist/tiles/store.js +1 -1
- package/dist/tiles/store.js.map +1 -1
- package/dist/tiles/writer.d.ts +1 -1
- package/dist/tiles/writer.d.ts.map +1 -1
- package/dist/tiles/writer.js +1 -1
- package/dist/tiles/writer.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +27 -6
- package/public/config.html +14 -0
- package/public/index.html +15 -0
- package/public/js/config.js +939 -0
- package/public/js/config.js.map +7 -0
- package/public/js/render-worker.js +866 -0
- package/public/js/render-worker.js.map +7 -0
- package/public/js/renderer.js +1159 -0
- package/public/js/renderer.js.map +7 -0
- package/public/renderer.html +14 -0
- package/public/styles.css +110 -0
|
@@ -0,0 +1,1159 @@
|
|
|
1
|
+
// node_modules/signalk-plotterext-bus/dist/chunk-4JN5YUOC.js
|
|
2
|
+
var BUS_ID = "plotterExt/1";
|
|
3
|
+
var RPC_ERRORS = {
|
|
4
|
+
PARSE_ERROR: -32700,
|
|
5
|
+
INVALID_REQUEST: -32600,
|
|
6
|
+
METHOD_NOT_FOUND: -32601,
|
|
7
|
+
INVALID_PARAMS: -32602,
|
|
8
|
+
INTERNAL_ERROR: -32603,
|
|
9
|
+
HOST_ERROR: -32e3,
|
|
10
|
+
TIMEOUT: -32001,
|
|
11
|
+
CONNECTION_CLOSED: -32002
|
|
12
|
+
};
|
|
13
|
+
var RpcError = class _RpcError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
data;
|
|
16
|
+
constructor(message, opts = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "RpcError";
|
|
19
|
+
this.code = opts.code ?? RPC_ERRORS.HOST_ERROR;
|
|
20
|
+
const data = { ...opts.data ?? {} };
|
|
21
|
+
if (opts.reason !== void 0) data.reason = opts.reason;
|
|
22
|
+
this.data = Object.keys(data).length > 0 ? data : void 0;
|
|
23
|
+
}
|
|
24
|
+
get reason() {
|
|
25
|
+
return typeof this.data?.reason === "string" ? this.data.reason : void 0;
|
|
26
|
+
}
|
|
27
|
+
toErrorObject() {
|
|
28
|
+
return {
|
|
29
|
+
code: this.code,
|
|
30
|
+
message: this.message,
|
|
31
|
+
...this.data ? { data: this.data } : {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
static fromErrorObject(err) {
|
|
35
|
+
return new _RpcError(err.message, { code: err.code, data: err.data });
|
|
36
|
+
}
|
|
37
|
+
/** Normalize any thrown value into an RpcError suitable for the wire. */
|
|
38
|
+
static from(err) {
|
|
39
|
+
if (err instanceof _RpcError) return err;
|
|
40
|
+
if (err instanceof Error) {
|
|
41
|
+
return new _RpcError(err.message, { code: RPC_ERRORS.INTERNAL_ERROR });
|
|
42
|
+
}
|
|
43
|
+
return new _RpcError(String(err), { code: RPC_ERRORS.INTERNAL_ERROR });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var EVENT_READY = "bus.ready";
|
|
47
|
+
var EVENT_HANDSHAKE = "bus.handshake";
|
|
48
|
+
function matchesPattern(pattern, name) {
|
|
49
|
+
if (pattern === name) return true;
|
|
50
|
+
return match(pattern.split("."), 0, name.split("."), 0);
|
|
51
|
+
}
|
|
52
|
+
function match(p, pi, n, ni) {
|
|
53
|
+
while (pi < p.length) {
|
|
54
|
+
const seg = p[pi];
|
|
55
|
+
if (seg === "**") {
|
|
56
|
+
if (pi === p.length - 1) return true;
|
|
57
|
+
for (let skip = ni; skip <= n.length; skip++) {
|
|
58
|
+
if (match(p, pi + 1, n, skip)) return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (ni >= n.length) return false;
|
|
63
|
+
if (seg !== "*" && seg !== n[ni]) return false;
|
|
64
|
+
pi++;
|
|
65
|
+
ni++;
|
|
66
|
+
}
|
|
67
|
+
return ni === n.length;
|
|
68
|
+
}
|
|
69
|
+
function matchesAny(patterns, name) {
|
|
70
|
+
for (const pattern of patterns) {
|
|
71
|
+
if (matchesPattern(pattern, name)) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
function wrap(msg) {
|
|
76
|
+
return { bus: BUS_ID, msg };
|
|
77
|
+
}
|
|
78
|
+
function unwrap(data) {
|
|
79
|
+
if (typeof data !== "object" || data === null) return null;
|
|
80
|
+
const env = data;
|
|
81
|
+
if (env.bus !== BUS_ID) return null;
|
|
82
|
+
return isJsonRpcMessage(env.msg) ? env.msg : null;
|
|
83
|
+
}
|
|
84
|
+
function isJsonRpcMessage(v) {
|
|
85
|
+
if (typeof v !== "object" || v === null) return false;
|
|
86
|
+
const m = v;
|
|
87
|
+
if (m.jsonrpc !== "2.0") return false;
|
|
88
|
+
if (typeof m.method === "string") {
|
|
89
|
+
return m.id === void 0 || typeof m.id === "string" || typeof m.id === "number";
|
|
90
|
+
}
|
|
91
|
+
const idOk = typeof m.id === "string" || typeof m.id === "number" || m.id === null;
|
|
92
|
+
if (!idOk) return false;
|
|
93
|
+
const hasResult = "result" in m;
|
|
94
|
+
const err = m.error;
|
|
95
|
+
const hasError = typeof err === "object" && err !== null && typeof err.code === "number" && typeof err.message === "string";
|
|
96
|
+
return hasResult ? !("error" in m) : hasError;
|
|
97
|
+
}
|
|
98
|
+
function isRequest(msg) {
|
|
99
|
+
return "method" in msg && "id" in msg && msg.id !== void 0;
|
|
100
|
+
}
|
|
101
|
+
function isNotification(msg) {
|
|
102
|
+
return "method" in msg && (!("id" in msg) || msg.id === void 0);
|
|
103
|
+
}
|
|
104
|
+
function isResponse(msg) {
|
|
105
|
+
return !("method" in msg);
|
|
106
|
+
}
|
|
107
|
+
function windowPort(peer, opts = {}) {
|
|
108
|
+
const listenWindow = opts.listenWindow ?? globalThis;
|
|
109
|
+
const origin = opts.origin ?? listenWindow.location?.origin ?? "*";
|
|
110
|
+
return {
|
|
111
|
+
post(data, transfer) {
|
|
112
|
+
if (transfer?.length) {
|
|
113
|
+
peer.postMessage(data, { targetOrigin: origin, transfer });
|
|
114
|
+
} else {
|
|
115
|
+
peer.postMessage(data, origin);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
listen(handler) {
|
|
119
|
+
const fn = (ev) => {
|
|
120
|
+
if (ev.source !== peer) return;
|
|
121
|
+
if (origin !== "*" && ev.origin !== origin) return;
|
|
122
|
+
handler(ev.data);
|
|
123
|
+
};
|
|
124
|
+
listenWindow.addEventListener("message", fn);
|
|
125
|
+
return () => listenWindow.removeEventListener("message", fn);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
var DEFAULT_CALL_TIMEOUT_MS = 1e4;
|
|
130
|
+
var BusEndpoint = class {
|
|
131
|
+
callTimeoutMs;
|
|
132
|
+
port;
|
|
133
|
+
unlisten;
|
|
134
|
+
onError;
|
|
135
|
+
pending = /* @__PURE__ */ new Map();
|
|
136
|
+
methods = /* @__PURE__ */ new Map();
|
|
137
|
+
eventHandlers = /* @__PURE__ */ new Set();
|
|
138
|
+
idPrefix = Math.random().toString(36).slice(2, 8);
|
|
139
|
+
seq = 0;
|
|
140
|
+
closed = false;
|
|
141
|
+
constructor(opts) {
|
|
142
|
+
this.port = opts.port;
|
|
143
|
+
this.callTimeoutMs = opts.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
144
|
+
this.onError = opts.onError ?? ((err) => console.warn("[plotterext-bus]", err));
|
|
145
|
+
this.unlisten = this.port.listen((data) => this.onData(data));
|
|
146
|
+
}
|
|
147
|
+
registerMethod(name, handler) {
|
|
148
|
+
this.methods.set(name, handler);
|
|
149
|
+
}
|
|
150
|
+
unregisterMethod(name) {
|
|
151
|
+
this.methods.delete(name);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Handle incoming notifications whose names match any of the wildcard
|
|
155
|
+
* patterns. Returns an unsubscribe function. This is local dispatch only;
|
|
156
|
+
* telling the peer which events to forward is a separate concern
|
|
157
|
+
* (`events.subscribe`).
|
|
158
|
+
*/
|
|
159
|
+
onEvent(patterns, fn) {
|
|
160
|
+
const entry = { patterns, fn };
|
|
161
|
+
this.eventHandlers.add(entry);
|
|
162
|
+
return () => this.eventHandlers.delete(entry);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Send a notification (an event) to the peer. `opts.transfer` moves the
|
|
166
|
+
* listed objects instead of copying them.
|
|
167
|
+
*/
|
|
168
|
+
notify(method, params, opts = {}) {
|
|
169
|
+
this.send(
|
|
170
|
+
{ jsonrpc: "2.0", method, ...params !== void 0 ? { params } : {} },
|
|
171
|
+
opts.transfer
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Call a method on the peer; resolves with its result. `opts.transfer`
|
|
176
|
+
* moves the listed objects (e.g. an `ImageBitmap` inside `params`) instead
|
|
177
|
+
* of copying them.
|
|
178
|
+
*/
|
|
179
|
+
call(method, params, opts = {}) {
|
|
180
|
+
if (this.closed) {
|
|
181
|
+
return Promise.reject(
|
|
182
|
+
new RpcError("Bus endpoint is closed", {
|
|
183
|
+
code: RPC_ERRORS.CONNECTION_CLOSED,
|
|
184
|
+
reason: "CLOSED"
|
|
185
|
+
})
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const id = `${this.idPrefix}-${++this.seq}`;
|
|
189
|
+
const timeoutMs = opts.timeoutMs ?? this.callTimeoutMs;
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const timer2 = timeoutMs > 0 ? setTimeout(() => {
|
|
192
|
+
this.pending.delete(id);
|
|
193
|
+
reject(
|
|
194
|
+
new RpcError(`Call timed out after ${timeoutMs}ms: ${method}`, {
|
|
195
|
+
code: RPC_ERRORS.TIMEOUT,
|
|
196
|
+
reason: "TIMEOUT"
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
}, timeoutMs) : null;
|
|
200
|
+
this.pending.set(id, { resolve, reject, timer: timer2 });
|
|
201
|
+
this.send(
|
|
202
|
+
{
|
|
203
|
+
jsonrpc: "2.0",
|
|
204
|
+
id,
|
|
205
|
+
method,
|
|
206
|
+
...params !== void 0 ? { params } : {}
|
|
207
|
+
},
|
|
208
|
+
opts.transfer
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
close() {
|
|
213
|
+
if (this.closed) return;
|
|
214
|
+
this.closed = true;
|
|
215
|
+
this.unlisten();
|
|
216
|
+
for (const [, p] of this.pending) {
|
|
217
|
+
if (p.timer) clearTimeout(p.timer);
|
|
218
|
+
p.reject(
|
|
219
|
+
new RpcError("Bus endpoint closed", {
|
|
220
|
+
code: RPC_ERRORS.CONNECTION_CLOSED,
|
|
221
|
+
reason: "CLOSED"
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
this.pending.clear();
|
|
226
|
+
this.eventHandlers.clear();
|
|
227
|
+
}
|
|
228
|
+
send(msg, transfer) {
|
|
229
|
+
if (this.closed) return;
|
|
230
|
+
this.port.post(wrap(msg), transfer);
|
|
231
|
+
}
|
|
232
|
+
onData(data) {
|
|
233
|
+
const msg = unwrap(data);
|
|
234
|
+
if (!msg) return;
|
|
235
|
+
if (isResponse(msg)) {
|
|
236
|
+
this.onResponse(msg);
|
|
237
|
+
} else if (isRequest(msg)) {
|
|
238
|
+
void this.onRequest(msg);
|
|
239
|
+
} else if (isNotification(msg)) {
|
|
240
|
+
this.onNotification(msg.method, msg.params);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
onResponse(msg) {
|
|
244
|
+
if (msg.id === null) return;
|
|
245
|
+
const p = this.pending.get(msg.id);
|
|
246
|
+
if (!p) return;
|
|
247
|
+
this.pending.delete(msg.id);
|
|
248
|
+
if (p.timer) clearTimeout(p.timer);
|
|
249
|
+
if ("error" in msg) {
|
|
250
|
+
p.reject(RpcError.fromErrorObject(msg.error));
|
|
251
|
+
} else {
|
|
252
|
+
p.resolve(msg.result);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async onRequest(msg) {
|
|
256
|
+
const handler = this.methods.get(msg.method);
|
|
257
|
+
if (!handler) {
|
|
258
|
+
this.send({
|
|
259
|
+
jsonrpc: "2.0",
|
|
260
|
+
id: msg.id,
|
|
261
|
+
error: {
|
|
262
|
+
code: RPC_ERRORS.METHOD_NOT_FOUND,
|
|
263
|
+
message: `Method not found: ${msg.method}`
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const result = await handler(msg.params, { endpoint: this });
|
|
270
|
+
this.send({
|
|
271
|
+
jsonrpc: "2.0",
|
|
272
|
+
id: msg.id,
|
|
273
|
+
result: result === void 0 ? null : result
|
|
274
|
+
});
|
|
275
|
+
} catch (err) {
|
|
276
|
+
this.send({
|
|
277
|
+
jsonrpc: "2.0",
|
|
278
|
+
id: msg.id,
|
|
279
|
+
error: RpcError.from(err).toErrorObject()
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
onNotification(name, params) {
|
|
284
|
+
for (const entry of [...this.eventHandlers]) {
|
|
285
|
+
if (matchesAny(entry.patterns, name)) {
|
|
286
|
+
try {
|
|
287
|
+
entry.fn(name, params);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
this.onError(err);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// node_modules/signalk-plotterext-bus/dist/chunk-N42ZRM4R.js
|
|
297
|
+
var ExtensionClient = class {
|
|
298
|
+
handshake;
|
|
299
|
+
endpoint;
|
|
300
|
+
constructor(endpoint, handshake) {
|
|
301
|
+
this.endpoint = endpoint;
|
|
302
|
+
this.handshake = handshake;
|
|
303
|
+
}
|
|
304
|
+
get context() {
|
|
305
|
+
return this.handshake.context;
|
|
306
|
+
}
|
|
307
|
+
get apiVersion() {
|
|
308
|
+
return this.handshake.apiVersion;
|
|
309
|
+
}
|
|
310
|
+
get capabilities() {
|
|
311
|
+
return this.handshake.capabilities;
|
|
312
|
+
}
|
|
313
|
+
hasCapability(id) {
|
|
314
|
+
return this.handshake.capabilities.includes(id);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Call a host API method. `opts.transfer` moves the listed objects instead
|
|
318
|
+
* of copying them — only `overlay.frame` needs it in this version.
|
|
319
|
+
*/
|
|
320
|
+
call(method, params, opts) {
|
|
321
|
+
return this.endpoint.call(method, params, opts);
|
|
322
|
+
}
|
|
323
|
+
/** Send a notification to the host. */
|
|
324
|
+
notify(method, params) {
|
|
325
|
+
this.endpoint.notify(method, params);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Subscribe to host events matching wildcard patterns. Registers both the
|
|
329
|
+
* host-side forwarding subscription and local dispatch; the returned
|
|
330
|
+
* function tears down both.
|
|
331
|
+
*/
|
|
332
|
+
async subscribe(patterns, handler) {
|
|
333
|
+
const off = this.endpoint.onEvent(patterns, handler);
|
|
334
|
+
let subscriptionId;
|
|
335
|
+
try {
|
|
336
|
+
const result = await this.call("events.subscribe", { patterns });
|
|
337
|
+
subscriptionId = result.subscriptionId;
|
|
338
|
+
} catch (err) {
|
|
339
|
+
off();
|
|
340
|
+
throw err;
|
|
341
|
+
}
|
|
342
|
+
return async () => {
|
|
343
|
+
off();
|
|
344
|
+
await this.call("events.unsubscribe", { subscriptionId }).catch(() => {
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Publish an event onto the bus (capability `events.publish`). The host
|
|
350
|
+
* delivers it to every context in `scope` whose subscriptions match —
|
|
351
|
+
* including this one — exactly as it delivers host events. `scope` defaults
|
|
352
|
+
* to `'all'` (any extension); `'extension'` keeps it to this extension's own
|
|
353
|
+
* contexts. Resolves once the host has delivered it; there is no reply.
|
|
354
|
+
*/
|
|
355
|
+
async publish(topic, params, scope) {
|
|
356
|
+
await this.call("events.publish", {
|
|
357
|
+
topic,
|
|
358
|
+
...params !== void 0 ? { params } : {},
|
|
359
|
+
...scope ? { scope } : {}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** Host-persisted key/value state (see spec: State Storage). */
|
|
363
|
+
state = {
|
|
364
|
+
get: async (keys, scope) => {
|
|
365
|
+
const result = await this.call("state.get", {
|
|
366
|
+
...scope ? { scope } : {},
|
|
367
|
+
...keys ? { keys } : {}
|
|
368
|
+
});
|
|
369
|
+
return result.values ?? {};
|
|
370
|
+
},
|
|
371
|
+
set: async (values, scope) => {
|
|
372
|
+
await this.call("state.set", {
|
|
373
|
+
...scope ? { scope } : {},
|
|
374
|
+
values
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
/** Signal K data relayed by the host (capabilities signalk.stream / .put). */
|
|
379
|
+
signalk = {
|
|
380
|
+
/**
|
|
381
|
+
* Subscribe to Signal K path values. The host publishes them as
|
|
382
|
+
* `sk.<path>` events; this helper hides the event-name mapping and
|
|
383
|
+
* establishes both the event-forwarding subscription and the host's
|
|
384
|
+
* upstream Signal K subscription.
|
|
385
|
+
*/
|
|
386
|
+
subscribe: async (paths, handler) => {
|
|
387
|
+
const patterns = paths.map((p) => `sk.${p}`);
|
|
388
|
+
const offEvents = await this.subscribe(
|
|
389
|
+
patterns,
|
|
390
|
+
(_name, params) => handler(params)
|
|
391
|
+
);
|
|
392
|
+
let subscriptionId;
|
|
393
|
+
try {
|
|
394
|
+
const result = await this.call("signalk.subscribe", { paths });
|
|
395
|
+
subscriptionId = result.subscriptionId;
|
|
396
|
+
} catch (err) {
|
|
397
|
+
await offEvents();
|
|
398
|
+
throw err;
|
|
399
|
+
}
|
|
400
|
+
return async () => {
|
|
401
|
+
await offEvents();
|
|
402
|
+
await this.call("signalk.unsubscribe", { subscriptionId }).catch(
|
|
403
|
+
() => {
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
};
|
|
407
|
+
},
|
|
408
|
+
put: (path, value) => {
|
|
409
|
+
return this.call("signalk.put", { path, value });
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* The host's visible routes (capability `routes`). Thin **typed** wrappers over
|
|
414
|
+
* the host's `route.*` methods — each just delegates to `this.call(...)`, so a
|
|
415
|
+
* plain-JS extension can call `client.call('route.replace', …)` directly and a
|
|
416
|
+
* TypeScript extension gets the typed `client.route.replace(…)` sugar with no
|
|
417
|
+
* behavioural difference. Follow lifecycle + mutations by subscribing to
|
|
418
|
+
* `route.**` events (`RouteVisibleEvent` / `RouteDirtyEvent` / `RouteSavedEvent`
|
|
419
|
+
* / `RouteHiddenEvent`). Further operations (rename/point ops) extend this
|
|
420
|
+
* surface as the capability fills out.
|
|
421
|
+
*/
|
|
422
|
+
route = {
|
|
423
|
+
list: async () => {
|
|
424
|
+
const result = await this.call("route.list");
|
|
425
|
+
return result.routes ?? [];
|
|
426
|
+
},
|
|
427
|
+
create: async (opts) => {
|
|
428
|
+
return await this.call("route.create", opts);
|
|
429
|
+
},
|
|
430
|
+
get: async (routeId) => {
|
|
431
|
+
return await this.call("route.get", { routeId });
|
|
432
|
+
},
|
|
433
|
+
replace: async (routeId, points) => {
|
|
434
|
+
return await this.call("route.replace", { routeId, points });
|
|
435
|
+
},
|
|
436
|
+
save: async (routeId, opts) => {
|
|
437
|
+
return await this.call("route.save", {
|
|
438
|
+
routeId,
|
|
439
|
+
...opts ?? {}
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
show: async (ref) => {
|
|
443
|
+
return await this.call("route.show", { ref });
|
|
444
|
+
},
|
|
445
|
+
hide: async (routeId) => {
|
|
446
|
+
await this.call("route.hide", { routeId });
|
|
447
|
+
},
|
|
448
|
+
delete: async (routeId) => {
|
|
449
|
+
await this.call("route.delete", { routeId });
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
/**
|
|
453
|
+
* The host's chart layers (capability `charts`). Thin **typed** wrappers over
|
|
454
|
+
* the host's `chart.*` methods — a lightweight facade over the charts the host
|
|
455
|
+
* already manages: enumerate them, toggle visibility, opacity and stacking
|
|
456
|
+
* order (all batch). It does not create, add, or delete chart sources. Follow
|
|
457
|
+
* changes (from any origin, including the host's own chart controls) by
|
|
458
|
+
* subscribing to `chart.**` events (`ChartVisibilityEvent` / `ChartOpacityEvent`
|
|
459
|
+
* / `ChartOrderEvent` / `ChartTimeEvent`) and re-reading `chart.list` where
|
|
460
|
+
* needed. As with every wrapper, a plain-JS extension can call
|
|
461
|
+
* `client.call('chart.list', …)` directly with no behavioural difference.
|
|
462
|
+
*/
|
|
463
|
+
chart = {
|
|
464
|
+
list: async () => {
|
|
465
|
+
const result = await this.call("chart.list");
|
|
466
|
+
return result.charts ?? [];
|
|
467
|
+
},
|
|
468
|
+
setVisibility: async (ids, visible) => {
|
|
469
|
+
await this.call("chart.setVisibility", { ids, visible });
|
|
470
|
+
},
|
|
471
|
+
setOpacity: async (ids, opacity) => {
|
|
472
|
+
await this.call("chart.setOpacity", { ids, opacity });
|
|
473
|
+
},
|
|
474
|
+
setOrder: async (order) => {
|
|
475
|
+
await this.call("chart.setOrder", { order });
|
|
476
|
+
},
|
|
477
|
+
/**
|
|
478
|
+
* Retarget time-varying charts to an ISO 8601 instant, or back to their
|
|
479
|
+
* live frame with `null` (sub-capability `charts.time`). Follow changes via
|
|
480
|
+
* the `chart.time` event (`ChartTimeEvent`).
|
|
481
|
+
*/
|
|
482
|
+
setTime: async (ids, time) => {
|
|
483
|
+
await this.call("chart.setTime", { ids, time });
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* The host's night-vision display mode (capability `nightMode`). Read the
|
|
488
|
+
* current `{ enabled, auto }` state, change it, and follow changes by
|
|
489
|
+
* subscribing to the `nightMode.changed` event (`NightModeChangedEvent`).
|
|
490
|
+
* `set` expresses the three states the spec defines: force on
|
|
491
|
+
* (`{ enabled: true }`), force off (`{ enabled: false }`), follow the server
|
|
492
|
+
* (`{ auto: true }`); setting `enabled` implies `auto: false`. As with every
|
|
493
|
+
* wrapper, a plain-JS extension can call `client.call('nightMode.get' |
|
|
494
|
+
* 'nightMode.set', …)` directly with no behavioural difference.
|
|
495
|
+
*/
|
|
496
|
+
nightMode = {
|
|
497
|
+
get: async () => {
|
|
498
|
+
return await this.call("nightMode.get");
|
|
499
|
+
},
|
|
500
|
+
set: async (state) => {
|
|
501
|
+
await this.call("nightMode.set", state);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* Panel visibility (capability `panels.state`): the state of each of this
|
|
506
|
+
* extension's loaded panels. Follow changes with the `panel.state` event,
|
|
507
|
+
* which every context of the extension receives; subscribe first, then
|
|
508
|
+
* `list()`, so no change is missed. A plain-JS extension can call
|
|
509
|
+
* `client.call('ui.listPanels')` with no behavioural difference.
|
|
510
|
+
*/
|
|
511
|
+
panels = {
|
|
512
|
+
list: async () => {
|
|
513
|
+
const r = await this.call("ui.listPanels");
|
|
514
|
+
return r.panels;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
/**
|
|
518
|
+
* Image map overlays (capability `overlays.image`): map layers this
|
|
519
|
+
* extension's background runtime draws. The host asks for frames with the
|
|
520
|
+
* `overlay.request` event (`OverlayRequestEvent`) and reports visibility
|
|
521
|
+
* with `overlay.state` (`OverlayStateEvent`); the runtime answers each
|
|
522
|
+
* request with `frame()`, keeping one frame in flight. `frame()` transfers
|
|
523
|
+
* `image`, so the caller no longer owns it afterwards. A plain-JS extension
|
|
524
|
+
* can call `client.call('overlay.frame', params, { transfer: [image] })`
|
|
525
|
+
* with no behavioural difference.
|
|
526
|
+
*/
|
|
527
|
+
overlays = {
|
|
528
|
+
list: async () => {
|
|
529
|
+
const r = await this.call("overlay.list");
|
|
530
|
+
return r.overlays;
|
|
531
|
+
},
|
|
532
|
+
setVisibility: async (ids, visible) => {
|
|
533
|
+
await this.call("overlay.setVisibility", { ids, visible });
|
|
534
|
+
},
|
|
535
|
+
setOpacity: async (ids, opacity) => {
|
|
536
|
+
await this.call("overlay.setOpacity", { ids, opacity });
|
|
537
|
+
},
|
|
538
|
+
frame: async (params) => {
|
|
539
|
+
return await this.call("overlay.frame", params, {
|
|
540
|
+
transfer: [params.image]
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Floating windows (capability `windows`): show one of this extension's
|
|
546
|
+
* iframe panels in a window over the chart. Geometry is a request — read the
|
|
547
|
+
* actual `bounds` from the returned `WindowState`, and follow changes with
|
|
548
|
+
* the `window.bounds` / `window.state` / `window.closed` events. Windows
|
|
549
|
+
* belong to the extension, so any of its contexts may manage them; in a
|
|
550
|
+
* `window` context, `update` / `focus` / `close` default to the window
|
|
551
|
+
* itself when `windowId` is left out. A plain-JS extension can call
|
|
552
|
+
* `client.call('ui.openWindow', …)` and friends directly with no
|
|
553
|
+
* behavioural difference.
|
|
554
|
+
*/
|
|
555
|
+
windows = {
|
|
556
|
+
open: async (params) => {
|
|
557
|
+
return await this.call("ui.openWindow", params);
|
|
558
|
+
},
|
|
559
|
+
update: async (params) => {
|
|
560
|
+
return await this.call("ui.updateWindow", params);
|
|
561
|
+
},
|
|
562
|
+
focus: async (windowId) => {
|
|
563
|
+
await this.call("ui.focusWindow", windowId === void 0 ? {} : { windowId });
|
|
564
|
+
},
|
|
565
|
+
close: async (windowId) => {
|
|
566
|
+
await this.call("ui.closeWindow", windowId === void 0 ? {} : { windowId });
|
|
567
|
+
},
|
|
568
|
+
list: async () => {
|
|
569
|
+
const r = await this.call("ui.listWindows");
|
|
570
|
+
return r.windows;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
/**
|
|
574
|
+
* Apply a stored resource group to the host's display (capability
|
|
575
|
+
* `resourceGroups`). Resolves to `{ applied }` — the types the host made a
|
|
576
|
+
* best-effort attempt to carry out; a type it did not act on is omitted.
|
|
577
|
+
* Follow applies from any origin (including the user's own group picker) via
|
|
578
|
+
* the `resourceGroup.applied` event (`ResourceGroupAppliedEvent`). Group CRUD
|
|
579
|
+
* is not here: use the server's resources API (`/resources/groups`). A
|
|
580
|
+
* plain-JS extension can call `client.call('resourceGroup.apply', { id })`
|
|
581
|
+
* directly with no behavioural difference.
|
|
582
|
+
*/
|
|
583
|
+
resourceGroup = {
|
|
584
|
+
apply: async (id) => {
|
|
585
|
+
return await this.call("resourceGroup.apply", {
|
|
586
|
+
id
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
/**
|
|
591
|
+
* The host's chart viewport (capability `map`). Read the current
|
|
592
|
+
* `{ center, zoom, bounds }`, drive it, and follow it by subscribing to the
|
|
593
|
+
* `map.view` event (`MapViewEvent`), which carries the same shape and fires
|
|
594
|
+
* once per settled pan/zoom. Seed with `getView` so a follower has a view
|
|
595
|
+
* before the first change. As with every wrapper, a plain-JS extension can
|
|
596
|
+
* call `client.call('map.getView' | 'map.center' | 'map.fitBounds', …)`
|
|
597
|
+
* directly with no behavioural difference.
|
|
598
|
+
*/
|
|
599
|
+
map = {
|
|
600
|
+
getView: async () => {
|
|
601
|
+
return await this.call("map.getView");
|
|
602
|
+
},
|
|
603
|
+
center: async (position, zoom) => {
|
|
604
|
+
await this.call("map.center", {
|
|
605
|
+
position,
|
|
606
|
+
...typeof zoom === "number" ? { zoom } : {}
|
|
607
|
+
});
|
|
608
|
+
},
|
|
609
|
+
/**
|
|
610
|
+
* Fit the chart to `[west, south, east, north]`. A box crossing the
|
|
611
|
+
* antimeridian may be given as `west > east` (the API's form) or unwrapped
|
|
612
|
+
* (`east` past 180); the host fits it the short way round.
|
|
613
|
+
*/
|
|
614
|
+
fitBounds: async (bounds) => {
|
|
615
|
+
await this.call("map.fitBounds", { bounds });
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
close() {
|
|
619
|
+
this.endpoint.close();
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
function connectExtension(opts = {}) {
|
|
623
|
+
const port = opts.port ?? windowPort(globalThis.parent, {
|
|
624
|
+
origin: "*"
|
|
625
|
+
});
|
|
626
|
+
const endpoint = new BusEndpoint({
|
|
627
|
+
port,
|
|
628
|
+
callTimeoutMs: opts.callTimeoutMs,
|
|
629
|
+
onError: opts.onError
|
|
630
|
+
});
|
|
631
|
+
const readyParams = opts.id ? { id: opts.id } : void 0;
|
|
632
|
+
return new Promise((resolve, reject) => {
|
|
633
|
+
let done = false;
|
|
634
|
+
const off = endpoint.onEvent([EVENT_HANDSHAKE], (_name, params) => {
|
|
635
|
+
if (done) return;
|
|
636
|
+
done = true;
|
|
637
|
+
cleanup();
|
|
638
|
+
resolve(new ExtensionClient(endpoint, params));
|
|
639
|
+
});
|
|
640
|
+
const interval = setInterval(
|
|
641
|
+
() => endpoint.notify(EVENT_READY, readyParams),
|
|
642
|
+
opts.readyIntervalMs ?? 250
|
|
643
|
+
);
|
|
644
|
+
const timeout = setTimeout(() => {
|
|
645
|
+
if (done) return;
|
|
646
|
+
done = true;
|
|
647
|
+
cleanup();
|
|
648
|
+
endpoint.close();
|
|
649
|
+
reject(
|
|
650
|
+
new RpcError("Timed out waiting for host handshake", {
|
|
651
|
+
code: RPC_ERRORS.TIMEOUT,
|
|
652
|
+
reason: "HANDSHAKE_TIMEOUT"
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
}, opts.timeoutMs ?? 1e4);
|
|
656
|
+
const cleanup = () => {
|
|
657
|
+
off();
|
|
658
|
+
clearInterval(interval);
|
|
659
|
+
clearTimeout(timeout);
|
|
660
|
+
};
|
|
661
|
+
endpoint.notify(EVENT_READY, readyParams);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/web/shared/live-depth.ts
|
|
666
|
+
function queryString(params) {
|
|
667
|
+
return Object.entries(params).filter(([, v]) => v !== void 0 && v !== "").map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
|
|
668
|
+
}
|
|
669
|
+
var RAD = Math.PI / 180;
|
|
670
|
+
var DEPTH_HISTORY_METHOD = "min";
|
|
671
|
+
function historyValuesQuery(opts) {
|
|
672
|
+
return queryString({
|
|
673
|
+
context: opts.context,
|
|
674
|
+
paths: `navigation.position,${opts.depthPath}:${DEPTH_HISTORY_METHOD},navigation.headingTrue:average`,
|
|
675
|
+
from: new Date(opts.from).toISOString(),
|
|
676
|
+
to: new Date(opts.to).toISOString(),
|
|
677
|
+
resolution: opts.resolution
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
function toPosition(value) {
|
|
681
|
+
if (Array.isArray(value) && value.length >= 2) {
|
|
682
|
+
const [lon, lat] = value;
|
|
683
|
+
if (Number.isFinite(lon) && Number.isFinite(lat)) {
|
|
684
|
+
return [lon, lat];
|
|
685
|
+
}
|
|
686
|
+
return void 0;
|
|
687
|
+
}
|
|
688
|
+
if (value && typeof value === "object") {
|
|
689
|
+
const v = value;
|
|
690
|
+
if (typeof v.latitude === "number" && typeof v.longitude === "number") {
|
|
691
|
+
return [v.longitude, v.latitude];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return void 0;
|
|
695
|
+
}
|
|
696
|
+
function parseDepthSeries(result, depthPath2) {
|
|
697
|
+
const values = result?.values;
|
|
698
|
+
const data = result?.data;
|
|
699
|
+
if (!Array.isArray(values) || !Array.isArray(data)) {
|
|
700
|
+
return [];
|
|
701
|
+
}
|
|
702
|
+
const depthCol = values.findIndex((v) => v?.path === depthPath2);
|
|
703
|
+
if (depthCol < 0) {
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
const posCol = values.findIndex((v) => v?.path === "navigation.position");
|
|
707
|
+
const headingCol = values.findIndex(
|
|
708
|
+
(v) => v?.path === "navigation.headingTrue"
|
|
709
|
+
);
|
|
710
|
+
const rows = [];
|
|
711
|
+
for (const row of data) {
|
|
712
|
+
if (!Array.isArray(row)) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
const time = Date.parse(String(row[0]));
|
|
716
|
+
const depth = row[depthCol + 1];
|
|
717
|
+
if (!Number.isFinite(time) || typeof depth !== "number" || !Number.isFinite(depth) || depth < 0) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const rec = { time, depth };
|
|
721
|
+
if (posCol >= 0) {
|
|
722
|
+
const position = toPosition(row[posCol + 1]);
|
|
723
|
+
if (position) {
|
|
724
|
+
rec.position = position;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (headingCol >= 0) {
|
|
728
|
+
const heading = row[headingCol + 1];
|
|
729
|
+
if (typeof heading === "number" && Number.isFinite(heading)) {
|
|
730
|
+
rec.heading = heading;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
rows.push(rec);
|
|
734
|
+
}
|
|
735
|
+
return rows;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/web/shared/settings.ts
|
|
739
|
+
var DEFAULT_SETTINGS = {
|
|
740
|
+
enabled: true,
|
|
741
|
+
source: "history",
|
|
742
|
+
quality: "auto",
|
|
743
|
+
historyWindowHours: 1,
|
|
744
|
+
historyResolutionS: 0.1,
|
|
745
|
+
shadeLimitMetres: 10,
|
|
746
|
+
contourIntervalMetres: 1,
|
|
747
|
+
cellPx: 24,
|
|
748
|
+
mergeTolerance: 0.15,
|
|
749
|
+
maxDepthMetres: 100,
|
|
750
|
+
safetyDepthMetres: 3
|
|
751
|
+
};
|
|
752
|
+
var ENUM_VALUES = {
|
|
753
|
+
source: ["history", "tiles"],
|
|
754
|
+
quality: ["auto", "high", "balanced", "low"]
|
|
755
|
+
};
|
|
756
|
+
function mergeSettings(stored) {
|
|
757
|
+
const out = { ...DEFAULT_SETTINGS };
|
|
758
|
+
if (!stored) {
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
for (const key of Object.keys(
|
|
762
|
+
DEFAULT_SETTINGS
|
|
763
|
+
)) {
|
|
764
|
+
const value = stored[key];
|
|
765
|
+
if (typeof value !== typeof DEFAULT_SETTINGS[key]) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const allowed = ENUM_VALUES[key];
|
|
769
|
+
if (allowed && !allowed.includes(value)) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
out[key] = value;
|
|
773
|
+
}
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/tiles/core/grid.ts
|
|
778
|
+
var TILE_SIZE = 256;
|
|
779
|
+
var BASE_ZOOM = 20;
|
|
780
|
+
var MIN_ZOOM = 4;
|
|
781
|
+
var NODATA = 65535;
|
|
782
|
+
var RECORD_BYTES = 5;
|
|
783
|
+
var EARTH_RADIUS = 6378137;
|
|
784
|
+
var ORIGIN_SHIFT = Math.PI * EARTH_RADIUS;
|
|
785
|
+
var WORLD_WIDTH = 2 * ORIGIN_SHIFT;
|
|
786
|
+
function decodeDepth(cm) {
|
|
787
|
+
return cm === NODATA ? NaN : cm / 100;
|
|
788
|
+
}
|
|
789
|
+
function decodeTile(bytes) {
|
|
790
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
791
|
+
const cells = /* @__PURE__ */ new Map();
|
|
792
|
+
for (let o = 0; o + RECORD_BYTES <= view.byteLength; o += RECORD_BYTES) {
|
|
793
|
+
const cx = view.getUint8(o);
|
|
794
|
+
const cy = view.getUint8(o + 1);
|
|
795
|
+
const depth = view.getUint16(o + 2, true);
|
|
796
|
+
const count = view.getUint8(o + 4);
|
|
797
|
+
cells.set(cy * TILE_SIZE + cx, { depth, count });
|
|
798
|
+
}
|
|
799
|
+
return cells;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/tiles/core/tile-source.ts
|
|
803
|
+
var RAD2 = Math.PI / 180;
|
|
804
|
+
function zoomForCellSize(cellMetres, minZoom = MIN_ZOOM, baseZoom = BASE_ZOOM) {
|
|
805
|
+
if (!(cellMetres > 0)) return baseZoom;
|
|
806
|
+
const ideal = Math.ceil(
|
|
807
|
+
Math.log2(2 * Math.PI * 6378137 / (TILE_SIZE * cellMetres))
|
|
808
|
+
);
|
|
809
|
+
return Math.max(minZoom, Math.min(baseZoom, ideal));
|
|
810
|
+
}
|
|
811
|
+
function lonToTileX(lon, z) {
|
|
812
|
+
return (lon + 180) / 360 * Math.pow(2, z);
|
|
813
|
+
}
|
|
814
|
+
function latToTileY(lat, z) {
|
|
815
|
+
const clamped = Math.max(-85.05112878, Math.min(85.05112878, lat));
|
|
816
|
+
const latRad = clamped * RAD2;
|
|
817
|
+
return (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * Math.pow(2, z);
|
|
818
|
+
}
|
|
819
|
+
function tileRangeForBbox(bbox, z) {
|
|
820
|
+
const n = Math.pow(2, z);
|
|
821
|
+
if (bbox.length !== 4) {
|
|
822
|
+
return { xs: [], ys: [] };
|
|
823
|
+
}
|
|
824
|
+
const [west, south, east, north] = bbox;
|
|
825
|
+
const xStart = Math.floor(lonToTileX(west, z));
|
|
826
|
+
let xEnd = Math.floor(lonToTileX(east, z));
|
|
827
|
+
if (xEnd < xStart) {
|
|
828
|
+
xEnd += n;
|
|
829
|
+
}
|
|
830
|
+
const xCount = Math.min(n, xEnd - xStart + 1);
|
|
831
|
+
const xs = [];
|
|
832
|
+
for (let i = 0; i < xCount; i++) {
|
|
833
|
+
xs.push(((xStart + i) % n + n) % n);
|
|
834
|
+
}
|
|
835
|
+
const yStart = Math.max(0, Math.floor(latToTileY(north, z)));
|
|
836
|
+
const yEnd = Math.min(n - 1, Math.floor(latToTileY(south, z)));
|
|
837
|
+
const ys = [];
|
|
838
|
+
for (let y = yStart; y <= yEnd; y++) {
|
|
839
|
+
ys.push(y);
|
|
840
|
+
}
|
|
841
|
+
return { xs, ys };
|
|
842
|
+
}
|
|
843
|
+
function tileCellCentre(z, x, y, key) {
|
|
844
|
+
const n = Math.pow(2, z);
|
|
845
|
+
const cx = key % TILE_SIZE;
|
|
846
|
+
const cy = Math.floor(key / TILE_SIZE);
|
|
847
|
+
const fx = (x + (cx + 0.5) / TILE_SIZE) / n;
|
|
848
|
+
const fy = (y + (cy + 0.5) / TILE_SIZE) / n;
|
|
849
|
+
const lon = fx * 360 - 180;
|
|
850
|
+
const lat = Math.atan(Math.sinh(Math.PI * (1 - 2 * fy))) / RAD2;
|
|
851
|
+
return [lon, lat];
|
|
852
|
+
}
|
|
853
|
+
function cellsToSamples(z, x, y, cells) {
|
|
854
|
+
const samples = [];
|
|
855
|
+
for (const [key, cell] of cells) {
|
|
856
|
+
const depth = decodeDepth(cell.depth);
|
|
857
|
+
if (!Number.isFinite(depth)) continue;
|
|
858
|
+
const [lon, lat] = tileCellCentre(z, x, y, key);
|
|
859
|
+
samples.push({ lon, lat, depth });
|
|
860
|
+
}
|
|
861
|
+
return samples;
|
|
862
|
+
}
|
|
863
|
+
function decodeTileSamples(z, x, y, bytes) {
|
|
864
|
+
return cellsToSamples(z, x, y, decodeTile(bytes));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/tiles/core/correction.ts
|
|
868
|
+
var EARTH_RADIUS_M = 6378137;
|
|
869
|
+
var DEG_TO_RAD = Math.PI / 180;
|
|
870
|
+
var RAD_TO_DEG = 180 / Math.PI;
|
|
871
|
+
function correctToTransducer(position, heading, offset) {
|
|
872
|
+
if (heading === void 0 || !Number.isFinite(heading) || !offset.forward && !offset.starboard) {
|
|
873
|
+
return position;
|
|
874
|
+
}
|
|
875
|
+
const cos = Math.cos(heading);
|
|
876
|
+
const sin = Math.sin(heading);
|
|
877
|
+
const north = offset.forward * cos - offset.starboard * sin;
|
|
878
|
+
const east = offset.forward * sin + offset.starboard * cos;
|
|
879
|
+
const lat = position.lat + north / EARTH_RADIUS_M * RAD_TO_DEG;
|
|
880
|
+
const lon = position.lon + east / (EARTH_RADIUS_M * Math.cos(position.lat * DEG_TO_RAD)) * RAD_TO_DEG;
|
|
881
|
+
const wrapped = ((lon + 180) % 360 + 360) % 360 - 180;
|
|
882
|
+
return { lon: wrapped, lat };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/web/renderer.ts
|
|
886
|
+
var PLUGIN_ID = "signalk-livedepth-plugin";
|
|
887
|
+
var ASSET_BASE = `/plotterext/${PLUGIN_ID}`;
|
|
888
|
+
var BATHY_BASE = `${ASSET_BASE}/bathymetry`;
|
|
889
|
+
var DEBOUNCE_MS = 400;
|
|
890
|
+
var POLL_MS = 15e3;
|
|
891
|
+
var MAX_TILE_FETCHES = 24;
|
|
892
|
+
var LIVE_FLUSH_MS = 300;
|
|
893
|
+
var DEFAULT_DEPTH_PATH = "environment.depth.belowTransducer";
|
|
894
|
+
var client = await connectExtension();
|
|
895
|
+
var worker = new Worker(new URL("./render-worker.js", import.meta.url), {
|
|
896
|
+
type: "module"
|
|
897
|
+
});
|
|
898
|
+
var settings = { ...DEFAULT_SETTINGS };
|
|
899
|
+
var transducer = null;
|
|
900
|
+
var depthPath = DEFAULT_DEPTH_PATH;
|
|
901
|
+
var vessel = {};
|
|
902
|
+
var showFootprint = false;
|
|
903
|
+
var baseSamples = [];
|
|
904
|
+
var lastReq = null;
|
|
905
|
+
var busy = false;
|
|
906
|
+
var dirty = false;
|
|
907
|
+
var rendering = null;
|
|
908
|
+
var renderingLive = false;
|
|
909
|
+
var liveInterval = 200;
|
|
910
|
+
var lastLiveAt = 0;
|
|
911
|
+
var perfScale = 0;
|
|
912
|
+
var perfFrameMs = 0;
|
|
913
|
+
var framesSeen = 0;
|
|
914
|
+
var framesSince = Date.now();
|
|
915
|
+
var timer;
|
|
916
|
+
var pendingLive = [];
|
|
917
|
+
var liveFlushTimer;
|
|
918
|
+
async function loadSettings() {
|
|
919
|
+
try {
|
|
920
|
+
const values = await client.state.get();
|
|
921
|
+
settings = mergeSettings(values);
|
|
922
|
+
} catch {
|
|
923
|
+
settings = { ...DEFAULT_SETTINGS };
|
|
924
|
+
}
|
|
925
|
+
worker.postMessage({ type: "settings", settings });
|
|
926
|
+
if (settings.source === "tiles") {
|
|
927
|
+
startLive();
|
|
928
|
+
} else {
|
|
929
|
+
stopLive();
|
|
930
|
+
}
|
|
931
|
+
schedule();
|
|
932
|
+
}
|
|
933
|
+
async function loadMeta() {
|
|
934
|
+
try {
|
|
935
|
+
const res = await fetch(`${BATHY_BASE}/meta`, {
|
|
936
|
+
credentials: "same-origin"
|
|
937
|
+
});
|
|
938
|
+
if (!res.ok) return;
|
|
939
|
+
const meta = await res.json();
|
|
940
|
+
transducer = meta.transducer ?? null;
|
|
941
|
+
if (meta.depthPath) depthPath = meta.depthPath;
|
|
942
|
+
vessel = meta.vessel ?? {};
|
|
943
|
+
showFootprint = meta.debug?.vesselFootprint === true;
|
|
944
|
+
publishVessel();
|
|
945
|
+
} catch {
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function correct(lon, lat, heading) {
|
|
949
|
+
return transducer ? correctToTransducer({ lon, lat }, heading, transducer) : { lon, lat };
|
|
950
|
+
}
|
|
951
|
+
async function loadUnits() {
|
|
952
|
+
try {
|
|
953
|
+
const res = await client.call("units.get");
|
|
954
|
+
if (res?.units?.depth) {
|
|
955
|
+
worker.postMessage({ type: "units", depth: res.units.depth });
|
|
956
|
+
}
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async function fetchHistory() {
|
|
961
|
+
const windowMs = settings.historyWindowHours * 36e5;
|
|
962
|
+
const to = Date.now();
|
|
963
|
+
const query = historyValuesQuery({
|
|
964
|
+
depthPath,
|
|
965
|
+
from: to - windowMs,
|
|
966
|
+
to,
|
|
967
|
+
resolution: Number(settings.historyResolutionS.toPrecision(3))
|
|
968
|
+
});
|
|
969
|
+
const res = await fetch(`/signalk/v2/api/history/values?${query}`, {
|
|
970
|
+
credentials: "same-origin"
|
|
971
|
+
});
|
|
972
|
+
if (!res.ok) return;
|
|
973
|
+
const rows = parseDepthSeries(await res.json(), depthPath);
|
|
974
|
+
const out = [];
|
|
975
|
+
for (const row of rows) {
|
|
976
|
+
if (!row.position) continue;
|
|
977
|
+
const { lon, lat } = correct(row.position[0], row.position[1], row.heading);
|
|
978
|
+
out.push({ lon, lat, depth: row.depth });
|
|
979
|
+
}
|
|
980
|
+
baseSamples = out;
|
|
981
|
+
publishBase();
|
|
982
|
+
}
|
|
983
|
+
async function fetchTiles(req) {
|
|
984
|
+
const cellMetres = Math.max(req.resolution, 0.01);
|
|
985
|
+
let zoom = zoomForCellSize(cellMetres, MIN_ZOOM, BASE_ZOOM);
|
|
986
|
+
while (zoom > MIN_ZOOM) {
|
|
987
|
+
const { xs: xs2, ys: ys2 } = tileRangeForBbox(req.bounds, zoom);
|
|
988
|
+
if (xs2.length * ys2.length <= MAX_TILE_FETCHES) break;
|
|
989
|
+
zoom--;
|
|
990
|
+
}
|
|
991
|
+
const { xs, ys } = tileRangeForBbox(req.bounds, zoom);
|
|
992
|
+
const out = [];
|
|
993
|
+
await Promise.all(
|
|
994
|
+
xs.flatMap(
|
|
995
|
+
(x) => ys.map(async (y) => {
|
|
996
|
+
try {
|
|
997
|
+
const res = await fetch(`${BATHY_BASE}/tiles/${zoom}/${x}/${y}`, {
|
|
998
|
+
credentials: "same-origin"
|
|
999
|
+
});
|
|
1000
|
+
if (!res.ok) return;
|
|
1001
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
1002
|
+
out.push(...decodeTileSamples(zoom, x, y, bytes));
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
})
|
|
1006
|
+
)
|
|
1007
|
+
);
|
|
1008
|
+
baseSamples = out;
|
|
1009
|
+
publishBase();
|
|
1010
|
+
}
|
|
1011
|
+
function schedule() {
|
|
1012
|
+
clearTimeout(timer);
|
|
1013
|
+
timer = setTimeout(() => void refresh(), DEBOUNCE_MS);
|
|
1014
|
+
}
|
|
1015
|
+
async function refresh() {
|
|
1016
|
+
if (!settings.enabled) return;
|
|
1017
|
+
if (settings.source === "history") {
|
|
1018
|
+
await fetchHistory();
|
|
1019
|
+
} else if (lastReq) {
|
|
1020
|
+
await fetchTiles(lastReq);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function publishBase() {
|
|
1024
|
+
worker.postMessage({ type: "base", samples: baseSamples });
|
|
1025
|
+
dirty = true;
|
|
1026
|
+
if (lastReq) draw(true);
|
|
1027
|
+
}
|
|
1028
|
+
var liveUnsub;
|
|
1029
|
+
function startLive() {
|
|
1030
|
+
if (settings.source !== "tiles" || liveUnsub) return;
|
|
1031
|
+
void client.signalk.subscribe(
|
|
1032
|
+
["navigation.position", depthPath, "navigation.headingTrue"],
|
|
1033
|
+
(ev) => {
|
|
1034
|
+
if (ev.path === "navigation.position") {
|
|
1035
|
+
const p = ev.value;
|
|
1036
|
+
if (Array.isArray(p)) {
|
|
1037
|
+
livePosition = [p[0], p[1]];
|
|
1038
|
+
} else if (typeof p?.longitude === "number" && typeof p?.latitude === "number") {
|
|
1039
|
+
livePosition = [p.longitude, p.latitude];
|
|
1040
|
+
}
|
|
1041
|
+
} else if (ev.path === depthPath && typeof ev.value === "number") {
|
|
1042
|
+
liveDepth = ev.value;
|
|
1043
|
+
} else if (ev.path === "navigation.headingTrue" && typeof ev.value === "number") {
|
|
1044
|
+
liveHeading = ev.value;
|
|
1045
|
+
}
|
|
1046
|
+
publishVessel();
|
|
1047
|
+
sampleLive();
|
|
1048
|
+
}
|
|
1049
|
+
).then((un) => {
|
|
1050
|
+
liveUnsub = un;
|
|
1051
|
+
}).catch(() => {
|
|
1052
|
+
});
|
|
1053
|
+
clearInterval(liveFlushTimer);
|
|
1054
|
+
liveFlushTimer = setInterval(flushLive, LIVE_FLUSH_MS);
|
|
1055
|
+
}
|
|
1056
|
+
function stopLive() {
|
|
1057
|
+
clearInterval(liveFlushTimer);
|
|
1058
|
+
liveFlushTimer = void 0;
|
|
1059
|
+
void liveUnsub?.();
|
|
1060
|
+
liveUnsub = void 0;
|
|
1061
|
+
}
|
|
1062
|
+
var livePosition = null;
|
|
1063
|
+
var liveDepth = null;
|
|
1064
|
+
var liveHeading;
|
|
1065
|
+
function publishVessel() {
|
|
1066
|
+
if (!showFootprint) return;
|
|
1067
|
+
worker.postMessage({
|
|
1068
|
+
type: "vessel",
|
|
1069
|
+
position: livePosition,
|
|
1070
|
+
heading: liveHeading,
|
|
1071
|
+
loa: vessel.loa,
|
|
1072
|
+
beam: vessel.beam
|
|
1073
|
+
});
|
|
1074
|
+
dirty = true;
|
|
1075
|
+
if (lastReq) draw(true);
|
|
1076
|
+
}
|
|
1077
|
+
function sampleLive() {
|
|
1078
|
+
if (!livePosition || liveDepth === null) return;
|
|
1079
|
+
const { lon, lat } = correct(livePosition[0], livePosition[1], liveHeading);
|
|
1080
|
+
pendingLive.push({ lon, lat, depth: liveDepth });
|
|
1081
|
+
}
|
|
1082
|
+
function flushLive() {
|
|
1083
|
+
if (pendingLive.length === 0) return;
|
|
1084
|
+
const live = pendingLive;
|
|
1085
|
+
pendingLive = [];
|
|
1086
|
+
worker.postMessage({ type: "live", samples: live });
|
|
1087
|
+
dirty = true;
|
|
1088
|
+
if (lastReq) draw(true);
|
|
1089
|
+
}
|
|
1090
|
+
function draw(live = false) {
|
|
1091
|
+
if (busy || !lastReq || !settings.enabled) return;
|
|
1092
|
+
if (live && Date.now() - lastLiveAt < liveInterval) return;
|
|
1093
|
+
busy = true;
|
|
1094
|
+
rendering = lastReq;
|
|
1095
|
+
renderingLive = live;
|
|
1096
|
+
dirty = false;
|
|
1097
|
+
worker.postMessage({ type: "render", req: rendering });
|
|
1098
|
+
}
|
|
1099
|
+
worker.onmessage = (e) => {
|
|
1100
|
+
const msg = e.data;
|
|
1101
|
+
if (msg.type === "perf") {
|
|
1102
|
+
perfScale = msg.scale;
|
|
1103
|
+
perfFrameMs = msg.frameMs;
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const req = rendering;
|
|
1107
|
+
const live = renderingLive;
|
|
1108
|
+
busy = false;
|
|
1109
|
+
rendering = null;
|
|
1110
|
+
if (msg.type !== "frame" || !req) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (live) lastLiveAt = Date.now();
|
|
1114
|
+
void client.overlays.frame({
|
|
1115
|
+
overlayId: req.overlayId,
|
|
1116
|
+
requestId: live ? void 0 : req.requestId,
|
|
1117
|
+
extent: req.extent,
|
|
1118
|
+
image: msg.image
|
|
1119
|
+
}).catch(() => {
|
|
1120
|
+
});
|
|
1121
|
+
framesSeen++;
|
|
1122
|
+
const now = Date.now();
|
|
1123
|
+
if (now - framesSince >= 5e3) {
|
|
1124
|
+
const fps = framesSeen * 1e3 / (now - framesSince);
|
|
1125
|
+
console.info(
|
|
1126
|
+
`[LiveDepth] ${fps.toFixed(1)} frames/s \xB7 render scale ${perfScale.toFixed(2)} \xB7 worker ${perfFrameMs.toFixed(1)} ms/frame`
|
|
1127
|
+
);
|
|
1128
|
+
framesSeen = 0;
|
|
1129
|
+
framesSince = now;
|
|
1130
|
+
}
|
|
1131
|
+
if (dirty || lastReq !== req) draw(live);
|
|
1132
|
+
};
|
|
1133
|
+
await client.subscribe(["overlay.request"], (_name, payload) => {
|
|
1134
|
+
const req = payload;
|
|
1135
|
+
lastReq = req;
|
|
1136
|
+
if (typeof req.liveInterval === "number" && req.liveInterval > 0) {
|
|
1137
|
+
liveInterval = req.liveInterval;
|
|
1138
|
+
}
|
|
1139
|
+
if (settings.source === "tiles") schedule();
|
|
1140
|
+
draw();
|
|
1141
|
+
});
|
|
1142
|
+
await client.subscribe(["overlay.state"], (_name, payload) => {
|
|
1143
|
+
const state = payload;
|
|
1144
|
+
if (!state.visible || state.suspended) {
|
|
1145
|
+
worker.postMessage({ type: "release" });
|
|
1146
|
+
stopLive();
|
|
1147
|
+
} else {
|
|
1148
|
+
startLive();
|
|
1149
|
+
schedule();
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
await client.subscribe(["state.changed"], () => void loadSettings());
|
|
1153
|
+
await loadMeta();
|
|
1154
|
+
await loadSettings();
|
|
1155
|
+
await loadUnits();
|
|
1156
|
+
setInterval(() => {
|
|
1157
|
+
if (settings.enabled && settings.source === "history") schedule();
|
|
1158
|
+
}, POLL_MS);
|
|
1159
|
+
//# sourceMappingURL=renderer.js.map
|