signalk-livedepth-plugin 0.2.0 → 0.3.0
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 +67 -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 +3 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -3
- 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 +78 -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 +9 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +32 -10
- package/dist/plugin.js.map +1 -1
- package/dist/streams/live.d.ts +1 -1
- package/dist/streams/live.d.ts.map +1 -1
- package/dist/streams/live.js +1 -1
- 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 +42 -28
- 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 +41 -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/package.json +27 -6
- package/public/config.html +14 -0
- package/public/index.html +15 -0
- package/public/js/config.js +901 -0
- package/public/js/config.js.map +7 -0
- package/public/js/render-worker.js +706 -0
- package/public/js/render-worker.js.map +7 -0
- package/public/js/renderer.js +1273 -0
- package/public/js/renderer.js.map +7 -0
- package/public/renderer.html +14 -0
- package/public/styles.css +110 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
// src/web/shared/bathymetry.ts
|
|
2
|
+
var DEFAULT_COVERAGE_CELLS = 2;
|
|
3
|
+
var DEFAULT_COVERAGE_MIN_M = 2;
|
|
4
|
+
var DEFAULT_IDW_POWER = 2;
|
|
5
|
+
var DEFAULT_SHALLOW_BIAS = 0.5;
|
|
6
|
+
var DEFAULT_MAX_CELLS = 1024;
|
|
7
|
+
var EARTH_RADIUS = 6378137;
|
|
8
|
+
var MERCATOR_MAX_LAT = 85.05112878;
|
|
9
|
+
var RAD = Math.PI / 180;
|
|
10
|
+
var mercX = (lon) => lon * RAD * EARTH_RADIUS;
|
|
11
|
+
var mercY = (lat) => EARTH_RADIUS * Math.log(
|
|
12
|
+
Math.tan(
|
|
13
|
+
Math.PI / 4 + Math.max(-MERCATOR_MAX_LAT, Math.min(MERCATOR_MAX_LAT, lat)) * RAD / 2
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
function medianConsecutiveGap(samples2) {
|
|
17
|
+
if (samples2.length < 2) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const gaps = [];
|
|
21
|
+
for (let i = 1; i < samples2.length; i++) {
|
|
22
|
+
const ax = mercX(samples2[i - 1].lon);
|
|
23
|
+
const ay = mercY(samples2[i - 1].lat);
|
|
24
|
+
gaps.push(
|
|
25
|
+
Math.hypot(mercX(samples2[i].lon) - ax, mercY(samples2[i].lat) - ay)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
gaps.sort((a, b) => a - b);
|
|
29
|
+
return gaps[Math.floor(gaps.length / 2)];
|
|
30
|
+
}
|
|
31
|
+
function buildDepthField(samples2, extent, options) {
|
|
32
|
+
const cellMetres = options.cellMetres;
|
|
33
|
+
if (!(cellMetres > 0) || extent.length !== 4 || !extent.every((v) => Number.isFinite(v)) || extent[2] <= extent[0] || extent[3] <= extent[1]) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const power = options.power ?? DEFAULT_IDW_POWER;
|
|
37
|
+
const shallowBias = Math.max(
|
|
38
|
+
0,
|
|
39
|
+
Math.min(1, options.shallowBias ?? DEFAULT_SHALLOW_BIAS)
|
|
40
|
+
);
|
|
41
|
+
const maxCells = options.maxCells ?? DEFAULT_MAX_CELLS;
|
|
42
|
+
const width = Math.min(
|
|
43
|
+
maxCells,
|
|
44
|
+
Math.ceil((extent[2] - extent[0]) / cellMetres)
|
|
45
|
+
);
|
|
46
|
+
const height = Math.min(
|
|
47
|
+
maxCells,
|
|
48
|
+
Math.ceil((extent[3] - extent[1]) / cellMetres)
|
|
49
|
+
);
|
|
50
|
+
if (width <= 0 || height <= 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const minX = extent[0];
|
|
54
|
+
const minY = extent[1];
|
|
55
|
+
const gridExtent = [
|
|
56
|
+
minX,
|
|
57
|
+
minY,
|
|
58
|
+
minX + width * cellMetres,
|
|
59
|
+
minY + height * cellMetres
|
|
60
|
+
];
|
|
61
|
+
const depth = new Float32Array(width * height).fill(NaN);
|
|
62
|
+
const wsum = new Float32Array(width * height);
|
|
63
|
+
const wdsum = new Float32Array(width * height);
|
|
64
|
+
const minSeen = new Float32Array(width * height).fill(Infinity);
|
|
65
|
+
const coverageRadius = Math.max(
|
|
66
|
+
cellMetres,
|
|
67
|
+
options.coverageMinMetres ?? DEFAULT_COVERAGE_MIN_M,
|
|
68
|
+
options.coverageMetres ?? (options.coverageCells ?? DEFAULT_COVERAGE_CELLS) * cellMetres
|
|
69
|
+
);
|
|
70
|
+
const coverage2 = coverageRadius * coverageRadius;
|
|
71
|
+
const radiusCells = Math.min(64, Math.ceil(coverageRadius / cellMetres));
|
|
72
|
+
for (const s of samples2) {
|
|
73
|
+
if (!Number.isFinite(s.lon) || !Number.isFinite(s.lat) || !Number.isFinite(s.depth) || s.depth < 0) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const sx = mercX(s.lon);
|
|
77
|
+
const sy = mercY(s.lat);
|
|
78
|
+
const cx = Math.floor((sx - minX) / cellMetres);
|
|
79
|
+
const cy = Math.floor((sy - minY) / cellMetres);
|
|
80
|
+
for (let dy = -radiusCells; dy <= radiusCells; dy++) {
|
|
81
|
+
const gy = cy + dy;
|
|
82
|
+
if (gy < 0 || gy >= height) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const cellCenterY = minY + (gy + 0.5) * cellMetres;
|
|
86
|
+
const ddy = cellCenterY - sy;
|
|
87
|
+
for (let dx = -radiusCells; dx <= radiusCells; dx++) {
|
|
88
|
+
const gx = cx + dx;
|
|
89
|
+
if (gx < 0 || gx >= width) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const cellCenterX = minX + (gx + 0.5) * cellMetres;
|
|
93
|
+
const ddx = cellCenterX - sx;
|
|
94
|
+
const dist2 = ddx * ddx + ddy * ddy;
|
|
95
|
+
if (dist2 > coverage2) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const idx = gy * width + gx;
|
|
99
|
+
const w = 1 / (Math.pow(dist2, power / 2) + 1e-6);
|
|
100
|
+
wsum[idx] += w;
|
|
101
|
+
wdsum[idx] += w * s.depth;
|
|
102
|
+
if (s.depth < minSeen[idx]) {
|
|
103
|
+
minSeen[idx] = s.depth;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (let i = 0; i < depth.length; i++) {
|
|
109
|
+
const w = wsum[i];
|
|
110
|
+
if (w <= 0) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const idw = wdsum[i] / w;
|
|
114
|
+
const shallowest = minSeen[i];
|
|
115
|
+
depth[i] = (1 - shallowBias) * idw + shallowBias * Math.min(idw, shallowest);
|
|
116
|
+
}
|
|
117
|
+
return { width, height, extent: gridExtent, depth };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/web/shared/shaders.ts
|
|
121
|
+
var DEPTH_VERT_SRC = `#version 300 es
|
|
122
|
+
in vec2 aPos;
|
|
123
|
+
out vec2 vUv;
|
|
124
|
+
void main() { vUv = aPos * 0.5 + 0.5; gl_Position = vec4(aPos, 0.0, 1.0); }`;
|
|
125
|
+
var DEPTH_FRAG_SRC = `#version 300 es
|
|
126
|
+
precision highp float;
|
|
127
|
+
in vec2 vUv;
|
|
128
|
+
out vec4 fragColor;
|
|
129
|
+
uniform sampler2D uDepth;
|
|
130
|
+
uniform sampler2D uLut;
|
|
131
|
+
uniform vec2 uTexel; // 1 / field size in texels
|
|
132
|
+
uniform vec4 uView; // viewport: origin.xy, size.xy (metres)
|
|
133
|
+
uniform vec4 uField; // field: origin.xy, size.xy (metres)
|
|
134
|
+
uniform float uShadeLimit;
|
|
135
|
+
uniform int uLevels;
|
|
136
|
+
uniform float uInterval;
|
|
137
|
+
uniform float uLineWidth;
|
|
138
|
+
bool uncov(float v) { return v < 0.0; } // -1: no coverage
|
|
139
|
+
float sampleDepth(vec2 uv) {
|
|
140
|
+
vec2 fc = uv / uTexel - 0.5;
|
|
141
|
+
vec2 f = fract(fc);
|
|
142
|
+
vec2 b = (floor(fc) + 0.5) * uTexel;
|
|
143
|
+
float d00 = texture(uDepth, b).r;
|
|
144
|
+
float d10 = texture(uDepth, b + vec2(uTexel.x, 0.0)).r;
|
|
145
|
+
float d01 = texture(uDepth, b + vec2(0.0, uTexel.y)).r;
|
|
146
|
+
float d11 = texture(uDepth, b + uTexel).r;
|
|
147
|
+
if (uncov(d00) || uncov(d10) || uncov(d01) || uncov(d11)) {
|
|
148
|
+
return texture(uDepth, uv).r; // near an edge/gap: no interpolation
|
|
149
|
+
}
|
|
150
|
+
return mix(mix(d00, d10, f.x), mix(d01, d11, f.x), f.y);
|
|
151
|
+
}
|
|
152
|
+
void main() {
|
|
153
|
+
// viewport pixel -> world metres -> field uv
|
|
154
|
+
vec2 world = uView.xy + vUv * uView.zw;
|
|
155
|
+
vec2 fuv = (world - uField.xy) / uField.zw;
|
|
156
|
+
if (fuv.x < 0.0 || fuv.x > 1.0 || fuv.y < 0.0 || fuv.y > 1.0) { discard; }
|
|
157
|
+
float d = sampleDepth(fuv);
|
|
158
|
+
if (uncov(d)) { discard; }
|
|
159
|
+
// Shading bands, only up to the shade limit; deeper water is left to the chart.
|
|
160
|
+
bool shaded = d < uShadeLimit;
|
|
161
|
+
vec3 fill = vec3(0.0);
|
|
162
|
+
if (shaded) {
|
|
163
|
+
float bandF = d / uShadeLimit * float(uLevels);
|
|
164
|
+
float band = floor(bandF);
|
|
165
|
+
float tCenter = (band + 0.5) / float(uLevels);
|
|
166
|
+
fill = texture(uLut, vec2(clamp(tCenter, 0.0, 1.0), 0.5)).rgb;
|
|
167
|
+
}
|
|
168
|
+
// Isobars at every absolute multiple of the interval, over ALL data.
|
|
169
|
+
float ci = d / uInterval;
|
|
170
|
+
float dv = min(fract(ci), 1.0 - fract(ci));
|
|
171
|
+
float px = dv / max(fwidth(ci), 1e-6);
|
|
172
|
+
float line = 1.0 - smoothstep(uLineWidth, uLineWidth + 1.5, px);
|
|
173
|
+
vec3 col = mix(fill, vec3(0.0), line);
|
|
174
|
+
float alpha = max(line, shaded ? 1.0 : 0.0);
|
|
175
|
+
fragColor = vec4(col, alpha);
|
|
176
|
+
}`;
|
|
177
|
+
|
|
178
|
+
// src/web/shared/live-depth.ts
|
|
179
|
+
var DEFAULT_SHADE_LIMIT_M = 10;
|
|
180
|
+
var EARTH_RADIUS2 = 6378137;
|
|
181
|
+
var MERCATOR_MAX_LAT2 = 85.05112878;
|
|
182
|
+
var RAD2 = Math.PI / 180;
|
|
183
|
+
function mercX2(lon) {
|
|
184
|
+
return lon * RAD2 * EARTH_RADIUS2;
|
|
185
|
+
}
|
|
186
|
+
function mercY2(lat) {
|
|
187
|
+
const c = Math.max(-MERCATOR_MAX_LAT2, Math.min(MERCATOR_MAX_LAT2, lat));
|
|
188
|
+
return EARTH_RADIUS2 * Math.log(Math.tan(Math.PI / 4 + c * RAD2 / 2));
|
|
189
|
+
}
|
|
190
|
+
function lonOf(x) {
|
|
191
|
+
return x / (RAD2 * EARTH_RADIUS2);
|
|
192
|
+
}
|
|
193
|
+
function latOf(y) {
|
|
194
|
+
return (2 * Math.atan(Math.exp(y / EARTH_RADIUS2)) - Math.PI / 2) / RAD2;
|
|
195
|
+
}
|
|
196
|
+
var SHADE_RAMP = [
|
|
197
|
+
[0, 26, 132, 216],
|
|
198
|
+
[0.3, 96, 178, 232],
|
|
199
|
+
[0.55, 156, 209, 240],
|
|
200
|
+
[0.78, 205, 232, 248],
|
|
201
|
+
[1, 255, 255, 255]
|
|
202
|
+
];
|
|
203
|
+
var SHADE_CONTOUR_LEVELS = 6;
|
|
204
|
+
function shadeColor(t) {
|
|
205
|
+
const c = Math.min(1, Math.max(0, Number.isFinite(t) ? t : 0));
|
|
206
|
+
let lo = SHADE_RAMP[0];
|
|
207
|
+
let hi = SHADE_RAMP[SHADE_RAMP.length - 1];
|
|
208
|
+
for (let i = 0; i < SHADE_RAMP.length - 1; i++) {
|
|
209
|
+
if (c >= SHADE_RAMP[i][0] && c <= SHADE_RAMP[i + 1][0]) {
|
|
210
|
+
lo = SHADE_RAMP[i];
|
|
211
|
+
hi = SHADE_RAMP[i + 1];
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const span = hi[0] - lo[0];
|
|
216
|
+
const f = span > 0 ? (c - lo[0]) / span : 0;
|
|
217
|
+
const lerp = (a, b) => Math.round(a + (b - a) * f);
|
|
218
|
+
return [lerp(lo[1], hi[1]), lerp(lo[2], hi[2]), lerp(lo[3], hi[3])];
|
|
219
|
+
}
|
|
220
|
+
function depthColor(depth, shadeLimit) {
|
|
221
|
+
const limit = shadeLimit > 0 ? shadeLimit : DEFAULT_SHADE_LIMIT_M;
|
|
222
|
+
return shadeColor(depth / limit);
|
|
223
|
+
}
|
|
224
|
+
function depthCss(depth, shadeLimit, alpha = 1) {
|
|
225
|
+
const [r, g, b] = depthColor(depth, shadeLimit);
|
|
226
|
+
return `rgba(${r},${g},${b},${alpha})`;
|
|
227
|
+
}
|
|
228
|
+
function aggregateMinDepth(samples2, cellSizeMetres, maxDepthMetres = Infinity) {
|
|
229
|
+
if (!(cellSizeMetres > 0)) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
const cells = /* @__PURE__ */ new Map();
|
|
233
|
+
for (const s of samples2) {
|
|
234
|
+
if (!Number.isFinite(s.lon) || !Number.isFinite(s.lat) || !Number.isFinite(s.depth) || s.depth < 0 || s.depth > maxDepthMetres) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const x = mercX2(s.lon);
|
|
238
|
+
const y = mercY2(s.lat);
|
|
239
|
+
const ix = Math.floor(x / cellSizeMetres);
|
|
240
|
+
const iy = Math.floor(y / cellSizeMetres);
|
|
241
|
+
const key = `${ix},${iy}`;
|
|
242
|
+
const c = cells.get(key);
|
|
243
|
+
if (c) {
|
|
244
|
+
c.sx += x;
|
|
245
|
+
c.sy += y;
|
|
246
|
+
c.n++;
|
|
247
|
+
if (s.depth < c.depth) {
|
|
248
|
+
c.depth = s.depth;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
cells.set(key, { ix, iy, sx: x, sy: y, n: 1, depth: s.depth });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const out = [];
|
|
255
|
+
cells.forEach((c) => {
|
|
256
|
+
out.push({
|
|
257
|
+
ix: c.ix,
|
|
258
|
+
iy: c.iy,
|
|
259
|
+
lon: lonOf(c.sx / c.n),
|
|
260
|
+
lat: latOf(c.sy / c.n),
|
|
261
|
+
depth: c.depth,
|
|
262
|
+
count: c.n
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
function orderDeepToShallow(cells) {
|
|
268
|
+
return [...cells].sort((a, b) => b.depth - a.depth);
|
|
269
|
+
}
|
|
270
|
+
function selectShoals(samples2, safetyDepthMetres) {
|
|
271
|
+
return samples2.filter(
|
|
272
|
+
(s) => Number.isFinite(s.depth) && s.depth >= 0 && s.depth <= safetyDepthMetres
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
var DEFAULT_MERGE_TOLERANCE = 0.15;
|
|
276
|
+
var MERGE_MAX_RADIUS_CELLS = 2.5;
|
|
277
|
+
function mergeDepthCells(cells, cellSizeMetres, toleranceOrMaxRadius = DEFAULT_MERGE_TOLERANCE, maxRadiusCells = MERGE_MAX_RADIUS_CELLS) {
|
|
278
|
+
if (cells.length === 0 || !(cellSizeMetres > 0)) {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
const tolerance = Math.max(0, toleranceOrMaxRadius);
|
|
282
|
+
const maxRadius = Math.max(cellSizeMetres, maxRadiusCells * cellSizeMetres);
|
|
283
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
284
|
+
cells.forEach((c) => byKey.set(`${c.ix},${c.iy}`, c));
|
|
285
|
+
const visited = /* @__PURE__ */ new Set();
|
|
286
|
+
const ordered = [...cells].sort((a, b) => a.depth - b.depth);
|
|
287
|
+
const neighbours = [
|
|
288
|
+
[1, 0],
|
|
289
|
+
[-1, 0],
|
|
290
|
+
[0, 1],
|
|
291
|
+
[0, -1],
|
|
292
|
+
[1, 1],
|
|
293
|
+
[1, -1],
|
|
294
|
+
[-1, 1],
|
|
295
|
+
[-1, -1]
|
|
296
|
+
];
|
|
297
|
+
const clusters = [];
|
|
298
|
+
for (const start of ordered) {
|
|
299
|
+
if (visited.has(`${start.ix},${start.iy}`)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const anchor = start.depth;
|
|
303
|
+
const ax = mercX2(start.lon);
|
|
304
|
+
const ay = mercY2(start.lat);
|
|
305
|
+
const members = [];
|
|
306
|
+
const queue = [start];
|
|
307
|
+
visited.add(`${start.ix},${start.iy}`);
|
|
308
|
+
while (queue.length) {
|
|
309
|
+
const cell = queue.pop();
|
|
310
|
+
if (!cell) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
members.push(cell);
|
|
314
|
+
for (const [dx, dy] of neighbours) {
|
|
315
|
+
const key = `${cell.ix + dx},${cell.iy + dy}`;
|
|
316
|
+
const nb = byKey.get(key);
|
|
317
|
+
if (!nb || visited.has(key)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (nb.depth - anchor > Math.max(tolerance * anchor, 0.1)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (Math.hypot(mercX2(nb.lon) - ax, mercY2(nb.lat) - ay) > maxRadius) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
visited.add(key);
|
|
327
|
+
queue.push(nb);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let cx = 0;
|
|
331
|
+
let cy = 0;
|
|
332
|
+
let total = 0;
|
|
333
|
+
let minDepth = Infinity;
|
|
334
|
+
members.forEach((m) => {
|
|
335
|
+
cx += mercX2(m.lon) * m.count;
|
|
336
|
+
cy += mercY2(m.lat) * m.count;
|
|
337
|
+
total += m.count;
|
|
338
|
+
if (m.depth < minDepth) {
|
|
339
|
+
minDepth = m.depth;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
cx /= total;
|
|
343
|
+
cy /= total;
|
|
344
|
+
let radius = 0;
|
|
345
|
+
members.forEach((m) => {
|
|
346
|
+
radius = Math.max(
|
|
347
|
+
radius,
|
|
348
|
+
Math.hypot(mercX2(m.lon) - cx, mercY2(m.lat) - cy)
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
clusters.push({
|
|
352
|
+
lon: lonOf(cx),
|
|
353
|
+
lat: latOf(cy),
|
|
354
|
+
depth: minDepth,
|
|
355
|
+
count: total,
|
|
356
|
+
radiusMetres: Math.min(maxRadius, radius + cellSizeMetres / 2)
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return clusters;
|
|
360
|
+
}
|
|
361
|
+
function depthClusters(samples2, cellSizeMetres, tolerance = DEFAULT_MERGE_TOLERANCE, maxDepthMetres = Infinity, maxRadiusCells = MERGE_MAX_RADIUS_CELLS) {
|
|
362
|
+
return orderDeepToShallow(
|
|
363
|
+
mergeDepthCells(
|
|
364
|
+
aggregateMinDepth(samples2, cellSizeMetres, maxDepthMetres),
|
|
365
|
+
cellSizeMetres,
|
|
366
|
+
tolerance,
|
|
367
|
+
maxRadiusCells
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/web/render-worker.ts
|
|
373
|
+
var MAX_FIELD_CELLS = 1024;
|
|
374
|
+
var LABEL_FONT_MIN = 12;
|
|
375
|
+
var LABEL_FONT_MAX = 20;
|
|
376
|
+
var LABEL_MIN_RADIUS_PX = 8;
|
|
377
|
+
var ORIGIN_SHIFT = Math.PI * 6378137;
|
|
378
|
+
var WORLD_WIDTH = 2 * ORIGIN_SHIFT;
|
|
379
|
+
var samples = [];
|
|
380
|
+
var settings = null;
|
|
381
|
+
var field = null;
|
|
382
|
+
var fieldDirty = true;
|
|
383
|
+
var depthUnit = "m";
|
|
384
|
+
function formatDepth(metres) {
|
|
385
|
+
return depthUnit === "foot" ? (metres / 0.3048).toFixed(0) : metres.toFixed(1);
|
|
386
|
+
}
|
|
387
|
+
var glCanvas = null;
|
|
388
|
+
var gl = null;
|
|
389
|
+
var program = null;
|
|
390
|
+
var quad = null;
|
|
391
|
+
var depthTexture = null;
|
|
392
|
+
var lutTexture = null;
|
|
393
|
+
var aPos = 0;
|
|
394
|
+
var uDepth = null;
|
|
395
|
+
var uLut = null;
|
|
396
|
+
var uTexel = null;
|
|
397
|
+
var uView = null;
|
|
398
|
+
var uField = null;
|
|
399
|
+
var uShadeLimit = null;
|
|
400
|
+
var uLevels = null;
|
|
401
|
+
var uInterval = null;
|
|
402
|
+
var uLineWidth = null;
|
|
403
|
+
function compile(ctx, type, src) {
|
|
404
|
+
const shader = ctx.createShader(type);
|
|
405
|
+
if (!shader) return null;
|
|
406
|
+
ctx.shaderSource(shader, src);
|
|
407
|
+
ctx.compileShader(shader);
|
|
408
|
+
if (!ctx.getShaderParameter(shader, ctx.COMPILE_STATUS)) {
|
|
409
|
+
ctx.deleteShader(shader);
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
return shader;
|
|
413
|
+
}
|
|
414
|
+
function initWebgl() {
|
|
415
|
+
if (gl) return true;
|
|
416
|
+
glCanvas = new OffscreenCanvas(1, 1);
|
|
417
|
+
const ctx = glCanvas.getContext("webgl2", {
|
|
418
|
+
alpha: true,
|
|
419
|
+
premultipliedAlpha: true
|
|
420
|
+
});
|
|
421
|
+
if (!ctx) return false;
|
|
422
|
+
const vs = compile(ctx, ctx.VERTEX_SHADER, DEPTH_VERT_SRC);
|
|
423
|
+
const fs = compile(ctx, ctx.FRAGMENT_SHADER, DEPTH_FRAG_SRC);
|
|
424
|
+
const prog = ctx.createProgram();
|
|
425
|
+
if (!vs || !fs || !prog) return false;
|
|
426
|
+
ctx.attachShader(prog, vs);
|
|
427
|
+
ctx.attachShader(prog, fs);
|
|
428
|
+
ctx.linkProgram(prog);
|
|
429
|
+
if (!ctx.getProgramParameter(prog, ctx.LINK_STATUS)) return false;
|
|
430
|
+
gl = ctx;
|
|
431
|
+
program = prog;
|
|
432
|
+
aPos = ctx.getAttribLocation(prog, "aPos");
|
|
433
|
+
uDepth = ctx.getUniformLocation(prog, "uDepth");
|
|
434
|
+
uLut = ctx.getUniformLocation(prog, "uLut");
|
|
435
|
+
uTexel = ctx.getUniformLocation(prog, "uTexel");
|
|
436
|
+
uView = ctx.getUniformLocation(prog, "uView");
|
|
437
|
+
uField = ctx.getUniformLocation(prog, "uField");
|
|
438
|
+
uShadeLimit = ctx.getUniformLocation(prog, "uShadeLimit");
|
|
439
|
+
uLevels = ctx.getUniformLocation(prog, "uLevels");
|
|
440
|
+
uInterval = ctx.getUniformLocation(prog, "uInterval");
|
|
441
|
+
uLineWidth = ctx.getUniformLocation(prog, "uLineWidth");
|
|
442
|
+
quad = ctx.createBuffer();
|
|
443
|
+
ctx.bindBuffer(ctx.ARRAY_BUFFER, quad);
|
|
444
|
+
ctx.bufferData(
|
|
445
|
+
ctx.ARRAY_BUFFER,
|
|
446
|
+
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
|
|
447
|
+
ctx.STATIC_DRAW
|
|
448
|
+
);
|
|
449
|
+
depthTexture = ctx.createTexture();
|
|
450
|
+
ctx.bindTexture(ctx.TEXTURE_2D, depthTexture);
|
|
451
|
+
ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.NEAREST);
|
|
452
|
+
ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MAG_FILTER, ctx.NEAREST);
|
|
453
|
+
ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE);
|
|
454
|
+
ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE);
|
|
455
|
+
lutTexture = ctx.createTexture();
|
|
456
|
+
const lut = new Uint8Array(256 * 4);
|
|
457
|
+
for (let i = 0; i < 256; i++) {
|
|
458
|
+
const [r, g, b] = shadeColor(i / 255);
|
|
459
|
+
lut[i * 4] = r;
|
|
460
|
+
lut[i * 4 + 1] = g;
|
|
461
|
+
lut[i * 4 + 2] = b;
|
|
462
|
+
lut[i * 4 + 3] = 255;
|
|
463
|
+
}
|
|
464
|
+
ctx.bindTexture(ctx.TEXTURE_2D, lutTexture);
|
|
465
|
+
ctx.texImage2D(
|
|
466
|
+
ctx.TEXTURE_2D,
|
|
467
|
+
0,
|
|
468
|
+
ctx.RGBA,
|
|
469
|
+
256,
|
|
470
|
+
1,
|
|
471
|
+
0,
|
|
472
|
+
ctx.RGBA,
|
|
473
|
+
ctx.UNSIGNED_BYTE,
|
|
474
|
+
lut
|
|
475
|
+
);
|
|
476
|
+
ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MIN_FILTER, ctx.LINEAR);
|
|
477
|
+
ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_MAG_FILTER, ctx.LINEAR);
|
|
478
|
+
ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_S, ctx.CLAMP_TO_EDGE);
|
|
479
|
+
ctx.texParameteri(ctx.TEXTURE_2D, ctx.TEXTURE_WRAP_T, ctx.CLAMP_TO_EDGE);
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
function releaseGpu() {
|
|
483
|
+
if (gl) {
|
|
484
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
485
|
+
}
|
|
486
|
+
gl = null;
|
|
487
|
+
program = null;
|
|
488
|
+
quad = null;
|
|
489
|
+
depthTexture = null;
|
|
490
|
+
lutTexture = null;
|
|
491
|
+
glCanvas = null;
|
|
492
|
+
}
|
|
493
|
+
function uploadField(f) {
|
|
494
|
+
if (!gl || !depthTexture) return;
|
|
495
|
+
const gpu = new Float32Array(f.depth.length);
|
|
496
|
+
for (let i = 0; i < f.depth.length; i++) {
|
|
497
|
+
const d = f.depth[i];
|
|
498
|
+
gpu[i] = Number.isNaN(d) ? -1 : d;
|
|
499
|
+
}
|
|
500
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
501
|
+
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
|
|
502
|
+
gl.texImage2D(
|
|
503
|
+
gl.TEXTURE_2D,
|
|
504
|
+
0,
|
|
505
|
+
gl.R32F,
|
|
506
|
+
f.width,
|
|
507
|
+
f.height,
|
|
508
|
+
0,
|
|
509
|
+
gl.RED,
|
|
510
|
+
gl.FLOAT,
|
|
511
|
+
gpu
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
function buildField() {
|
|
515
|
+
field = null;
|
|
516
|
+
if (samples.length === 0 || !settings) return;
|
|
517
|
+
let minX = Infinity;
|
|
518
|
+
let minY = Infinity;
|
|
519
|
+
let maxX = -Infinity;
|
|
520
|
+
let maxY = -Infinity;
|
|
521
|
+
for (const s of samples) {
|
|
522
|
+
const x = mercX2(s.lon);
|
|
523
|
+
const y = mercY2(s.lat);
|
|
524
|
+
minX = Math.min(minX, x);
|
|
525
|
+
maxX = Math.max(maxX, x);
|
|
526
|
+
minY = Math.min(minY, y);
|
|
527
|
+
maxY = Math.max(maxY, y);
|
|
528
|
+
}
|
|
529
|
+
const padX = (maxX - minX) * 0.1 + 1;
|
|
530
|
+
const padY = (maxY - minY) * 0.1 + 1;
|
|
531
|
+
const extent = [
|
|
532
|
+
minX - padX,
|
|
533
|
+
minY - padY,
|
|
534
|
+
maxX + padX,
|
|
535
|
+
maxY + padY
|
|
536
|
+
];
|
|
537
|
+
const extMax = Math.max(extent[2] - extent[0], extent[3] - extent[1]);
|
|
538
|
+
const tiles = settings.source === "tiles";
|
|
539
|
+
const gap = tiles ? 0 : medianConsecutiveGap(samples);
|
|
540
|
+
const minCover = tiles ? 0 : DEFAULT_COVERAGE_MIN_M;
|
|
541
|
+
const cell = Math.max(gap, extMax / MAX_FIELD_CELLS, minCover / 8, 0.01);
|
|
542
|
+
const built = buildDepthField(samples, extent, {
|
|
543
|
+
cellMetres: cell,
|
|
544
|
+
coverageMetres: Math.max(cell * 1.5, gap * 1.5),
|
|
545
|
+
coverageMinMetres: minCover
|
|
546
|
+
});
|
|
547
|
+
if (!built) return;
|
|
548
|
+
field = built;
|
|
549
|
+
uploadField(built);
|
|
550
|
+
}
|
|
551
|
+
function renderGpu(box, canvasW, canvasH, ratio) {
|
|
552
|
+
const f = field;
|
|
553
|
+
if (!gl || !f || !glCanvas) return;
|
|
554
|
+
if (glCanvas.width !== canvasW || glCanvas.height !== canvasH) {
|
|
555
|
+
glCanvas.width = canvasW;
|
|
556
|
+
glCanvas.height = canvasH;
|
|
557
|
+
}
|
|
558
|
+
gl.viewport(0, 0, canvasW, canvasH);
|
|
559
|
+
gl.clearColor(0, 0, 0, 0);
|
|
560
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
561
|
+
gl.useProgram(program);
|
|
562
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
563
|
+
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
|
|
564
|
+
gl.activeTexture(gl.TEXTURE1);
|
|
565
|
+
gl.bindTexture(gl.TEXTURE_2D, lutTexture);
|
|
566
|
+
const ext = f.extent;
|
|
567
|
+
const fieldCentreX = (ext[0] + ext[2]) / 2;
|
|
568
|
+
const viewCentreX = (box[0] + box[2]) / 2;
|
|
569
|
+
const shift = Math.round((fieldCentreX - viewCentreX) / WORLD_WIDTH) * WORLD_WIDTH;
|
|
570
|
+
gl.uniform4f(
|
|
571
|
+
uView,
|
|
572
|
+
box[0] + shift - ext[0],
|
|
573
|
+
box[1] - ext[1],
|
|
574
|
+
box[2] - box[0],
|
|
575
|
+
box[3] - box[1]
|
|
576
|
+
);
|
|
577
|
+
gl.uniform4f(uField, 0, 0, ext[2] - ext[0], ext[3] - ext[1]);
|
|
578
|
+
gl.uniform2f(uTexel, 1 / f.width, 1 / f.height);
|
|
579
|
+
gl.uniform1i(uDepth, 0);
|
|
580
|
+
gl.uniform1i(uLut, 1);
|
|
581
|
+
gl.uniform1f(uShadeLimit, Math.max(0.01, settings?.shadeLimitMetres ?? 10));
|
|
582
|
+
gl.uniform1i(uLevels, SHADE_CONTOUR_LEVELS);
|
|
583
|
+
gl.uniform1f(uInterval, Math.max(0.01, settings?.contourIntervalMetres ?? 1));
|
|
584
|
+
gl.uniform1f(uLineWidth, Math.max(1, ratio));
|
|
585
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, quad);
|
|
586
|
+
gl.enableVertexAttribArray(aPos);
|
|
587
|
+
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
|
588
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
589
|
+
}
|
|
590
|
+
function drawOverlay(ctx, box, canvasW, canvasH, resolution, ratio, zoom) {
|
|
591
|
+
if (!settings) return;
|
|
592
|
+
const boxW = box[2] - box[0];
|
|
593
|
+
const boxH = box[3] - box[1];
|
|
594
|
+
const centreX = (box[0] + box[2]) / 2;
|
|
595
|
+
const toPixel = (lon, lat) => {
|
|
596
|
+
const mx = mercX2(lon);
|
|
597
|
+
const my = mercY2(lat);
|
|
598
|
+
const x = mx + Math.round((centreX - mx) / WORLD_WIDTH) * WORLD_WIDTH;
|
|
599
|
+
return [(x - box[0]) / boxW * canvasW, (box[3] - my) / boxH * canvasH];
|
|
600
|
+
};
|
|
601
|
+
ctx.textAlign = "center";
|
|
602
|
+
ctx.textBaseline = "middle";
|
|
603
|
+
const dot = (p, depth) => {
|
|
604
|
+
ctx.beginPath();
|
|
605
|
+
ctx.arc(
|
|
606
|
+
p[0],
|
|
607
|
+
p[1],
|
|
608
|
+
Math.max(1.5, settings.cellPx * 0.12) * ratio,
|
|
609
|
+
0,
|
|
610
|
+
Math.PI * 2
|
|
611
|
+
);
|
|
612
|
+
ctx.fillStyle = depthCss(depth, settings.shadeLimitMetres, 0.95);
|
|
613
|
+
ctx.fill();
|
|
614
|
+
};
|
|
615
|
+
const label = (text, p, fontCss) => {
|
|
616
|
+
ctx.font = `${Math.round(fontCss * ratio)}px Roboto, Arial, sans-serif`;
|
|
617
|
+
ctx.lineWidth = Math.max(2, 2 * ratio);
|
|
618
|
+
ctx.strokeStyle = "rgba(255,255,255,0.85)";
|
|
619
|
+
ctx.strokeText(text, p[0], p[1]);
|
|
620
|
+
ctx.fillStyle = "rgba(0,0,0,0.95)";
|
|
621
|
+
ctx.fillText(text, p[0], p[1]);
|
|
622
|
+
};
|
|
623
|
+
selectShoals(samples, settings.safetyDepthMetres).forEach(
|
|
624
|
+
(s) => dot(toPixel(s.lon, s.lat), s.depth)
|
|
625
|
+
);
|
|
626
|
+
const cellSize = Math.max(1, settings.cellPx * resolution);
|
|
627
|
+
const clusters = depthClusters(
|
|
628
|
+
samples,
|
|
629
|
+
cellSize,
|
|
630
|
+
settings.mergeTolerance,
|
|
631
|
+
settings.maxDepthMetres
|
|
632
|
+
);
|
|
633
|
+
for (const c of clusters) {
|
|
634
|
+
const p = toPixel(c.lon, c.lat);
|
|
635
|
+
dot(p, c.depth);
|
|
636
|
+
const radiusPx = c.radiusMetres / resolution * ratio;
|
|
637
|
+
if (radiusPx >= LABEL_MIN_RADIUS_PX) {
|
|
638
|
+
const fontCss = Math.max(
|
|
639
|
+
LABEL_FONT_MIN,
|
|
640
|
+
Math.min(LABEL_FONT_MAX, (zoom - 8) * 1.5 + LABEL_FONT_MIN)
|
|
641
|
+
);
|
|
642
|
+
label(formatDepth(c.depth), p, fontCss);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function render(req) {
|
|
647
|
+
try {
|
|
648
|
+
if (!initWebgl()) {
|
|
649
|
+
post({ type: "error", message: "WebGL2 unavailable" });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (fieldDirty || !field) {
|
|
653
|
+
buildField();
|
|
654
|
+
fieldDirty = false;
|
|
655
|
+
}
|
|
656
|
+
const { extent, width, height, pixelRatio, resolution, zoom } = req;
|
|
657
|
+
const box = [
|
|
658
|
+
extent[0],
|
|
659
|
+
extent[1],
|
|
660
|
+
extent[2],
|
|
661
|
+
extent[3]
|
|
662
|
+
];
|
|
663
|
+
const out = new OffscreenCanvas(width, height);
|
|
664
|
+
const ctx = out.getContext("2d");
|
|
665
|
+
if (!ctx) {
|
|
666
|
+
post({ type: "error", message: "no 2D context" });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
ctx.clearRect(0, 0, width, height);
|
|
670
|
+
if (field) {
|
|
671
|
+
renderGpu(box, width, height, pixelRatio);
|
|
672
|
+
ctx.drawImage(glCanvas, 0, 0);
|
|
673
|
+
}
|
|
674
|
+
drawOverlay(ctx, box, width, height, resolution, pixelRatio, zoom);
|
|
675
|
+
const image = out.transferToImageBitmap();
|
|
676
|
+
post({ type: "frame", requestId: req.requestId, image }, [image]);
|
|
677
|
+
} catch (err) {
|
|
678
|
+
post({ type: "error", message: String(err) });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function post(message, transfer) {
|
|
682
|
+
self.postMessage(message, transfer ?? []);
|
|
683
|
+
}
|
|
684
|
+
self.onmessage = (e) => {
|
|
685
|
+
const msg = e.data;
|
|
686
|
+
switch (msg.type) {
|
|
687
|
+
case "samples":
|
|
688
|
+
samples = msg.samples;
|
|
689
|
+
fieldDirty = true;
|
|
690
|
+
break;
|
|
691
|
+
case "settings":
|
|
692
|
+
settings = msg.settings;
|
|
693
|
+
fieldDirty = true;
|
|
694
|
+
break;
|
|
695
|
+
case "units":
|
|
696
|
+
depthUnit = msg.depth;
|
|
697
|
+
break;
|
|
698
|
+
case "render":
|
|
699
|
+
render(msg.req);
|
|
700
|
+
break;
|
|
701
|
+
case "release":
|
|
702
|
+
releaseGpu();
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
//# sourceMappingURL=render-worker.js.map
|