@rhizomatics/signalk-esl-plugin 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.
Files changed (51) hide show
  1. package/README.md +109 -0
  2. package/dist/cli/index.d.ts +2 -0
  3. package/dist/cli/index.js +244 -0
  4. package/dist/cli/liveContext.d.ts +11 -0
  5. package/dist/cli/liveContext.js +72 -0
  6. package/dist/cli/log.d.ts +5 -0
  7. package/dist/cli/log.js +18 -0
  8. package/dist/config.d.ts +74 -0
  9. package/dist/config.js +193 -0
  10. package/dist/devices/bleDiscovery.d.ts +54 -0
  11. package/dist/devices/bleDiscovery.js +134 -0
  12. package/dist/devices/registry.d.ts +4 -0
  13. package/dist/devices/registry.js +15 -0
  14. package/dist/devices/types.d.ts +70 -0
  15. package/dist/devices/types.js +2 -0
  16. package/dist/devices/zhsunyco/encode.d.ts +12 -0
  17. package/dist/devices/zhsunyco/encode.js +47 -0
  18. package/dist/devices/zhsunyco/index.d.ts +11 -0
  19. package/dist/devices/zhsunyco/index.js +148 -0
  20. package/dist/devices/zhsunyco/metadata.d.ts +20 -0
  21. package/dist/devices/zhsunyco/metadata.js +98 -0
  22. package/dist/devices/zhsunyco/protocol.d.ts +49 -0
  23. package/dist/devices/zhsunyco/protocol.js +86 -0
  24. package/dist/httpJson.d.ts +6 -0
  25. package/dist/httpJson.js +40 -0
  26. package/dist/index.d.ts +26 -0
  27. package/dist/index.js +23 -0
  28. package/dist/pathMeta.d.ts +16 -0
  29. package/dist/pathMeta.js +21 -0
  30. package/dist/plugin.d.ts +2 -0
  31. package/dist/plugin.js +99 -0
  32. package/dist/render/binding.d.ts +53 -0
  33. package/dist/render/binding.js +168 -0
  34. package/dist/render/fonts.d.ts +9 -0
  35. package/dist/render/fonts.js +16 -0
  36. package/dist/render/formatters.d.ts +18 -0
  37. package/dist/render/formatters.js +78 -0
  38. package/dist/render/png.d.ts +3 -0
  39. package/dist/render/png.js +10 -0
  40. package/dist/render/svgRenderer.d.ts +32 -0
  41. package/dist/render/svgRenderer.js +80 -0
  42. package/dist/render/types.d.ts +26 -0
  43. package/dist/render/types.js +2 -0
  44. package/dist/repaintScheduler.d.ts +6 -0
  45. package/dist/repaintScheduler.js +193 -0
  46. package/dist/resolveApiUrl.d.ts +28 -0
  47. package/dist/resolveApiUrl.js +62 -0
  48. package/dist/unitCategories.d.ts +11 -0
  49. package/dist/unitCategories.js +46 -0
  50. package/package.json +70 -0
  51. package/templates/tide.svg +213 -0
