@signalk/charts-plugin 2.3.0 → 3.0.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 (37) hide show
  1. package/README.md +30 -10
  2. package/package.json +24 -6
  3. package/plugin/charts.d.ts +1 -0
  4. package/plugin/charts.js +254 -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 +352 -0
  9. package/plugin/types.d.ts +38 -0
  10. package/plugin/types.js +2 -0
  11. package/index.js +0 -1
  12. package/src/charts.js +0 -175
  13. package/src/constants.js +0 -3
  14. package/src/plugin.js +0 -233
  15. package/test/charts/empty-test/.gitkeep +0 -0
  16. package/test/charts/test.mbtiles +0 -0
  17. package/test/charts/tms-tiles/4/8/10.png +0 -0
  18. package/test/charts/tms-tiles/4/8/11.png +0 -0
  19. package/test/charts/tms-tiles/4/9/10.png +0 -0
  20. package/test/charts/tms-tiles/4/9/11.png +0 -0
  21. package/test/charts/tms-tiles/5/16/21.png +0 -0
  22. package/test/charts/tms-tiles/5/16/22.png +0 -0
  23. package/test/charts/tms-tiles/5/16/23.png +0 -0
  24. package/test/charts/tms-tiles/5/17/21.png +0 -0
  25. package/test/charts/tms-tiles/5/17/22.png +0 -0
  26. package/test/charts/tms-tiles/5/17/23.png +0 -0
  27. package/test/charts/tms-tiles/5/18/21.png +0 -0
  28. package/test/charts/tms-tiles/5/18/22.png +0 -0
  29. package/test/charts/tms-tiles/5/18/23.png +0 -0
  30. package/test/charts/tms-tiles/tilemapresource.xml +0 -14
  31. package/test/charts/unpacked-tiles/3/2/3.png +0 -0
  32. package/test/charts/unpacked-tiles/4/4/6.png +0 -0
  33. package/test/charts/unpacked-tiles/4/5/6.png +0 -0
  34. package/test/charts/unpacked-tiles/metadata.json +0 -15
  35. package/test/charts-2/test2.mbtiles +0 -0
  36. package/test/expected-charts.json +0 -53
  37. package/test/plugin-test.js +0 -196
