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.
Files changed (86) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +67 -41
  3. package/dist/collector.d.ts +1 -1
  4. package/dist/collector.d.ts.map +1 -1
  5. package/dist/config.d.ts +52 -3
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +41 -4
  8. package/dist/config.js.map +1 -1
  9. package/dist/plotterext/manifest.d.ts +24 -0
  10. package/dist/plotterext/manifest.d.ts.map +1 -0
  11. package/dist/plotterext/manifest.js +79 -0
  12. package/dist/plotterext/manifest.js.map +1 -0
  13. package/dist/plotterext/provider.d.ts +7 -0
  14. package/dist/plotterext/provider.d.ts.map +1 -0
  15. package/dist/plotterext/provider.js +61 -0
  16. package/dist/plotterext/provider.js.map +1 -0
  17. package/dist/plugin.d.ts +15 -0
  18. package/dist/plugin.d.ts.map +1 -1
  19. package/dist/plugin.js +40 -8
  20. package/dist/plugin.js.map +1 -1
  21. package/dist/streams/live.d.ts +16 -4
  22. package/dist/streams/live.d.ts.map +1 -1
  23. package/dist/streams/live.js +164 -43
  24. package/dist/streams/live.js.map +1 -1
  25. package/dist/tiles/api.d.ts +21 -11
  26. package/dist/tiles/api.d.ts.map +1 -1
  27. package/dist/tiles/api.js +36 -27
  28. package/dist/tiles/api.js.map +1 -1
  29. package/dist/tiles/core/accumulator.d.ts +37 -0
  30. package/dist/tiles/core/accumulator.d.ts.map +1 -0
  31. package/dist/tiles/core/accumulator.js +75 -0
  32. package/dist/tiles/core/accumulator.js.map +1 -0
  33. package/dist/tiles/core/correction.d.ts +32 -0
  34. package/dist/tiles/core/correction.d.ts.map +1 -0
  35. package/dist/tiles/core/correction.js +43 -0
  36. package/dist/tiles/core/correction.js.map +1 -0
  37. package/dist/tiles/core/grid.d.ts +64 -0
  38. package/dist/tiles/core/grid.d.ts.map +1 -0
  39. package/dist/tiles/core/grid.js +127 -0
  40. package/dist/tiles/core/grid.js.map +1 -0
  41. package/dist/tiles/core/index.d.ts +7 -0
  42. package/dist/tiles/core/index.d.ts.map +1 -0
  43. package/dist/tiles/core/index.js +7 -0
  44. package/dist/tiles/core/index.js.map +1 -0
  45. package/dist/tiles/core/store.d.ts +37 -0
  46. package/dist/tiles/core/store.d.ts.map +1 -0
  47. package/dist/tiles/core/store.js +86 -0
  48. package/dist/tiles/core/store.js.map +1 -0
  49. package/dist/tiles/core/tile-source.d.ts +50 -0
  50. package/dist/tiles/core/tile-source.d.ts.map +1 -0
  51. package/dist/tiles/core/tile-source.js +86 -0
  52. package/dist/tiles/core/tile-source.js.map +1 -0
  53. package/dist/tiles/core/types.d.ts +11 -0
  54. package/dist/tiles/core/types.d.ts.map +1 -0
  55. package/dist/tiles/core/types.js +2 -0
  56. package/dist/tiles/core/types.js.map +1 -0
  57. package/dist/tiles/index.d.ts +1 -1
  58. package/dist/tiles/index.d.ts.map +1 -1
  59. package/dist/tiles/index.js +1 -1
  60. package/dist/tiles/index.js.map +1 -1
  61. package/dist/tiles/seed.d.ts +1 -1
  62. package/dist/tiles/seed.d.ts.map +1 -1
  63. package/dist/tiles/seed.js +1 -1
  64. package/dist/tiles/seed.js.map +1 -1
  65. package/dist/tiles/store.d.ts +1 -1
  66. package/dist/tiles/store.d.ts.map +1 -1
  67. package/dist/tiles/store.js +1 -1
  68. package/dist/tiles/store.js.map +1 -1
  69. package/dist/tiles/writer.d.ts +1 -1
  70. package/dist/tiles/writer.d.ts.map +1 -1
  71. package/dist/tiles/writer.js +1 -1
  72. package/dist/tiles/writer.js.map +1 -1
  73. package/dist/tsconfig.tsbuildinfo +1 -1
  74. package/dist/types.d.ts +3 -0
  75. package/dist/types.d.ts.map +1 -1
  76. package/package.json +27 -6
  77. package/public/config.html +14 -0
  78. package/public/index.html +15 -0
  79. package/public/js/config.js +939 -0
  80. package/public/js/config.js.map +7 -0
  81. package/public/js/render-worker.js +866 -0
  82. package/public/js/render-worker.js.map +7 -0
  83. package/public/js/renderer.js +1159 -0
  84. package/public/js/renderer.js.map +7 -0
  85. package/public/renderer.html +14 -0
  86. package/public/styles.css +110 -0
