signalk-livedepth-plugin 0.3.0 → 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.
@@ -739,6 +739,7 @@ function parseDepthSeries(result, depthPath2) {
739
739
  var DEFAULT_SETTINGS = {
740
740
  enabled: true,
741
741
  source: "history",
742
+ quality: "auto",
742
743
  historyWindowHours: 1,
743
744
  historyResolutionS: 0.1,
744
745
  shadeLimitMetres: 10,
@@ -748,6 +749,10 @@ var DEFAULT_SETTINGS = {
748
749
  maxDepthMetres: 100,
749
750
  safetyDepthMetres: 3
750
751
  };
752
+ var ENUM_VALUES = {
753
+ source: ["history", "tiles"],
754
+ quality: ["auto", "high", "balanced", "low"]
755
+ };
751
756
  function mergeSettings(stored) {
752
757
  const out = { ...DEFAULT_SETTINGS };
753
758
  if (!stored) {
@@ -757,9 +762,14 @@ function mergeSettings(stored) {
757
762
  DEFAULT_SETTINGS
758
763
  )) {
759
764
  const value = stored[key];
760
- if (typeof value === typeof DEFAULT_SETTINGS[key]) {
761
- out[key] = value;
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;
762
771
  }
772
+ out[key] = value;
763
773
  }
764
774
  return out;
765
775
  }
@@ -773,38 +783,9 @@ var RECORD_BYTES = 5;
773
783
  var EARTH_RADIUS = 6378137;
774
784
  var ORIGIN_SHIFT = Math.PI * EARTH_RADIUS;
775
785
  var WORLD_WIDTH = 2 * ORIGIN_SHIFT;
776
- var clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
777
- function tileFraction(lon, lat, z) {
778
- const n = Math.pow(2, z);
779
- const latRad = clamp(lat, -85.05112878, 85.05112878) * Math.PI / 180;
780
- const x = (lon + 180) / 360 * n;
781
- const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n;
782
- return { x, y, n };
783
- }
784
- function lngLatToCell(lon, lat, z) {
785
- const { x: fx, y: fy, n } = tileFraction(lon, lat, z);
786
- const x = clamp(Math.floor(fx), 0, n - 1);
787
- const y = clamp(Math.floor(fy), 0, n - 1);
788
- const cx = clamp(Math.floor((fx - x) * TILE_SIZE), 0, TILE_SIZE - 1);
789
- const cy = clamp(Math.floor((fy - y) * TILE_SIZE), 0, TILE_SIZE - 1);
790
- return { x, y, cx, cy };
791
- }
792
- function encodeDepth(metres) {
793
- if (!Number.isFinite(metres)) return NODATA;
794
- return clamp(Math.round(metres * 100), 0, NODATA - 1);
795
- }
796
786
  function decodeDepth(cm) {
797
787
  return cm === NODATA ? NaN : cm / 100;
798
788
  }
799
- function mergeCell(cells, key, depthCm, count = 1) {
800
- const existing = cells.get(key);
801
- if (!existing) {
802
- cells.set(key, { depth: depthCm, count: Math.min(255, count) });
803
- return;
804
- }
805
- if (depthCm < existing.depth) existing.depth = depthCm;
806
- existing.count = Math.min(255, existing.count + count);
807
- }
808
789
  function decodeTile(bytes) {
809
790
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
810
791
  const cells = /* @__PURE__ */ new Map();
@@ -817,133 +798,6 @@ function decodeTile(bytes) {
817
798
  }
818
799
  return cells;
819
800
  }
820
- function minPoolTile(children) {
821
- const parent = /* @__PURE__ */ new Map();
822
- children.forEach((child, index) => {
823
- if (!child) return;
824
- const dx = index & 1;
825
- const dy = index >> 1;
826
- for (const [key, { depth, count }] of child) {
827
- const cx = key % TILE_SIZE;
828
- const cy = Math.floor(key / TILE_SIZE);
829
- const pcx = dx * TILE_SIZE + cx >> 1;
830
- const pcy = dy * TILE_SIZE + cy >> 1;
831
- mergeCell(parent, pcy * TILE_SIZE + pcx, depth, count);
832
- }
833
- });
834
- return parent;
835
- }
836
-
837
- // src/tiles/core/store.ts
838
- function createMemoryTileStore() {
839
- const tiles = /* @__PURE__ */ new Map();
840
- const meta = /* @__PURE__ */ new Map();
841
- const key = (z, x, y) => `${z}/${x}/${y}`;
842
- const read = (z, x, y) => tiles.get(key(z, x, y));
843
- const write = (z, x, y, cells) => {
844
- if (cells.size === 0) {
845
- tiles.delete(key(z, x, y));
846
- return;
847
- }
848
- tiles.set(key(z, x, y), cells);
849
- };
850
- const merge = (z, x, y, incoming) => {
851
- const cells = read(z, x, y) ?? /* @__PURE__ */ new Map();
852
- for (const [k, cell] of incoming) {
853
- const existing = cells.get(k);
854
- if (!existing) {
855
- cells.set(k, { depth: cell.depth, count: cell.count });
856
- } else {
857
- if (cell.depth < existing.depth) existing.depth = cell.depth;
858
- existing.count = Math.min(255, existing.count + cell.count);
859
- }
860
- }
861
- write(z, x, y, cells);
862
- };
863
- const updateAncestors = (z, x, y) => {
864
- let lz = z;
865
- let lx = x;
866
- let ly = y;
867
- while (lz > MIN_ZOOM) {
868
- const pz = lz - 1;
869
- const px = lx >> 1;
870
- const py = ly >> 1;
871
- const children = [
872
- read(lz, px * 2, py * 2),
873
- read(lz, px * 2 + 1, py * 2),
874
- read(lz, px * 2, py * 2 + 1),
875
- read(lz, px * 2 + 1, py * 2 + 1)
876
- ];
877
- write(pz, px, py, minPoolTile(children));
878
- lz = pz;
879
- lx = px;
880
- ly = py;
881
- }
882
- };
883
- return {
884
- read,
885
- write,
886
- merge,
887
- updateAncestors,
888
- listTiles: (z) => {
889
- const out = [];
890
- for (const key2 of tiles.keys()) {
891
- const [tz, tx, ty] = key2.split("/");
892
- if (Number(tz) === z) {
893
- out.push({ x: Number(tx), y: Number(ty) });
894
- }
895
- }
896
- return out;
897
- },
898
- getMeta: (k) => meta.get(k),
899
- setMeta: (k, v) => {
900
- meta.set(k, v);
901
- },
902
- transaction: (fn) => fn(),
903
- clear: () => {
904
- tiles.clear();
905
- },
906
- close: () => {
907
- tiles.clear();
908
- }
909
- };
910
- }
911
-
912
- // src/tiles/core/accumulator.ts
913
- function createTileAccumulator(store, { baseZoom = BASE_ZOOM, ancestors = true } = {}) {
914
- const dirty2 = /* @__PURE__ */ new Map();
915
- return {
916
- add(sample) {
917
- if (!Number.isFinite(sample.lon) || !Number.isFinite(sample.lat) || !Number.isFinite(sample.depth) || sample.depth < 0) {
918
- return;
919
- }
920
- const { x, y, cx, cy } = lngLatToCell(sample.lon, sample.lat, baseZoom);
921
- const key = `${x},${y}`;
922
- let tile = dirty2.get(key);
923
- if (!tile) {
924
- tile = { z: baseZoom, x, y, cells: /* @__PURE__ */ new Map() };
925
- dirty2.set(key, tile);
926
- }
927
- mergeCell(tile.cells, cy * TILE_SIZE + cx, encodeDepth(sample.depth));
928
- },
929
- flush() {
930
- if (dirty2.size === 0) return;
931
- const tiles = [...dirty2.values()];
932
- dirty2.clear();
933
- store.transaction(() => {
934
- for (const tile of tiles) {
935
- store.merge(tile.z, tile.x, tile.y, tile.cells);
936
- if (ancestors) {
937
- store.updateAncestors(tile.z, tile.x, tile.y);
938
- }
939
- }
940
- });
941
- },
942
- get pending() {
943
- return dirty2.size;
944
- }
945
- };
946
- }
947
801
 
948
802
  // src/tiles/core/tile-source.ts
949
803
  var RAD2 = Math.PI / 180;
@@ -997,14 +851,14 @@ function tileCellCentre(z, x, y, key) {
997
851
  return [lon, lat];
998
852
  }
999
853
  function cellsToSamples(z, x, y, cells) {
1000
- const samples2 = [];
854
+ const samples = [];
1001
855
  for (const [key, cell] of cells) {
1002
856
  const depth = decodeDepth(cell.depth);
1003
857
  if (!Number.isFinite(depth)) continue;
1004
858
  const [lon, lat] = tileCellCentre(z, x, y, key);
1005
- samples2.push({ lon, lat, depth });
859
+ samples.push({ lon, lat, depth });
1006
860
  }
1007
- return samples2;
861
+ return samples;
1008
862
  }
1009
863
  function decodeTileSamples(z, x, y, bytes) {
1010
864
  return cellsToSamples(z, x, y, decodeTile(bytes));
@@ -1035,7 +889,7 @@ var BATHY_BASE = `${ASSET_BASE}/bathymetry`;
1035
889
  var DEBOUNCE_MS = 400;
1036
890
  var POLL_MS = 15e3;
1037
891
  var MAX_TILE_FETCHES = 24;
1038
- var LIVE_FLUSH_MS = 2e3;
892
+ var LIVE_FLUSH_MS = 300;
1039
893
  var DEFAULT_DEPTH_PATH = "environment.depth.belowTransducer";
1040
894
  var client = await connectExtension();
1041
895
  var worker = new Worker(new URL("./render-worker.js", import.meta.url), {
@@ -1044,7 +898,9 @@ var worker = new Worker(new URL("./render-worker.js", import.meta.url), {
1044
898
  var settings = { ...DEFAULT_SETTINGS };
1045
899
  var transducer = null;
1046
900
  var depthPath = DEFAULT_DEPTH_PATH;
1047
- var samples = [];
901
+ var vessel = {};
902
+ var showFootprint = false;
903
+ var baseSamples = [];
1048
904
  var lastReq = null;
1049
905
  var busy = false;
1050
906
  var dirty = false;
@@ -1052,11 +908,12 @@ var rendering = null;
1052
908
  var renderingLive = false;
1053
909
  var liveInterval = 200;
1054
910
  var lastLiveAt = 0;
911
+ var perfScale = 0;
912
+ var perfFrameMs = 0;
913
+ var framesSeen = 0;
914
+ var framesSince = Date.now();
1055
915
  var timer;
1056
- var lastZoom = 0;
1057
- var clientStore = createMemoryTileStore();
1058
- var clientAccumulator = createTileAccumulator(clientStore);
1059
- var liveDirty = false;
916
+ var pendingLive = [];
1060
917
  var liveFlushTimer;
1061
918
  async function loadSettings() {
1062
919
  try {
@@ -1082,6 +939,9 @@ async function loadMeta() {
1082
939
  const meta = await res.json();
1083
940
  transducer = meta.transducer ?? null;
1084
941
  if (meta.depthPath) depthPath = meta.depthPath;
942
+ vessel = meta.vessel ?? {};
943
+ showFootprint = meta.debug?.vesselFootprint === true;
944
+ publishVessel();
1085
945
  } catch {
1086
946
  }
1087
947
  }
@@ -1117,8 +977,8 @@ async function fetchHistory() {
1117
977
  const { lon, lat } = correct(row.position[0], row.position[1], row.heading);
1118
978
  out.push({ lon, lat, depth: row.depth });
1119
979
  }
1120
- samples = out;
1121
- publish();
980
+ baseSamples = out;
981
+ publishBase();
1122
982
  }
1123
983
  async function fetchTiles(req) {
1124
984
  const cellMetres = Math.max(req.resolution, 0.01);
@@ -1128,7 +988,6 @@ async function fetchTiles(req) {
1128
988
  if (xs2.length * ys2.length <= MAX_TILE_FETCHES) break;
1129
989
  zoom--;
1130
990
  }
1131
- lastZoom = zoom;
1132
991
  const { xs, ys } = tileRangeForBbox(req.bounds, zoom);
1133
992
  const out = [];
1134
993
  await Promise.all(
@@ -1146,16 +1005,8 @@ async function fetchTiles(req) {
1146
1005
  })
1147
1006
  )
1148
1007
  );
1149
- for (const x of xs) {
1150
- for (const y of ys) {
1151
- const cells = clientStore.read(lastZoom, x, y);
1152
- if (cells && cells.size) {
1153
- out.push(...cellsToSamples(lastZoom, x, y, cells));
1154
- }
1155
- }
1156
- }
1157
- samples = out;
1158
- publish();
1008
+ baseSamples = out;
1009
+ publishBase();
1159
1010
  }
1160
1011
  function schedule() {
1161
1012
  clearTimeout(timer);
@@ -1169,8 +1020,8 @@ async function refresh() {
1169
1020
  await fetchTiles(lastReq);
1170
1021
  }
1171
1022
  }
1172
- function publish() {
1173
- worker.postMessage({ type: "samples", samples });
1023
+ function publishBase() {
1024
+ worker.postMessage({ type: "base", samples: baseSamples });
1174
1025
  dirty = true;
1175
1026
  if (lastReq) draw(true);
1176
1027
  }
@@ -1180,13 +1031,19 @@ function startLive() {
1180
1031
  void client.signalk.subscribe(
1181
1032
  ["navigation.position", depthPath, "navigation.headingTrue"],
1182
1033
  (ev) => {
1183
- if (ev.path === "navigation.position" && Array.isArray(ev.value)) {
1184
- livePosition = ev.value;
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
+ }
1185
1041
  } else if (ev.path === depthPath && typeof ev.value === "number") {
1186
1042
  liveDepth = ev.value;
1187
1043
  } else if (ev.path === "navigation.headingTrue" && typeof ev.value === "number") {
1188
1044
  liveHeading = ev.value;
1189
1045
  }
1046
+ publishVessel();
1190
1047
  sampleLive();
1191
1048
  }
1192
1049
  ).then((un) => {
@@ -1205,17 +1062,30 @@ function stopLive() {
1205
1062
  var livePosition = null;
1206
1063
  var liveDepth = null;
1207
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
+ }
1208
1077
  function sampleLive() {
1209
1078
  if (!livePosition || liveDepth === null) return;
1210
1079
  const { lon, lat } = correct(livePosition[0], livePosition[1], liveHeading);
1211
- clientAccumulator.add({ lon, lat, depth: liveDepth });
1212
- liveDirty = true;
1080
+ pendingLive.push({ lon, lat, depth: liveDepth });
1213
1081
  }
1214
1082
  function flushLive() {
1215
- if (!liveDirty) return;
1216
- liveDirty = false;
1217
- clientAccumulator.flush();
1218
- if (settings.source === "tiles" && lastReq) void fetchTiles(lastReq);
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);
1219
1089
  }
1220
1090
  function draw(live = false) {
1221
1091
  if (busy || !lastReq || !settings.enabled) return;
@@ -1228,6 +1098,11 @@ function draw(live = false) {
1228
1098
  }
1229
1099
  worker.onmessage = (e) => {
1230
1100
  const msg = e.data;
1101
+ if (msg.type === "perf") {
1102
+ perfScale = msg.scale;
1103
+ perfFrameMs = msg.frameMs;
1104
+ return;
1105
+ }
1231
1106
  const req = rendering;
1232
1107
  const live = renderingLive;
1233
1108
  busy = false;
@@ -1243,6 +1118,16 @@ worker.onmessage = (e) => {
1243
1118
  image: msg.image
1244
1119
  }).catch(() => {
1245
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
+ }
1246
1131
  if (dirty || lastReq !== req) draw(live);
1247
1132
  };
1248
1133
  await client.subscribe(["overlay.request"], (_name, payload) => {
@@ -1251,6 +1136,7 @@ await client.subscribe(["overlay.request"], (_name, payload) => {
1251
1136
  if (typeof req.liveInterval === "number" && req.liveInterval > 0) {
1252
1137
  liveInterval = req.liveInterval;
1253
1138
  }
1139
+ if (settings.source === "tiles") schedule();
1254
1140
  draw();
1255
1141
  });
1256
1142
  await client.subscribe(["overlay.state"], (_name, payload) => {