package/src/plugin.js DELETED
@@ -1,233 +0,0 @@
1
- const debug = require('debug')('signalk-charts-plugin')
2
- const Promise = require('bluebird')
3
- const _ = require('lodash')
4
- const path = require('path')
5
- const fs = require('fs')
6
- const Charts = require('./charts')
7
- const {apiRoutePrefix} = require('./constants')
8
-
9
- const MIN_ZOOM = 1
10
- const MAX_ZOOM = 19
11
-
12
- module.exports = function(app) {
13
- let chartProviders = []
14
- let pluginStarted = false
15
- const configBasePath = app.config.configPath
16
- const defaultChartsPath = path.join(configBasePath, "/charts")
17
- ensureDirectoryExists(defaultChartsPath)
18
-
19
- function start(props) {
20
- const chartPaths = _.isEmpty(props.chartPaths)
21
- ? [defaultChartsPath]
22
- : resolveUniqueChartPaths(props.chartPaths, configBasePath)
23
- const onlineProviders = _.reduce(props.onlineChartProviders, (result, data) => {
24
- const provider = convertOnlineProviderConfig(data)
25
- result[provider.identifier] = provider
26
- return result
27
- }, {})
28
- debug(`Start charts plugin. Chart paths: ${chartPaths.join(', ')}, online charts: ${onlineProviders.length}`)
29
-
30
- const loadProviders = Promise.mapSeries(chartPaths, chartPath => Charts.findCharts(chartPath))
31
- .then(list => _.reduce(list, (result, charts) => _.merge({}, result, charts), {}))
32
- return loadProviders.then(charts => {
33
- console.log(`Chart plugin: Found ${_.keys(charts).length} charts from ${chartPaths.join(', ')}`)
34
- chartProviders = _.merge({}, charts, onlineProviders)
35
- // Do not register routes if plugin has been started once already
36
- pluginStarted === false && registerRoutes()
37
- pluginStarted = true
38
- }).catch(e => {
39
- console.error(`Error loading chart providers`, e.message)
40
- chartProviders = {}
41
- })
42
- }
43
-
44
- function stop() {
45
- debug("Chart plugin stopped")
46
- }
47
-
48
- function registerRoutes() {
49
- app.get(apiRoutePrefix + '/charts/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)', (req, res) => {
50
- const { identifier, z, x, y } = req.params
51
- const provider = chartProviders[identifier]
52
- if (!provider) {
53
- res.sendStatus(404)
54
- return
55
- }
56
- switch (provider._fileFormat) {
57
- case 'directory':
58
- return serveTileFromFilesystem(res, provider, z, x, y)
59
- case 'mbtiles':
60
- return serveTileFromMbtiles(res, provider, z, x, y)
61
- default:
62
- console.error(`Unknown chart provider fileformat ${provider._fileFormat}`)
63
- res.status(500).send()
64
- }
65
- })
66
-
67
- app.get(apiRoutePrefix + "/charts/:identifier", (req, res) => {
68
- const { identifier } = req.params
69
- const provider = chartProviders[identifier]
70
- if (provider) {
71
- return res.json(sanitizeProvider(provider))
72
- } else {
73
- return res.status(404).send('Not found')
74
- }
75
- })
76
-
77
- app.get(apiRoutePrefix + "/charts", (req, res) => {
78
- const sanitized = _.mapValues(chartProviders, sanitizeProvider)
79
- res.json(sanitized)
80
- })
81
- }
82
-
83
- return {
84
- id: 'charts',
85
- name: 'Signal K Charts',
86
- description: 'Singal K Charts resource',
87
- schema: {
88
- title: 'Signal K Charts',
89
- description: `Add one or more paths to find charts. Defaults to "${defaultChartsPath}"`,
90
- type: 'object',
91
- properties: {
92
- chartPaths: {
93
- type: 'array',
94
- title: 'Chart paths',
95
- items: {
96
- type: 'string',
97
- title: "Path",
98
- description: `Path for chart files, relative to "${configBasePath}"`
99
- }
100
- },
101
- onlineChartProviders: {
102
- type: 'array',
103
- title: 'Online chart providers',
104
- items: {
105
- type: 'object',
106
- title: 'Provider',
107
- required: ['name', 'minzoom', 'maxzoom', 'format', 'url'],
108
- properties: {
109
- name: {
110
- type: 'string',
111
- title: 'Name'
112
- },
113
- description: {
114
- type: 'string',
115
- title: 'Description'
116
- },
117
- minzoom: {
118
- type: 'number',
119
- title: `Minimum zoom level, between [${MIN_ZOOM}, ${MAX_ZOOM}]`,
120
- maximum: MAX_ZOOM,
121
- minimum: MIN_ZOOM,
122
- default: MIN_ZOOM,
123
- },
124
- maxzoom: {
125
- type: 'number',
126
- title: `Maximum zoom level, between [${MIN_ZOOM}, ${MAX_ZOOM}]`,
127
- maximum: MAX_ZOOM,
128
- minimum: MIN_ZOOM,
129
- default: 15,
130
- },
131
- serverType: {
132
- type: 'string',
133
- title: 'Server Type',
134
- default: 'tilelayer',
135
- enum: ['tilelayer', 'WMS']
136
- },
137
- format: {
138
- type: 'string',
139
- title: 'Format',
140
- default: 'png',
141
- enum: ['png', 'jpg']
142
- },
143
- url: {
144
- type: 'string',
145
- title: 'URL',
146
- description: 'Map URL (for tilelayer include {z}, {x} and {y} parameters, e.g. "http://example.org/{z}/{x}/{y}.png")'
147
- },
148
- layers: {
149
- type: 'array',
150
- title: 'Layers',
151
- description: '(WMS only) ',
152
- items: {
153
- title: 'Layer Name',
154
- description: 'Name of layer to display',
155
- type: 'string'
156
- }
157
- }
158
- }
159
- }
160
- }
161
- }
162
- },
163
- start,
164
- stop
165
- }
166
- }
167
-
168
-
169
- const responseHttpOptions = {
170
- headers: {
171
- 'Cache-Control': 'public, max-age=7776000' // 90 days
172
- }
173
- }
174
-
175
- function resolveUniqueChartPaths(chartPaths, configBasePath) {
176
- const paths = _.map(chartPaths, chartPath => path.resolve(configBasePath, chartPath))
177
- return _.uniq(paths)
178
- }
179
-
180
- function convertOnlineProviderConfig(provider) {
181
- const id = _.kebabCase(_.deburr(provider.name))
182
- return {
183
- name: provider.name,
184
- description: provider.description,
185
- bounds: [-180, -90, 180, 90],
186
- minzoom: Math.min(Math.max(1, provider.minzoom), 19),
187
- maxzoom: Math.min(Math.max(1, provider.maxzoom), 19),
188
- format: provider.format,
189
- scale: 'N/A',
190
- identifier: id,
191
- tilemapUrl: provider.url,
192
- type: (provider.serverType) ? provider.serverType : 'tilelayer',
193
- chartLayers: (provider.layers) ? provider.layers : null
194
- }
195
- }
196
-
197
- function sanitizeProvider(provider) {
198
- return _.omit(provider, ['_filePath', '_fileFormat', '_mbtilesHandle', '_flipY'])
199
- }
200
-
201
- function ensureDirectoryExists (path) {
202
- if (!fs.existsSync(path)) {
203
- fs.mkdirSync(path)
204
- }
205
- }
206
-
207
- function serveTileFromFilesystem(res, provider, z, x, y) {
208
- const {identifier, format, _flipY, _filePath} = provider
209
- const flippedY = Math.pow(2, z) - 1 - y
210
- const file = path.resolve(_filePath, `${z}/${x}/${_flipY ? flippedY : y}.${format}`)
211
- res.sendFile(file, responseHttpOptions, (err) => {
212
- if (err && err.code === 'ENOENT') {
213
- res.sendStatus(404)
214
- } else if (err) {
215
- throw err
216
- }
217
- })
218
- }
219
-
220
- function serveTileFromMbtiles(res, provider, z, x, y) {
221
- provider._mbtilesHandle.getTile(z, x, y, (err, tile, headers) => {
222
- if (err && err.message && err.message === 'Tile does not exist') {
223
- res.sendStatus(404)
224
- } else if (err) {
225
- console.error(`Error fetching tile ${provider.identifier}/${z}/${x}/${y}:`, err)
226
- res.sendStatus(500)
227
- } else {
228
- headers['Cache-Control'] = responseHttpOptions.headers['Cache-Control']
229
- res.writeHead(200, headers)
230
- res.end(tile)
231
- }
232
- })
233
- }
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
- }