@rhizomatics/signalk-einklabel-plugin 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.5.0
2
+ ## Font Handling
3
+ - Update set of built-in fonts for consistent Roboto monospace and serif
4
+ - Update `tide.svg` template to use explicit font names to work around `resvg-wasm` limitations
5
+ ## Offline Design
6
+ - Offline template development now possible using example data,
7
+ - Pass `-e` or `--example-data` as a CLI argument, pointing to a set of example JSON files
8
+ - Examples of example data provided in plugin and at https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/examples
9
+ - Works with `render`,`paint`,`field` and `fields`
1
10
  # 0.4.7
2
11
  - Fix node-ble imports
3
12
  # 0.4.6
package/README.md CHANGED
@@ -10,13 +10,26 @@ Since they are designed to be used in large quantity in small shops, they are ch
10
10
 
11
11
  Unlike some eInk projects, this plugin doesn't require any physical modification to the labels, or loading any new firmware. It can send an image to a supported shelf label fresh out of the box.
12
12
 
13
+ ## Installation
14
+
15
+ Look for **eInk Label Instrument** in the [SignalK AppStore]() on your
16
+ server ( under *Apps & Plugins* on the latest version).
17
+
18
+ ### Using Outside of SignalK
19
+
20
+ The plugin can also be installed as a stand-alone module, which can be useful for designing templates away from the boat, and makes available the `esl-cli` command line tool for scanning devices and debugging templates.
21
+
22
+ ```bash
23
+ npm install @rhizomatics/signalk-einklabel-plugin
24
+ ```
25
+
13
26
  ## Examples
14
27
 
15
28
  ### Tide Clock
16
29
 
17
30
  ![Tide Clock](docs/assets/screenshots/example_tidal_clock.png)
18
31
 