@@ -0,0 +1,53 @@
1
+ import { TemplateContext } from './types';
2
+ declare const SOURCES: readonly ["signalk", "resources"];
3
+ type Source = (typeof SOURCES)[number];
4
+ /**
5
+ * Parsed form of a `<desc>`'s `key=value,key=value` content - see `parseBinding` for the grammar.
6
+ */
7
+ export interface Binding {
8
+ source: Source;
9
+ /** `'self'` (default) or any other literal SignalK context as shown in the Data Browser, e.g. `vessels.urn:mrn:imo:mmsi:232345678`. */
10
+ context: string;
11
+ /** Required when `source === 'resources'` - the Resources API resource type, e.g. `tides`, `waypoints`. */
12
+ resource?: string;
13
+ path: string;
14
+ /** A named formatter (see `./formatters.ts`), or `'raw'` to suppress automatic unit conversion (see `renderBinding`). */
15
+ format?: string;
16
+ /** Explicit unit-preferences category (e.g. `depth`, `speed`, `temperature`) for a numeric value with no path metadata of its own, e.g. a `source=resources` value - see `../unitCategories.ts`. */
17
+ category?: string;
18
+ round?: number;
19
+ }
20
+ /**
21
+ * Parses a `<desc>` element's text content into a `Binding`, e.g.
22
+ * `source=resources,resource=tides,path=extremes.[0].level,category=depth,round=2` or, using the
23
+ * defaults (`source=signalk,context=self`), plain `path=navigation.speedOverGround` (auto-converts via
24
+ * that path's own metadata - see `renderBinding`). A bare path with no `key=value` pairs at all, e.g.
25
+ * `environment.forecast.description`, is shorthand for `path=environment.forecast.description`
26
+ * (source/context still default to signalk/self) - SignalK paths never contain `=`, so its absence
27
+ * unambiguously signals this shorthand.
28
+ */
29
+ export declare function parseBinding(desc: string): Binding;
30
+ /**
31
+ * Parses every `<text>` element's `<desc>` binding out of raw SVG source - lets a caller discover what
32
+ * data a template needs before fetching anything, with no separate config declaring it (see
33
+ * `assembleRawContext` in repaintScheduler.ts).
34
+ */
35
+ export declare function findBindings(svgSource: string): Binding[];
36
+ /** Resolves a parsed `Binding` against the render context assembled by `assembleRawContext`. */
37
+ export declare function resolveBinding(binding: Binding, context: TemplateContext): unknown;
38
+ /**
39
+ * Resolves a binding and renders it to text exactly as `SvgRenderer` does for a `<desc>` - shared so
40
+ * the CLI's `field`/`fields` commands show the same thing a real render would.
41
+ *
42
+ * Precedence for a numeric value:
43
+ * 1. An explicit named `format=` (anything other than `raw`) - `local_time`/`utc_offset`/`position`.
44
+ * 2. An explicit `category=` - for values with no path metadata of their own, e.g. a `source=resources`
45
+ * value.
46
+ * 3. Otherwise, a `signalk`-sourced value auto-converts to its path's own preferred display unit (from
47
+ * `context.pathMeta`) by default - `format=raw` opts out of this step only.
48
+ * 4. Falls through to `round=` (`toFixed`), `JSON.stringify` for an unformatted object/array value
49
+ * (e.g. a path that resolved to a whole sub-tree rather than a leaf) instead of the useless
50
+ * `String(value)` -> `"[object Object]"`, else `String`.
51
+ */
52
+ export declare function renderBinding(binding: Binding, context: TemplateContext): string;
53
+ export {};
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseBinding = parseBinding;
4
+ exports.findBindings = findBindings;
5
+ exports.resolveBinding = resolveBinding;
6
+ exports.renderBinding = renderBinding;
7
+ const xmldom_1 = require("@xmldom/xmldom");
8
+ const formatters_1 = require("./formatters");
9
+ const SOURCES = ['signalk', 'resources'];
10
+ const KNOWN_KEYS = new Set(['source', 'context', 'resource', 'path', 'format', 'category', 'round']);
11
+ /**
12
+ * Parses a `<desc>` element's text content into a `Binding`, e.g.
13
+ * `source=resources,resource=tides,path=extremes.[0].level,category=depth,round=2` or, using the
14
+ * defaults (`source=signalk,context=self`), plain `path=navigation.speedOverGround` (auto-converts via
15
+ * that path's own metadata - see `renderBinding`). A bare path with no `key=value` pairs at all, e.g.
16
+ * `environment.forecast.description`, is shorthand for `path=environment.forecast.description`
17
+ * (source/context still default to signalk/self) - SignalK paths never contain `=`, so its absence
18
+ * unambiguously signals this shorthand.
19
+ */
20
+ function parseBinding(desc) {
21
+ const trimmedDesc = desc.trim();
22
+ if (trimmedDesc && !trimmedDesc.includes('=')) {
23
+ return { source: 'signalk', context: 'self', path: trimmedDesc };
24
+ }
25
+ const fields = {};
26
+ for (const pair of desc.split(',')) {
27
+ const trimmed = pair.trim();
28
+ if (!trimmed)
29
+ continue;
30
+ const eq = trimmed.indexOf('=');
31
+ if (eq < 0) {
32
+ throw new Error(`invalid binding "${desc}" - expected "key=value" pairs, got "${trimmed}"`);
33
+ }
34
+ const key = trimmed.slice(0, eq).trim();
35
+ if (!KNOWN_KEYS.has(key)) {
36
+ throw new Error(`invalid binding "${desc}" - unknown key "${key}"`);
37
+ }
38
+ fields[key] = trimmed.slice(eq + 1).trim();
39
+ }
40
+ const source = (fields.source ?? 'signalk');
41
+ if (!SOURCES.includes(source)) {
42
+ throw new Error(`invalid binding "${desc}" - unknown source "${source}"`);
43
+ }
44
+ const context = fields.context ?? 'self';
45
+ if (source === 'resources' && !fields.resource) {
46
+ throw new Error(`invalid binding "${desc}" - source=resources requires a "resource" key`);
47
+ }
48
+ if (!fields.path) {
49
+ throw new Error(`invalid binding "${desc}" - missing required "path" key`);
50
+ }
51
+ return {
52
+ source,
53
+ context,
54
+ resource: fields.resource,
55
+ path: fields.path,
56
+ format: fields.format,
57
+ category: fields.category,
58
+ round: fields.round !== undefined ? Number(fields.round) : undefined,
59
+ };
60
+ }
61
+ /**
62
+ * Parses every `<text>` element's `<desc>` binding out of raw SVG source - lets a caller discover what
63
+ * data a template needs before fetching anything, with no separate config declaring it (see
64
+ * `assembleRawContext` in repaintScheduler.ts).
65
+ */
66
+ function findBindings(svgSource) {
67
+ const doc = new xmldom_1.DOMParser().parseFromString(svgSource, 'image/svg+xml');
68
+ const elements = doc.getElementsByTagName('text');
69
+ const bindings = [];
70
+ for (let i = 0; i < elements.length; i++) {
71
+ const desc = elements.item(i)?.getElementsByTagName('desc').item(0);
72
+ if (desc?.textContent) {
73
+ bindings.push(parseBinding(desc.textContent));
74
+ }
75
+ }
76
+ return bindings;
77
+ }
78
+ /** Supports both `a.[0].b` and `a[0].b` array index notation, matching `setAtPath` in repaintScheduler.ts. */
79
+ function getAtPath(obj, path) {
80
+ const segments = path
81
+ .replace(/\[(\d+)\]/g, '.$1')
82
+ .split('.')
83
+ .filter((segment) => segment.length > 0);
84
+ let node = obj;
85
+ for (const segment of segments) {
86
+ if (node === null || typeof node !== 'object')
87
+ return undefined;
88
+ node = node[segment];
89
+ }
90
+ return node;
91
+ }
92
+ /** Resolves a parsed `Binding` against the render context assembled by `assembleRawContext`. */
93
+ function resolveBinding(binding, context) {
94
+ if (binding.source === 'signalk') {
95
+ const signalk = context.signalk;
96
+ const vessel = signalk?.[binding.context];
97
+ if (vessel === undefined) {
98
+ throw new Error(`binding references context "${binding.context}" which is not present in the render context`);
99
+ }
100
+ return getAtPath(vessel, binding.path);
101
+ }
102
+ const resources = context.resources;
103
+ const resource = resources?.[binding.resource];
104
+ if (resource === undefined) {
105
+ throw new Error(`binding references resource "${binding.resource}" which is not present in the render context`);
106
+ }
107
+ return getAtPath(resource, binding.path);
108
+ }
109
+ /**
110
+ * Looks up a `signalk`-sourced binding's path in `context.pathMeta` - a flat `{ [context]:
111
+ * { [dottedPath]: { displayUnits } } }` map (see `../pathMeta.ts`), matching the flat shape
112
+ * `GET .../vessels/<context>/meta` itself returns, unlike `context.signalk`'s nested tree. Unlike
113
+ * `resolveBinding`, never throws - metadata is always best-effort (a `source=resources` binding, a
114
+ * path with no metadata, or a server unreachable at `signalkApiUrl` all resolve to "no metadata"
115
+ * rather than an error).
116
+ */
117
+ function resolveDisplayUnits(binding, context) {
118
+ if (binding.source !== 'signalk')
119
+ return undefined;
120
+ const pathMeta = context.pathMeta;
121
+ return pathMeta?.[binding.context]?.[binding.path]?.displayUnits;
122
+ }
123
+ /**
124
+ * Looks up an explicit `category=` binding's resolved conversion info from `context.categories` (built
125
+ * by `fetchCategoryDisplayUnits` in `../unitCategories.ts`) - same throw-on-missing pattern as
126
+ * `resolveBinding`'s context/resource lookups, since naming a category is a declared dependency.
127
+ */
128
+ function resolveCategoryDisplayUnits(binding, context) {
129
+ const categories = context.categories;
130
+ const displayUnits = categories?.[binding.category];
131
+ if (!displayUnits) {
132
+ throw new Error(`binding references category "${binding.category}" which is not present in the render context`);
133
+ }
134
+ return displayUnits;
135
+ }
136
+ /**
137
+ * Resolves a binding and renders it to text exactly as `SvgRenderer` does for a `<desc>` - shared so
138
+ * the CLI's `field`/`fields` commands show the same thing a real render would.
139
+ *
140
+ * Precedence for a numeric value:
141
+ * 1. An explicit named `format=` (anything other than `raw`) - `local_time`/`utc_offset`/`position`.
142
+ * 2. An explicit `category=` - for values with no path metadata of their own, e.g. a `source=resources`
143
+ * value.
144
+ * 3. Otherwise, a `signalk`-sourced value auto-converts to its path's own preferred display unit (from
145
+ * `context.pathMeta`) by default - `format=raw` opts out of this step only.
146
+ * 4. Falls through to `round=` (`toFixed`), `JSON.stringify` for an unformatted object/array value
147
+ * (e.g. a path that resolved to a whole sub-tree rather than a leaf) instead of the useless
148
+ * `String(value)` -> `"[object Object]"`, else `String`.
149
+ */
150
+ function renderBinding(binding, context) {
151
+ const value = resolveBinding(binding, context);
152
+ if (binding.format && binding.format !== 'raw')
153
+ return (0, formatters_1.applyFormat)(binding.format, value, context, binding.round);
154
+ if (typeof value === 'number') {
155
+ if (binding.category)
156
+ return (0, formatters_1.formatDisplayUnits)(value, resolveCategoryDisplayUnits(binding, context), binding.round);
157
+ const displayUnits = binding.format === 'raw' ? undefined : resolveDisplayUnits(binding, context);
158
+ if (displayUnits)
159
+ return (0, formatters_1.formatDisplayUnits)(value, displayUnits, binding.round);
160
+ if (binding.round !== undefined)
161
+ return value.toFixed(binding.round);
162
+ }
163
+ if (value === null || value === undefined)
164
+ return '';
165
+ if (typeof value === 'object')
166
+ return JSON.stringify(value);
167
+ return String(value);
168
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * One font per CSS generic family bucket (monospace/sans-serif/serif), bundled so rendering
3
+ * doesn't depend on the host having any fonts installed - resvg-wasm cannot see host fonts at
4
+ * all (see project memory). Must be .woff2 - resvg-wasm silently fails to render .woff.
5
+ * resvg auto-classifies each loaded font from its own metadata; there's no proven way to
6
+ * control which one wins if two loaded fonts share a generic bucket, so keep this to one per
7
+ * bucket rather than adding e.g. bold variants here.
8
+ */
9
+ export declare const DEFAULT_FONT_PATHS: string[];
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_FONT_PATHS = void 0;
4
+ /**
5
+ * One font per CSS generic family bucket (monospace/sans-serif/serif), bundled so rendering
6
+ * doesn't depend on the host having any fonts installed - resvg-wasm cannot see host fonts at
7
+ * all (see project memory). Must be .woff2 - resvg-wasm silently fails to render .woff.
8
+ * resvg auto-classifies each loaded font from its own metadata; there's no proven way to
9
+ * control which one wins if two loaded fonts share a generic bucket, so keep this to one per
10
+ * bucket rather than adding e.g. bold variants here.
11
+ */
12
+ exports.DEFAULT_FONT_PATHS = [
13
+ require.resolve('@fontsource/jetbrains-mono/files/jetbrains-mono-latin-400-normal.woff2'),
14
+ require.resolve('@fontsource/roboto/files/roboto-latin-400-normal.woff2'),
15
+ require.resolve('@fontsource/playfair-display/files/playfair-display-latin-400-normal.woff2'),
16
+ ];
@@ -0,0 +1,18 @@
1
+ import { TemplateContext } from './types';
2
+ /**
3
+ * Matches `displayUnits` on a path's own metadata (`app.getMetadata(path).displayUnits` in the live
4
+ * plugin, the `.../meta` tree over HTTP in the CLI) - SignalK's per-path unit-preference info, fully
5
+ * resolved (formula/symbol ready to use), unlike the global `/signalk/v1/unitpreferences/active`
6
+ * endpoint which only gives a bare `targetUnit` name with no conversion math at all.
7
+ */
8
+ export interface DisplayUnits {
9
+ category: string;
10
+ targetUnit: string;
11
+ formula?: string;
12
+ symbol?: string;
13
+ displayFormat?: string;
14
+ }
15
+ /** Converts a base-SI value (always what SignalK paths deliver) to its metadata's preferred display unit and formats it with the unit's symbol, e.g. 3.42 -> "11.2ft". */
16
+ export declare function formatDisplayUnits(value: number, displayUnits: DisplayUnits, round: number | undefined): string;
17
+ /** Applies a named `format=` formatter to a resolved binding value. */
18
+ export declare function applyFormat(name: string, value: unknown, context: TemplateContext, round: number | undefined): string;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatDisplayUnits = formatDisplayUnits;
4
+ exports.applyFormat = applyFormat;
5
+ const luxon_1 = require("luxon");
6
+ const mathjs_1 = require("mathjs");
7
+ /** Converts a base-SI value (always what SignalK paths deliver) to its metadata's preferred display unit and formats it with the unit's symbol, e.g. 3.42 -> "11.2ft". */
8
+ function formatDisplayUnits(value, displayUnits, round) {
9
+ const converted = displayUnits.formula ? Number((0, mathjs_1.evaluate)(displayUnits.formula, { value })) : value;
10
+ const decimals = round ?? (displayUnits.displayFormat?.includes('.') ? displayUnits.displayFormat.split('.')[1].length : 0);
11
+ const symbol = displayUnits.symbol ?? displayUnits.targetUnit;
12
+ return `${converted.toFixed(decimals)}${symbol}`;
13
+ }
14
+ /**
15
+ * The local vessel's timezone (`signalk.self.environment.time.timezoneRegion`), regardless of which
16
+ * vessel's value is being formatted - the display's own clock/locale is what matters, not the data
17
+ * source's. Falls back to UTC when unset.
18
+ */
19
+ function selfTimezone(context) {
20
+ const signalk = context.signalk;
21
+ const self = signalk?.self;
22
+ const environment = self?.environment;
23
+ return environment?.time?.timezoneRegion || 'utc';
24
+ }
25
+ /**
26
+ * Shows the explicit IANA zone name rather than an abbreviation (e.g. "BST") - UK tide tables are
27
+ * officially published in GMT, so the basis for the displayed time must be unambiguous rather than
28
+ * just locally styled.
29
+ */
30
+ function formatLocalTime(value, context) {
31
+ if (typeof value !== 'string')
32
+ return '';
33
+ const dt = luxon_1.DateTime.fromISO(value, { zone: 'utc' }).setZone(selfTimezone(context));
34
+ return dt.isValid ? dt.toFormat('HH:mm') : '';
35
+ }
36
+ /** e.g. "27 Jun" - day of month (no leading zero) and abbreviated month name, in the local vessel's timezone. */
37
+ function formatDayMonth(value, context) {
38
+ if (typeof value !== 'string')
39
+ return '';
40
+ const dt = luxon_1.DateTime.fromISO(value, { zone: 'utc' }).setZone(selfTimezone(context));
41
+ return dt.isValid ? dt.toFormat('d MMM') : '';
42
+ }
43
+ /**
44
+ * IANA region names are ambiguous about DST (e.g. "Europe/London" is UTC+00:00 in winter, UTC+01:00
45
+ * in summer); show the numeric offset actually in effect.
46
+ */
47
+ function formatUtcOffset(value) {
48
+ if (typeof value !== 'string' || !value)
49
+ return '';
50
+ const dt = luxon_1.DateTime.now().setZone(value);
51
+ return dt.isValid ? `UTC${dt.toFormat('ZZ')}` : '';
52
+ }
53
+ function formatPosition(value, round) {
54
+ const position = value;
55
+ if (typeof position?.latitude !== 'number' || typeof position?.longitude !== 'number')
56
+ return '';
57
+ const decimals = round ?? 4;
58
+ const lat = Math.abs(position.latitude).toFixed(decimals);
59
+ const lon = Math.abs(position.longitude).toFixed(decimals);
60
+ const latHemisphere = position.latitude >= 0 ? 'N' : 'S';
61
+ const lonHemisphere = position.longitude >= 0 ? 'E' : 'W';
62
+ return `${lat}°${latHemisphere} ${lon}°${lonHemisphere}`;
63
+ }
64
+ /** Applies a named `format=` formatter to a resolved binding value. */
65
+ function applyFormat(name, value, context, round) {
66
+ switch (name) {
67
+ case 'local_time':
68
+ return formatLocalTime(value, context);
69
+ case 'day_mon':
70
+ return formatDayMonth(value, context);
71
+ case 'utc_offset':
72
+ return formatUtcOffset(value);
73
+ case 'position':
74
+ return formatPosition(value, round);
75
+ default:
76
+ throw new Error(`unknown format "${name}"`);
77
+ }
78
+ }
@@ -0,0 +1,3 @@
1
+ import { Bitmap } from './types';
2
+ /** Encodes a common Bitmap as PNG bytes, for previewing templates without a physical device. */
3
+ export declare function bitmapToPng(bitmap: Bitmap): Buffer;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bitmapToPng = bitmapToPng;
4
+ const pngjs_1 = require("pngjs");
5
+ /** Encodes a common Bitmap as PNG bytes, for previewing templates without a physical device. */
6
+ function bitmapToPng(bitmap) {
7
+ const png = new pngjs_1.PNG({ width: bitmap.width, height: bitmap.height });
8
+ png.data = Buffer.from(bitmap.data);
9
+ return pngjs_1.PNG.sync.write(png);
10
+ }
@@ -0,0 +1,32 @@
1
+ import { Bitmap, Renderer, TemplateContext } from './types';
2
+ /**
3
+ * Renders an SVG+binding template to a common RGBA bitmap.
4
+ *
5
+ * Binding model: a `<text>` element with a `<desc>` child has that child's
6
+ * content parsed as a flat `key=value,key=value` binding against the render
7
+ * context and substituted in as the element's text, e.g.
8
+ * `<desc>source=resources,resource=tides,path=extremes.[0].time,format=local_time</desc>`
9
+ * (see `./binding.ts` for the grammar and `./formatters.ts` for the `format=` registry).
10
+ * The `<text>` element's own visible content is left untouched in the source
11
+ * file - it's just a placeholder so the template looks sane while laying it
12
+ * out in an SVG editor - and is only overwritten in the in-memory copy used
13
+ * for this render. `<text>` elements with no `<desc>` are left as static text.
14
+ * Scoped to `<text>` rather than all elements with an id - setting `textContent`
15
+ * on a structural element (e.g. the root `<svg>`) wipes its children, and
16
+ * `getElementsByTagName` is a live NodeList, so that previously truncated the
17
+ * whole tree and rendered blank.
18
+ *
19
+ * resvg-wasm cannot see the host's installed fonts (`loadSystemFonts`/`fontFiles`
20
+ * are silently no-ops under plain Node) - it only renders text if given font
21
+ * bytes directly via `fontBuffers`, read from disk by us. Without at least one
22
+ * font path configured, all text elements render as nothing, with no error.
23
+ * Defaults to the bundled monospace/sans-serif/serif trio (see ./fonts.ts) so
24
+ * templates can use plain CSS generic font-family keywords.
25
+ */
26
+ export declare class SvgRenderer implements Renderer {
27
+ private readonly fontPaths;
28
+ private fontBuffers?;
29
+ constructor(fontPaths?: string[]);
30
+ private loadFontBuffers;
31
+ render(svgTemplatePath: string, context: TemplateContext, width: number, height: number): Promise<Bitmap>;
32
+ }
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SvgRenderer = void 0;
4
+ const promises_1 = require("fs/promises");
5
+ const xmldom_1 = require("@xmldom/xmldom");
6
+ const resvg_wasm_1 = require("@resvg/resvg-wasm");
7
+ const binding_1 = require("./binding");
8
+ const fonts_1 = require("./fonts");
9
+ let wasmReady;
10
+ function ensureWasmInitialized() {
11
+ if (!wasmReady) {
12
+ wasmReady = (0, promises_1.readFile)(require.resolve('@resvg/resvg-wasm/index_bg.wasm')).then((buffer) => (0, resvg_wasm_1.initWasm)(buffer));
13
+ }
14
+ return wasmReady;
15
+ }
16
+ /**
17
+ * Renders an SVG+binding template to a common RGBA bitmap.
18
+ *
19
+ * Binding model: a `<text>` element with a `<desc>` child has that child's
20
+ * content parsed as a flat `key=value,key=value` binding against the render
21
+ * context and substituted in as the element's text, e.g.
22
+ * `<desc>source=resources,resource=tides,path=extremes.[0].time,format=local_time</desc>`
23
+ * (see `./binding.ts` for the grammar and `./formatters.ts` for the `format=` registry).
24
+ * The `<text>` element's own visible content is left untouched in the source
25
+ * file - it's just a placeholder so the template looks sane while laying it
26
+ * out in an SVG editor - and is only overwritten in the in-memory copy used
27
+ * for this render. `<text>` elements with no `<desc>` are left as static text.
28
+ * Scoped to `<text>` rather than all elements with an id - setting `textContent`
29
+ * on a structural element (e.g. the root `<svg>`) wipes its children, and
30
+ * `getElementsByTagName` is a live NodeList, so that previously truncated the
31
+ * whole tree and rendered blank.
32
+ *
33
+ * resvg-wasm cannot see the host's installed fonts (`loadSystemFonts`/`fontFiles`
34
+ * are silently no-ops under plain Node) - it only renders text if given font
35
+ * bytes directly via `fontBuffers`, read from disk by us. Without at least one
36
+ * font path configured, all text elements render as nothing, with no error.
37
+ * Defaults to the bundled monospace/sans-serif/serif trio (see ./fonts.ts) so
38
+ * templates can use plain CSS generic font-family keywords.
39
+ */
40
+ class SvgRenderer {
41
+ constructor(fontPaths = fonts_1.DEFAULT_FONT_PATHS) {
42
+ this.fontPaths = fontPaths;
43
+ if (fontPaths.length === 0) {
44
+ throw new Error('SvgRenderer requires at least one font path - resvg-wasm cannot use host system fonts');
45
+ }
46
+ }
47
+ loadFontBuffers() {
48
+ if (!this.fontBuffers) {
49
+ this.fontBuffers = Promise.all(this.fontPaths.map(async (path) => new Uint8Array(await (0, promises_1.readFile)(path))));
50
+ }
51
+ return this.fontBuffers;
52
+ }
53
+ async render(svgTemplatePath, context, width, height) {
54
+ const [, fontBuffers] = await Promise.all([ensureWasmInitialized(), this.loadFontBuffers()]);
55
+ const svgSource = await (0, promises_1.readFile)(svgTemplatePath, 'utf-8');
56
+ const doc = new xmldom_1.DOMParser().parseFromString(svgSource, 'image/svg+xml');
57
+ const elements = doc.getElementsByTagName('text');
58
+ for (let i = 0; i < elements.length; i++) {
59
+ const element = elements.item(i);
60
+ if (!element)
61
+ continue;
62
+ const descElement = element.getElementsByTagName('desc').item(0);
63
+ if (!descElement)
64
+ continue;
65
+ const binding = (0, binding_1.parseBinding)(descElement.textContent ?? '');
66
+ element.textContent = (0, binding_1.renderBinding)(binding, context);
67
+ }
68
+ const svgOutput = new xmldom_1.XMLSerializer().serializeToString(doc);
69
+ const resvg = new resvg_wasm_1.Resvg(svgOutput, {
70
+ fitTo: { mode: 'width', value: width },
71
+ font: { fontBuffers },
72
+ });
73
+ const rendered = resvg.render();
74
+ if (rendered.width !== width || rendered.height !== height) {
75
+ throw new Error(`rendered size ${rendered.width}x${rendered.height} does not match requested ${width}x${height} - check the template's width/height/viewBox`);
76
+ }
77
+ return { width: rendered.width, height: rendered.height, data: rendered.pixels };
78
+ }
79
+ }
80
+ exports.SvgRenderer = SvgRenderer;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Common raster output produced by the SVG renderer, before any
3
+ * vendor-specific colour quantisation or bit-packing is applied.
4
+ */
5
+ export interface Bitmap {
6
+ width: number;
7
+ height: number;
8
+ /** RGBA, 4 bytes per pixel, row-major, top-left origin */
9
+ data: Uint8Array;
10
+ }
11
+ /**
12
+ * Render context a template's `<desc>` bindings resolve against - see `./binding.ts`. Shaped as
13
+ * `{ signalk: { self: {...}, [vesselContext]: {...} }, resources: { [resourceName]: ... },
14
+ * pathMeta: { [context]: { [dottedPath]: { displayUnits } } }, categories: { [categoryName]:
15
+ * DisplayUnits } }` by `assembleRawContext` in repaintScheduler.ts, fetched fresh from a template's own
16
+ * bindings every repaint - no separate config declares what a template needs. `pathMeta` is flat
17
+ * (dotted-path keyed, matching `GET .../vessels/<context>/meta`'s own shape), unlike `signalk`'s nested
18
+ * tree - it backs automatic unit conversion for a `signalk`-sourced numeric value (`format=raw` opts
19
+ * out). `categories` backs an explicit `category=` binding, for a value with no path metadata of its
20
+ * own (see `../unitCategories.ts`). `meta` (unrelated, plugin-injected) holds the repaint timestamp -
21
+ * see `considerRepaint`.
22
+ */
23
+ export type TemplateContext = Record<string, unknown>;
24
+ export interface Renderer {
25
+ render(svgTemplatePath: string, context: TemplateContext, width: number, height: number): Promise<Bitmap>;
26
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import { ServerAPI } from '@signalk/server-api';
2
+ import { PluginConfig } from './config';
3
+ export interface RepaintScheduler {
4
+ stop(): void;
5
+ }
6
+ export declare function startRepaintScheduler(app: ServerAPI, config: PluginConfig): RepaintScheduler;