signalk-livedepth-plugin 0.3.0 → 0.3.3
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 +58 -0
- package/dist/config.d.ts +85 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +60 -1
- package/dist/config.js.map +1 -1
- package/dist/plotterext/manifest.d.ts.map +1 -1
- package/dist/plotterext/manifest.js +4 -3
- package/dist/plotterext/manifest.js.map +1 -1
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +21 -3
- package/dist/plugin.js.map +1 -1
- package/dist/streams/live.d.ts +15 -3
- package/dist/streams/live.d.ts.map +1 -1
- package/dist/streams/live.js +208 -42
- package/dist/streams/live.js.map +1 -1
- package/dist/tiles/core/tile-source.d.ts +9 -0
- package/dist/tiles/core/tile-source.d.ts.map +1 -1
- package/dist/tiles/core/tile-source.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 +1 -1
- package/public/js/config.js +40 -2
- package/public/js/config.js.map +2 -2
- package/public/js/render-worker.js +225 -65
- package/public/js/render-worker.js.map +3 -3
- package/public/js/renderer.js +76 -190
- package/public/js/renderer.js.map +4 -4
|
@@ -28,7 +28,7 @@ function medianConsecutiveGap(samples2) {
|
|
|
28
28
|
gaps.sort((a, b) => a - b);
|
|
29
29
|
return gaps[Math.floor(gaps.length / 2)];
|
|
30
30
|
}
|
|
31
|
-
function
|
|
31
|
+
function createDepthFieldBuilder(extent, options) {
|
|
32
32
|
const cellMetres = options.cellMetres;
|
|
33
33
|
if (!(cellMetres > 0) || extent.length !== 4 || !extent.every((v) => Number.isFinite(v)) || extent[2] <= extent[0] || extent[3] <= extent[1]) {
|
|
34
34
|
return null;
|
|
@@ -69,52 +69,76 @@ function buildDepthField(samples2, extent, options) {
|
|
|
69
69
|
);
|
|
70
70
|
const coverage2 = coverageRadius * coverageRadius;
|
|
71
71
|
const radiusCells = Math.min(64, Math.ceil(coverageRadius / cellMetres));
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
const field = { width, height, extent: gridExtent, depth };
|
|
73
|
+
function refresh([rx0, ry0, rx1, ry1]) {
|
|
74
|
+
for (let gy = ry0; gy < ry1; gy++) {
|
|
75
|
+
const row = gy * width;
|
|
76
|
+
for (let gx = rx0; gx < rx1; gx++) {
|
|
77
|
+
const i = row + gx;
|
|
78
|
+
const w = wsum[i];
|
|
79
|
+
if (w <= 0) {
|
|
80
|
+
depth[i] = NaN;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const idw = wdsum[i] / w;
|
|
84
|
+
const shallowest = minSeen[i];
|
|
85
|
+
depth[i] = (1 - shallowBias) * idw + shallowBias * Math.min(idw, shallowest);
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
}
|
|
89
|
+
function add(samples2) {
|
|
90
|
+
let rect = null;
|
|
91
|
+
const grow = (gx, gy) => {
|
|
92
|
+
if (!rect) {
|
|
93
|
+
rect = [gx, gy, gx + 1, gy + 1];
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (gx < rect[0]) rect[0] = gx;
|
|
97
|
+
if (gy < rect[1]) rect[1] = gy;
|
|
98
|
+
if (gx + 1 > rect[2]) rect[2] = gx + 1;
|
|
99
|
+
if (gy + 1 > rect[3]) rect[3] = gy + 1;
|
|
100
|
+
};
|
|
101
|
+
for (const s of samples2) {
|
|
102
|
+
if (!Number.isFinite(s.lon) || !Number.isFinite(s.lat) || !Number.isFinite(s.depth) || s.depth < 0) {
|
|
83
103
|
continue;
|
|
84
104
|
}
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
105
|
+
const sx = mercX(s.lon);
|
|
106
|
+
const sy = mercY(s.lat);
|
|
107
|
+
const cx = Math.floor((sx - minX) / cellMetres);
|
|
108
|
+
const cy = Math.floor((sy - minY) / cellMetres);
|
|
109
|
+
for (let dy = -radiusCells; dy <= radiusCells; dy++) {
|
|
110
|
+
const gy = cy + dy;
|
|
111
|
+
if (gy < 0 || gy >= height) {
|
|
90
112
|
continue;
|
|
91
113
|
}
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
const cellCenterY = minY + (gy + 0.5) * cellMetres;
|
|
115
|
+
const ddy = cellCenterY - sy;
|
|
116
|
+
for (let dx = -radiusCells; dx <= radiusCells; dx++) {
|
|
117
|
+
const gx = cx + dx;
|
|
118
|
+
if (gx < 0 || gx >= width) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const cellCenterX = minX + (gx + 0.5) * cellMetres;
|
|
122
|
+
const ddx = cellCenterX - sx;
|
|
123
|
+
const dist2 = ddx * ddx + ddy * ddy;
|
|
124
|
+
if (dist2 > coverage2) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const idx = gy * width + gx;
|
|
128
|
+
const w = 1 / (Math.pow(dist2, power / 2) + 1e-6);
|
|
129
|
+
wsum[idx] += w;
|
|
130
|
+
wdsum[idx] += w * s.depth;
|
|
131
|
+
if (s.depth < minSeen[idx]) {
|
|
132
|
+
minSeen[idx] = s.depth;
|
|
133
|
+
}
|
|
134
|
+
grow(gx, gy);
|
|
104
135
|
}
|
|
105
136
|
}
|
|
106
137
|
}
|
|
138
|
+
if (rect) refresh(rect);
|
|
139
|
+
return rect;
|
|
107
140
|
}
|
|
108
|
-
|
|
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 };
|
|
141
|
+
return { field, add };
|
|
118
142
|
}
|
|
119
143
|
|
|
120
144
|
// src/web/shared/shaders.ts
|
|
@@ -369,6 +393,13 @@ function depthClusters(samples2, cellSizeMetres, tolerance = DEFAULT_MERGE_TOLER
|
|
|
369
393
|
);
|
|
370
394
|
}
|
|
371
395
|
|
|
396
|
+
// src/web/shared/settings.ts
|
|
397
|
+
var QUALITY_SCALE = {
|
|
398
|
+
high: 1,
|
|
399
|
+
balanced: 0.6,
|
|
400
|
+
low: 0.4
|
|
401
|
+
};
|
|
402
|
+
|
|
372
403
|
// src/web/render-worker.ts
|
|
373
404
|
var MAX_FIELD_CELLS = 1024;
|
|
374
405
|
var LABEL_FONT_MIN = 12;
|
|
@@ -377,10 +408,27 @@ var LABEL_MIN_RADIUS_PX = 8;
|
|
|
377
408
|
var ORIGIN_SHIFT = Math.PI * 6378137;
|
|
378
409
|
var WORLD_WIDTH = 2 * ORIGIN_SHIFT;
|
|
379
410
|
var samples = [];
|
|
411
|
+
var baseSamples = [];
|
|
412
|
+
var liveSamples = [];
|
|
380
413
|
var settings = null;
|
|
381
|
-
var
|
|
414
|
+
var builder = null;
|
|
382
415
|
var fieldDirty = true;
|
|
416
|
+
var vessel = { position: null };
|
|
383
417
|
var depthUnit = "m";
|
|
418
|
+
var samplesVersion = 0;
|
|
419
|
+
var shoalCache = null;
|
|
420
|
+
var clusterCache = null;
|
|
421
|
+
var AUTO_FRAME_BUDGET_MS = 10;
|
|
422
|
+
var AUTO_ADJUST_EVERY = 30;
|
|
423
|
+
var PERF_REPORT_EVERY = 20;
|
|
424
|
+
var autoScale = 0.7;
|
|
425
|
+
var frameAvgMs = 0;
|
|
426
|
+
var framesSinceAdjust = 0;
|
|
427
|
+
var perfFrames = 0;
|
|
428
|
+
function renderScale() {
|
|
429
|
+
const quality = settings?.quality ?? "auto";
|
|
430
|
+
return quality === "auto" ? autoScale : QUALITY_SCALE[quality];
|
|
431
|
+
}
|
|
384
432
|
function formatDepth(metres) {
|
|
385
433
|
return depthUnit === "foot" ? (metres / 0.3048).toFixed(0) : metres.toFixed(1);
|
|
386
434
|
}
|
|
@@ -511,14 +559,34 @@ function uploadField(f) {
|
|
|
511
559
|
gpu
|
|
512
560
|
);
|
|
513
561
|
}
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
562
|
+
function uploadRegion(f, [x0, y0, x1, y1]) {
|
|
563
|
+
if (!gl || !depthTexture) return;
|
|
564
|
+
const w = x1 - x0;
|
|
565
|
+
const h = y1 - y0;
|
|
566
|
+
if (w <= 0 || h <= 0) return;
|
|
567
|
+
const gpu = new Float32Array(w * h);
|
|
568
|
+
for (let y = 0; y < h; y++) {
|
|
569
|
+
const src = (y0 + y) * f.width + x0;
|
|
570
|
+
const dst = y * w;
|
|
571
|
+
for (let x = 0; x < w; x++) {
|
|
572
|
+
const d = f.depth[src + x];
|
|
573
|
+
gpu[dst + x] = Number.isNaN(d) ? -1 : d;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
577
|
+
gl.bindTexture(gl.TEXTURE_2D, depthTexture);
|
|
578
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, x0, y0, w, h, gl.RED, gl.FLOAT, gpu);
|
|
579
|
+
}
|
|
580
|
+
function rebuildField() {
|
|
581
|
+
builder = null;
|
|
582
|
+
samples = [...baseSamples, ...liveSamples];
|
|
583
|
+
const source = baseSamples.length ? baseSamples : samples;
|
|
584
|
+
if (source.length === 0 || !settings) return;
|
|
517
585
|
let minX = Infinity;
|
|
518
586
|
let minY = Infinity;
|
|
519
587
|
let maxX = -Infinity;
|
|
520
588
|
let maxY = -Infinity;
|
|
521
|
-
for (const s of
|
|
589
|
+
for (const s of source) {
|
|
522
590
|
const x = mercX2(s.lon);
|
|
523
591
|
const y = mercY2(s.lat);
|
|
524
592
|
minX = Math.min(minX, x);
|
|
@@ -536,20 +604,33 @@ function buildField() {
|
|
|
536
604
|
];
|
|
537
605
|
const extMax = Math.max(extent[2] - extent[0], extent[3] - extent[1]);
|
|
538
606
|
const tiles = settings.source === "tiles";
|
|
539
|
-
const gap = tiles ? 0 : medianConsecutiveGap(
|
|
607
|
+
const gap = tiles ? 0 : medianConsecutiveGap(source);
|
|
540
608
|
const minCover = tiles ? 0 : DEFAULT_COVERAGE_MIN_M;
|
|
541
609
|
const cell = Math.max(gap, extMax / MAX_FIELD_CELLS, minCover / 8, 0.01);
|
|
542
|
-
const
|
|
610
|
+
const next = createDepthFieldBuilder(extent, {
|
|
543
611
|
cellMetres: cell,
|
|
544
612
|
coverageMetres: Math.max(cell * 1.5, gap * 1.5),
|
|
545
613
|
coverageMinMetres: minCover
|
|
546
614
|
});
|
|
547
|
-
if (!
|
|
548
|
-
|
|
549
|
-
|
|
615
|
+
if (!next) return;
|
|
616
|
+
next.add(baseSamples);
|
|
617
|
+
if (liveSamples.length) next.add(liveSamples);
|
|
618
|
+
builder = next;
|
|
619
|
+
uploadField(next.field);
|
|
620
|
+
}
|
|
621
|
+
function addLive(newSamples) {
|
|
622
|
+
liveSamples.push(...newSamples);
|
|
623
|
+
samples.push(...newSamples);
|
|
624
|
+
samplesVersion++;
|
|
625
|
+
if (!builder) {
|
|
626
|
+
fieldDirty = true;
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const rect = builder.add(newSamples);
|
|
630
|
+
if (rect) uploadRegion(builder.field, rect);
|
|
550
631
|
}
|
|
551
632
|
function renderGpu(box, canvasW, canvasH, ratio) {
|
|
552
|
-
const f = field;
|
|
633
|
+
const f = builder?.field;
|
|
553
634
|
if (!gl || !f || !glCanvas) return;
|
|
554
635
|
if (glCanvas.width !== canvasW || glCanvas.height !== canvasH) {
|
|
555
636
|
glCanvas.width = canvasW;
|
|
@@ -620,16 +701,28 @@ function drawOverlay(ctx, box, canvasW, canvasH, resolution, ratio, zoom) {
|
|
|
620
701
|
ctx.fillStyle = "rgba(0,0,0,0.95)";
|
|
621
702
|
ctx.fillText(text, p[0], p[1]);
|
|
622
703
|
};
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
704
|
+
const shoalKey = `${samplesVersion}:${settings.safetyDepthMetres}`;
|
|
705
|
+
if (!shoalCache || shoalCache.key !== shoalKey) {
|
|
706
|
+
shoalCache = {
|
|
707
|
+
key: shoalKey,
|
|
708
|
+
value: selectShoals(samples, settings.safetyDepthMetres)
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
shoalCache.value.forEach((s) => dot(toPixel(s.lon, s.lat), s.depth));
|
|
626
712
|
const cellSize = Math.max(1, settings.cellPx * resolution);
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
713
|
+
const clusterKey = `${samplesVersion}:${cellSize}:${settings.mergeTolerance}:${settings.maxDepthMetres}`;
|
|
714
|
+
if (!clusterCache || clusterCache.key !== clusterKey) {
|
|
715
|
+
clusterCache = {
|
|
716
|
+
key: clusterKey,
|
|
717
|
+
value: depthClusters(
|
|
718
|
+
samples,
|
|
719
|
+
cellSize,
|
|
720
|
+
settings.mergeTolerance,
|
|
721
|
+
settings.maxDepthMetres
|
|
722
|
+
)
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
const clusters = clusterCache.value;
|
|
633
726
|
for (const c of clusters) {
|
|
634
727
|
const p = toPixel(c.lon, c.lat);
|
|
635
728
|
dot(p, c.depth);
|
|
@@ -642,6 +735,36 @@ function drawOverlay(ctx, box, canvasW, canvasH, resolution, ratio, zoom) {
|
|
|
642
735
|
label(formatDepth(c.depth), p, fontCss);
|
|
643
736
|
}
|
|
644
737
|
}
|
|
738
|
+
const v = vessel;
|
|
739
|
+
if (v.position && v.heading !== void 0 && v.loa && v.beam) {
|
|
740
|
+
const cx = mercX2(v.position[0]);
|
|
741
|
+
const cy = mercY2(v.position[1]);
|
|
742
|
+
const cos = Math.cos(v.heading);
|
|
743
|
+
const sin = Math.sin(v.heading);
|
|
744
|
+
const halfLength = v.loa / 2;
|
|
745
|
+
const halfBeam = v.beam / 2;
|
|
746
|
+
const corners = [
|
|
747
|
+
[halfLength, halfBeam],
|
|
748
|
+
[halfLength, -halfBeam],
|
|
749
|
+
[-halfLength, -halfBeam],
|
|
750
|
+
[-halfLength, halfBeam]
|
|
751
|
+
].map(
|
|
752
|
+
([fwd, stbd]) => toPixel(
|
|
753
|
+
lonOf(cx + fwd * sin + stbd * cos),
|
|
754
|
+
latOf(cy + fwd * cos - stbd * sin)
|
|
755
|
+
)
|
|
756
|
+
);
|
|
757
|
+
ctx.beginPath();
|
|
758
|
+
corners.forEach(
|
|
759
|
+
([px, py], i) => i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py)
|
|
760
|
+
);
|
|
761
|
+
ctx.closePath();
|
|
762
|
+
ctx.lineWidth = Math.max(1.5, 1.5 * ratio);
|
|
763
|
+
ctx.strokeStyle = "rgba(255, 0, 255, 0.95)";
|
|
764
|
+
ctx.stroke();
|
|
765
|
+
ctx.fillStyle = "rgba(255, 0, 255, 0.12)";
|
|
766
|
+
ctx.fill();
|
|
767
|
+
}
|
|
645
768
|
}
|
|
646
769
|
function render(req) {
|
|
647
770
|
try {
|
|
@@ -649,8 +772,8 @@ function render(req) {
|
|
|
649
772
|
post({ type: "error", message: "WebGL2 unavailable" });
|
|
650
773
|
return;
|
|
651
774
|
}
|
|
652
|
-
if (fieldDirty || !
|
|
653
|
-
|
|
775
|
+
if (fieldDirty || !builder) {
|
|
776
|
+
rebuildField();
|
|
654
777
|
fieldDirty = false;
|
|
655
778
|
}
|
|
656
779
|
const { extent, width, height, pixelRatio, resolution, zoom } = req;
|
|
@@ -660,6 +783,10 @@ function render(req) {
|
|
|
660
783
|
extent[2],
|
|
661
784
|
extent[3]
|
|
662
785
|
];
|
|
786
|
+
const scale = renderScale();
|
|
787
|
+
const surfaceW = Math.max(1, Math.round(width * scale));
|
|
788
|
+
const surfaceH = Math.max(1, Math.round(height * scale));
|
|
789
|
+
const started = performance.now();
|
|
663
790
|
const out = new OffscreenCanvas(width, height);
|
|
664
791
|
const ctx = out.getContext("2d");
|
|
665
792
|
if (!ctx) {
|
|
@@ -667,12 +794,38 @@ function render(req) {
|
|
|
667
794
|
return;
|
|
668
795
|
}
|
|
669
796
|
ctx.clearRect(0, 0, width, height);
|
|
670
|
-
if (
|
|
671
|
-
renderGpu(box,
|
|
672
|
-
ctx.drawImage(
|
|
797
|
+
if (builder) {
|
|
798
|
+
renderGpu(box, surfaceW, surfaceH, pixelRatio * scale);
|
|
799
|
+
ctx.drawImage(
|
|
800
|
+
glCanvas,
|
|
801
|
+
0,
|
|
802
|
+
0,
|
|
803
|
+
surfaceW,
|
|
804
|
+
surfaceH,
|
|
805
|
+
0,
|
|
806
|
+
0,
|
|
807
|
+
width,
|
|
808
|
+
height
|
|
809
|
+
);
|
|
673
810
|
}
|
|
674
811
|
drawOverlay(ctx, box, width, height, resolution, pixelRatio, zoom);
|
|
675
812
|
const image = out.transferToImageBitmap();
|
|
813
|
+
const frameMs = performance.now() - started;
|
|
814
|
+
if ((settings?.quality ?? "auto") === "auto") {
|
|
815
|
+
frameAvgMs = frameAvgMs === 0 ? frameMs : frameAvgMs * 0.8 + frameMs * 0.2;
|
|
816
|
+
if (++framesSinceAdjust >= AUTO_ADJUST_EVERY) {
|
|
817
|
+
framesSinceAdjust = 0;
|
|
818
|
+
if (frameAvgMs > AUTO_FRAME_BUDGET_MS * 1.25) {
|
|
819
|
+
autoScale = Math.max(0.4, autoScale - 0.1);
|
|
820
|
+
} else if (frameAvgMs < AUTO_FRAME_BUDGET_MS * 0.6) {
|
|
821
|
+
autoScale = Math.min(1, autoScale + 0.1);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (++perfFrames >= PERF_REPORT_EVERY) {
|
|
826
|
+
perfFrames = 0;
|
|
827
|
+
post({ type: "perf", scale, frameMs });
|
|
828
|
+
}
|
|
676
829
|
post({ type: "frame", requestId: req.requestId, image }, [image]);
|
|
677
830
|
} catch (err) {
|
|
678
831
|
post({ type: "error", message: String(err) });
|
|
@@ -684,9 +837,16 @@ function post(message, transfer) {
|
|
|
684
837
|
self.onmessage = (e) => {
|
|
685
838
|
const msg = e.data;
|
|
686
839
|
switch (msg.type) {
|
|
687
|
-
case "
|
|
688
|
-
|
|
840
|
+
case "base":
|
|
841
|
+
baseSamples = msg.samples;
|
|
689
842
|
fieldDirty = true;
|
|
843
|
+
samplesVersion++;
|
|
844
|
+
break;
|
|
845
|
+
case "live":
|
|
846
|
+
addLive(msg.samples);
|
|
847
|
+
break;
|
|
848
|
+
case "vessel":
|
|
849
|
+
vessel = msg;
|
|
690
850
|
break;
|
|
691
851
|
case "settings":
|
|
692
852
|
settings = msg.settings;
|