@signalk/charts-plugin 2.3.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +80 -17
  2. package/package.json +27 -8
  3. package/plugin/charts.d.ts +1 -0
  4. package/plugin/charts.js +243 -0
  5. package/plugin/constants.d.ts +4 -0
  6. package/plugin/constants.js +7 -0
  7. package/plugin/index.d.ts +1 -0
  8. package/plugin/index.js +353 -0
  9. package/plugin/types.d.ts +38 -0
  10. package/plugin/types.js +2 -0
  11. package/.github/workflows/publish.yml +0 -18
  12. package/.github/workflows/release_on_tag.yml +0 -27
  13. package/.github/workflows/require_pr_label.yml +0 -13
  14. package/.github/workflows/test.yml +0 -22
  15. package/index.js +0 -1
  16. package/src/charts.js +0 -175
  17. package/src/constants.js +0 -3
  18. package/src/plugin.js +0 -231
  19. package/test/charts/empty-test/.gitkeep +0 -0
  20. package/test/charts/test.mbtiles +0 -0
  21. package/test/charts/tms-tiles/4/8/10.png +0 -0
  22. package/test/charts/tms-tiles/4/8/11.png +0 -0
  23. package/test/charts/tms-tiles/4/9/10.png +0 -0
  24. package/test/charts/tms-tiles/4/9/11.png +0 -0
  25. package/test/charts/tms-tiles/5/16/21.png +0 -0
  26. package/test/charts/tms-tiles/5/16/22.png +0 -0
  27. package/test/charts/tms-tiles/5/16/23.png +0 -0
  28. package/test/charts/tms-tiles/5/17/21.png +0 -0
  29. package/test/charts/tms-tiles/5/17/22.png +0 -0
  30. package/test/charts/tms-tiles/5/17/23.png +0 -0
  31. package/test/charts/tms-tiles/5/18/21.png +0 -0
  32. package/test/charts/tms-tiles/5/18/22.png +0 -0
  33. package/test/charts/tms-tiles/5/18/23.png +0 -0
  34. package/test/charts/tms-tiles/tilemapresource.xml +0 -14
  35. package/test/charts/unpacked-tiles/3/2/3.png +0 -0
  36. package/test/charts/unpacked-tiles/4/4/6.png +0 -0
  37. package/test/charts/unpacked-tiles/4/5/6.png +0 -0
  38. package/test/charts/unpacked-tiles/metadata.json +0 -15
  39. package/test/charts-2/test2.mbtiles +0 -0
  40. package/test/expected-charts.json +0 -53
  41. package/test/plugin-test.js +0 -196
package/README.md CHANGED
@@ -1,49 +1,112 @@
1
- # Signal K Node server Charts plugin
1
+ # Signal K server Charts plugin
2
2
 
3
3
  Signal K Node server plugin to provide chart metadata, such as name, description and location of the actual chart tile data.
4
4
 
5
- ### Usage
5
+ Chart metadata is derived from the following supported chart file types:
6
+ - MBTiles _(.mbtiles)_
7
+ - TMS _(tilemapresource.xml and tiles)_
6
8
 
7
- 1. Install "Signal K Charts" plugin from Signal K Appstore
9
+ Additionally, chart metadata can be entered via the plugin configuration for other chart sources and types _(e.g. WMS, WMTS, S-57 tiles and tilejson)_.
8
10
 
9
- 2. Add "Chart paths" in plugin configuration. Defaults to `${signalk-configuration-path}/charts`
11
+ Chart metadata is made available to both v1 and v2 Signal K `resources` api paths.
10
12
 
11
- <img src="https://user-images.githubusercontent.com/1435910/39382493-57c1e4dc-4a6e-11e8-93e1-cedb4c7662f4.png" alt="Chart paths configuration" width="450"/>
13
+ | Server Version | API | Path |
14
+ |--- |--- |--- |
15
+ | 1.x.x | v1 | `/signalk/v1/api/resources/charts` |
16
+ | 2.x.x | v2 | `/signalk/v2/api/resources/charts` |
12
17
 
13
- 3. Put charts into selected paths
18
+
19
+ _Note: Version 2 resource paths will only be made available on Signal K server v2.0.0 and later_
14
20
 
15
- 4. Add optional online chart providers
21
+ ## Usage
16
22
 
