@rhizomatics/signalk-einklabel-plugin 0.4.7 → 0.5.1
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 +11 -0
- package/README.md +77 -10
- package/dist/cli/index.js +12 -4
- package/dist/cli/liveContext.d.ts +19 -0
- package/dist/cli/liveContext.js +75 -0
- package/dist/render/fonts.d.ts +24 -3
- package/dist/render/fonts.js +32 -6
- package/dist/render/svgRenderer.js +22 -1
- package/docs/assets/inkscape_desc.png +0 -0
- package/docs/assets/screenshots/example_tidal_clock.png +0 -0
- package/package.json +6 -6
- package/templates/tide.svg +74 -74
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# 0.5.1
|
|
2
|
+
- Work around `resvg-wasm` font limitations by overriding generic font family with matching font name prior to rendering
|
|
3
|
+
# 0.5.0
|
|
4
|
+
## Font Handling
|
|
5
|
+
- Update set of built-in fonts for consistent Roboto monospace and serif
|
|
6
|
+
- Update `tide.svg` template to use explicit font names to work around `resvg-wasm` limitations
|
|
7
|
+
## Offline Design
|
|
8
|
+
- Offline template development now possible using example data,
|
|
9
|
+
- Pass `-e` or `--example-data` as a CLI argument, pointing to a set of example JSON files
|
|
10
|
+
- Examples of example data provided in plugin and at https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/examples
|
|
11
|
+
- Works with `render`,`paint`,`field` and `fields`
|
|
1
12
|
# 0.4.7
|
|
2
13
|
- Fix node-ble imports
|
|
3
14
|
# 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
|

