@signalk/freeboard-sk 2.6.0-rc2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  ### v2.6.0
4
4
 
5
- - **Added**: Ability to show/hide toolbar buttons on both sides of the screen.
5
+ - **Added**: Ability to show / hide toolbar buttons on both sides of the screen.
6
6
  - **Added**: Route builder. 1st release allows creating a route from waypoints.
7
+ - **Added**: Support for displaying S57 vector charts converted using [s57-tiler](https://github.com/wdantuma/s57-tiler).
8
+ - **Updated**: Resources created outside of Freeboard UI (plugins, etc.) are now selected for display on the map. Previously these had to be manually selected from the relevant list.
7
9
  - **Fixed**: Wind speed values in vessel popover showing 0 rather than - when no value is present.
8
10
 
9
11
  ### v2.5.0
package/README.md CHANGED
@@ -32,6 +32,7 @@ Charts are sourced from the `/resources/charts` path on the Signal K server and
32
32
 
33
33
  - Image tiles _(XYZ)_
34
34
  - Vector Tiles _(MVT / PBF)_
35
+ - [S57 ENC's converted to vector tiles](#S57-charts) _(MVT / PBF)_
35
36
  - WMS _(Web Map Server)_
36
37
  - WMTS _(Web Map Tile Server)_
37
38
  - PMTiles _(ProtoMap files)_
@@ -116,6 +117,27 @@ _Note: The `Signal K Instrument Panel` app will be displayed if no user selectio
116
117
 
117
118
  ---
118
119
 
120
+ ### S57 Charts
121
+
122
+ Freeboard-SK is able to display S57 ENC charts that have been converted to vector tiles with [s57-tiler](https://github.com/wdantuma/s57-tiler). _(See the [README](https://github.com/wdantuma/s57-tiler) for instructions how to create the vectortiles from downloaded S57 ENC's.)_
123
+
124
+ See [Open CPN chart sources](https://opencpn.org/OpenCPN/info/chartsource.html) for a list of locations to source charts.
125
+
126
+ _Note: Only unencrypted ENC's are supported (no S63 support)._
127
+
128
+ **_Requires: @signalk/charts-plugin_**
129
+
130
+
131
+ ![S57 chart](https://github.com/SignalK/freeboard-sk/assets/38519157/a93b3889-d1c8-4df7-9f6f-97a1666fbf77)
132
+
133
+ Rendering of the Shallow, safety and deep depths and can be configured in the settings dialog
134
+
135
+ ![S57 Settings](https://github.com/SignalK/freeboard-sk/assets/38519157/0409492b-1ee7-4905-b5b0-e5fc8e68bc9a)
136
+
137
+ _Note: This functionality is not a replacement for official navigational charts_
138
+
139
+ ---
140
+
119
141
  ### Experiments:
120
142
 
121
143
  Features that are not ready for "prime time" are made available as experiments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalk/freeboard-sk",
3
- "version": "2.6.0-rc2",
3
+ "version": "2.6.0",
4
4
  "description": "Openlayers chart plotter implementation for Signal K",
5
5
  "keywords": [
6
6
  "signalk-webapp",
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initAnchorApi = void 0;
4
+ const fetch_1 = require("../lib/fetch");
5
+ let server;
6
+ let hostPath;
7
+ const apiBasePath = '/signalk/v2/api/vessels/self/navigation/anchor';
8
+ const anchorPlugin = {
9
+ has: false,
10
+ enabled: false,
11
+ version: ''
12
+ };
13
+ let pluginPath;
14
+ const msgPluginNotFound = 'signalk-anchor-alarm is not installed!';
15
+ const initAnchorApi = async (app) => {
16
+ server = app;
17
+ server.debug(`** initAnchorApi() **`);
18
+ // detect signalk-anchor-alarm plugin
19
+ anchorPlugin.has = true;
20
+ try {
21
+ let port = 3000;
22
+ if (typeof server.config?.getExternalPort === 'function') {
23
+ server.debug('*** getExternalPort()', server.config.getExternalPort());
24
+ port = server.config.getExternalPort();
25
+ }
26
+ hostPath = `${server.config.ssl ? 'https' : 'http'}://localhost:${port}`;
27
+ // temp patch for detection issue
28
+ pluginPath = `/plugins/anchoralarm`;
29
+ anchorPlugin.enabled = true;
30
+ /*
31
+ const url = `${hostPath}/plugins`;
32
+ const r: Array<{ id: string }> = await fetch(url);
33
+ r.forEach(
34
+ (plugin: { id: string; version: string; data: { enabled: boolean } }) => {
35
+ if (plugin.id === 'anchoralarm') {
36
+ pluginPath = `/plugins/${plugin.id}`;
37
+ anchorPlugin.has = true;
38
+ anchorPlugin.version = plugin.version;
39
+ anchorPlugin.enabled = plugin.data.enabled;
40
+ }
41
+ }
42
+ );*/
43
+ }
44
+ catch (e) {
45
+ anchorPlugin.has = false;
46
+ }
47
+ server.debug('*** Anchor Alarm Plugin detected:', anchorPlugin.has);
48
+ server.debug('*** Anchor Alarm Plugin enabled:', anchorPlugin.enabled);
49
+ server.debug('*** Anchor Alarm Plugin API Path', `${hostPath}${pluginPath}`);
50
+ if (anchorPlugin.has) {
51
+ initApiEndpoints();
52
+ }
53
+ };
54
+ exports.initAnchorApi = initAnchorApi;
55
+ const initApiEndpoints = () => {
56
+ server.debug(`** Registering Anchor API endpoint(s) **`);
57
+ server.post(`${apiBasePath}/drop`, async (req, res) => {
58
+ server.debug(`** POST ${apiBasePath}/drop`);
59
+ if (!anchorPlugin.has) {
60
+ res.status(400).json({
61
+ state: 'COMPLETED',
62
+ statusCode: 400,
63
+ message: msgPluginNotFound
64
+ });
65
+ return;
66
+ }
67
+ try {
68
+ const r = await (0, fetch_1.post)(`${hostPath}${pluginPath}/dropAnchor`, '{}');
69
+ res.status(r.statusCode).json(r);
70
+ }
71
+ catch (e) {
72
+ // fix plugin returned 401 error code when no position is available
73
+ if (e.statusCode === 401 && e.message.indexOf('no position') !== -1) {
74
+ e.statusCode = 400;
75
+ }
76
+ res.status(e.statusCode).json(e);
77
+ }
78
+ });
79
+ server.post(`${apiBasePath}/raise`, async (req, res) => {
80
+ server.debug(`** POST ${apiBasePath}/raise`);
81
+ if (!anchorPlugin.has) {
82
+ res.status(400).json({
83
+ state: 'COMPLETED',
84
+ statusCode: 400,
85
+ message: msgPluginNotFound
86
+ });
87
+ return;
88
+ }
89
+ try {
90
+ const r = await (0, fetch_1.post)(`${hostPath}${pluginPath}/raiseAnchor`, '{}');
91
+ res.status(r.statusCode).json(r);
92
+ }
93
+ catch (e) {
94
+ res.status(e.statusCode).json(e);
95
+ }
96
+ });
97
+ server.post(`${apiBasePath}/radius`, async (req, res) => {
98
+ server.debug(`** POST ${apiBasePath}/radius`);
99
+ if (!anchorPlugin.has) {
100
+ res.status(400).json({
101
+ state: 'COMPLETED',
102
+ statusCode: 400,
103
+ message: msgPluginNotFound
104
+ });
105
+ return;
106
+ }
107
+ try {
108
+ const val = req.body.value && typeof req.body.value === 'number'
109
+ ? { radius: req.body.value }
110
+ : {};
111
+ const r = await (0, fetch_1.post)(`${hostPath}${pluginPath}/setRadius`, JSON.stringify(val));
112
+ res.status(r.statusCode).json(r);
113
+ }
114
+ catch (e) {
115
+ res.status(e.statusCode).json(e);
116
+ }
117
+ });
118
+ };
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ // NOAA
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.NOAA = void 0;
5
+ const fetch_1 = require("./fetch");
6
+ const weather_1 = require("../weather");
7
+ var CARDINAL_POINTS;
8
+ (function (CARDINAL_POINTS) {
9
+ CARDINAL_POINTS[CARDINAL_POINTS["N"] = 0] = "N";
10
+ CARDINAL_POINTS[CARDINAL_POINTS["NNE"] = 22.5] = "NNE";
11
+ CARDINAL_POINTS[CARDINAL_POINTS["NE"] = 45] = "NE";
12
+ CARDINAL_POINTS[CARDINAL_POINTS["ENE"] = 67.5] = "ENE";
13
+ CARDINAL_POINTS[CARDINAL_POINTS["E"] = 90] = "E";
14
+ CARDINAL_POINTS[CARDINAL_POINTS["ESE"] = 112.5] = "ESE";
15
+ CARDINAL_POINTS[CARDINAL_POINTS["SE"] = 135] = "SE";
16
+ CARDINAL_POINTS[CARDINAL_POINTS["SSE"] = 157.5] = "SSE";
17
+ CARDINAL_POINTS[CARDINAL_POINTS["S"] = 180] = "S";
18
+ CARDINAL_POINTS[CARDINAL_POINTS["SSW"] = 202.5] = "SSW";
19
+ CARDINAL_POINTS[CARDINAL_POINTS["SW"] = 225] = "SW";
20
+ CARDINAL_POINTS[CARDINAL_POINTS["WSW"] = 247.5] = "WSW";
21
+ CARDINAL_POINTS[CARDINAL_POINTS["W"] = 270] = "W";
22
+ CARDINAL_POINTS[CARDINAL_POINTS["WNW"] = 292.5] = "WNW";
23
+ CARDINAL_POINTS[CARDINAL_POINTS["NW"] = 315] = "NW";
24
+ CARDINAL_POINTS[CARDINAL_POINTS["NNW"] = 337.5] = "NNW";
25
+ })(CARDINAL_POINTS || (CARDINAL_POINTS = {}));
26
+ class NOAA {
27
+ settings;
28
+ constructor(config) {
29
+ this.settings = config;
30
+ }
31
+ getUrl(position) {
32
+ const api = 'https://api.weather.gov/points';
33
+ if (!this.settings.apiKey || !position) {
34
+ return '';
35
+ }
36
+ else {
37
+ return `${api}/${position.latitude.toFixed(4)},${position.longitude.toFixed(4)}`;
38
+ }
39
+ }
40
+ fetchData = async (position) => {
41
+ const url = this.getUrl(position);
42
+ //console.log(`url`, url)
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ const response = await (0, fetch_1.fetch)(url);
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ let forecasts = [];
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ let observations = [];
49
+ // observations
50
+ if (response?.properties?.observationStations) {
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ const stations = await (0, fetch_1.fetch)(response.properties.observationStations);
53
+ observations = await (0, fetch_1.fetch)(`${stations.features[0].id}/observations/latest`);
54
+ //console.log(`observations`, observations)
55
+ }
56
+ // forecasts
57
+ if (response?.properties?.forecastHourly) {
58
+ forecasts = await (0, fetch_1.fetch)(response.properties.forecastHourly);
59
+ //console.log(`forecasts`, forecasts)
60
+ }
61
+ // warnings
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ const warnings = await (0, fetch_1.fetch)(`https://api.weather.gov/alerts/active?point=${position.latitude.toFixed(4)},${position.longitude.toFixed(4)}`);
64
+ //console.log(`warnings`, warnings)
65
+ return this.parseResponse({
66
+ position: position,
67
+ forecasts: forecasts?.properties?.periods ?? [],
68
+ observations: observations?.properties ?? null,
69
+ warnings: warnings?.features ?? []
70
+ });
71
+ };
72
+ parseResponse = (wData) => {
73
+ const res = {};
74
+ res[weather_1.defaultStationId] = {
75
+ name: 'Weather data relative to supplied position.',
76
+ position: {
77
+ latitude: wData.latitude,
78
+ longitude: wData.longitude
79
+ },
80
+ observations: this.parseNoaaObservations(wData.observations),
81
+ forecasts: this.parseNoaaForecasts(wData.forecasts),
82
+ warnings: this.parseNoaaWarnings(wData.warnings)
83
+ };
84
+ return res;
85
+ };
86
+ parseNoaaObservations(wData) {
87
+ const data = [];
88
+ let obs;
89
+ let v;
90
+ if (wData) {
91
+ obs = {
92
+ timestamp: wData.timestamp ?? null,
93
+ description: wData.textDescription ?? null
94
+ };
95
+ v = wData.visibility.value
96
+ ? wData.visibility.unitCode === 'wmoUnit:m'
97
+ ? wData.visibility.value
98
+ : wData.visibility.value
99
+ : null;
100
+ obs.outside.horizontalVisibility = v;
101
+ v = wData.temperature.value
102
+ ? wData.temperature.unitCode === 'wmoUnit:degC'
103
+ ? wData.temperature.value + 273.15
104
+ : wData.temperature.value
105
+ : null;
106
+ obs.outside.temperature = v;
107
+ v = wData.dewpoint.value
108
+ ? wData.dewpoint.unitCode === 'wmoUnit:degC'
109
+ ? wData.dewpoint.value + 273.15
110
+ : wData.dewpoint.value
111
+ : null;
112
+ obs.outside.dewPointTemperature = v;
113
+ v = wData.seaLevelPressure.value
114
+ ? wData.seaLevelPressure.unitCode === 'wmoUnit:Pa'
115
+ ? wData.seaLevelPressure.value
116
+ : wData.seaLevelPressure.value
117
+ : null;
118
+ obs.outside.pressure = v;
119
+ v = wData.relativeHumidity.value
120
+ ? wData.relativeHumidity.unitCode === 'wmoUnit:percent'
121
+ ? wData.relativeHumidity.value / 100
122
+ : wData.relativeHumidity.value
123
+ : null;
124
+ obs.outside.relativeHumidity = v;
125
+ v = wData.windSpeed.value
126
+ ? wData.windSpeed.unitCode === 'wmoUnit:km_h-1'
127
+ ? wData.windSpeed.value / 3.6
128
+ : wData.windSpeed.value
129
+ : null;
130
+ obs.wind.speedTrue = v;
131
+ v = wData.windDirection.value
132
+ ? wData.windDirection.unitCode === 'wmoUnit:degree_(angle)'
133
+ ? wData.windDirection.value * (Math.PI / 180)
134
+ : wData.windDirection.value
135
+ : null;
136
+ obs.wind.directionTrue = v;
137
+ v = wData.windGust.value
138
+ ? wData.windGust.unitCode === 'wmoUnit:km_h-1'
139
+ ? wData.windGust.value / 3.6
140
+ : wData.windGust.value
141
+ : null;
142
+ obs.wind.gust = v;
143
+ data.push(obs);
144
+ }
145
+ return data;
146
+ }
147
+ parseNoaaForecasts(forecasts) {
148
+ const data = [];
149
+ if (forecasts && Array.isArray(forecasts)) {
150
+ forecasts.forEach((f) => {
151
+ const forecast = {
152
+ timestamp: f.startTime ?? null,
153
+ description: f.shortForecast ?? null
154
+ };
155
+ forecast.outside.temperature =
156
+ typeof f.temperature !== 'undefined'
157
+ ? ((f.temperature - 32) * 5) / 9 + 273.15
158
+ : null;
159
+ forecast.wind.speedTrue =
160
+ parseInt(f.windSpeed.split(' ')[0]) / 2.237 ?? null;
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ const wd = f.windDirection
163
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
164
+ CARDINAL_POINTS[f.windDirection]
165
+ : 0;
166
+ forecast.wind.directionTrue = f.windDirection
167
+ ? (Math.PI / 180) * wd
168
+ : null;
169
+ data.push(forecast);
170
+ });
171
+ }
172
+ return data;
173
+ }
174
+ parseNoaaWarnings(alerts) {
175
+ const data = [];
176
+ if (alerts && Array.isArray(alerts)) {
177
+ alerts.forEach((alert) => {
178
+ const warn = {
179
+ startTime: alert?.properties?.effective ?? null,
180
+ endTime: alert?.properties?.ends ?? null,
181
+ details: alert?.properties?.description ?? null,
182
+ source: alert?.properties?.senderName ?? null,
183
+ type: alert?.properties?.messageType ?? null
184
+ };
185
+ data.push(warn);
186
+ });
187
+ }
188
+ return data;
189
+ }
190
+ }
191
+ exports.NOAA = NOAA;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ // OpenWeather
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.OpenWeather = void 0;
5
+ const fetch_1 = require("./fetch");
6
+ const weather_1 = require("../weather");
7
+ class OpenWeather {
8
+ settings;
9
+ constructor(config) {
10
+ this.settings = config;
11
+ }
12
+ getUrl(position) {
13
+ const api = 'https://api.openweathermap.org/data/2.5/onecall';
14
+ if (!this.settings.apiKey || !position) {
15
+ return '';
16
+ }
17
+ else {
18
+ return `${api}?lat=${position.latitude}&lon=${position.longitude}&exclude=minutely,daily&appid=${this.settings.apiKey}`;
19
+ }
20
+ }
21
+ fetchData = async (position) => {
22
+ const url = this.getUrl(position);
23
+ const response = await (0, fetch_1.fetch)(url);
24
+ return this.parseResponse(response);
25
+ };
26
+ parseResponse = (owData) => {
27
+ const res = {};
28
+ res[weather_1.defaultStationId] = {
29
+ id: weather_1.defaultStationId,
30
+ name: 'Weather data relative to supplied position.',
31
+ position: {
32
+ latitude: owData.lat,
33
+ longitude: owData.lon
34
+ },
35
+ observations: this.parseOWObservations(owData),
36
+ forecasts: this.parseOWForecasts(owData),
37
+ warnings: this.parseOWWarnings(owData)
38
+ };
39
+ return res;
40
+ };
41
+ parseOWObservations(owData) {
42
+ //server.debug(JSON.stringify(weatherData.current))
43
+ const data = [];
44
+ let obs;
45
+ if (owData && owData.current) {
46
+ const current = owData.current;
47
+ obs = {
48
+ date: current.dt
49
+ ? new Date(current.dt * 1000).toISOString()
50
+ : new Date().toISOString(),
51
+ description: current.weather[0].description ?? '',
52
+ sun: {
53
+ sunrise: new Date(current.sunrise * 1000).toISOString() ?? null,
54
+ sunset: new Date(current.sunset * 1000).toISOString() ?? null
55
+ },
56
+ horizontalVisibility: current.visibility ?? null,
57
+ outside: {
58
+ uvIndex: current.uvi ?? null,
59
+ cloudCover: current.clouds ?? null,
60
+ temperature: current.temp ?? null,
61
+ feelsLikeTemperature: current.feels_like ?? null,
62
+ dewPointTemperature: current.dew_point ?? null,
63
+ pressure: current.pressure ? current.pressure * 100 : null,
64
+ absoluteHumidity: current.humidity ?? null,
65
+ precipitationType: current.rain && typeof current.rain['1h'] !== 'undefined'
66
+ ? 'rain'
67
+ : current.snow && typeof current.snow['1h'] !== 'undefined'
68
+ ? 'snow'
69
+ : null,
70
+ precipitationVolume: current.rain && typeof current.rain['1h'] !== 'undefined'
71
+ ? current.rain['1h']
72
+ : current.snow && typeof current.snow['1h'] !== 'undefined'
73
+ ? current.snow['1h']
74
+ : null
75
+ },
76
+ water: {},
77
+ wind: {
78
+ speedTrue: current.wind_speed ?? null,
79
+ directionTrue: typeof current.wind_deg !== 'undefined'
80
+ ? (Math.PI / 180) * current.wind_deg
81
+ : null
82
+ }
83
+ };
84
+ data.push(obs);
85
+ }
86
+ return data;
87
+ }
88
+ parseOWForecasts(owData, period = 'hourly') {
89
+ //server.debug(JSON.stringify(owData[period]))
90
+ const data = [];
91
+ if (owData && owData[period] && Array.isArray(owData[period])) {
92
+ const forecasts = owData[period];
93
+ forecasts.forEach((f) => {
94
+ const forecast = {
95
+ date: f.dt
96
+ ? new Date(f.dt * 1000).toISOString()
97
+ : new Date().toISOString(),
98
+ description: f.weather[0].description ?? '',
99
+ sun: {},
100
+ outside: {},
101
+ water: {},
102
+ wind: {}
103
+ };
104
+ if (period === 'daily') {
105
+ forecast.sun.sunrise =
106
+ new Date(f.sunrise * 1000).toISOString() ?? null;
107
+ forecast.sun.sunset = new Date(f.sunset * 1000).toISOString() ?? null;
108
+ forecast.outside.minTemperature = f.temp.min ?? null;
109
+ forecast.outside.maxTemperature = f.temp.max ?? null;
110
+ forecast.outside.feelsLikeTemperature = f.feels_like.day ?? null;
111
+ }
112
+ else {
113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ forecast.outside.feelsLikeTemperature = f.feels_like ?? null;
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ forecast.outside.temperature = f.temp ?? null;
117
+ }
118
+ forecast.outside.dewPointTemperature = f.dew_point ?? null;
119
+ forecast.outside.uvIndex = f.uvi ?? null;
120
+ forecast.outside.cloudCover = f.clouds ?? null;
121
+ forecast.outside.pressure =
122
+ typeof f.pressure !== 'undefined' ? f.pressure * 100 : null;
123
+ forecast.outside.absoluteHumidity = f.humidity ?? null;
124
+ forecast.wind.speedTrue = f.wind_speed ?? null;
125
+ forecast.wind.directionTrue =
126
+ typeof f.wind_deg !== 'undefined'
127
+ ? (Math.PI / 180) * f.wind_deg
128
+ : null;
129
+ forecast.wind.gust = f.wind_gust ?? null;
130
+ if (f.rain && typeof f.rain['1h'] !== 'undefined') {
131
+ forecast.outside.precipitationType = 'rain';
132
+ forecast.outside.precipitationVolume = f.rain['1h'] ?? null;
133
+ }
134
+ else if (f.snow && typeof f.snow['1h'] !== 'undefined') {
135
+ forecast.outside.precipitationType = 'snow';
136
+ forecast.outside.precipitationVolume = f.snow['1h'] ?? null;
137
+ }
138
+ data.push(forecast);
139
+ });
140
+ }
141
+ return data;
142
+ }
143
+ parseOWWarnings(owData) {
144
+ //server.debug(JSON.stringify(weatherData.alerts))
145
+ const data = [];
146
+ if (owData && owData.alerts) {
147
+ const alerts = owData.alerts;
148
+ alerts.forEach((alert) => {
149
+ const warn = {
150
+ startTime: alert.start
151
+ ? new Date(alert.start * 1000).toISOString()
152
+ : null,
153
+ endTime: alert.end
154
+ ? new Date(alert.start * 1000).toISOString()
155
+ : null,
156
+ details: alert.description ?? null,
157
+ source: alert.sender_name ?? null,
158
+ type: alert.event ?? null
159
+ };
160
+ data.push(warn);
161
+ });
162
+ }
163
+ return data;
164
+ }
165
+ }
166
+ exports.OpenWeather = OpenWeather;
File without changes