17
- <img src="https://user-images.githubusercontent.com/1435910/45048136-c65d2e80-b083-11e8-99db-01e8cece9f89.png" alt="Online chart providers configuration" width="450"/>
23
+ 1. Install `@signalk/signalk-charts` from the Signal K Server Appstore
18
24
 
19
- _WMS example:_
20
- ![image](https://user-images.githubusercontent.com/38519157/102832518-90077100-443e-11eb-9a1d-d0806bb2b10b.png)
25
+ 2. Configure the plugin in the Admin UI _(**Server -> Plugin Config -> Signal K Charts**)_
21
26
 
22
- 5. Activate plugin
27
+ 3. Activate the plugin
23
28
 
24
- 6. Use one of the client apps supporting Signal K charts, for example:
29
+ Chart metadata will then be available to client apps via the resources api `/resources/charts` for example:
25
30
  - [Freeboard SK](https://www.npmjs.com/package/@signalk/freeboard-sk)
26
31
  - [Tuktuk Chart Plotter](https://www.npmjs.com/package/tuktuk-chart-plotter)
27
32
 
33
+
34
+ ## Configuration
35
+
36
+
37
+ ### Local Chart Files
38
+
39
+ To use chart files stored on the Signal K Server the plugin needs to know where your local chart files
40
+ are stored to generate the chart metadata.
41
+
42
+ You can either:
43
+ 1. Put the chart files in the default location _(`/home/<user>/.signalk/charts`)_
44
+ 2. Add configuration entries for the folders where the chart files are stored.
45
+
46
+ <img src="https://user-images.githubusercontent.com/1435910/39382493-57c1e4dc-4a6e-11e8-93e1-cedb4c7662f4.png" alt="Chart paths configuration" width="450"/>
47
+
48
+ >**Note:** After chart files have been added to folders they will be processed after the plugin has been restarted! _(disable / enable the plugin)_
49
+
50
+
51
+ ### Online chart providers
52
+
53
+ If your chart source is not local to the Signal K Server you can add "Online Chart Providers" and enter the required charts metadata for the source.
54
+
55
+ You will need to provide the following information:
56
+ 1. A chart name for client applications to display
57
+ 2. The URL to the chart source
58
+ 3. Select the chart image format
59
+ 4. The minimum and maximum zoom levels where chart data is available.
60
+
61
+ You can also provide a description detailing the chart content.
62
+
63
+ <img src="https://github.com/user-attachments/assets/77cb3aaf-5471-4e55-b05d-aad70cacab6a" alt="Online chart providers configuration" width="450"/>
64
+
65
+ For WMS & WMTS sources you can specify the layers you wish to display.
66
+
67
+ <img src="https://github.com/user-attachments/assets/b9bfba38-8468-4eca-aeb3-96a80fcbc7a6" alt="Online chart provider layers" width="450"/>
68
+
69
+
28
70
  ### Supported chart formats
29
71
 
30
- - [MBTiles](https://github.com/mapbox/mbtiles-spec) file
72
+ - [MBTiles](https://github.com/mapbox/mbtiles-spec) files
31
73
  - Directory with cached [TMS](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification) tiles and `tilemapresource.xml`
32
74
  - Directory with XYZ tiles and `metadata.json`
33
75
  - Online [TMS](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification)
34
76
 
35
77
  Publicly available MBTiles charts can be found from:
36
- - [NOAA Nautical charts](http://tileservice.charts.noaa.gov/), quick download [cheat-sheet](https://github.com/vokkim/noaa-nautical-charts)
78
+ - [NOAA Nautical charts](https://distribution.charts.noaa.gov/ncds/index.html)
37
79
  - [Finnish Transport Agency nautical charts](https://github.com/vokkim/rannikkokartat-mbtiles)
38
80
  - [Signal K World Coastline Map](https://github.com/netAction/signalk-world-coastline-map), download [MBTiles release](https://github.com/netAction/signalk-world-coastline-map/releases/download/v1.0/signalk-world-coastline-map-database.tgz)
39
81
 
82
+
83
+ ---
84
+
40
85
  ### API
41
86
 
42
87
  Plugin adds support for `/resources/charts` endpoints described in [Signal K specification](http://signalk.org/specification/1.0.0/doc/otherBranches.html#resourcescharts):
43
88
 
44
- - `GET /signalk/v1/api/resources/charts/` returns metadata for all available charts
45
- - `GET /signalk/v1/api/resources/charts/${identifier}/` returns metadata for selected chart
46
- - `GET /signalk/v1/api/resources/charts/${identifier}/${z}/${x}/${y}` returns a single tile for selected offline chart. As charts-plugin isn't proxy, online charts is not available via this request. You should look the metadata to find proper request.
89
+ #### List available charts
90
+
91
+ ```bash
92
+ GET /signalk/v2/api/resources/charts/`
93
+ ```
94
+
95
+ #### Return metadata for selected chart
96
+
97
+ ```bash
98
+
99
+ GET /signalk/v2/api/resources/charts/${identifier}`
100
+ ```
101
+
102
+ #### Chart Tiles
103
+ Chart tiles are retrieved using the url defined in the chart metadata.
104
+
105
+ For local chart files located in the Chart Path(s) defined in the plugin configuration, the url will be:
106
+
107
+ ```bash
108
+ /signalk/chart-tiles/${identifier}/${z}/${x}/${y}
109
+ ```
47
110
 
48
111
  License
49
112
  -------
package/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "@signalk/charts-plugin",
3
- "version": "2.3.1",
3
+ "version": "3.1.0",
4
4
  "description": "Signal K plugin to provide chart support for Signal K server",
5
- "main": "index.js",
5
+ "main": "plugin/index.js",
6
6
  "scripts": {
7
+ "build": "tsc",
8
+ "build-declaration": "tsc --declaration --allowJs false",
9
+ "watch": "npm run build -- -w",
10
+ "start": "npm run build -- -w",
11
+ "prepare": "tsc",
12
+ "prettier": "prettier -w src/",
13
+ "lint": "eslint -c .eslintrc.js --ext .ts --ext .js --fix src/",
14
+ "format": "npm run prettier && npm run lint",
15
+ "ci-lint": "eslint -c .eslintrc.js --ext .ts --ext .js src/ && prettier --check src/",
7
16
  "test": "mocha",
8
- "deploy": "npm test && git tag -a v$npm_package_version -m 'Release v$npm_package_version'; git push --tags; npm publish --access public --scope @signalk ."
17
+ "deploy": "npm build && npm test && git tag -a v$npm_package_version -m 'Release v$npm_package_version'; git push --tags; npm publish --access public --scope @signalk ."
9
18
  },
10
19
  "license": "Apache-2.0",
11
20
  "keywords": [
@@ -22,20 +31,30 @@
22
31
  },
23
32
  "dependencies": {
24
33
  "@mapbox/mbtiles": "^0.12.1",
25
- "baconjs": "1.0.1",
34
+ "@signalk/server-api": "^2.0.0",
26
35
  "bluebird": "3.5.1",
27
36
  "lodash": "^4.17.11",
28
- "xml2js": "0.4.19"
37
+ "xml2js": "0.6.2"
29
38
  },
30
39
  "repository": {
31
40
  "type": "git",
32
41
  "url": "https://github.com/SignalK/charts-plugin"
33
42
  },
34
43
  "devDependencies": {
35
- "body-parser": "1.18.2",
44
+ "@types/baconjs": "^0.7.34",
45
+ "@types/express": "^4.17.17",
46
+ "@types/lodash": "^4.14.191",
47
+ "@types/node": "^18.14.4",
48
+ "@typescript-eslint/eslint-plugin": "^5.52.0",
49
+ "@typescript-eslint/parser": "^5.52.0",
50
+ "body-parser": "^1.18.2",
36
51
  "chai": "4.1.2",
37
52
  "chai-http": "^4.2.1",
38
- "express": "4.16.2",
39
- "mocha": "5.0.0"
53
+ "eslint": "^8.34.0",
54
+ "eslint-config-prettier": "^8.6.0",
55
+ "express": "^4.19.2",
56
+ "mocha": "^11.7.1",
57
+ "prettier": "^2.8.4",
58
+ "typescript": "^4.5.4"
40
59
  }
41
60
  }
@@ -0,0 +1 @@
1
+ export declare function findCharts(chartBaseDir: string): Promise<any>;
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.findCharts = void 0;
30
+ const bluebird = __importStar(require("bluebird"));
31
+ const path_1 = __importDefault(require("path"));
32
+ const mbtiles_1 = __importDefault(require("@mapbox/mbtiles"));
33
+ const xml2js = __importStar(require("xml2js"));
34
+ const fs_1 = require("fs");
35
+ const _ = __importStar(require("lodash"));
36
+ function findCharts(chartBaseDir) {
37
+ return fs_1.promises
38
+ .readdir(chartBaseDir, { withFileTypes: true })
39
+ .then((files) => {
40
+ return bluebird.mapSeries(files, (file) => {
41
+ const isMbtilesFile = file.name.match(/\.mbtiles$/i);
42
+ const filePath = path_1.default.resolve(chartBaseDir, file.name);
43
+ const isDirectory = file.isDirectory();
44
+ if (isMbtilesFile) {
45
+ return openMbtilesFile(filePath, file.name);
46
+ }
47
+ else if (isDirectory) {
48
+ return directoryToMapInfo(filePath, file.name);
49
+ }
50
+ else {
51
+ return Promise.resolve(null);
52
+ }
53
+ });
54
+ })
55
+ .then((result) => _.filter(result, _.identity))
56
+ .then((charts) => _.reduce(charts,
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ (result, chart) => {
59
+ result[chart.identifier] = chart;
60
+ return result;
61
+ }, {}))
62
+ .catch((err) => {
63
+ console.error(`Error reading charts directory ${chartBaseDir}:${err.message}`);
64
+ });
65
+ }
66
+ exports.findCharts = findCharts;
67
+ function openMbtilesFile(file, filename) {
68
+ return (new Promise((resolve, reject) => {
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ new mbtiles_1.default(file, (err, mbtiles) => {
71
+ if (err) {
72
+ return reject(err);
73
+ }
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ mbtiles.getInfo((err, metadata) => {
76
+ if (err) {
77
+ return reject(err);
78
+ }
79
+ return resolve({ mbtiles, metadata });
80
+ });
81
+ });
82
+ })
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ .then((res) => {
85
+ if (_.isEmpty(res.metadata) || res.metadata.bounds === undefined) {
86
+ return null;
87
+ }
88
+ const identifier = filename.replace(/\.mbtiles$/i, '');
89
+ const data = {
90
+ _fileFormat: 'mbtiles',
91
+ _filePath: file,
92
+ _mbtilesHandle: res.mbtiles,
93
+ _flipY: false,
94
+ identifier,
95
+ name: res.metadata.name || res.metadata.id,
96
+ description: res.metadata.description,
97
+ bounds: res.metadata.bounds,
98
+ minzoom: res.metadata.minzoom,
99
+ maxzoom: res.metadata.maxzoom,
100
+ format: res.metadata.format,
101
+ type: 'tilelayer',
102
+ scale: parseInt(res.metadata.scale) || 250000,
103
+ v1: {
104
+ tilemapUrl: `~tilePath~/${identifier}/{z}/{x}/{y}`,
105
+ chartLayers: res.metadata.vector_layers
106
+ ? parseVectorLayers(res.metadata.vector_layers)
107
+ : []
108
+ },
109
+ v2: {
110
+ url: `~tilePath~/${identifier}/{z}/{x}/{y}`,
111
+ layers: res.metadata.vector_layers
112
+ ? parseVectorLayers(res.metadata.vector_layers)
113
+ : []
114
+ }
115
+ };
116
+ return data;
117
+ })
118
+ .catch((e) => {
119
+ console.error(`Error loading chart ${file}`, e.message);
120
+ return null;
121
+ }));
122
+ }
123
+ function parseVectorLayers(layers) {
124
+ return layers.map((l) => l.id);
125
+ }
126
+ function directoryToMapInfo(file, identifier) {
127
+ async function loadInfo() {
128
+ const tilemapResource = path_1.default.join(file, 'tilemapresource.xml');
129
+ const metadataJson = path_1.default.join(file, 'metadata.json');
130
+ try {
131
+ await fs_1.promises.stat(tilemapResource);
132
+ return parseTilemapResource(tilemapResource);
133
+ }
134
+ catch {
135
+ try {
136
+ await fs_1.promises.stat(metadataJson);
137
+ return parseMetadataJson(metadataJson);
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ }
144
+ return loadInfo()
145
+ .then((info) => {
146
+ if (info) {
147
+ if (!info.format) {
148
+ console.error(`Missing format metadata for chart ${identifier}`);
149
+ return null;
150
+ }
151
+ info.identifier = identifier;
152
+ (info._fileFormat = 'directory'),
153
+ (info._filePath = file),
154
+ (info.v1 = {
155
+ tilemapUrl: `~tilePath~/${identifier}/{z}/{x}/{y}`,
156
+ chartLayers: []
157
+ });
158
+ info.v2 = {
159
+ url: `~tilePath~/${identifier}/{z}/{x}/{y}`,
160
+ layers: []
161
+ };
162
+ return info;
163
+ }
164
+ return null;
165
+ })
166
+ .catch((e) => {
167
+ console.error(`Error getting charts from ${file}`, e.message);
168
+ return undefined;
169
+ });
170
+ }
171
+ function parseTilemapResource(tilemapResource) {
172
+ return (fs_1.promises
173
+ .readFile(tilemapResource)
174
+ .then(bluebird.promisify(xml2js.parseString))
175
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
+ .then((parsed) => {
177
+ const result = parsed.TileMap;
178
+ const name = _.get(result, 'Title.0');
179
+ const format = _.get(result, 'TileFormat.0.$.extension');
180
+ const scale = _.get(result, 'Metadata.0.$.scale');
181
+ const bbox = _.get(result, 'BoundingBox.0.$');
182
+ const zoomLevels = _.map(_.get(result, 'TileSets.0.TileSet') || [], (set) => parseInt(_.get(set, '$.href')));
183
+ const res = {
184
+ _flipY: true,
185
+ name,
186
+ description: name,
187
+ bounds: bbox
188
+ ? [
189
+ parseFloat(bbox.minx),
190
+ parseFloat(bbox.miny),
191
+ parseFloat(bbox.maxx),
192
+ parseFloat(bbox.maxy)
193
+ ]
194
+ : undefined,
195
+ minzoom: !_.isEmpty(zoomLevels) ? _.min(zoomLevels) : undefined,
196
+ maxzoom: !_.isEmpty(zoomLevels) ? _.max(zoomLevels) : undefined,
197
+ format,
198
+ type: 'tilelayer',
199
+ scale: parseInt(scale) || 250000,
200
+ identifier: '',
201
+ _filePath: ''
202
+ };
203
+ return res;
204
+ }));
205
+ }
206
+ function parseMetadataJson(metadataJson) {
207
+ return fs_1.promises
208
+ .readFile(metadataJson, { encoding: 'utf8' })
209
+ .then((txt) => {
210
+ return JSON.parse(txt);
211
+ })
212
+ .then((metadata) => {
213
+ function parseBounds(bounds) {
214
+ if (_.isString(bounds)) {
215
+ return _.map(bounds.split(','), (bound) => parseFloat(_.trim(bound)));
216
+ }
217
+ else if (_.isArray(bounds) && bounds.length === 4) {
218
+ return bounds;
219
+ }
220
+ else {
221
+ return undefined;
222
+ }
223
+ }
224
+ const res = {
225
+ _flipY: false,
226
+ name: metadata.name || metadata.id,
227
+ description: metadata.description || '',
228
+ bounds: parseBounds(metadata.bounds),
229
+ minzoom: parseIntIfNotUndefined(metadata.minzoom),
230
+ maxzoom: parseIntIfNotUndefined(metadata.maxzoom),
231
+ format: metadata.format,
232
+ type: metadata.type,
233
+ scale: parseInt(metadata.scale) || 250000,
234
+ identifier: '',
235
+ _filePath: ''
236
+ };
237
+ return res;
238
+ });
239
+ }
240
+ function parseIntIfNotUndefined(val) {
241
+ const parsed = parseInt(val);
242
+ return _.isFinite(parsed) ? parsed : undefined;
243
+ }
@@ -0,0 +1,4 @@
1
+ export declare const apiRoutePrefix: {
2
+ 1: string;
3
+ 2: string;
4
+ };
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiRoutePrefix = void 0;
4
+ exports.apiRoutePrefix = {
5
+ 1: '/signalk/v1/api/resources',
6
+ 2: '/signalk/v2/api/resources'
7
+ };
@@ -0,0 +1 @@
1
+ export {};