19
- The tide clock needs the [signalk-tides](https://github.com/openwatersio/signalk-tides) plugin to be installed and publishing tides to the Resources API. It can however be customized to run with other APIs or take data only from SignalK data paths.
32
+ The tide clock needs the [signalk-tides](https://github.com/openwatersio/signalk-tides) plugin to be installed and publishing tides to the Resources API. The [tide.svg](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/templates/tide.svg) can be customized to run with other APIs or take data only from SignalK data paths.
20
33
 
21
34
  ## Templating
22
35
 
@@ -68,15 +81,15 @@ These can all be combined as in `source=resources,resource=tides,path=extremes[2
68
81
 
69
82
  ### Fonts
70
83
 
71
- Three font types are loaded by default, use the font name or simply 'sans-serif' in the SVG editor and choose size and weight (bold, semi-bold etc). Some labels will make a decent attempt to gray scale. Use the simple pure red, yellow, white, black to match the label's limited colour choice (some labels only offer black and white, or black/white/red).
84
+ Three font types are loaded by default, use the exact font name in the SVG editor and choose size and weight (bold, semi-bold etc). Some labels will make a decent attempt to gray scale. Use the simple pure red, yellow, white, black to match the label's limited colour choice (some labels only offer black and white, or black/white/red). If a font can't be matched it will default to (sans-serif) Roboto.
72
85
 
73
- * `serif` - Playfair Display
74
- * `sans-serif` - Roboto
75
- * `monospace` - JetBrains Mono
86
+ * `serif` - `Roboto Serif`
87
+ * `sans-serif` - `Roboto`
88
+ * `monospace` - `Roboto Mono`
76
89
 
77
90
  ## Command Line Interface
78
91
 
79
- To get fast feedback on templates and shelf devices without updating and configuring SignalK, a CLI is provided that has these commands.
92
+ To get fast feedback on templates and shelf devices without updating and configuring SignalK, a CLI call `esl-cli` is provided when the module is manually installed that has these commands. Use `--help` to get all the options.
80
93
 
81
94
  - `vendors` - list supported vendors
82
95
  - `scan` - report supported devices found from a BLE scan
@@ -85,11 +98,15 @@ See also the commands useful for debugging under [Developing Templates]
85
98
  - `render` - transform an SVG template and data into a PNG
86
99
  - `paint` - render an SVG template and data to a selected ESL
87
100
 
101
+ The width, height, vertical offset and colour palette for the device is taken from the internal register of devices, however can be overridden on the command line. This could be used to help you choose what size of label to buy, or to get an unsupported label working.
102
+
103
+ ( The CLI can also be run from a checked out module, or by opening a terminal shell at `~/.signalk/node_modules/@rhizomatics/signalk-einklabel-plugin`, as `npm run cli -- command --args` )
104
+
88
105
  ## Vendors
89
106
 
90
107
  ### Zhsunyco
91
108
 
92
- Also known as 'Suny'
109
+ Also known as 'Suny' and 'WOLink'.
93
110
 
94
111
  - [BLE ESLs](https://www.zhsunyco.com/digital-display-solution-for-small-retail-business/ble-esl-solution/)
95
112
  - The range of labels available on retail sites like AliExpress may be larger than on their corporate site
@@ -103,16 +120,18 @@ Python code for a variety of their labels at https://github.com/roxburghm/zhsuny
103
120
  The primary things managed and provided by the plugin are:
104
121
 
105
122
  * ESL Vendor
123
+ - Sub-package per vendor
106
124
  * ESL Device
125
+ - Metadata in the vendor package, using a `pid` or sometimes `pid` combined with `hwid` in the BLE results to pinpoint a model
107
126
  * SVG Template
108
127
  * SignalK API base URL
109
128
  - Used for automatic unit conversion on `signalk`-sourced numeric values and for resolving an explicit `category=` binding - neither has an in-process equivalent, both go via this server's own REST API
110
129
  - Optional: left blank, the plugin probes the probable values in likelihood order at startup - `http://localhost:3000`, `http://localhost`, `https://localhost`. Set it explicitly to skip probing
111
130
  - Either way, errors clearly if nothing responds (wrong port) or the probe is rejected (anonymous read access not enabled) - these endpoints must allow anonymous read access, since the plugin has no login flow
112
131
 
113
- ### Extending
132
+ ## Extending
114
133
 
115
- #### Hardware
134
+ ### Hardware
116
135
 
117
136
  Additional vendors and devices can be added by a separate npm package that implements the `VendorDriver` interface and registers itself - there's no scanning of installed packages, registration is always an explicit call by the extension's own code.
118
137
 
@@ -120,15 +139,63 @@ Additional vendors and devices can be added by a separate npm package that imple
120
139
  - In the SignalK runtime, call this from the extension's own plugin `start()`. In the CLI, load the extension with `esl-cli --require <module> <command>`.
121
140
  - Declare this package as a `peerDependency` (not a regular dependency) in the extension package, so npm resolves a single shared copy of the registry.
122
141
 
123
- #### Developing Templates
142
+ ### Developing Templates
124
143
 
125
144
  Templates can be added to the configurable directory. [Inkscape](https://inkscape.org) free, open source, and recommended for editing templates, or your own favourite editor, or by hand in a text editor for hard core (or just tidying up the template side).
126
145
 
146
+ ![Example Field Definition](docs/assets/inkscape_desc.png)
147
+
148
+ The object ID and label aren't used by the plugin, only the description is used to define fields. You can also add in ordinary text fields without field definitions, as labels, logos, help text etc.
149
+
150
+ Placeholder text isn't necessary, and is ignored by the plugin, but makes it much easier to visualize the result.
151
+
127
152
  Inkscape adds its own metadata to images, which can be stripped off by exporting a simple SVG, although can be left in place with no harm; main reason to simplify the SVG is manual changes in a text editor.
128
153
 
154
+ Due to a limitation in the `resvg-wasm` library used to turn SVGs into images, the font family must match exactly one of the installed fonts - `Roboto` (sans serif), `Roboto Serif` or `Roboto Mono` rather than a generic `serif`,`sans-serif`,`monospace` font family. Inkscape has its own fonts, which won't match what's available in the SignalK plugin, so for more precise design, install [Roboto from Google](https://fonts.google.com/specimen/Roboto) via the web page, `brew` on MacOS or similar.
155
+
156
+ ### Debugging Templates
157
+
129
158
  The `esl-cli` can be used to debug and validate templates quickly:
130
159
 
131
160
  * `render` - Render templates with SignalK data and write to a local PNG file
132
161
  * `paint` - Render templates with SignalK data and send to selected ESL device
133
162
  * `fields` - List the fields in the template, with the source specification and the rendered data value
134
163
  * `field` - Accept a source specification (outside of any template context) and return the rendered value if available
164
+
165
+ ### Examples
166
+
167
+ #### List all Fields and Rendered Values
168
+
169
+ ```bash
170
+ npm run cli -- fields -t templates/tide.svg -u http://localhost
171
+ ```
172
+
173
+ ```
174
+ id spec value
175
+ station.name source=resources,resource=tides,path=station.name Tobermory
176
+ source.name. source=resources,resource=tides,path=station.source.name TICON-4
177
+ last_repaint source=einklabel,path=repainted,format=local_datetime_short 30 Jun 26 00:08
178
+ extremes.0 source=resources,resource=tides,path=extremes[0].label Low
179
+ extremes.1 source=resources,resource=tides,path=extremes[1].label High
180
+ extremes.2 source=resources,resource=tides,path=extremes[2].label Low
181
+ timezoneRegion source=einklabel,path=local_zone BST
182
+ lat source=resources,resource=tides,path=station.datums.LAT,category=depth 0.2m
183
+ hat source=resources,resource=tides,path=station.datums.HAT,category=depth 5.2m
184
+ extremes.2.level source=resources,resource=tides,path=extremes[2].level,category=depth 1.1m
185
+ extremes.2.time source=resources,resource=tides,path=extremes[2].time,format=local_time 13:15
186
+ extremes.1.level source=resources,resource=tides,path=extremes[1].level,category=depth 3.8m
187
+ extremes.1.time source=resources,resource=tides,path=extremes[1].time,format=local_time 07:05
188
+ extremes.0.time source=resources,resource=tides,path=extremes[0].time,format=local_time 01:21
189
+ extremes.0.time-8 source=resources,resource=tides,path=extremes[0].time,format=day_mon 30 Jun
190
+ extremes.0.time-8-5 source=resources,resource=tides,path=extremes[1].time,format=day_mon 30 Jun
191
+ extremes.0.time-8-9 source=resources,resource=tides,path=extremes[2].time,format=day_mon 30 Jun
192
+ extremes.0.level source=resources,resource=tides,path=extremes[0].level,category=depth 1.3m
193
+ ```
194
+
195
+ ### Offline Working
196
+
197
+ `render` and `paint` need a `--url` argument to point to the SignalK server to retrieve data. If you don't have access to one, you can use `--example-data` or `-e` to point to a directory of example data, which is bundled with the plugin or available in GitHub at [examples](https://github.com/rhizomatics/signalk-einklabel-plugin/tree/main/examples). This also allows you to write templates for resource APIs that aren't available yet.
198
+
199
+ - `vessels.json` - The standard SignalK vessel paths
200
+ - `resources/xxxx.json` - The output of the `xxxx` resources API call
201
+ - `categories.json` - SignalK unit categories needed for `category=depth` type formatting
package/dist/cli/index.js CHANGED
@@ -27,6 +27,10 @@ function parseColours(code) {
27
27
  }
28
28
  return colours;
29
29
  }
30
+ /** Shared by every command that takes -u/--url and -e/--example-data - -e wins when both are present (-u always carries a default, so its mere presence doesn't mean it was explicitly chosen). */
31
+ function assembleContext(opts, bindings) {
32
+ return opts.exampleData ? (0, liveContext_1.assembleExampleContext)(opts.exampleData, bindings) : (0, liveContext_1.assembleLiveContext)(opts.url, bindings);
33
+ }
30
34
  /** Connects long enough to read the advertised name and manufacturer ID, then matches against registered drivers. */
31
35
  async function identifyVendor(address) {
32
36
  const { bluetooth, destroy } = (0, bleDiscovery_1.createBluetooth)();
@@ -137,6 +141,7 @@ program
137
141
  .requiredOption('-a, --address <address>', 'BLE address of the device')
138
142
  .requiredOption('-t, --template <path>', 'path to SVG template')
139
143
  .option('-u, --url <url>', 'SignalK server base URL - resolves the template\'s source=signalk/resources bindings', DEFAULT_SIGNALK_URL)
144
+ .option('-e, --example-data <dir>', 'load vessels/resources from local example JSON files in <dir> (e.g. ./examples) instead of a live SignalK server - alternative to -u')
140
145
  .option('-k, --aes-key <hex>', 'AES-128 key for device authentication, as 32 hex characters - defaults to the vendor\'s stock key if omitted')
141
146
  .option('-w, --width <px>', 'render width', '416')
142
147
  .option('--height <px>', 'render height', '240')
@@ -160,7 +165,7 @@ program
160
165
  }
161
166
  : undefined;
162
167
  const bindings = (0, binding_1.findBindings)(await (0, promises_1.readFile)(opts.template, 'utf-8'));
163
- const context = await (0, liveContext_1.assembleLiveContext)(opts.url, bindings);
168
+ const context = await assembleContext(opts, bindings);
164
169
  const renderer = new svgRenderer_1.SvgRenderer();
165
170
  const bitmap = await renderer.render(opts.template, context, Number(opts.width), Number(opts.height));
166
171
  const connectTimeoutMs = Number(opts.connectTimeout) * 1000;
@@ -178,12 +183,13 @@ program
178
183
  .requiredOption('-t, --template <path>', 'path to SVG template')
179
184
  .requiredOption('-o, --output <path>', 'output PNG path')
180
185
  .option('-u, --url <url>', 'SignalK server base URL - resolves the template\'s source=signalk/resources bindings', DEFAULT_SIGNALK_URL)
186
+ .option('-e, --example-data <dir>', 'load vessels/resources from local example JSON files in <dir> (e.g. ./examples) instead of a live SignalK server - alternative to -u')
181
187
  .option('-w, --width <px>', 'render width', '416')
182
188
  .option('--height <px>', 'render height', '240')
183
189
  .option('-f, --font <path>', 'override a bundled font with this file (repeatable) - defaults to the bundled monospace/sans-serif/serif trio', (value, previous = []) => [...previous, value])
184
190
  .action(async (opts) => {
185
191
  const bindings = (0, binding_1.findBindings)(await (0, promises_1.readFile)(opts.template, 'utf-8'));
186
- const context = await (0, liveContext_1.assembleLiveContext)(opts.url, bindings);
192
+ const context = await assembleContext(opts, bindings);
187
193
  const renderer = opts.font ? new svgRenderer_1.SvgRenderer(opts.font) : new svgRenderer_1.SvgRenderer();
188
194
  const bitmap = await renderer.render(opts.template, context, Number(opts.width), Number(opts.height));
189
195
  await (0, promises_1.writeFile)(opts.output, (0, png_1.bitmapToPng)(bitmap));
@@ -194,6 +200,7 @@ program
194
200
  .description('List every <desc> binding in a template by element id, with its source spec and resolved value')
195
201
  .requiredOption('-t, --template <path>', 'path to SVG template')
196
202
  .option('-u, --url <url>', 'SignalK server base URL - resolves the template\'s source=signalk/resources bindings', DEFAULT_SIGNALK_URL)
203
+ .option('-e, --example-data <dir>', 'load vessels/resources from local example JSON files in <dir> (e.g. ./examples) instead of a live SignalK server - alternative to -u')
197
204
  .action(async (opts) => {
198
205
  const doc = new xmldom_1.DOMParser().parseFromString(await (0, promises_1.readFile)(opts.template, 'utf-8'), 'image/svg+xml');
199
206
  const elements = doc.getElementsByTagName('text');
@@ -216,7 +223,7 @@ program
216
223
  if (row.error || !row.binding)
217
224
  return [row.id, row.desc, row.error ?? ''];
218
225
  try {
219
- const context = await (0, liveContext_1.assembleLiveContext)(opts.url, [row.binding]);
226
+ const context = await assembleContext(opts, [row.binding]);
220
227
  return [row.id, row.desc, (0, binding_1.renderBinding)(row.binding, context)];
221
228
  }
222
229
  catch (err) {
@@ -233,9 +240,10 @@ program
233
240
  .description('Resolve a single binding spec directly against a live SignalK server, with no template')
234
241
  .argument('<spec>', 'binding spec, e.g. "source=resources,resource=tides,path=station.name" or a bare SignalK path')
235
242
  .option('-u, --url <url>', 'SignalK server base URL - resolves the spec\'s source=signalk/resources binding', DEFAULT_SIGNALK_URL)
243
+ .option('-e, --example-data <dir>', 'load vessels/resources from local example JSON files in <dir> (e.g. ./examples) instead of a live SignalK server - alternative to -u')
236
244
  .action(async (spec, opts) => {
237
245
  const binding = (0, binding_1.parseBinding)(spec);
238
- const context = await (0, liveContext_1.assembleLiveContext)(opts.url, [binding]);
246
+ const context = await assembleContext(opts, [binding]);
239
247
  console.log((0, binding_1.renderBinding)(binding, context));
240
248
  });
241
249
  program.parseAsync(process.argv).catch((err) => {
@@ -10,3 +10,22 @@ import { TemplateContext } from '../render/types';
10
10
  * navigate `path` within it.
11
11
  */
12
12
  export declare function assembleLiveContext(signalkUrl: string, bindings: Binding[]): Promise<TemplateContext>;
13
+ /**
14
+ * `-e/--example-data` counterpart to `assembleLiveContext`, for previewing a template with no SignalK
15
+ * server at all. Reads the same `{ signalk, resources, categories }` shapes a real server would
16
+ * return, but from static JSON files under `examplesDir`:
17
+ * - `<examplesDir>/vessels.json` - the full data model (`{ "vessels": { "self": {...}, ... } }`,
18
+ * delta-tree leaves wrapped as `{ value, ... }` exactly like the REST API - see `unwrapSignalkTree`),
19
+ * sliced down to whichever `context=` subtree each `source=signalk` binding needs.
20
+ * - `<examplesDir>/resources/<name>.json` - one file per `resource=<name>`, holding that resource's
21
+ * data directly (no wrapper), matching the Resources API's response shape.
22
+ * - `<examplesDir>/categories.json` - a flat `{ "<category>": DisplayUnits, ... }` map, standing in for
23
+ * what `fetchCategoryDisplayUnits` would otherwise resolve from a server's unit-preferences config
24
+ * (categoryToBaseUnit + active preset + conversion definitions - no static-file equivalent for that
25
+ * 3-way join, so this file holds the already-resolved result directly).
26
+ *
27
+ * `pathMeta` has no static-file equivalent (it's live server-side config - see `fetchPathMeta`), so
28
+ * it's left empty: automatic unit conversion falls back to a path's `category=` binding if it has one,
29
+ * or shows the raw value otherwise.
30
+ */
31
+ export declare function assembleExampleContext(examplesDir: string, bindings: Binding[]): Promise<TemplateContext>;
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.assembleLiveContext = assembleLiveContext;
7
+ exports.assembleExampleContext = assembleExampleContext;
8
+ const promises_1 = require("fs/promises");
9
+ const path_1 = __importDefault(require("path"));
4
10
  const formatters_1 = require("../render/formatters");
5
11
  const httpJson_1 = require("../httpJson");
6
12
  const unitCategories_1 = require("../unitCategories");
@@ -75,3 +81,72 @@ async function assembleLiveContext(signalkUrl, bindings) {
75
81
  const meta = { repainted: new Date().toISOString(), local_zone: (0, formatters_1.resolveLocalZoneAbbreviation)({ signalk }) };
76
82
  return { signalk, resources, pathMeta, categories, meta };
77
83
  }
84
+ async function readJsonFile(filePath) {
85
+ let raw;
86
+ try {
87
+ raw = await (0, promises_1.readFile)(filePath, 'utf-8');
88
+ }
89
+ catch (err) {
90
+ throw new Error(`could not read example data file ${filePath} - ${err.message}`);
91
+ }
92
+ return JSON.parse(raw);
93
+ }
94
+ /**
95
+ * `-e/--example-data` counterpart to `assembleLiveContext`, for previewing a template with no SignalK
96
+ * server at all. Reads the same `{ signalk, resources, categories }` shapes a real server would
97
+ * return, but from static JSON files under `examplesDir`:
98
+ * - `<examplesDir>/vessels.json` - the full data model (`{ "vessels": { "self": {...}, ... } }`,
99
+ * delta-tree leaves wrapped as `{ value, ... }` exactly like the REST API - see `unwrapSignalkTree`),
100
+ * sliced down to whichever `context=` subtree each `source=signalk` binding needs.
101
+ * - `<examplesDir>/resources/<name>.json` - one file per `resource=<name>`, holding that resource's
102
+ * data directly (no wrapper), matching the Resources API's response shape.
103
+ * - `<examplesDir>/categories.json` - a flat `{ "<category>": DisplayUnits, ... }` map, standing in for
104
+ * what `fetchCategoryDisplayUnits` would otherwise resolve from a server's unit-preferences config
105
+ * (categoryToBaseUnit + active preset + conversion definitions - no static-file equivalent for that
106
+ * 3-way join, so this file holds the already-resolved result directly).
107
+ *
108
+ * `pathMeta` has no static-file equivalent (it's live server-side config - see `fetchPathMeta`), so
109
+ * it's left empty: automatic unit conversion falls back to a path's `category=` binding if it has one,
110
+ * or shows the raw value otherwise.
111
+ */
112
+ async function assembleExampleContext(examplesDir, bindings) {
113
+ const signalk = {};
114
+ const contexts = new Set(bindings.filter((binding) => binding.source === 'signalk').map((binding) => binding.context));
115
+ if (contexts.size > 0) {
116
+ const vesselsPath = path_1.default.join(examplesDir, 'vessels.json');
117
+ (0, log_1.logDebug)(`reading ${vesselsPath}`);
118
+ const vessels = await readJsonFile(vesselsPath);
119
+ for (const context of contexts) {
120
+ const segments = contextPath(context).split('/');
121
+ let node = vessels;
122
+ for (const segment of segments) {
123
+ node = node === null || typeof node !== 'object' ? undefined : node[segment];
124
+ }
125
+ if (node === undefined) {
126
+ throw new Error(`example data file ${vesselsPath} has no "${segments.join('.')}" subtree for context "${context}"`);
127
+ }
128
+ signalk[context] = unwrapSignalkTree(node);
129
+ }
130
+ }
131
+ const resources = {};
132
+ const resourceNames = new Set(bindings.filter((binding) => binding.source === 'resources').map((binding) => binding.resource));
133
+ for (const name of resourceNames) {
134
+ const resourcePath = path_1.default.join(examplesDir, 'resources', `${name}.json`);
135
+ (0, log_1.logDebug)(`reading ${resourcePath}`);
136
+ resources[name] = await readJsonFile(resourcePath);
137
+ }
138
+ const categoryNames = new Set(bindings.filter((binding) => binding.category).map((binding) => binding.category));
139
+ let categories = {};
140
+ if (categoryNames.size > 0) {
141
+ const categoriesPath = path_1.default.join(examplesDir, 'categories.json');
142
+ (0, log_1.logDebug)(`reading ${categoriesPath}`);
143
+ categories = (await readJsonFile(categoriesPath));
144
+ for (const category of categoryNames) {
145
+ if (!(category in categories)) {
146
+ throw new Error(`example data file ${categoriesPath} has no "${category}" entry`);
147
+ }
148
+ }
149
+ }
150
+ const meta = { repainted: new Date().toISOString(), local_zone: (0, formatters_1.resolveLocalZoneAbbreviation)({ signalk }) };
151
+ return { signalk, resources, pathMeta: {}, categories, meta };
152
+ }
@@ -2,8 +2,21 @@
2
2
  * One font per CSS generic family bucket (monospace/sans-serif/serif), bundled so rendering
3
3
  * doesn't depend on the host having any fonts installed - resvg-wasm cannot see host fonts at
4
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.
5
+ *
6
+ * resvg-wasm does NOT route generic keywords (font-family:serif/sans-serif/monospace) to the
7
+ * "right" loaded font by metadata - a generic keyword, or any font-family value that doesn't
8
+ * exactly string-match a loaded font's own name, silently falls back to whichever loaded font
9
+ * happens to work for the requested glyphs (in practice: array order). Templates MUST reference
10
+ * each font by its exact embedded family name (e.g. `font-family:'Roboto Mono'` in the SVG's
11
+ * style=, not the bare `serif`/`monospace` keyword) for resvg to actually pick it - see
12
+ * templates/tide.svg and project memory. Keep this to one font per bucket rather than adding
13
+ * e.g. bold variants - there's no reliable way to pick between two fonts sharing a literal name.
14
+ *
15
+ * Must be the static (non-variable) @fontsource builds, not @fontsource-variable - every
16
+ * @fontsource-variable/roboto-{mono,serif} subset renders zero visible glyphs under resvg-wasm
17
+ * (loads without error, but produces a blank image - confirmed by counting non-white output
18
+ * pixels), and every @fontsource-variable/roboto-flex subset crashes resvg-wasm's font parser
19
+ * outright with a WASM "unreachable" trap (a Rust panic) - see project memory. The static Roboto
20
+ * Mono/Serif builds used here render correctly and keep the whole set in the Roboto super-family.
8
21
  */
9
22
  export declare const DEFAULT_FONT_PATHS: string[];
@@ -5,12 +5,26 @@ exports.DEFAULT_FONT_PATHS = void 0;
5
5
  * One font per CSS generic family bucket (monospace/sans-serif/serif), bundled so rendering
6
6
  * doesn't depend on the host having any fonts installed - resvg-wasm cannot see host fonts at
7
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.
8
+ *
9
+ * resvg-wasm does NOT route generic keywords (font-family:serif/sans-serif/monospace) to the
10
+ * "right" loaded font by metadata - a generic keyword, or any font-family value that doesn't
11
+ * exactly string-match a loaded font's own name, silently falls back to whichever loaded font
12
+ * happens to work for the requested glyphs (in practice: array order). Templates MUST reference
13
+ * each font by its exact embedded family name (e.g. `font-family:'Roboto Mono'` in the SVG's
14
+ * style=, not the bare `serif`/`monospace` keyword) for resvg to actually pick it - see
15
+ * templates/tide.svg and project memory. Keep this to one font per bucket rather than adding
16
+ * e.g. bold variants - there's no reliable way to pick between two fonts sharing a literal name.
17
+ *
18
+ * Must be the static (non-variable) @fontsource builds, not @fontsource-variable - every
19
+ * @fontsource-variable/roboto-{mono,serif} subset renders zero visible glyphs under resvg-wasm
20
+ * (loads without error, but produces a blank image - confirmed by counting non-white output
21
+ * pixels), and every @fontsource-variable/roboto-flex subset crashes resvg-wasm's font parser
22
+ * outright with a WASM "unreachable" trap (a Rust panic) - see project memory. The static Roboto
23
+ * Mono/Serif builds used here render correctly and keep the whole set in the Roboto super-family.
11
24
  */
12
25
  exports.DEFAULT_FONT_PATHS = [
13
- require.resolve('@fontsource/jetbrains-mono/files/jetbrains-mono-latin-400-normal.woff2'),
26
+ // first font is the default
14
27
  require.resolve('@fontsource/roboto/files/roboto-latin-400-normal.woff2'),
15
- require.resolve('@fontsource/playfair-display/files/playfair-display-latin-400-normal.woff2'),
28
+ require.resolve('@fontsource/roboto-mono/files/roboto-mono-latin-400-normal.woff2'),
29
+ require.resolve('@fontsource/roboto-serif/files/roboto-serif-latin-400-normal.woff2'),
16
30
  ];
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-einklabel-plugin",
3
- "version": "0.4.7",
3
+ "version": "0.5.0",
4
4
  "description": "SignalK plugin that renders selected SignalK data to eInk Electronic Shelf Labels",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -43,17 +43,17 @@
43
43
  "node": ">=18"
44
44
  },
45
45
  "dependencies": {
46
- "@fontsource/jetbrains-mono": "^5.2.8",
47
- "@fontsource/playfair-display": "^5.2.8",
48
46
  "@fontsource/roboto": "^5.2.10",
47
+ "@fontsource/roboto-mono": "^5.2.9",
48
+ "@fontsource/roboto-serif": "^5.2.9",
49
+ "@naugehyde/node-ble": "^1.13.5",
49
50
  "@resvg/resvg-wasm": "^2.6.2",
50
51
  "@xmldom/xmldom": "^0.9.10",
51
52
  "commander": "^12.1.0",
53
+ "form-data": "^4.0.6",
52
54
  "luxon": "^3.7.2",
53
55
  "mathjs": "^15.2.0",
54
- "@naugehyde/node-ble": "^1.13.5",
55
- "pngjs": "^7.0.0",
56
- "form-data":"^4.0.6"
56
+ "pngjs": "^7.0.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@signalk/server-api": "^2.28.0",
@@ -23,11 +23,11 @@
23
23
  inkscape:pagecheckerboard="0"
24
24
  inkscape:deskcolor="#d1d1d1"
25
25
  inkscape:zoom="1.3906433"
26
- inkscape:cx="245.92935"
27
- inkscape:cy="75.145079"
26
+ inkscape:cx="176.537"
27
+ inkscape:cy="132.67241"
28
28
  inkscape:window-width="1488"
29
29
  inkscape:window-height="674"
30
- inkscape:window-x="88"
30
+ inkscape:window-x="24"
31
31
  inkscape:window-y="33"
32
32
  inkscape:window-maximized="0"
33
33
  inkscape:current-layer="svg1"
@@ -71,24 +71,24 @@
71
71
  font-size="18"
72
72
  font-family="serif"
73
73
  fill="yellow"
74
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:32px;font-family:serif;-inkscape-font-specification:'serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffff00;stroke:#000000;stroke-width:1.2278;stroke-opacity:1"
75
- transform="scale(0.89274878,1.1201359)">Tobermory<desc
74
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;font-family:'Roboto Serif 20pt';-inkscape-font-specification:'Roboto Serif 20pt';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffff00;stroke:#000000;stroke-width:1.2278;stroke-opacity:1"
75
+ transform="scale(0.89274878,1.1201359)">Tidal Station Name<desc
76
76
  id="desc1">source=resources,resource=tides,path=station.name</desc></text>
77
77
  <text
78
- id="station.name-8"
78
+ id="source"
79
79
  x="347.95166"
80
80
  y="226.1548"
81
81
  font-size="18"
82
82
  font-family="serif"
83
83
  fill="black"
84
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000;stroke-width:3.80045"
84
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:'Roboto';-inkscape-font-specification:'Roboto',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000;stroke-width:3.80045"
85
85
  transform="scale(0.95907863,1.0426674)"><desc
86
86
  id="desc21">source=resources,resource=tides,path=station.source.name</desc><tspan
87
87
  sodipodi:role="line"
88
88
  id="tspan20"
89
89
  x="347.95166"
90
90
  y="226.1548"
91
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.33333319px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:3.80045">SOURCE</tspan></text>
91
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3332995px;font-family:'Roboto';-inkscape-font-specification:'Roboto',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:3.80045">SOURCE</tspan></text>
92
92
  <text
93
93
  id="last_repaint"
94
94
  x="4.3477793"
@@ -96,38 +96,38 @@
96
96
  font-size="18"
97
97
  font-family="serif"
98
98
  fill="black"
99
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.19309"
99
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:'Roboto';-inkscape-font-specification:'Roboto',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.19309"
100
100
  transform="scale(0.85412558,1.170788)"><desc
101
101
  id="desc12">source=einklabel,path=repainted,format=local_datetime_short</desc><tspan
102
102
  sodipodi:role="line"
103
103
  id="tspan3">27 Jun 26 18:05</tspan></text>
104
104
  <text
105
105
  id="extremes.0"
106
- x="7.9661016"
107
- y="60"
106
+ x="8.6851931"
107
+ y="57.123634"
108
108
  font-size="14"
109
109
  font-family="monospace"
110
110
  fill="black"
111
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;font-family:monospace;-inkscape-font-specification:'monospace, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
112
- id="desc13">source=resources,resource=tides,path=extremes[0].label</desc>High</text>
111
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
112
+ id="desc13">source=resources,resource=tides,path=extremes[0].label</desc>High or Low</text>
113
113
  <text
114
114
  id="extremes.1"
115
- x="13.35283"
116
- y="118.57627"
115
+ x="7.6000967"
116
+ y="117.85719"
117
117
  font-size="14"
118
118
  font-family="monospace"
119
119
  fill="black"
120
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;font-family:monospace;-inkscape-font-specification:'monospace, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
121
- id="desc14">source=resources,resource=tides,path=extremes[1].label</desc>Low</text>
120
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
121
+ id="desc14">source=resources,resource=tides,path=extremes[1].label</desc>High or Low</text>
122
122
  <text
123
123
  id="extremes.2"
124
- x="7.9661016"
125
- y="177.55931"
124
+ x="7.2470098"
125
+ y="176.12112"
126
126
  font-size="14"
127
127
  font-family="monospace"
128
128
  fill="red"
129
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;font-family:monospace;-inkscape-font-specification:'monospace, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
130
- id="desc15">source=resources,resource=tides,path=extremes[2].label</desc>High</text>
129
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
130
+ id="desc15">source=resources,resource=tides,path=extremes[2].label</desc>High or Low</text>
131
131
  <text
132
132
  id="environment.time.timezoneRegion"
133
133
  x="220.79686"
@@ -135,152 +135,152 @@
135
135
  font-size="10.373px"
136
136
  font-family="sans-serif"
137
137
  fill="#ffff00"
138
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;direction:rtl;fill:#000000;stroke-width:0.864419"
138
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;direction:rtl;fill:#000000;stroke-width:0.864419"
139
139
  transform="scale(0.86441854,1.156847)">UTC+01:00<desc
140
140
  id="desc11">source=einklabel,path=local_zone</desc></text>
141
141
  <text
142
142
  id="lat"
143
- x="217.936"
144
- y="27.84404"
143
+ x="289.84515"
144
+ y="24.967674"
145
145
  font-size="12px"
146
146
  font-family="sans-serif"
147
147
  fill="#ffff00"
148
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-opacity:1"><desc
149
- id="desc18">source=resources,resource=tides,path=station.datums.LAT,category=depth</desc><tspan
148
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:Roboto;-inkscape-font-specification:'\'Roboto, Normal\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-opacity:1"><desc
149
+ id="desc18">source=resources,resource=tides,path=station.datums.LAT,category=depth,round=1</desc><tspan
150
150
  sodipodi:role="line"
151
151
  id="tspan17"
152
- x="217.936"
153
- y="27.84404">0.1m</tspan></text>
152
+ x="289.84515"
153
+ y="24.967674">0.1m</tspan></text>
154
154
  <text
155
155
  id="hat"
156
- x="306.83963"
157
- y="27.924137"
156
+ x="363.64786"
157
+ y="25.766863"
158
158
  font-size="12px"
159
159
  font-family="sans-serif"
160
160
  fill="#ffff00"
161
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-opacity:1"><desc
162
- id="desc19">source=resources,resource=tides,path=station.datums.HAT,category=depth</desc><tspan
161
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:Roboto;-inkscape-font-specification:'\'Roboto, Normal\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-opacity:1"><desc
162
+ id="desc19">source=resources,resource=tides,path=station.datums.HAT,category=depth,round=1</desc><tspan
163
163
  sodipodi:role="line"
164
164
  id="tspan18"
165
- x="306.83963"
166
- y="27.924137">6.0m</tspan></text>
165
+ x="363.64786"
166
+ y="25.766863">6.0m</tspan></text>
167
167
  <text
168
168
  id="extremes.2.level"
169
- x="335.54352"
170
- y="168.08856"
169
+ x="334.61356"
170
+ y="159.19211"
171
171
  font-size="14"
172
172
  font-family="monospace"
173
173
  fill="red"
174
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.78893"
174
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.78893"
175
175
  transform="scale(0.7732368,1.2932649)"
176
176
  inkscape:transform-center-x="11.578953"
177
177
  inkscape:transform-center-y="-96.464172"><tspan
178
178
  sodipodi:role="line"
179
179
  id="tspan2-0"
180
- x="335.54352"
181
- y="168.08856"
182
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.78893">3.3m</tspan><desc
180
+ x="334.61356"
181
+ y="159.19211"
182
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.78893">3.3m</tspan><desc
183
183
  id="desc6">source=resources,resource=tides,path=extremes[2].level,category=depth</desc></text>
184
184
  <text
185
185
  id="extremes.2.time"
186
- x="164.78383"
187
- y="154.23825"
186
+ x="194.47879"
187
+ y="146.1584"
188
188
  font-size="14"
189
189
  font-family="monospace"
190
190
  fill="red"
191
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333281px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.51352"
192
- transform="scale(0.7022624,1.4239692)">15:53<desc
191
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.51352"
192
+ transform="scale(0.70226239,1.4239692)">15:53<desc
193
193
  id="desc10">source=resources,resource=tides,path=extremes[2].time,format=local_time</desc></text>
194
194
  <text
195
195
  id="extremes.1.level"
196
- x="345.0629"
197
- y="118.90553"
196
+ x="341.23608"
197
+ y="110.25762"
198
198
  font-size="14"
199
199
  font-family="monospace"
200
200
  fill="red"
201
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.701"
201
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.701"
202
202
  transform="scale(0.75163533,1.3304324)"
203
203
  inkscape:transform-center-y="-140.38618"
204
204
  inkscape:transform-center-x="-0.00038058817"><tspan
205
205
  sodipodi:role="line"
206
206
  id="tspan2-1"
207
- x="345.0629"
208
- y="118.90553"
209
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.701">1.6m</tspan><desc
207
+ x="341.23608"
208
+ y="110.25762"
209
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.701">1.6m</tspan><desc
210
210
  id="desc5">source=resources,resource=tides,path=extremes[1].level,category=depth</desc></text>
211
211
  <text
212
212
  id="extremes.1.time"
213
- x="161.1239"
214
- y="115.52532"
213
+ x="187.55594"
214
+ y="108.65867"
215
215
  font-size="14"
216
216
  font-family="monospace"
217
217
  fill="red"
218
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333356px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.48364"
218
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.48364"
219
219
  transform="scale(0.7345432,1.3613903)">09:36<desc
220
220
  id="desc8">source=resources,resource=tides,path=extremes[1].time,format=local_time</desc></text>
221
221
  <text
222
222
  id="extremes.0.time"
223
- x="156.47511"
224
- y="73.368919"
223
+ x="184.40854"
224
+ y="64.242691"
225
225
  font-size="14"
226
226
  font-family="monospace"
227
227
  fill="red"
228
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333325px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.55397"
228
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.55397"
229
229
  transform="scale(0.74654856,1.3394976)">03:27<desc
230
230
  id="desc3">source=resources,resource=tides,path=extremes[0].time,format=local_time</desc></text>
231
231
  <text
232
232
  id="extremes.0.time-8"
233
233
  x="10.326324"
234
- y="77.594147"
234
+ y="74.630478"
235
235
  font-size="14"
236
236
  font-family="monospace"
237
237
  fill="red"
238
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.6667px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.32646"
238
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.32646"
239
239
  transform="scale(0.82428216,1.2131768)"><desc
240
240
  id="desc2">source=resources,resource=tides,path=extremes[0].time,format=day_mon</desc>27 Jun</text>
241
241
  <text
242
242
  id="extremes.0.time-8-5"
243
- x="10.83737"
244
- y="126.90092"
243
+ x="9.9649849"
244
+ y="123.34451"
245
245
  font-size="14"
246
246
  font-family="monospace"
247
247
  fill="red"
248
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.6667px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.32646"
248
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.32646"
249
249
  transform="scale(0.82428216,1.2131768)"><desc
250
250
  id="desc7">source=resources,resource=tides,path=extremes[1].time,format=day_mon</desc>27 Jun</text>
251
251
  <text
252
252
  id="extremes.0.time-8-9"
253
253
  x="12.934491"
254
- y="161.07082"
254
+ y="156.77794"
255
255
  font-size="14"
256
256
  font-family="monospace"
257
257
  fill="red"
258
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:26.6667px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.4652"
258
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.4652"
259
259
  transform="scale(0.74623156,1.3400666)"><desc
260
260
  id="desc9">source=resources,resource=tides,path=extremes[2].time,format=day_mon</desc>27 Jun</text>
261
261
  <text
262
262
  id="extremes.0.level"
263
- x="336.3447"
264
- y="76.456078"
263
+ x="334.47769"
264
+ y="65.931427"
265
265
  font-size="14"
266
266
  font-family="monospace"
267
267
  fill="red"
268
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.71992"
268
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.71992"
269
269
  transform="scale(0.7703174,1.2981662)"><tspan
270
270
  sodipodi:role="line"
271
271
  id="tspan2"
272
- x="336.3447"
273
- y="76.456078"
274
- style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.33333333px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.71992">3.4m</tspan><desc
272
+ x="334.47769"
273
+ y="65.931427"
274
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:'Roboto Mono';-inkscape-font-specification:'Roboto Mono, Bold',monospaced;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.71992">3.4m</tspan><desc
275
275
  id="desc4">source=resources,resource=tides,path=extremes[0].level,category=depth</desc></text>
276
276
  <text
277
277
  xml:space="preserve"
278
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-width:0.612283;stroke-opacity:1"
279
- x="274.69302"
280
- y="27.325481"
278
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:Roboto;-inkscape-font-specification:'\'Roboto, Normal\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-width:0.612283;stroke-opacity:1"
279
+ x="340.84946"
280
+ y="25.168207"
281
281
  id="text19"><tspan
282
282
  sodipodi:role="line"
283
283
  id="tspan19"
284
- x="274.69302"
285
- y="27.325481">-&gt;</tspan></text>
284
+ x="340.84946"
285
+ y="25.168207">-&gt;</tspan></text>
286
286
  </svg>