package/dist/tiles/api.js CHANGED
@@ -1,22 +1,29 @@
1
- import { BASE_ZOOM, MIN_ZOOM, RECORD_BYTES, TILE_SIZE, encodeTile, } from "signalk-livedepth-tiles";
1
+ import { BASE_ZOOM, MIN_ZOOM, RECORD_BYTES, TILE_SIZE, encodeTile, } from "./core/index.js";
2
+ import { ASSET_BASE } from "../plotterext/manifest.js";
2
3
  /** Wire format of a `/bathymetry/tiles` response. */
3
4
  export const BATHYMETRY_FORMAT = "sparse-v1";
5
+ /** Top-level, non-admin route the store is served from. */
6
+ export const BATHYMETRY_BASE = `${ASSET_BASE}/bathymetry`;
4
7
  /**
5
- * Serve the gridded store to a chart client.
8
+ * Serve the gridded store to the extension runtime — and any other chart
9
+ * client.
6
10
  *
7
- * Mounted by the Signal K server at `/plugins/<pluginId>`, e.g.
8
- * `GET /plugins/crowd-depth/bathymetry/tiles/20/12345/6789`. A client seeds the
9
- * current view from these tiles, then keeps its own copy up to date from the
10
- * same live `navigation.position` + `environment.depth.*` stream the plugin
11
- * consumes.
11
+ * Mounted at a top-level, non-admin path
12
+ * (`/plotterext/<id>/bathymetry/…`) so an extension iframe loaded for a
13
+ * read-only user can reach it: the provider guide warns that the `/plugins/*`
14
+ * path is admin-gated. A client seeds its view from these tiles, then keeps its
15
+ * own copy current from the live `navigation.position` + `environment.depth.*`
16
+ * stream the plugin consumes.
12
17
  *
13
- * `getStore`/`getMeta` are read per request because the router may be
14
- * registered before the plugin's `start()` has built the store.
18
+ * `getStore`/`getMeta` are read per request because the routes are registered
19
+ * before the plugin's `start()` has built the store.
15
20
  */
