@rhizomatics/signalk-einklabel-plugin 0.4.1 → 0.4.3

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.4.3
2
+ - Fixed datetime values when running from plugin were blank while CLI was fine
3
+ - Added an example Resources API output from signalk-tides plugin
4
+ - Relaid out the example tide clock, adding the tidal range (LAT to HAT) and the source of tide data
5
+ - Alternative source of time zone info, `source=einklabel,path=local_zone`
6
+ # 0.4.2
7
+ - Fix High/Low display for tide clock template
8
+ - Fix last repaint time for display being hour out
9
+ - Change default name of template directory to 'einklabel/templates'
1
10
  # 0.4.1
2
11
  - Packaging fixes
3
12
  # 0.4.0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ** BETA - basic functionality, limited vendor/product support **
4
4
 
5
- A SignalK plugin to display data from SignalK paths, APIs and plugins on Electronic Shelf Labels over a Bluetooth Low Energy (BLE) connection.
5
+ A SignalK plugin to display data from SignalK paths, APIs and plugins on Electronic Shelf Labels over a Bluetooth Low Energy (BLE) connection using simple SVG templates.
6
6
 
7
7
  Electronic Shelf Labels (ESLs) are [eInk](https://en.wikipedia.org/wiki/E_Ink) devices that consume very little battery energy, presuming they are not constantly updated - the battery is used only when the display changes, and a periodic BLE check for incoming changes. Perfect for info that changes only once or twice a day, like tidal information.
8
8
 
@@ -30,13 +30,16 @@ In the `description` of the SVG text box, use a comma separated set of key value
30
30
 
31
31
  For example, `path=environment.forecast.description` uses the default data source (the `self` vessel context) and the named SignalK path. A bare path with no key/value pairs at all, e.g. just `environment.forecast.description`, is shorthand for the same thing. Overriding the default context can be done with `path=environment.forecast.description,context=vessels.urn:mrn:imo:mmsi:232345678` - the `context` value must match a real SignalK context exactly as it appears in the Data Browser.
32
32
 
33
- #### SignalK REST APIs
33
+ #### SignalK REST APIs
34
34
 
35
35
  The source can be overridden to use the SignalK server's Resources API instead. For example, `source=resources,resource=tides,path=station.name` picks the `tides` resource and pulls the `station.name` path out of the JSON response - this works for any resource type (`tides`, `waypoints`, `routes`, ...), and needs nothing configured: the plugin reaches the Resources API directly. Where a resource is specified, it will be fetched once for that render, and subsequent fields sourced from the same resource use that cached response.
36
36
 
37
37
  #### Plugin Data
38
38
 
39
- `source=einklabel` reads data injected by the plugin itself, rather than from SignalK. Currently the only such value is `path=repainted`, the timestamp of the current repaint - for example `source=einklabel,path=repainted,format=local_datetime_short` to show when the label was last updated.
39
+ `source=einklabel` reads data injected by the plugin itself, rather than from SignalK. Available paths:
40
+
41
+ * `path=repainted` - the timestamp of the current repaint - for example `source=einklabel,path=repainted,format=local_datetime_short` to show when the label was last updated.
42
+ * `path=local_zone` - a short zone name (e.g. `BST`) for the same timezone used for `local_time`/`day_mon`/`local_datetime_short` (see above) - a fallback for `environment.time.timezoneRegion,format=utc_offset` on installs that never publish that path, since it needs no SignalK metadata of its own. Falls back to a plain UTC offset like `GMT+1` where the host's locale has no real abbreviation for the zone.
40
43
 
41
44
  #### Customizing Output
42
45
 
@@ -51,6 +54,8 @@ A `format` can be specified to make the value easier to understand. The supporte
51
54
 
52
55
  SignalK's unit preferences are used to automatically convert a `signalk`-sourced numeric value to its preferred display unit, and append a unit symbol like `kt` or `m`, unless `format=raw` is specified to switch that off. However, when using plugin or API data there may be no path metadata to convert from (for example `signalk-tides` publishes tide data to the Resources API, and `level` is a raw metre value with no SignalK path of its own) - in these cases an explicit `category` can be given instead, and the unit preferences will be applied the same way, for example `category=depth` for the tides level figure.
53
56
 
57
+ Note that for dates and times, the server timezone must be set correctly, for example `Europe/London` rather than the default `Etc/UTC`. This can be done on Linux using `timedatectl` or `raspi-config` if using a Raspberry Pi with Raspian.
58
+
54
59
  Common categories:
55
60
 
56
61
  * `depth` - Use the SignalK preferred depth unit, make the conversion if needed, and tack on the unit name as a suffix
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assembleLiveContext = assembleLiveContext;
4
+ const formatters_1 = require("../render/formatters");
4
5
  const httpJson_1 = require("../httpJson");
5
6
  const unitCategories_1 = require("../unitCategories");
6
7
  const pathMeta_1 = require("../pathMeta");
@@ -71,6 +72,6 @@ async function assembleLiveContext(signalkUrl, bindings) {
71
72
  const categories = await (0, unitCategories_1.fetchCategoryDisplayUnits)(signalkUrl, categoryNames);
72
73
  // Matches `considerRepaint` in repaintScheduler.ts - the CLI has no real device repaint to time, so
73
74
  // a `source=einklabel,path=repainted` binding just resolves to "now", same as a live render would.
74
- const meta = { repainted: new Date().toISOString() };
75
+ const meta = { repainted: new Date().toISOString(), local_zone: (0, formatters_1.resolveLocalZoneAbbreviation)({ signalk }) };
75
76
  return { signalk, resources, pathMeta, categories, meta };
76
77
  }
package/dist/config.js CHANGED
@@ -13,7 +13,7 @@ const resolveApiUrl_1 = require("./resolveApiUrl");
13
13
  /** The package's own bundled `templates/` directory (ships alongside `dist/`, see package.json's `files`) - templates here are always available, but a same-named template in the user's `templatesDir` takes priority. */
14
14
  const BUNDLED_TEMPLATES_DIR = (0, path_1.join)(__dirname, '..', 'templates');
15
15
  const SIGNALK_HOME_DIR = (0, path_1.join)((0, os_1.homedir)(), '.signalk');
16
- const DEFAULT_TEMPLATES_DIR = (0, path_1.join)(SIGNALK_HOME_DIR, 'esl', 'templates');
16
+ const DEFAULT_TEMPLATES_DIR = (0, path_1.join)(SIGNALK_HOME_DIR, 'einklabel', 'templates');
17
17
  function defaultConfig() {
18
18
  return {
19
19
  templatesDir: '',
@@ -20,7 +20,7 @@ export interface Binding {
20
20
  }
21
21
  /**
22
22
  * Parses a `<desc>` element's text content into a `Binding`, e.g.
23
- * `source=resources,resource=tides,path=extremes.[0].level,category=depth,round=2` or, using the
23
+ * `source=resources,resource=tides,path=extremes[0].level,category=depth,round=2` or, using the
24
24
  * defaults (`source=signalk,context=self`), plain `path=navigation.speedOverGround` (auto-converts via
25
25
  * that path's own metadata - see `renderBinding`). A bare path with no `key=value` pairs at all, e.g.
26
26
  * `environment.forecast.description`, is shorthand for `path=environment.forecast.description`
@@ -10,7 +10,7 @@ const SOURCES = ['signalk', 'resources', 'einklabel'];
10
10
  const KNOWN_KEYS = new Set(['source', 'context', 'resource', 'path', 'format', 'category', 'round']);
11
11
  /**
12
12
  * Parses a `<desc>` element's text content into a `Binding`, e.g.
13
- * `source=resources,resource=tides,path=extremes.[0].level,category=depth,round=2` or, using the
13
+ * `source=resources,resource=tides,path=extremes[0].level,category=depth,round=2` or, using the
14
14
  * defaults (`source=signalk,context=self`), plain `path=navigation.speedOverGround` (auto-converts via
15
15
  * that path's own metadata - see `renderBinding`). A bare path with no `key=value` pairs at all, e.g.
16
16
  * `environment.forecast.description`, is shorthand for `path=environment.forecast.description`
@@ -14,5 +14,15 @@ export interface DisplayUnits {
14
14
  }
15
15
  /** Converts a base-SI value (always what SignalK paths deliver) to its metadata's preferred display unit and formats it with the unit's symbol, e.g. 3.42 -> "11.2ft". */
16
16
  export declare function formatDisplayUnits(value: number, displayUnits: DisplayUnits, round: number | undefined): string;
17
+ /**
18
+ * Short zone name (e.g. "BST", or "GMT+1" where the host's own `Intl` locale has no real abbreviation
19
+ * for the zone) for the same zone `selfTimezone` resolves - an alternative for
20
+ * `environment.time.timezoneRegion,format=utc_offset` on installs that never publish that path (see
21
+ * `selfTimezone`'s own comment). Uses the host's default locale rather than pinning one, so it shows
22
+ * whatever `Intl` would show the host's own user - "GMT+1" is an acceptable fallback where that locale
23
+ * doesn't have a 3-letter abbreviation to offer. Resolved as a `source=einklabel,path=local_zone`
24
+ * binding (see `considerRepaint`).
25
+ */
26
+ export declare function resolveLocalZoneAbbreviation(context: TemplateContext): string;
17
27
  /** Applies a named `format=` formatter to a resolved binding value. */
18
28
  export declare function applyFormat(name: string, value: unknown, context: TemplateContext, round: number | undefined): string;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatDisplayUnits = formatDisplayUnits;
4
+ exports.resolveLocalZoneAbbreviation = resolveLocalZoneAbbreviation;
4
5
  exports.applyFormat = applyFormat;
5
6
  const luxon_1 = require("luxon");
6
7
  const mathjs_1 = require("mathjs");
@@ -14,13 +15,46 @@ function formatDisplayUnits(value, displayUnits, round) {
14
15
  /**
15
16
  * The local vessel's timezone (`signalk.self.environment.time.timezoneRegion`), regardless of which
16
17
  * vessel's value is being formatted - the display's own clock/locale is what matters, not the data
17
- * source's. Falls back to UTC when unset.
18
+ * source's. Most installs never publish that path (it needs a GPS/position-derived timezone plugin),
19
+ * so this falls back to the host machine's own configured zone - the same thing the SignalK Data
20
+ * Browser ends up showing, since it's just the browser's local `Intl` zone rather than anything the
21
+ * server itself publishes. For the live plugin that's the device's own OS timezone (set once via
22
+ * `raspi-config`/`timedatectl` et al, not GPS-derived); for the CLI it's the developer machine's.
18
23
  */
19
24
  function selfTimezone(context) {
20
25
  const signalk = context.signalk;
21
26
  const self = signalk?.self;
22
27
  const environment = self?.environment;
23
- return environment?.time?.timezoneRegion || 'utc';
28
+ return environment?.time?.timezoneRegion || Intl.DateTimeFormat().resolvedOptions().timeZone;
29
+ }
30
+ /**
31
+ * Short zone name (e.g. "BST", or "GMT+1" where the host's own `Intl` locale has no real abbreviation
32
+ * for the zone) for the same zone `selfTimezone` resolves - an alternative for
33
+ * `environment.time.timezoneRegion,format=utc_offset` on installs that never publish that path (see
34
+ * `selfTimezone`'s own comment). Uses the host's default locale rather than pinning one, so it shows
35
+ * whatever `Intl` would show the host's own user - "GMT+1" is an acceptable fallback where that locale
36
+ * doesn't have a 3-letter abbreviation to offer. Resolved as a `source=einklabel,path=local_zone`
37
+ * binding (see `considerRepaint`).
38
+ */
39
+ function resolveLocalZoneAbbreviation(context) {
40
+ const part = new Intl.DateTimeFormat(undefined, { timeZone: selfTimezone(context), timeZoneName: 'short' })
41
+ .formatToParts(new Date())
42
+ .find((p) => p.type === 'timeZoneName');
43
+ return part?.value ?? '';
44
+ }
45
+ /**
46
+ * A `resources`-sourced timestamp can arrive as either an ISO string (always true over HTTP, since
47
+ * `JSON.stringify` converts a `Date` to its ISO string automatically) or a live `Date` instance (an
48
+ * in-process `app.resourcesApi` call skips that serialisation step entirely, so a provider that hasn't
49
+ * itself called `.toISOString()` yet hands back the raw object) - accept both so the live plugin and
50
+ * the CLI render identically regardless of which path a binding's data took.
51
+ */
52
+ function parseTimestamp(value) {
53
+ if (value instanceof Date)
54
+ return luxon_1.DateTime.fromJSDate(value, { zone: 'utc' });
55
+ if (typeof value === 'string')
56
+ return luxon_1.DateTime.fromISO(value, { zone: 'utc' });
57
+ return undefined;
24
58
  }
25
59
  /**
26
60
  * Shows the explicit IANA zone name rather than an abbreviation (e.g. "BST") - UK tide tables are
@@ -28,24 +62,18 @@ function selfTimezone(context) {
28
62
  * just locally styled.
29
63
  */
30
64
  function formatLocalTime(value, context) {
31
- if (typeof value !== 'string')
32
- return '';
33
- const dt = luxon_1.DateTime.fromISO(value, { zone: 'utc' }).setZone(selfTimezone(context));
34
- return dt.isValid ? dt.toFormat('HH:mm') : '';
65
+ const dt = parseTimestamp(value)?.setZone(selfTimezone(context));
66
+ return dt?.isValid ? dt.toFormat('HH:mm') : '';
35
67
  }
36
68
  /** e.g. "27 Jun" - day of month (no leading zero) and abbreviated month name, in the local vessel's timezone. */
37
69
  function formatDayMonth(value, context) {
38
- if (typeof value !== 'string')
39
- return '';
40
- const dt = luxon_1.DateTime.fromISO(value, { zone: 'utc' }).setZone(selfTimezone(context));
41
- return dt.isValid ? dt.toFormat('d MMM') : '';
70
+ const dt = parseTimestamp(value)?.setZone(selfTimezone(context));
71
+ return dt?.isValid ? dt.toFormat('d MMM') : '';
42
72
  }
43
73
  /** e.g. "21 Jun 26 18:05" - day of month (no leading zero), abbreviated month, 2-digit year, and 24h time, in the local vessel's timezone. */
44
74
  function formatLocalDatetimeShort(value, context) {
45
- if (typeof value !== 'string')
46
- return '';
47
- const dt = luxon_1.DateTime.fromISO(value, { zone: 'utc' }).setZone(selfTimezone(context));
48
- return dt.isValid ? dt.toFormat('d MMM yy HH:mm') : '';
75
+ const dt = parseTimestamp(value)?.setZone(selfTimezone(context));
76
+ return dt?.isValid ? dt.toFormat('d MMM yy HH:mm') : '';
49
77
  }
50
78
  /**
51
79
  * IANA region names are ambiguous about DST (e.g. "Europe/London" is UTC+00:00 in winter, UTC+01:00
@@ -5,7 +5,7 @@ import { Bitmap, Renderer, TemplateContext } from './types';
5
5
  * Binding model: a `<text>` element with a `<desc>` child has that child's
6
6
  * content parsed as a flat `key=value,key=value` binding against the render
7
7
  * context and substituted in as the element's text, e.g.
8
- * `<desc>source=resources,resource=tides,path=extremes.[0].time,format=local_time</desc>`
8
+ * `<desc>source=resources,resource=tides,path=extremes[0].time,format=local_time</desc>`
9
9
  * (see `./binding.ts` for the grammar and `./formatters.ts` for the `format=` registry).
10
10
  * The `<text>` element's own visible content is left untouched in the source
11
11
  * file - it's just a placeholder so the template looks sane while laying it
@@ -19,7 +19,7 @@ function ensureWasmInitialized() {
19
19
  * Binding model: a `<text>` element with a `<desc>` child has that child's
20
20
  * content parsed as a flat `key=value,key=value` binding against the render
21
21
  * context and substituted in as the element's text, e.g.
22
- * `<desc>source=resources,resource=tides,path=extremes.[0].time,format=local_time</desc>`
22
+ * `<desc>source=resources,resource=tides,path=extremes[0].time,format=local_time</desc>`
23
23
  * (see `./binding.ts` for the grammar and `./formatters.ts` for the `format=` registry).
24
24
  * The `<text>` element's own visible content is left untouched in the source
25
25
  * file - it's just a placeholder so the template looks sane while laying it
@@ -62,8 +62,16 @@ class SvgRenderer {
62
62
  const descElement = element.getElementsByTagName('desc').item(0);
63
63
  if (!descElement)
64
64
  continue;
65
- const binding = (0, binding_1.parseBinding)(descElement.textContent ?? '');
66
- element.textContent = (0, binding_1.renderBinding)(binding, context);
65
+ // Scoped to this one element - a single bad/unavailable binding (e.g. a resource that hasn't
66
+ // loaded yet) must not blank out every other field on the label, nor abort the whole repaint.
67
+ try {
68
+ const binding = (0, binding_1.parseBinding)(descElement.textContent ?? '');
69
+ element.textContent = (0, binding_1.renderBinding)(binding, context);
70
+ }
71
+ catch (err) {
72
+ console.error(`field "${descElement.textContent}" failed to render: ${err.message}`);
73
+ element.textContent = 'ERROR';
74
+ }
67
75
  }
68
76
  const svgOutput = new xmldom_1.XMLSerializer().serializeToString(doc);
69
77
  const resvg = new resvg_wasm_1.Resvg(svgOutput, {
@@ -9,6 +9,7 @@ const bleDiscovery_1 = require("./devices/bleDiscovery");
9
9
  const registry_1 = require("./devices/registry");
10
10
  const svgRenderer_1 = require("./render/svgRenderer");
11
11
  const binding_1 = require("./render/binding");
12
+ const formatters_1 = require("./render/formatters");
12
13
  const unitCategories_1 = require("./unitCategories");
13
14
  const pathMeta_1 = require("./pathMeta");
14
15
  const resolveApiUrl_1 = require("./resolveApiUrl");
@@ -136,7 +137,10 @@ async function considerRepaint(app, config, device, state, getApiUrl) {
136
137
  app.debug(`"${device.friendlyName}": data unchanged, skipping repaint`);
137
138
  return;
138
139
  }
139
- const renderContext = { ...rawContext, meta: { repainted: new Date().toISOString() } };
140
+ const renderContext = {
141
+ ...rawContext,
142
+ meta: { repainted: new Date().toISOString(), local_zone: (0, formatters_1.resolveLocalZoneAbbreviation)(rawContext) },
143
+ };
140
144
  const renderer = new svgRenderer_1.SvgRenderer();
141
145
  const bitmap = await renderer.render(templatePath, renderContext, metadata.width, metadata.height - metadata.voffset);
142
146
  const connectTimeoutMs = config.paintConnectTimeoutSeconds * 1000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-einklabel-plugin",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",
@@ -22,15 +22,33 @@
22
22
  inkscape:pageopacity="0.0"
23
23
  inkscape:pagecheckerboard="0"
24
24
  inkscape:deskcolor="#d1d1d1"
25
- inkscape:zoom="0.98333333"
26
- inkscape:cx="250.16949"
27
- inkscape:cy="246.61017"
25
+ inkscape:zoom="1.3906433"
26
+ inkscape:cx="202.4243"
27
+ inkscape:cy="151.36879"
28
28
  inkscape:window-width="1488"
29
29
  inkscape:window-height="674"
30
30
  inkscape:window-x="0"
31
31
  inkscape:window-y="33"
32
32
  inkscape:window-maximized="0"
33
- inkscape:current-layer="svg1" />
33
+ inkscape:current-layer="svg1"
34
+ showgrid="true">
35
+ <inkscape:grid
36
+ id="grid1"
37
+ units="px"
38
+ originx="0"
39
+ originy="0"
40
+ spacingx="1"
41
+ spacingy="1"
42
+ empcolor="#0099e5"
43
+ empopacity="0.30196078"
44
+ color="#0099e5"
45
+ opacity="0.14901961"
46
+ empspacing="5"
47
+ dotted="false"
48
+ gridanglex="30"
49
+ gridanglez="30"
50
+ visible="true" />
51
+ </sodipodi:namedview>
34
52
  <rect
35
53
  x="1.0169492"
36
54
  y="2.0798673e-08"
@@ -58,20 +76,18 @@
58
76
  id="desc1">source=resources,resource=tides,path=station.name</desc></text>
59
77
  <text
60
78
  id="station.name-8"
61
- x="325.49939"
62
- y="195.87753"
79
+ x="440.67529"
80
+ y="196.47466"
63
81
  font-size="18"
64
82
  font-family="serif"
65
83
  fill="black"
66
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;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:#999999;stroke-width:2.39571"
67
- transform="scale(0.83037506,1.2042751)"><tspan
84
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;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:2.39571"
85
+ transform="scale(0.83037506,1.2042751)"><desc
86
+ id="desc21">source=resources,resource=tides,path=station.source.name</desc><tspan
68
87
  sodipodi:role="line"
69
- id="tspan1"
70
- x="325.49939"
71
- y="195.87753"
72
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12px;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:2.39571;fill:#999999">signalk-einklabel-plugin</tspan></text>
88
+ id="tspan20">SOURCE</tspan></text>
73
89
  <text
74
- id="station.name-8-2"
90
+ id="last_repaint"
75
91
  x="4.3477793"
76
92
  y="201.9895"
77
93
  font-size="18"
@@ -81,10 +97,7 @@
81
97
  transform="scale(0.85412558,1.170788)"><desc
82
98
  id="desc12">source=einklabel,path=repainted,format=local_datetime_short</desc><tspan
83
99
  sodipodi:role="line"
84
- id="tspan3"
85
- x="4.3477793"
86
- y="201.9895"
87
- style="stroke-width:1.19309;-inkscape-font-specification:'Sans, Normal';font-family:Sans;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:13.33333347px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal">Last updated 27 Jun 26 18:05</tspan></text>
100
+ id="tspan3">27 Jun 26 18:05</tspan></text>
88
101
  <text
89
102
  id="extremes.0"
90
103
  x="7.9661016"
@@ -92,7 +105,8 @@
92
105
  font-size="14"
93
106
  font-family="monospace"
94
107
  fill="black"
95
- 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">High Water</text>
108
+ 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
109
+ id="desc13">source=resources,resource=tides,path=extremes[0].label</desc>High</text>
96
110
  <text
97
111
  id="extremes.1"
98
112
  x="13.35283"
@@ -100,7 +114,8 @@
100
114
  font-size="14"
101
115
  font-family="monospace"
102
116
  fill="black"
103
- 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">Low Water</text>
117
+ 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
118
+ id="desc14">source=resources,resource=tides,path=extremes[1].label</desc>Low</text>
104
119
  <text
105
120
  id="extremes.2"
106
121
  x="7.9661016"
@@ -108,123 +123,159 @@
108
123
  font-size="14"
109
124
  font-family="monospace"
110
125
  fill="red"
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">High Water</text>
126
+ 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
127
+ id="desc15">source=resources,resource=tides,path=extremes[2].label</desc>High</text>
112
128
  <text
113
129
  id="environment.time.timezoneRegion"
114
- x="338.47461"
115
- y="17.457626"
130
+ x="440.41293"
131
+ y="11.361118"
132
+ font-size="10.373px"
133
+ font-family="sans-serif"
134
+ fill="#ffff00"
135
+ style="direction:rtl;fill:#000000;stroke-width:0.864419"
136
+ transform="scale(0.86441854,1.156847)">UTC+01:00<desc
137
+ id="desc11">source=einklabel,path=local_zone</desc></text>
138
+ <text
139
+ id="lat"
140
+ x="175.5096"
141
+ y="30.720407"
142
+ font-size="12px"
143
+ font-family="sans-serif"
144
+ fill="#ffff00"
145
+ style="fill:#ffff00;-inkscape-font-specification:'Sans, Normal';font-family:Sans;font-weight:normal;font-style:normal;font-stretch:normal;font-variant:normal;font-size:18.6667px;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke:#a6a6a6;stroke-opacity:1"><desc
146
+ id="desc18">source=resources,resource=tides,path=station.datums.LAT,category=depth</desc><tspan
147
+ sodipodi:role="line"
148
+ id="tspan17">0.1m</tspan></text>
149
+ <text
150
+ id="hat"
151
+ x="264.41321"
152
+ y="30.800503"
116
153
  font-size="12px"
117
154
  font-family="sans-serif"
118
155
  fill="#ffff00"
119
- style="fill:#000000">UTC+01:00<desc
120
- id="desc11">path=environment.time.timezoneRegion,format=utc_offset</desc></text>
156
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;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:#ffff00;stroke:#acacaa;stroke-opacity:1"><desc
157
+ id="desc19">source=resources,resource=tides,path=station.datums.HAT,category=depth</desc><tspan
158
+ sodipodi:role="line"
159
+ id="tspan18"
160
+ x="264.41321"
161
+ y="30.800503">6.0m</tspan></text>
121
162
  <text
122
163
  id="extremes.2.level"
123
- x="173.64978"
124
- y="199.27094"
164
+ x="335.54352"
165
+ y="168.08856"
125
166
  font-size="14"
126
167
  font-family="monospace"
127
168
  fill="red"
128
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:#000000;stroke-width:1.44101"
129
- transform="scale(0.95993086,1.0417417)"
130
- inkscape:transform-center-x="11.578901"
131
- inkscape:transform-center-y="-62.5909"><tspan
169
+ 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"
170
+ transform="scale(0.7732368,1.2932649)"
171
+ inkscape:transform-center-x="11.578953"
172
+ inkscape:transform-center-y="-96.464172"><tspan
132
173
  sodipodi:role="line"
133
174
  id="tspan2-0"
134
- x="173.64978"
135
- y="199.27094"
136
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:1.44101">3.3m</tspan><desc
137
- id="desc6">source=resources,resource=tides,path=extremes.[2].level,category=depth</desc></text>
175
+ x="335.54352"
176
+ y="168.08856"
177
+ 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
178
+ id="desc6">source=resources,resource=tides,path=extremes[2].level,category=depth</desc></text>
138
179
  <text
139
180
  id="extremes.2.time"
140
- x="31.471279"
141
- y="166.20743"
181
+ x="164.78383"
182
+ y="154.23825"
142
183
  font-size="14"
143
184
  font-family="monospace"
144
185
  fill="red"
145
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:#000000;stroke-width:1.31448"
146
- transform="scale(0.77910527,1.2835236)">15:53<desc
147
- id="desc10">source=resources,resource=tides,path=extremes.[2].time,format=local_time</desc></text>
186
+ 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"
187
+ transform="scale(0.7022624,1.4239692)">15:53<desc
188
+ id="desc10">source=resources,resource=tides,path=extremes[2].time,format=local_time</desc></text>
148
189
  <text
149
190
  id="extremes.1.level"
150
- x="182.71886"
151
- y="135.3521"
191
+ x="345.0629"
192
+ y="118.90553"
152
193
  font-size="14"
153
194
  font-family="monospace"
154
195
  fill="red"
155
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:#000000;stroke-width:1.40832"
156
- transform="scale(0.90784041,1.1015152)"
157
- inkscape:transform-center-y="-96.23206"
158
- inkscape:transform-center-x="-0.00043342573"><tspan
196
+ 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"
197
+ transform="scale(0.75163533,1.3304324)"
198
+ inkscape:transform-center-y="-140.38618"
199
+ inkscape:transform-center-x="-0.00038058817"><tspan
159
200
  sodipodi:role="line"
160
201
  id="tspan2-1"
161
- x="182.71886"
162
- y="135.3521"
163
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:1.40832">1.6m</tspan><desc
164
- id="desc5">source=resources,resource=tides,path=extremes.[1].level,category=depth</desc></text>
202
+ x="345.0629"
203
+ y="118.90553"
204
+ 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
205
+ id="desc5">source=resources,resource=tides,path=extremes[1].level,category=depth</desc></text>
165
206
  <text
166
207
  id="extremes.1.time"
167
- x="33.328102"
168
- y="118.54723"
208
+ x="161.1239"
209
+ y="115.52532"
169
210
  font-size="14"
170
211
  font-family="monospace"
171
212
  fill="red"
172
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:#000000;stroke-width:1.31609"
173
- transform="scale(0.77081545,1.2973274)">09:36<desc
174
- id="desc8">source=resources,resource=tides,path=extremes.[1].time,format=local_time</desc></text>
213
+ 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"
214
+ transform="scale(0.7345432,1.3613903)">09:36<desc
215
+ id="desc8">source=resources,resource=tides,path=extremes[1].time,format=local_time</desc></text>
175
216
  <text
176
217
  id="extremes.0.time"
177
- x="29.798925"
178
- y="79.890121"
218
+ x="156.47511"
219
+ y="73.368919"
179
220
  font-size="14"
180
221
  font-family="monospace"
181
222
  fill="red"
182
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:#000000;stroke-width:1.32646"
183
- transform="scale(0.82428216,1.2131768)">03:27<desc
184
- id="desc3">source=resources,resource=tides,path=extremes.[0].time,format=local_time</desc></text>
223
+ 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"
224
+ transform="scale(0.74654856,1.3394976)">03:27<desc
225
+ id="desc3">source=resources,resource=tides,path=extremes[0].time,format=local_time</desc></text>
185
226
  <text
186
227
  id="extremes.0.time-8"
187
- x="378.11157"
188
- y="79.890121"
228
+ x="10.326324"
229
+ y="77.594147"
189
230
  font-size="14"
190
231
  font-family="monospace"
191
232
  fill="red"
192
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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"
233
+ 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"
193
234
  transform="scale(0.82428216,1.2131768)"><desc
194
- id="desc2">source=resources,resource=tides,path=extremes.[0].time,format=day_mon</desc>27 Jun</text>
235
+ id="desc2">source=resources,resource=tides,path=extremes[0].time,format=day_mon</desc>27 Jun</text>
195
236
  <text
196
237
  id="extremes.0.time-8-5"
197
- x="378.11157"
198
- y="125.12273"
238
+ x="10.83737"
239
+ y="126.90092"
199
240
  font-size="14"
200
241
  font-family="monospace"
201
242
  fill="red"
202
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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"
243
+ 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"
203
244
  transform="scale(0.82428216,1.2131768)"><desc
204
- id="desc7">source=resources,resource=tides,path=extremes.[1].time,format=day_mon</desc>27 Jun</text>
245
+ id="desc7">source=resources,resource=tides,path=extremes[1].time,format=day_mon</desc>27 Jun</text>
205
246
  <text
206
247
  id="extremes.0.time-8-9"
207
- x="378.11157"
208
- y="174.46791"
248
+ x="12.934491"
249
+ y="161.07082"
209
250
  font-size="14"
210
251
  font-family="monospace"
211
252
  fill="red"
212
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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"
213
- transform="scale(0.82428216,1.2131768)"><desc
214
- id="desc9">source=resources,resource=tides,path=extremes.[2].time,format=day_mon</desc>27 Jun</text>
253
+ 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"
254
+ transform="scale(0.74623156,1.3400666)"><desc
255
+ id="desc9">source=resources,resource=tides,path=extremes[2].time,format=day_mon</desc>27 Jun</text>
215
256
  <text
216
257
  id="extremes.0.level"
217
- x="173.7571"
218
- y="88.829666"
258
+ x="336.3447"
259
+ y="76.456078"
219
260
  font-size="14"
220
261
  font-family="monospace"
221
262
  fill="red"
222
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:#000000;stroke-width:1.38653"
223
- transform="scale(0.95553786,1.046531)"><tspan
263
+ 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"
264
+ transform="scale(0.7703174,1.2981662)"><tspan
224
265
  sodipodi:role="line"
225
266
  id="tspan2"
226
- x="173.7571"
227
- y="88.829666"
228
- style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;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:1.38653">3.4m</tspan><desc
229
- id="desc4">source=resources,resource=tides,path=extremes.[0].level,category=depth</desc></text>
267
+ x="336.3447"
268
+ y="76.456078"
269
+ 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
270
+ id="desc4">source=resources,resource=tides,path=extremes[0].level,category=depth</desc></text>
271
+ <text
272
+ xml:space="preserve"
273
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:18.6667px;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:#ffff00;stroke:#acacaa;stroke-width:0.612283;stroke-opacity:1"
274
+ x="232.26662"
275
+ y="30.201849"
276
+ id="text19"><tspan
277
+ sodipodi:role="line"
278
+ id="tspan19"
279
+ x="232.26662"
280
+ y="30.201849">-&gt;</tspan></text>
230
281
  </svg>