@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/src/plugin.js DELETED
@@ -1,231 +0,0 @@
1
- const Promise = require('bluebird')
2
- const _ = require('lodash')
3
- const path = require('path')
4
- const fs = require('fs')
5
- const Charts = require('./charts')
6
- const {apiRoutePrefix} = require('./constants')
7
-
8
- const MIN_ZOOM = 1
9
- const MAX_ZOOM = 19
10
-
11
- module.exports = function(app) {
12
- let chartProviders = []
13
- let pluginStarted = false
14
- const configBasePath = app.config.configPath
15
- const defaultChartsPath = path.join(configBasePath, "/charts")
16
- ensureDirectoryExists(defaultChartsPath)
17
-
18
- function start(props) {
19
- const chartPaths = _.isEmpty(props.chartPaths)
20
- ? [defaultChartsPath]
21
- : resolveUniqueChartPaths(props.chartPaths, configBasePath)
22
- const onlineProviders = _.reduce(props.onlineChartProviders, (result, data) => {
23
- const provider = convertOnlineProviderConfig(data)
24
- result[provider.identifier] = provider
25
- return result
26
- }, {})
27
- app.debug(`Start charts plugin. Chart paths: ${chartPaths.join(', ')}, online charts: ${onlineProviders.length}`)
28
-
29
- const loadProviders = Promise.mapSeries(chartPaths, chartPath => Charts.findCharts(chartPath))
30
- .then(list => _.reduce(list, (result, charts) => _.merge({}, result, charts), {}))
31
- return loadProviders.then(charts => {
32
- app.debug(`Chart plugin: Found ${_.keys(charts).length} charts from ${chartPaths.join(', ')}`)
33
- chartProviders = _.merge({}, charts, onlineProviders)
34
- // Do not register routes if plugin has been started once already
35
- pluginStarted === false && registerRoutes()
36
- pluginStarted = true
37
- }).catch(e => {
38
- console.error(`Error loading chart providers`, e.message)
39
- chartProviders = {}
40
- })
41
- }
42
-
43
- function stop() {
44
- }
45
-
46
- function registerRoutes() {
47
- app.get(apiRoutePrefix + '/charts/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)', (req, res) => {
48
- const { identifier, z, x, y } = req.params
49
- const provider = chartProviders[identifier]
50
- if (!provider) {
51
- res.sendStatus(404)
52
- return
53
- }
54
- switch (provider._fileFormat) {
55
- case 'directory':
56
- return serveTileFromFilesystem(res, provider, z, x, y)
57
- case 'mbtiles':
58
- return serveTileFromMbtiles(res, provider, z, x, y)
59
- default:
60
- console.error(`Unknown chart provider fileformat ${provider._fileFormat}`)
61
- res.status(500).send()
62
- }
63
- })
64
-
65
- app.get(apiRoutePrefix + "/charts/:identifier", (req, res) => {
66
- const { identifier } = req.params
67
- const provider = chartProviders[identifier]
68
- if (provider) {
69
- return res.json(sanitizeProvider(provider))
70
- } else {
71
- return res.status(404).send('Not found')
72
- }
73
- })
74
-
75
- app.get(apiRoutePrefix + "/charts", (req, res) => {
76
- const sanitized = _.mapValues(chartProviders, sanitizeProvider)
77
- res.json(sanitized)
78
- })
79
- }
80
-
81
- return {
82
- id: 'charts',
83
- name: 'Signal K Charts',
84
- description: 'Singal K Charts resource',
85
- schema: {
86
- title: 'Signal K Charts',
87
- description: `Add one or more paths to find charts. Defaults to "${defaultChartsPath}"`,
88
- type: 'object',
89
- properties: {
90
- chartPaths: {
91
- type: 'array',
92
- title: 'Chart paths',
93
- items: {
94
- type: 'string',
95
- title: "Path",
96
- description: `Path for chart files, relative to "${configBasePath}"`
97
- }
98
- },
99
- onlineChartProviders: {
100
- type: 'array',
101
- title: 'Online chart providers',
102
- items: {
103
- type: 'object',
104
- title: 'Provider',
105
- required: ['name', 'minzoom', 'maxzoom', 'format', 'url'],
106
- properties: {
107
- name: {
108
- type: 'string',
109
- title: 'Name'
110
- },
111
- description: {
112
- type: 'string',
113
- title: 'Description'
114
- },
115
- minzoom: {
116
- type: 'number',
117
- title: `Minimum zoom level, between [${MIN_ZOOM}, ${MAX_ZOOM}]`,
118
- maximum: MAX_ZOOM,
119
- minimum: MIN_ZOOM,
120
- default: MIN_ZOOM,
121
- },
122
- maxzoom: {
123
- type: 'number',
124
- title: `Maximum zoom level, between [${MIN_ZOOM}, ${MAX_ZOOM}]`,
125
- maximum: MAX_ZOOM,
126
- minimum: MIN_ZOOM,
127
- default: 15,
128
- },
129
- serverType: {
130
- type: 'string',
131
- title: 'Server Type',
132
- default: 'tilelayer',
133
- enum: ['tilelayer', 'WMS']
134
- },
135
- format: {
136
- type: 'string',
137
- title: 'Format',
138
- default: 'png',
139
- enum: ['png', 'jpg']
140
- },
141
- url: {
142
- type: 'string',
143
- title: 'URL',
144
- description: 'Map URL (for tilelayer include {z}, {x} and {y} parameters, e.g. "http://example.org/{z}/{x}/{y}.png")'
145
- },
146
- layers: {
147
- type: 'array',
148
- title: 'Layers',
149
- description: '(WMS only) ',
150
- items: {
151
- title: 'Layer Name',
152
- description: 'Name of layer to display',
153
- type: 'string'
154
- }
155
- }
156
- }
157
- }
158
- }
159
- }
160
- },
161
- start,
162
- stop
163
- }
164
- }
165
-
166
-
167
- const responseHttpOptions = {
168
- headers: {
169
- 'Cache-Control': 'public, max-age=7776000' // 90 days
170
- }
171
- }
172
-
173
- function resolveUniqueChartPaths(chartPaths, configBasePath) {
174
- const paths = _.map(chartPaths, chartPath => path.resolve(configBasePath, chartPath))
175
- return _.uniq(paths)
176
- }
177
-
178
- function convertOnlineProviderConfig(provider) {
179
- const id = _.kebabCase(_.deburr(provider.name))
180
- return {
181
- name: provider.name,
182
- description: provider.description,
183
- bounds: [-180, -90, 180, 90],
184
- minzoom: Math.min(Math.max(1, provider.minzoom), 19),
185
- maxzoom: Math.min(Math.max(1, provider.maxzoom), 19),
186
- format: provider.format,
187
- scale: 'N/A',
188
- identifier: id,
189
- tilemapUrl: provider.url,
190
- type: (provider.serverType) ? provider.serverType : 'tilelayer',
191
- chartLayers: (provider.layers) ? provider.layers : null
192
- }
193
- }
194
-
195
- function sanitizeProvider(provider) {
196
- return _.omit(provider, ['_filePath', '_fileFormat', '_mbtilesHandle', '_flipY'])
197
- }
198
-
199
- function ensureDirectoryExists (path) {
200
- if (!fs.existsSync(path)) {
201
- fs.mkdirSync(path)
202
- }
203
- }
204
-
205
- function serveTileFromFilesystem(res, provider, z, x, y) {
206
- const {identifier, format, _flipY, _filePath} = provider
207
- const flippedY = Math.pow(2, z) - 1 - y
208
- const file = path.resolve(_filePath, `${z}/${x}/${_flipY ? flippedY : y}.${format}`)
209
- res.sendFile(file, responseHttpOptions, (err) => {
210
- if (err && err.code === 'ENOENT') {
211
- res.sendStatus(404)
212
- } else if (err) {
213
- throw err
214
- }
215
- })
216
- }
217
-
218
- function serveTileFromMbtiles(res, provider, z, x, y) {
219
- provider._mbtilesHandle.getTile(z, x, y, (err, tile, headers) => {
220
- if (err && err.message && err.message === 'Tile does not exist') {
221
- res.sendStatus(404)
222
- } else if (err) {
223
- console.error(`Error fetching tile ${provider.identifier}/${z}/${x}/${y}:`, err)
224
- res.sendStatus(500)
225
- } else {
226
- headers['Cache-Control'] = responseHttpOptions.headers['Cache-Control']
227
- res.writeHead(200, headers)
228
- res.end(tile)
229
- }
230
- })
231
- }
File without changes
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,14 +0,0 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <TileMap version="1.0.0" tilemapservice="http://tms.osgeo.org/1.0.0">
3
- <Title>Översikt Svenska Sjökort</Title>
4
- <Abstract></Abstract>
5
- <Metadata scale="4000000" mime-type="text/xml" />
6
- <SRS>EPSG:900913</SRS>
7
- <BoundingBox minx="6.924150554004035" miny="53.14584310531994" maxx="25.541777414896345" maxy="66.0258911558707"/>
8
- <Origin x="6.924150554004035" y="66.0258911558707" />
9
- <TileFormat width="256" height="256" mime-type="image/png" extension="png"/>
10
- <TileSets profile="mercator">
11
- <TileSet href="4" units-per-pixel="9783.93961875" order="4"/>
12
- <TileSet href="5" units-per-pixel="4891.969809375" order="5"/>
13
- </TileSets>
14
- </TileMap>
@@ -1,15 +0,0 @@
1
- {
2
- "profile": "mercator",
3
- "attribution": "NOAA",
4
- "name": "NOAA MBTiles test file",
5
- "format": "png",
6
- "basename": "MBTILES_19",
7
- "bounds": "-82.265625, 36.5978891331, -61.875, 31.952162238",
8
- "minzoom": "3",
9
- "version": "1",
10
- "maxzoom": "4",
11
- "tilejson": "2.0.0",
12
- "scheme": "xyz",
13
- "type": "overlay",
14
- "description": "CAROLINAS"
15
- }
Binary file
@@ -1,53 +0,0 @@
1
- {
2
- "test": {
3
- "bounds": [
4
- -82.265625,
5
- 36.5978891331,
6
- -61.875,
7
- 31.952162238
8
- ],
9
- "description": "CAROLINAS",
10
- "format": "png",
11
- "identifier": "test",
12
- "maxzoom": 5,
13
- "minzoom": 3,
14
- "name": "MBTILES_19",
15
- "scale": "250000",
16
- "tilemapUrl": "/signalk/v1/api/resources/charts/test/{z}/{x}/{y}",
17
- "type": "tilelayer"
18
- },
19
- "tms-tiles": {
20
- "bounds": [
21
- 6.924150554004035,
22
- 53.14584310531994,
23
- 25.541777414896345,
24
- 66.0258911558707
25
- ],
26
- "description": "Översikt Svenska Sjökort",
27
- "format": "png",
28
- "identifier": "tms-tiles",
29
- "maxzoom": 5,
30
- "minzoom": 4,
31
- "name": "Översikt Svenska Sjökort",
32
- "scale": "4000000",
33
- "tilemapUrl": "/signalk/v1/api/resources/charts/tms-tiles/{z}/{x}/{y}",
34
- "type": "tilelayer"
35
- },
36
- "unpacked-tiles": {
37
- "bounds": [
38
- -82.265625,
39
- 36.5978891331,
40
- -61.875,
41
- 31.952162238
42
- ],
43
- "description": "CAROLINAS",
44
- "format": "png",
45
- "identifier": "unpacked-tiles",
46
- "maxzoom": 4,
47
- "minzoom": 3,
48
- "name": "NOAA MBTiles test file",
49
- "scale": "250000",
50
- "tilemapUrl": "/signalk/v1/api/resources/charts/unpacked-tiles/{z}/{x}/{y}",
51
- "type": "tilelayer"
52
- }
53
- }
@@ -1,196 +0,0 @@
1
- 'use strict'
2
- const fs = require('fs')
3
- const _ = require('lodash')
4
- const path = require('path')
5
- const http = require('http')
6
- const chai = require('chai')
7
- const chaiHttp = require('chai-http')
8
- const Promise = require('bluebird')
9
- const express = require('express')
10
- const expect = chai.expect
11
- const Plugin = require('../src/plugin')
12
- const expectedCharts = require('./expected-charts.json')
13
-
14
- chai.use(chaiHttp)
15
-
16
- describe('GET /resources/charts', () => {
17
- let plugin
18
- let testServer
19
- beforeEach(() =>
20
- createDefaultApp()
21
- .then(({app, server}) => {
22
- plugin = Plugin(app)
23
- testServer = server
24
- })
25
- )
26
- afterEach(done => testServer.close(() => done()))
27
-
28
- it('returns all charts for default path', () => {
29
- return plugin.start({})
30
- .then(() => get(testServer, '/signalk/v1/api/resources/charts'))
31
- .then(result => {
32
- expect(result.status).to.equal(200)
33
- const resultCharts = result.body
34
- expect(_.keys(resultCharts).length).to.deep.equal(3)
35
- expect(resultCharts).to.deep.equal(expectedCharts)
36
- })
37
- })
38
-
39
- it('handle canonical paths', () => {
40
- return plugin.start({chartPaths: ['charts', path.resolve(__dirname, 'charts').toString()]})
41
- .then(() => get(testServer, '/signalk/v1/api/resources/charts'))
42
- .then(result => {
43
- expect(result.status).to.equal(200)
44
- const resultCharts = result.body
45
- expect(_.keys(resultCharts).length).to.deep.equal(3)
46
- })
47
- })
48
-
49
- it('returns all charts for multiple paths', () => {
50
- return plugin.start({chartPaths: ['charts', 'charts-2']})
51
- .then(() => get(testServer, '/signalk/v1/api/resources/charts'))
52
- .then(result => {
53
- expect(result.status).to.equal(200)
54
- const resultCharts = result.body
55
- expect(_.keys(resultCharts).length).to.deep.equal(4)
56
- expect(resultCharts['test2']).not.to.equal(undefined)
57
- })
58
- })
59
-
60
- it('returns empty charts for custom path', () => {
61
- return plugin.start({chartPaths: ['../src/']})
62
- .then(() => get(testServer, '/signalk/v1/api/resources/charts'))
63
- .then(result => {
64
- expect(result.status).to.equal(200)
65
- expect(result.body).to.deep.equal({})
66
- })
67
- })
68
-
69
- it('returns online chart providers', () => {
70
- return plugin.start({chartPaths: ['charts'], onlineChartProviders: [
71
- {name: 'Test Name', minzoom: 2, maxzoom: 15, format: 'jpg', url: 'https://example.com'}
72
- ]})
73
- .then(() => get(testServer, '/signalk/v1/api/resources/charts'))
74
- .then(result => {
75
- expect(result.body['test-name']).to.deep.equal({
76
- bounds: [-180, -90, 180, 90],
77
- format: 'jpg',
78
- identifier: 'test-name',
79
- maxzoom: 15,
80
- minzoom: 2,
81
- name: 'Test Name',
82
- scale: 'N/A',
83
- tilemapUrl: 'https://example.com',
84
- type: 'tilelayer'
85
- })
86
- })
87
- })
88
-
89
- it('returns one chart', () => {
90
- const identifier = 'test'
91
- return plugin.start({})
92
- .then(() => get(testServer, `/signalk/v1/api/resources/charts/${identifier}`))
93
- .then(result => {
94
- expect(result.status).to.equal(200)
95
- expect(result.body).to.deep.equal(expectedCharts[identifier])
96
- })
97
- })
98
-
99
- it('returns 404 for unknown chart', () => {
100
- return plugin.start({})
101
- .then(() => get(testServer, `/signalk/v1/api/resources/charts/foo`))
102
- .catch(e => e.response)
103
- .then(result => {
104
- expect(result.status).to.equal(404)
105
- })
106
- })
107
- })
108
-
109
- describe('GET /resources/charts/:identifier/:z/:x/:y', () => {
110
- let plugin
111
- let testServer
112
- beforeEach(() =>
113
- createDefaultApp()
114
- .then(({app, server}) => {
115
- plugin = Plugin(app)
116
- testServer = server
117
- })
118
- )
119
- afterEach(done => testServer.close(() => done()))
120
-
121
- it('returns correct tile from MBTiles file', () => {
122
- return plugin.start({})
123
- .then(() => get(testServer, '/signalk/v1/api/resources/charts/test/4/5/6'))
124
- .then(response => {
125
- // unpacked-tiles contains same tiles as the test.mbtiles file
126
- expectTileResponse(response, 'charts/unpacked-tiles/4/5/6.png', 'image/png')
127
- })
128
- })
129
-
130
- it('returns correct tile from directory', () => {
131
- const expectedTile = fs.readFileSync(path.resolve(__dirname, 'charts/unpacked-tiles/4/4/6.png'))
132
- return plugin.start({})
133
- .then(() => get(testServer, '/signalk/v1/api/resources/charts/unpacked-tiles/4/4/6'))
134
- .then(response => {
135
- expectTileResponse(response, 'charts/unpacked-tiles/4/4/6.png', 'image/png')
136
- })
137
- })
138
-
139
- it('returns correct tile from TMS directory', () => {
140
- const expectedTile = fs.readFileSync(path.resolve(__dirname, 'charts/tms-tiles/5/17/21.png'))
141
- // Y-coordinate flipped
142
- return plugin.start({})
143
- .then(() => get(testServer, '/signalk/v1/api/resources/charts/tms-tiles/5/17/10'))
144
- .then(response => {
145
- expectTileResponse(response, 'charts/tms-tiles/5/17/21.png', 'image/png')
146
- })
147
- })
148
-
149
- it('returns 404 for missing tile', () => {
150
- return plugin.start({})
151
- .then(() => get(testServer, '/signalk/v1/api/resources/charts/tms-tiles/5/55/10'))
152
- .catch(e => e.response)
153
- .then(response => {
154
- expect(response.status).to.equal(404)
155
- })
156
- })
157
-
158
- it('returns 404 for wrong chart identifier', () => {
159
- return plugin.start({})
160
- .then(() => get(testServer, '/signalk/v1/api/resources/charts/foo/4/4/6'))
161
- .catch(e => e.response)
162
- .then(response => {
163
- expect(response.status).to.equal(404)
164
- })
165
- })
166
- })
167
-
168
- const expectTileResponse = (response, expectedTilePath, expectedFormat) => {
169
- const expectedTile = fs.readFileSync(path.resolve(__dirname, expectedTilePath))
170
- expect(response.status).to.equal(200)
171
- expect(response.headers['content-type']).to.equal(expectedFormat)
172
- expect(response.headers['cache-control']).to.equal('public, max-age=7776000')
173
- expect(response.body.toString('hex')).to.deep.equal(expectedTile.toString('hex'))
174
- }
175
-
176
- const createDefaultApp = () => {
177
- let app = express()
178
- app.use(require('body-parser').json())
179
- app.config = { configPath: path.resolve(__dirname) }
180
- return new Promise((resolve, reject) => {
181
- const server = http.createServer(app)
182
- server.listen(() => {
183
- const {port} = server.address()
184
- app.set('port', port)
185
- console.log(`Test server on port ${port}`)
186
- resolve({app, server})
187
- })
188
- })
189
- }
190
-
191
- const get = (server, location) => {
192
- const baseUrl = `http://localhost:${server.address().port}`
193
- return chai
194
- .request(baseUrl)
195
- .get(location)
196
- }