16
- export function registerBathymetryRoutes(router, getStore, getMeta = () => ({}), seed) {
17
- router
18
- .access("readonly")
19
- .get("/bathymetry/meta", (_req, res) => {
21
+ export function registerBathymetryRoutes(app, getStore, getMeta = () => ({})) {
22
+ const a = app;
23
+ if (typeof a.get !== "function") {
24
+ return;
25
+ }
26
+ a.get(`${BATHYMETRY_BASE}/meta`, (_req, res) => {
20
27
  res.json({
21
28
  format: BATHYMETRY_FORMAT,
22
29
  baseZoom: BASE_ZOOM,
@@ -26,9 +33,7 @@ export function registerBathymetryRoutes(router, getStore, getMeta = () => ({}),
26
33
  ...getMeta(),
27
34
  });
28
35
  });
29
- router
30
- .access("readonly")
31
- .get("/bathymetry/tiles/:z/:x/:y", (req, res) => {
36
+ a.get(`${BATHYMETRY_BASE}/tiles/:z/:x/:y`, (req, res) => {
32
37
  const store = getStore();
33
38
  if (!store) {
34
39
  res.status(503).json({ error: "bathymetry store not ready" });
@@ -65,16 +70,20 @@ export function registerBathymetryRoutes(router, getStore, getMeta = () => ({}),
65
70
  .set("Cache-Control", "no-cache")
66
71
  .send(Buffer.from(encodeTile(cells)));
67
72
  });
68
- if (seed) {
69
- // Registered without access() so the trigger requires admin.
70
- router.get("/bathymetry/seed", (_req, res) => {
71
- res.json(seed.status());
72
- });
73
- router.post("/bathymetry/seed", (_req, res) => {
74
- // Fire and forget; the client polls GET for progress.
75
- seed.run().catch(() => { });
76
- res.status(202).json(seed.status());
77
- });
78
- }
73
+ }
74
+ /**
75
+ * The one-time history seed trigger, on the plugin's own router
76
+ * (`/plugins/<id>/bathymetry/seed`). Registered without `access()`, so it
77
+ * requires admin — it mutates the store.
78
+ */
79
+ export function registerSeedRoutes(router, seed) {
80
+ router.get("/bathymetry/seed", (_req, res) => {
81
+ res.json(seed.status());
82
+ });
83
+ router.post("/bathymetry/seed", (_req, res) => {
84
+ // Fire and forget; the client polls GET for progress.
85
+ seed.run().catch(() => { });
86
+ res.status(202).json(seed.status());
87
+ });
79
88
  }
80
89
  //# sourceMappingURL=api.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/tiles/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/tiles/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Folds soundings into the base-level tiles and, on
3
+ * {@link TileAccumulator.flush}, writes the changed tiles and min-pools their
4
+ * ancestors.
5
+ *
6
+ * Shared by the LiveDepth server plugin and the chart client, so both build
7
+ * identical tiles from the same live stream. Batched so many soundings become
8
+ * one transaction; used by the live writer and any bulk load.
9
+ */
10
+ import { Sounding } from "./types.js";
11
+ import { TileStore } from "./store.js";
12
+ export interface TileAccumulator {
13
+ add(sample: Sounding): void;
14
+ flush(): void;
15
+ /** Tiles touched since the last flush. */
16
+ readonly pending: number;
17
+ }
18
+ export interface AccumulatorOptions {
19
+ baseZoom?: number;
20
+ /** Rebuild each changed tile's ancestors on flush (the default). Set `false`
21
+ * for a bulk load and call {@link rebuildAncestors} once at the end: walking
22
+ * the whole pyramid for every changed tile on every batch repeats the same
23
+ * work whenever a tile is touched in many batches. */
24
+ ancestors?: boolean;
25
+ }
26
+ export declare function createTileAccumulator(store: TileStore, { baseZoom, ancestors }?: AccumulatorOptions): TileAccumulator;
27
+ /**
28
+ * Build every level of the pyramid from the base tiles in one bottom-up pass:
29
+ * pool each level into the one below, reading each child once. Used after a
30
+ * bulk load that ran with `ancestors: false`, so the whole pyramid is built
31
+ * once rather than re-walked per changed tile per batch.
32
+ */
33
+ export declare function rebuildAncestors(store: TileStore, { baseZoom, minZoom, }?: {
34
+ baseZoom?: number;
35
+ minZoom?: number;
36
+ }): void;
37
+ //# sourceMappingURL=accumulator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accumulator.d.ts","sourceRoot":"","sources":["../../../src/tiles/core/accumulator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAWtC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC5B,KAAK,IAAI,IAAI,CAAC;IACd,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;0DAGsD;IACtD,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,EAChB,EAAE,QAAoB,EAAE,SAAgB,EAAE,GAAE,kBAAuB,GAClE,eAAe,CA4CjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,EAChB,EACE,QAAoB,EACpB,OAAkB,GACnB,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9C,IAAI,CAwBN"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Folds soundings into the base-level tiles and, on
3
+ * {@link TileAccumulator.flush}, writes the changed tiles and min-pools their
4
+ * ancestors.
5
+ *
6
+ * Shared by the LiveDepth server plugin and the chart client, so both build
7
+ * identical tiles from the same live stream. Batched so many soundings become
8
+ * one transaction; used by the live writer and any bulk load.
9
+ */
10
+ import { BASE_ZOOM, MIN_ZOOM, TILE_SIZE, encodeDepth, lngLatToCell, mergeCell, minPoolTile, } from "./grid.js";
11
+ export function createTileAccumulator(store, { baseZoom = BASE_ZOOM, ancestors = true } = {}) {
12
+ const dirty = new Map();
13
+ return {
14
+ add(sample) {
15
+ if (!Number.isFinite(sample.lon) ||
16
+ !Number.isFinite(sample.lat) ||
17
+ !Number.isFinite(sample.depth) ||
18
+ sample.depth < 0) {
19
+ return;
20
+ }
21
+ const { x, y, cx, cy } = lngLatToCell(sample.lon, sample.lat, baseZoom);
22
+ const key = `${x},${y}`;
23
+ let tile = dirty.get(key);
24
+ if (!tile) {
25
+ tile = { z: baseZoom, x, y, cells: new Map() };
26
+ dirty.set(key, tile);
27
+ }
28
+ mergeCell(tile.cells, cy * TILE_SIZE + cx, encodeDepth(sample.depth));
29
+ },
30
+ flush() {
31
+ if (dirty.size === 0)
32
+ return;
33
+ const tiles = [...dirty.values()];
34
+ dirty.clear();
35
+ store.transaction(() => {
36
+ for (const tile of tiles) {
37
+ store.merge(tile.z, tile.x, tile.y, tile.cells);
38
+ if (ancestors) {
39
+ store.updateAncestors(tile.z, tile.x, tile.y);
40
+ }
41
+ }
42
+ });
43
+ },
44
+ get pending() {
45
+ return dirty.size;
46
+ },
47
+ };
48
+ }
49
+ /**
50
+ * Build every level of the pyramid from the base tiles in one bottom-up pass:
51
+ * pool each level into the one below, reading each child once. Used after a
52
+ * bulk load that ran with `ancestors: false`, so the whole pyramid is built
53
+ * once rather than re-walked per changed tile per batch.
54
+ */
55
+ export function rebuildAncestors(store, { baseZoom = BASE_ZOOM, minZoom = MIN_ZOOM, } = {}) {
56
+ for (let z = baseZoom; z > minZoom; z--) {
57
+ const parents = new Map();
58
+ for (const { x, y } of store.listTiles(z)) {
59
+ const px = x >> 1;
60
+ const py = y >> 1;
61
+ parents.set(`${px},${py}`, { x: px, y: py });
62
+ }
63
+ store.transaction(() => {
64
+ for (const { x, y } of parents.values()) {
65
+ store.write(z - 1, x, y, minPoolTile([
66
+ store.read(z, x * 2, y * 2),
67
+ store.read(z, x * 2 + 1, y * 2),
68
+ store.read(z, x * 2, y * 2 + 1),
69
+ store.read(z, x * 2 + 1, y * 2 + 1),
70
+ ]));
71
+ }
72
+ });
73
+ }
74
+ }
75
+ //# sourceMappingURL=accumulator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accumulator.js","sourceRoot":"","sources":["../../../src/tiles/core/accumulator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Vessel reference point → depth transducer correction.
3
+ *
4
+ * The recorded position is the reference point the server reports as
5
+ * `navigation.position`: the GNSS antenna, or — when the server's
6
+ * `gnssCorrection` is active — the Consistent Common Reference Point (CCRP,
7
+ * midships on the centreline). A depth sounding is measured at the transducer,
8
+ * which sits elsewhere on the hull, so its position must be shifted by the
9
+ * body-frame offset from that reference to the transducer, rotated by the true
10
+ * heading.
11
+ *
12
+ * The caller supplies the offset; this module neither knows nor guesses the
13
+ * reference, so it stays clear of the server's GNSS correction.
14
+ */
15
+ /** Body-frame offset of the transducer from the position reference, in metres:
16
+ * `forward` toward the bow, `starboard` to starboard. */
17
+ export interface TransducerOffset {
18
+ forward: number;
19
+ starboard: number;
20
+ }
21
+ /** A lon/lat position. */
22
+ export interface LonLat {
23
+ lon: number;
24
+ lat: number;
25
+ }
26
+ /**
27
+ * Shift `position` from the reference point to the transducer, rotating the
28
+ * body-frame `offset` by `heading` (true, radians, clockwise from north).
29
+ * Returns `position` unchanged when there is no heading or no offset.
30
+ */
31
+ export declare function correctToTransducer(position: LonLat, heading: number | undefined, offset: TransducerOffset): LonLat;
32
+ //# sourceMappingURL=correction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correction.d.ts","sourceRoot":"","sources":["../../../src/tiles/core/correction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;yDACyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,0BAA0B;AAC1B,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAMD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,MAAM,EAAE,gBAAgB,GACvB,MAAM,CAsBR"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Vessel reference point → depth transducer correction.
3
+ *
4
+ * The recorded position is the reference point the server reports as
5
+ * `navigation.position`: the GNSS antenna, or — when the server's
6
+ * `gnssCorrection` is active — the Consistent Common Reference Point (CCRP,
7
+ * midships on the centreline). A depth sounding is measured at the transducer,
8
+ * which sits elsewhere on the hull, so its position must be shifted by the
9
+ * body-frame offset from that reference to the transducer, rotated by the true
10
+ * heading.
11
+ *
12
+ * The caller supplies the offset; this module neither knows nor guesses the
13
+ * reference, so it stays clear of the server's GNSS correction.
14
+ */
15
+ const EARTH_RADIUS_M = 6378137;
16
+ const DEG_TO_RAD = Math.PI / 180;
17
+ const RAD_TO_DEG = 180 / Math.PI;
18
+ /**
19
+ * Shift `position` from the reference point to the transducer, rotating the
20
+ * body-frame `offset` by `heading` (true, radians, clockwise from north).
21
+ * Returns `position` unchanged when there is no heading or no offset.
22
+ */
23
+ export function correctToTransducer(position, heading, offset) {
24
+ if (heading === undefined ||
25
+ !Number.isFinite(heading) ||
26
+ (!offset.forward && !offset.starboard)) {
27
+ return position;
28
+ }
29
+ const cos = Math.cos(heading);
30
+ const sin = Math.sin(heading);
31
+ // +x body (toward bow) points along the heading; +y body (starboard) is 90°
32
+ // clockwise from it.
33
+ const north = offset.forward * cos - offset.starboard * sin;
34
+ const east = offset.forward * sin + offset.starboard * cos;
35
+ const lat = position.lat + (north / EARTH_RADIUS_M) * RAD_TO_DEG;
36
+ const lon = position.lon +
37
+ (east / (EARTH_RADIUS_M * Math.cos(position.lat * DEG_TO_RAD))) *
38
+ RAD_TO_DEG;
39
+ // wrap into [-180, 180] so a shift near the antimeridian stays in range
40
+ const wrapped = ((((lon + 180) % 360) + 360) % 360) - 180;
41
+ return { lon: wrapped, lat };
42
+ }
43
+ //# sourceMappingURL=correction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correction.js","sourceRoot":"","sources":["../../../src/tiles/core/correction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,MAAM,cAAc,GAAG,OAAO,CAAC;AAC/B,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE,GAAG,GAAG,CAAC;AACjC,MAAM,UAAU,GAAG,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC;AAEjC;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAAgB,EAChB,OAA2B,EAC3B,MAAwB;IAExB,IACE,OAAO,KAAK,SAAS;QACrB,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QACzB,CAAC,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EACtC,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9B,4EAA4E;IAC5E,qBAAqB;IACrB,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,GAAG,GAAG,GAAG,MAAM,CAAC,SAAS,GAAG,GAAG,CAAC;IAC5D,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,GAAG,GAAG,GAAG,MAAM,CAAC,SAAS,GAAG,GAAG,CAAC;IAC3D,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,cAAc,CAAC,GAAG,UAAU,CAAC;IACjE,MAAM,GAAG,GACP,QAAQ,CAAC,GAAG;QACZ,CAAC,IAAI,GAAG,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC;YAC7D,UAAU,CAAC;IACf,wEAAwE;IACxE,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;IAC1D,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Fixed-grid geometry for the bathymetry tile store.
3
+ *
4
+ * Shared by the LiveDepth server plugin and the chart client, so the two build
5
+ * and read identical tiles. The byte codec uses `DataView`/`Uint8Array`, which
6
+ * works both under Node and in the browser, against the `sparse-v1` wire
7
+ * format.
8
+ *
9
+ * A Web-Mercator XYZ quadtree: level `z` splits the world into `2^z` tiles of
10
+ * {@link TILE_SIZE} cells each, so a tile's cell grid is square on the ground.
11
+ * The base level is chosen so a cell is a fraction of a metre; coarser levels
12
+ * are the min-pooled parents (see {@link minPoolTile}), so a shoal is never lost
13
+ * when the chart is zoomed out.
14
+ *
15
+ * A tile is stored sparsely — only cells that have a sounding — because a
16
+ * vessel's track is a thin line through a tile and a dense 256x256 array would
17
+ * be almost entirely empty. Each occupied cell is a 5-byte record:
18
+ * `cx:u8, cy:u8, depthCm:u16le, count:u8`.
19
+ */
20
+ export declare const TILE_SIZE = 256;
21
+ /** Base level: cell ≈ 0.15 m at the equator (156543.03 / 2^20). */
22
+ export declare const BASE_ZOOM = 20;
23
+ /** Coarsest level built and served (≈ 2.4 km/cell at z4). Low enough that a
24
+ * regional zoom-out is covered by a handful of tiles; the min-pool keeps a
25
+ * shoal visible however far the chart is zoomed out. */
26
+ export declare const MIN_ZOOM = 4;
27
+ /** Decoded value for a cell with no sounding. Never stored. */
28
+ export declare const NODATA = 65535;
29
+ /** Bytes per occupied cell: cx(1) + cy(1) + depth(2) + count(1). */
30
+ export declare const RECORD_BYTES = 5;
31
+ /** One cell's aggregate: shallowest depth in centimetres and how many soundings. */
32
+ export type Cell = {
33
+ depth: number;
34
+ count: number;
35
+ };
36
+ /** A tile's occupied cells, keyed by `cy * TILE_SIZE + cx`. */
37
+ export type TileCells = Map<number, Cell>;
38
+ /** Ground size of one cell, in metres, on the equator. */
39
+ export declare function cellSize(z: number): number;
40
+ /** The level-z tile and the cell within it that contains a lon/lat. */
41
+ export declare function lngLatToCell(lon: number, lat: number, z: number): {
42
+ x: number;
43
+ y: number;
44
+ cx: number;
45
+ cy: number;
46
+ };
47
+ /** Quantise a depth in metres to the store's centimetre resolution. */
48
+ export declare function encodeDepth(metres: number): number;
49
+ /** Decode a stored depth in centimetres back to metres. */
50
+ export declare function decodeDepth(cm: number): number;
51
+ /** Merge a sounding into a cell, keeping the shallowest and counting samples. */
52
+ export declare function mergeCell(cells: TileCells, key: number, depthCm: number, count?: number): void;
53
+ /** Encode a tile's cells to its sparse on-the-wire representation. */
54
+ export declare function encodeTile(cells: TileCells): Uint8Array;
55
+ /** Decode a sparse tile buffer back to its cells. */
56
+ export declare function decodeTile(bytes: Uint8Array): TileCells;
57
+ /**
58
+ * Build a parent tile's cells from its four child tiles (level z+1, ordered
59
+ * `[southwest, southeast, northwest, northeast]` = `[(0,0),(1,0),(0,1),(1,1)]`
60
+ * in child-tile offsets). A parent cell keeps the **minimum** depth of its four
61
+ * children and the summed count, so zooming out never deepens a shoal.
62
+ */
63
+ export declare function minPoolTile(children: (TileCells | undefined)[]): TileCells;
64
+ //# sourceMappingURL=grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../../../src/tiles/core/grid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,mEAAmE;AACnE,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B;;wDAEwD;AACxD,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,+DAA+D;AAC/D,eAAO,MAAM,MAAM,QAAS,CAAC;AAC7B,oEAAoE;AACpE,eAAO,MAAM,YAAY,IAAI,CAAC;AAM9B,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACpD,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAK1C,0DAA0D;AAC1D,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1C;AAYD,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM;;;;;EAO/D;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGlD;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,iFAAiF;AACjF,wBAAgB,SAAS,CACvB,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,SAAI,GACR,IAAI,CAQN;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAYvD;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAWvD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,CAAC,SAAS,GAAG,SAAS,CAAC,EAAE,GAAG,SAAS,CAe1E"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Fixed-grid geometry for the bathymetry tile store.
3
+ *
4
+ * Shared by the LiveDepth server plugin and the chart client, so the two build
5
+ * and read identical tiles. The byte codec uses `DataView`/`Uint8Array`, which
6
+ * works both under Node and in the browser, against the `sparse-v1` wire
7
+ * format.
8
+ *
9
+ * A Web-Mercator XYZ quadtree: level `z` splits the world into `2^z` tiles of
10
+ * {@link TILE_SIZE} cells each, so a tile's cell grid is square on the ground.
11
+ * The base level is chosen so a cell is a fraction of a metre; coarser levels
12
+ * are the min-pooled parents (see {@link minPoolTile}), so a shoal is never lost
13
+ * when the chart is zoomed out.
14
+ *
15
+ * A tile is stored sparsely — only cells that have a sounding — because a
16
+ * vessel's track is a thin line through a tile and a dense 256x256 array would
17
+ * be almost entirely empty. Each occupied cell is a 5-byte record:
18
+ * `cx:u8, cy:u8, depthCm:u16le, count:u8`.
19
+ */
20
+ export const TILE_SIZE = 256;
21
+ /** Base level: cell ≈ 0.15 m at the equator (156543.03 / 2^20). */
22
+ export const BASE_ZOOM = 20;
23
+ /** Coarsest level built and served (≈ 2.4 km/cell at z4). Low enough that a
24
+ * regional zoom-out is covered by a handful of tiles; the min-pool keeps a
25
+ * shoal visible however far the chart is zoomed out. */
26
+ export const MIN_ZOOM = 4;
27
+ /** Decoded value for a cell with no sounding. Never stored. */
28
+ export const NODATA = 0xffff;
29
+ /** Bytes per occupied cell: cx(1) + cy(1) + depth(2) + count(1). */
30
+ export const RECORD_BYTES = 5;
31
+ const EARTH_RADIUS = 6378137;
32
+ const ORIGIN_SHIFT = Math.PI * EARTH_RADIUS; // 20037508.342789244
33
+ const WORLD_WIDTH = 2 * ORIGIN_SHIFT;
34
+ const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
35
+ /** Ground size of one cell, in metres, on the equator. */
36
+ export function cellSize(z) {
37
+ return WORLD_WIDTH / (TILE_SIZE * Math.pow(2, z));
38
+ }
39
+ /** Fractional tile coordinates of a lon/lat at level z (x east, y south). */
40
+ function tileFraction(lon, lat, z) {
41
+ const n = Math.pow(2, z);
42
+ const latRad = (clamp(lat, -85.05112878, 85.05112878) * Math.PI) / 180;
43
+ const x = ((lon + 180) / 360) * n;
44
+ const y = ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * n;
45
+ return { x, y, n };
46
+ }
47
+ /** The level-z tile and the cell within it that contains a lon/lat. */
48
+ export function lngLatToCell(lon, lat, z) {
49
+ const { x: fx, y: fy, n } = tileFraction(lon, lat, z);
50
+ const x = clamp(Math.floor(fx), 0, n - 1);
51
+ const y = clamp(Math.floor(fy), 0, n - 1);
52
+ const cx = clamp(Math.floor((fx - x) * TILE_SIZE), 0, TILE_SIZE - 1);
53
+ const cy = clamp(Math.floor((fy - y) * TILE_SIZE), 0, TILE_SIZE - 1);
54
+ return { x, y, cx, cy };
55
+ }
56
+ /** Quantise a depth in metres to the store's centimetre resolution. */
57
+ export function encodeDepth(metres) {
58
+ if (!Number.isFinite(metres))
59
+ return NODATA;
60
+ return clamp(Math.round(metres * 100), 0, NODATA - 1);
61
+ }
62
+ /** Decode a stored depth in centimetres back to metres. */
63
+ export function decodeDepth(cm) {
64
+ return cm === NODATA ? NaN : cm / 100;
65
+ }
66
+ /** Merge a sounding into a cell, keeping the shallowest and counting samples. */
67
+ export function mergeCell(cells, key, depthCm, count = 1) {
68
+ const existing = cells.get(key);
69
+ if (!existing) {
70
+ cells.set(key, { depth: depthCm, count: Math.min(255, count) });
71
+ return;
72
+ }
73
+ if (depthCm < existing.depth)
74
+ existing.depth = depthCm;
75
+ existing.count = Math.min(255, existing.count + count);
76
+ }
77
+ /** Encode a tile's cells to its sparse on-the-wire representation. */
78
+ export function encodeTile(cells) {
79
+ const bytes = new Uint8Array(cells.size * RECORD_BYTES);
80
+ const view = new DataView(bytes.buffer);
81
+ let offset = 0;
82
+ for (const [key, { depth, count }] of cells) {
83
+ view.setUint8(offset, key % TILE_SIZE);
84
+ view.setUint8(offset + 1, Math.floor(key / TILE_SIZE));
85
+ view.setUint16(offset + 2, depth, true);
86
+ view.setUint8(offset + 4, count);
87
+ offset += RECORD_BYTES;
88
+ }
89
+ return bytes;
90
+ }
91
+ /** Decode a sparse tile buffer back to its cells. */
92
+ export function decodeTile(bytes) {
93
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
94
+ const cells = new Map();
95
+ for (let o = 0; o + RECORD_BYTES <= view.byteLength; o += RECORD_BYTES) {
96
+ const cx = view.getUint8(o);
97
+ const cy = view.getUint8(o + 1);
98
+ const depth = view.getUint16(o + 2, true);
99
+ const count = view.getUint8(o + 4);
100
+ cells.set(cy * TILE_SIZE + cx, { depth, count });
101
+ }
102
+ return cells;
103
+ }
104
+ /**
105
+ * Build a parent tile's cells from its four child tiles (level z+1, ordered
106
+ * `[southwest, southeast, northwest, northeast]` = `[(0,0),(1,0),(0,1),(1,1)]`
107
+ * in child-tile offsets). A parent cell keeps the **minimum** depth of its four
108
+ * children and the summed count, so zooming out never deepens a shoal.
109
+ */
110
+ export function minPoolTile(children) {
111
+ const parent = new Map();
112
+ children.forEach((child, index) => {
113
+ if (!child)
114
+ return;
115
+ const dx = index & 1;
116
+ const dy = index >> 1;
117
+ for (const [key, { depth, count }] of child) {
118
+ const cx = key % TILE_SIZE;
119
+ const cy = Math.floor(key / TILE_SIZE);
120
+ const pcx = (dx * TILE_SIZE + cx) >> 1;
121
+ const pcy = (dy * TILE_SIZE + cy) >> 1;
122
+ mergeCell(parent, pcy * TILE_SIZE + pcx, depth, count);
123
+ }
124
+ });
125
+ return parent;
126
+ }
127
+ //# sourceMappingURL=grid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grid.js","sourceRoot":"","sources":["../../../src/tiles/core/grid.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export * from "./types.js";
2
+ export * from "./grid.js";
3
+ export * from "./store.js";
4
+ export * from "./accumulator.js";
5
+ export * from "./tile-source.js";
6
+ export * from "./correction.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tiles/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export * from "./types.js";
2
+ export * from "./grid.js";
3
+ export * from "./store.js";
4
+ export * from "./accumulator.js";
5
+ export * from "./tile-source.js";
6
+ export * from "./correction.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tiles/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The tile-store interface shared by both sides, plus an in-memory
3
+ * implementation.
4
+ *
5
+ * The LiveDepth plugin backs it with SQLite; the chart client keeps the tiles
6
+ * in a `Map` (bounded by the viewport it fetches, not the whole record). The
7
+ * shared {@link createTileAccumulator} and {@link minPoolTile} logic works
8
+ * against this interface so both build identical tiles.
9
+ */
10
+ import { TileCells } from "./grid.js";
11
+ export interface TileStore {
12
+ /** A tile's cells, or undefined when it holds no soundings. */
13
+ read(z: number, x: number, y: number): TileCells | undefined;
14
+ /** Replace a tile (deletes it when empty). */
15
+ write(z: number, x: number, y: number, cells: TileCells): void;
16
+ /** Merge cells into a tile, keeping the shallowest depth and summing counts. */
17
+ merge(z: number, x: number, y: number, incoming: TileCells): void;
18
+ /** Rebuild every ancestor of a changed base tile, up to {@link MIN_ZOOM}. */
19
+ updateAncestors(z: number, x: number, y: number): void;
20
+ /** Every occupied tile at a level (for a bulk pyramid rebuild). */
21
+ listTiles(z: number): {
22
+ x: number;
23
+ y: number;
24
+ }[];
25
+ /** Read a store-level key. */
26
+ getMeta(key: string): string | undefined;
27
+ /** Write a store-level key. */
28
+ setMeta(key: string, value: string): void;
29
+ /** Run `fn` inside a single transaction (a no-op batch on the client). */
30
+ transaction<T>(fn: () => T): T;
31
+ /** Drop every tile. */
32
+ clear(): void;
33
+ /** Release any resources (a no-op for the in-memory store). */
34
+ close(): void;
35
+ }
36
+ export declare function createMemoryTileStore(): TileStore;
37
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/tiles/core/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAY,SAAS,EAAe,MAAM,WAAW,CAAC;AAE7D,MAAM,WAAW,SAAS;IACxB,+DAA+D;IAC/D,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;IAC7D,8CAA8C;IAC9C,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/D,gFAAgF;IAChF,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAC;IAClE,6EAA6E;IAC7E,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvD,mEAAmE;IACnE,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjD,8BAA8B;IAC9B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACzC,+BAA+B;IAC/B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,0EAA0E;IAC1E,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,uBAAuB;IACvB,KAAK,IAAI,IAAI,CAAC;IACd,+DAA+D;IAC/D,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,qBAAqB,IAAI,SAAS,CAkFjD"}