|
|
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.
|
|
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
|
|
84
|
+
Three font types are loaded by default, use the generic font family, or 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` -
|
|
74
|
-
* `sans-serif` - Roboto
|
|
75
|
-
* `monospace` -
|
|
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
|
-
|
|
132
|
+
## Extending
|
|
114
133
|
|
|
115
|
-
|
|
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
|
-
|
|
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
|
+

|
|
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` is limited to `serif`,`sans-serif`,`monospace` or the exact name of one of the installed fonts - `Roboto` (sans serif), `Roboto Serif` or `Roboto Mono`. 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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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>;
|
package/dist/cli/liveContext.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/render/fonts.d.ts
CHANGED
|
@@ -2,8 +2,29 @@
|
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 may still use
|
|
10
|
+
* the generic keywords (Inkscape's default) - svgRenderer.ts rewrites them to the matching
|
|
11
|
+
* literal embedded name from GENERIC_FONT_FAMILY_MAP below before resvg ever sees the SVG - see
|
|
12
|
+
* project memory. Keep this to one font per bucket rather than adding e.g. bold variants - there's
|
|
13
|
+
* 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[];
|
|
23
|
+
/**
|
|
24
|
+
* Maps each CSS generic font-family keyword to the literal embedded name (nameID1) of the
|
|
25
|
+
* bundled font that backs it in DEFAULT_FONT_PATHS above. svgRenderer.ts rewrites every
|
|
26
|
+
* `font-family:sans-serif`/`serif`/`monospace` in a template to its literal name before handing
|
|
27
|
+
* the SVG to resvg-wasm, since resvg-wasm does not route generic keywords to a loaded font itself
|
|
28
|
+
* - see project memory. Keep these in sync with DEFAULT_FONT_PATHS if a bucket's font ever changes.
|
|
29
|
+
*/
|
|
30
|
+
export declare const GENERIC_FONT_FAMILY_MAP: Record<string, string>;
|
package/dist/render/fonts.js
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_FONT_PATHS = void 0;
|
|
3
|
+
exports.GENERIC_FONT_FAMILY_MAP = exports.DEFAULT_FONT_PATHS = void 0;
|
|
4
4
|
/**
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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 may still use
|
|
13
|
+
* the generic keywords (Inkscape's default) - svgRenderer.ts rewrites them to the matching
|
|
14
|
+
* literal embedded name from GENERIC_FONT_FAMILY_MAP below before resvg ever sees the SVG - see
|
|
15
|
+
* project memory. Keep this to one font per bucket rather than adding e.g. bold variants - there's
|
|
16
|
+
* 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
|
-
|
|
26
|
+
// first font is the default
|
|
14
27
|
require.resolve('@fontsource/roboto/files/roboto-latin-400-normal.woff2'),
|
|
15
|
-
require.resolve('@fontsource/
|
|
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
|
];
|
|
31
|
+
/**
|
|
32
|
+
* Maps each CSS generic font-family keyword to the literal embedded name (nameID1) of the
|
|
33
|
+
* bundled font that backs it in DEFAULT_FONT_PATHS above. svgRenderer.ts rewrites every
|
|
34
|
+
* `font-family:sans-serif`/`serif`/`monospace` in a template to its literal name before handing
|
|
35
|
+
* the SVG to resvg-wasm, since resvg-wasm does not route generic keywords to a loaded font itself
|
|
36
|
+
* - see project memory. Keep these in sync with DEFAULT_FONT_PATHS if a bucket's font ever changes.
|
|
37
|
+
*/
|
|
38
|
+
exports.GENERIC_FONT_FAMILY_MAP = {
|
|
39
|
+
'sans-serif': 'Roboto',
|
|
40
|
+
monospace: 'Roboto Mono',
|
|
41
|
+
serif: 'Roboto Serif 20pt',
|
|
42
|
+
};
|
|
@@ -13,6 +13,27 @@ function ensureWasmInitialized() {
|
|
|
13
13
|
}
|
|
14
14
|
return wasmReady;
|
|
15
15
|
}
|
|
16
|
+
// Only matches the CSS `font-family:` declaration inside a style="..." attribute, not the bare
|
|
17
|
+
// XML `font-family="..."` presentation attribute - rewriting the latter in place would require
|
|
18
|
+
// quote-aware handling of the surrounding XML attribute delimiter to avoid corrupting the
|
|
19
|
+
// document, and it's unnecessary: resvg-wasm (and any browser) prefers the style declaration
|
|
20
|
+
// whenever both are present, and every <text>/<tspan> in our templates sets one.
|
|
21
|
+
const GENERIC_FONT_FAMILY_PATTERN = /font-family\s*:\s*(['"]?)(sans-serif|serif|monospace)\1(?=\s*[;"])/g;
|
|
22
|
+
/**
|
|
23
|
+
* Rewrites CSS generic font-family keywords to the literal embedded name of the bundled font
|
|
24
|
+
* that backs each bucket (see GENERIC_FONT_FAMILY_MAP, ./fonts.ts), e.g. `font-family:sans-serif`
|
|
25
|
+
* becomes `font-family:'Roboto',sans-serif`. resvg-wasm only selects a font by exact name match -
|
|
26
|
+
* it does not route generic keywords to "the right" loaded font itself (see project memory) - so
|
|
27
|
+
* this lets templates use plain `serif`/`sans-serif`/`monospace` (as Inkscape writes by default)
|
|
28
|
+
* instead of every template author having to hardcode each bundled font's exact embedded name.
|
|
29
|
+
* The original generic keyword is kept as a trailing fallback so the rewritten value stays valid
|
|
30
|
+
* CSS for preview in e.g. Inkscape or a browser; it's inert as far as resvg-wasm is concerned.
|
|
31
|
+
*/
|
|
32
|
+
function expandGenericFontFamilies(svgSource) {
|
|
33
|
+
return svgSource.replace(GENERIC_FONT_FAMILY_PATTERN, (_match, _quote, generic) => {
|
|
34
|
+
return `font-family:'${fonts_1.GENERIC_FONT_FAMILY_MAP[generic]}',${generic}`;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
16
37
|
/**
|
|
17
38
|
* Renders an SVG+binding template to a common RGBA bitmap.
|
|
18
39
|
*
|
|
@@ -52,7 +73,7 @@ class SvgRenderer {
|
|
|
52
73
|
}
|
|
53
74
|
async render(svgTemplatePath, context, width, height) {
|
|
54
75
|
const [, fontBuffers] = await Promise.all([ensureWasmInitialized(), this.loadFontBuffers()]);
|
|
55
|
-
const svgSource = await (0, promises_1.readFile)(svgTemplatePath, 'utf-8');
|
|
76
|
+
const svgSource = expandGenericFontFamilies(await (0, promises_1.readFile)(svgTemplatePath, 'utf-8'));
|
|
56
77
|
const doc = new xmldom_1.DOMParser().parseFromString(svgSource, 'image/svg+xml');
|
|
57
78
|
const elements = doc.getElementsByTagName('text');
|
|
58
79
|
for (let i = 0; i < elements.length; i++) {
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizomatics/signalk-einklabel-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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
|
-
"
|
|
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",
|
package/templates/tide.svg
CHANGED
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
inkscape:pagecheckerboard="0"
|
|
24
24
|
inkscape:deskcolor="#d1d1d1"
|
|
25
25
|
inkscape:zoom="1.3906433"
|
|
26
|
-
inkscape:cx="
|
|
27
|
-
inkscape:cy="
|
|
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="
|
|
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:
|
|
75
|
-
transform="scale(0.89274878,1.1201359)">
|
|
74
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;font-family:serif;-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="
|
|
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
|
|
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:'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.
|
|
91
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3332995px;font-family:sans-serif;-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:
|
|
99
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:sans-serif;-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="
|
|
107
|
-
y="
|
|
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:
|
|
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:sans-serif;-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="
|
|
116
|
-
y="
|
|
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:
|
|
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:sans-serif;-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.
|
|
125
|
-
y="
|
|
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:
|
|
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:sans-serif;-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:'
|
|
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:'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="
|
|
144
|
-
y="
|
|
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:
|
|
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:sans-serif;-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="
|
|
153
|
-
y="
|
|
152
|
+
x="289.84515"
|
|
153
|
+
y="24.967674">0.1m</tspan></text>
|
|
154
154
|
<text
|
|
155
155
|
id="hat"
|
|
156
|
-
x="
|
|
157
|
-
y="
|
|
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:
|
|
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:sans-serif;-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="
|
|
166
|
-
y="
|
|
165
|
+
x="363.64786"
|
|
166
|
+
y="25.766863">6.0m</tspan></text>
|
|
167
167
|
<text
|
|
168
168
|
id="extremes.2.level"
|
|
169
|
-
x="
|
|
170
|
-
y="
|
|
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.
|
|
174
|
+
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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="
|
|
181
|
-
y="
|
|
182
|
-
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.
|
|
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:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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="
|
|
187
|
-
y="
|
|
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.
|
|
192
|
-
transform="scale(0.
|
|
191
|
+
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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="
|
|
197
|
-
y="
|
|
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.
|
|
201
|
+
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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="
|
|
208
|
-
y="
|
|
209
|
-
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.
|
|
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:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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="
|
|
214
|
-
y="
|
|
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.
|
|
218
|
+
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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="
|
|
224
|
-
y="
|
|
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.
|
|
228
|
+
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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="
|
|
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:
|
|
238
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-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="
|
|
244
|
-
y="
|
|
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:
|
|
248
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-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="
|
|
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:
|
|
258
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-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="
|
|
264
|
-
y="
|
|
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.
|
|
268
|
+
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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="
|
|
273
|
-
y="
|
|
274
|
-
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.
|
|
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:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;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:
|
|
279
|
-
x="
|
|
280
|
-
y="
|
|
278
|
+
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:sans-serif;-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="
|
|
285
|
-
y="
|
|
284
|
+
x="340.84946"
|
|
285
|
+
y="25.168207">-></tspan></text>
|
|
286
286
|
</svg>
|