From d5919ba41ea3b7c06b02eb28ee0b3172a5985dd5 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:23:43 +0930 Subject: [PATCH 01/20] Serve chart tiles from path outside of `/resources/charts`. Removing the use of a shared path aligns better with multi-provider support. Tile paths have a fully qualified path, remove use of relative path. --- CHANELOG.md | 15 ++++++++++ src/charts.ts | 8 ++--- src/constants.ts | 4 --- src/index.ts | 78 ++++++++++++++++++++++++++++++------------------ src/types.ts | 8 ++++- 5 files changed, 75 insertions(+), 38 deletions(-) create mode 100644 CHANELOG.md delete mode 100644 src/constants.ts diff --git a/CHANELOG.md b/CHANELOG.md new file mode 100644 index 0000000..5be70f4 --- /dev/null +++ b/CHANELOG.md @@ -0,0 +1,15 @@ +## Change Log + +### 3.1.0 + +- **Updated**: Moved the map tile url path to be out from under `resources/charts` to `/chart-tiles`. This better aligns with v2 multiple-provider support. + +- **Updated**: Tile url value is now a fully qualified url rather than a relative path. + + +--- + +### 3.0.0 + +- **Added**: Signal K v2 Resources API support. +- **Updated**: Ported to Typescript. \ No newline at end of file diff --git a/src/charts.ts b/src/charts.ts index 1ae32c9..b3022c8 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -80,13 +80,13 @@ function openMbtilesFile(file: string, filename: string) { type: 'tilelayer', scale: parseInt(res.metadata.scale) || 250000, v1: { - tilemapUrl: `~basePath~/charts/${identifier}/{z}/{x}/{y}`, + tilemapUrl: `~basePath~/${identifier}/{z}/{x}/{y}`, chartLayers: res.metadata.vector_layers ? parseVectorLayers(res.metadata.vector_layers) : [] }, v2: { - url: `~basePath~/charts/${identifier}/{z}/{x}/{y}`, + url: `~basePath~/${identifier}/{z}/{x}/{y}`, layers: res.metadata.vector_layers ? parseVectorLayers(res.metadata.vector_layers) : [] @@ -133,11 +133,11 @@ function directoryToMapInfo(file: string, identifier: string) { ;(info._fileFormat = 'directory'), (info._filePath = file), (info.v1 = { - tilemapUrl: `~basePath~/charts/${identifier}/{z}/{x}/{y}`, + tilemapUrl: `~basePath~/${identifier}/{z}/{x}/{y}`, chartLayers: [] }) info.v2 = { - url: `~basePath~/charts/${identifier}/{z}/{x}/{y}`, + url: `~basePath~/${identifier}/{z}/{x}/{y}`, layers: [] } diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index e3cd161..0000000 --- a/src/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const apiRoutePrefix = { - 1: '/signalk/v1/api/resources', - 2: '/signalk/v2/api/resources' -} diff --git a/src/index.ts b/src/index.ts index e76d479..c89cfdf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ import path from 'path' import fs from 'fs' import * as _ from 'lodash' import { findCharts } from './charts' -import { apiRoutePrefix } from './constants' import { ChartProvider, OnlineChartProvider } from './types' import { Request, Response, Application } from 'express' import { OutgoingHttpHeaders } from 'http' @@ -13,9 +12,6 @@ import { ResourceProviderRegistry } from '@signalk/server-api' -const MIN_ZOOM = 1 -const MAX_ZOOM = 24 - interface Config { chartPaths: string[] onlineChartProviders: OnlineChartProvider[] @@ -38,6 +34,11 @@ interface ChartProviderApp } } +const MIN_ZOOM = 1 +const MAX_ZOOM = 24 +let basePath: string +const chartTilesPath = 'chart-tiles' + module.exports = (app: ChartProviderApp): Plugin => { let chartProviders: { [key: string]: ChartProvider } = {} let pluginStarted = false @@ -47,7 +48,9 @@ module.exports = (app: ChartProviderApp): Plugin => { } const configBasePath = app.config.configPath const defaultChartsPath = path.join(configBasePath, '/charts') - const serverMajorVersion = app.config.version ? parseInt(app.config.version.split('.')[0]) : '1' + const serverMajorVersion = app.config.version + ? parseInt(app.config.version.split('.')[0]) + : '1' ensureDirectoryExists(defaultChartsPath) // ******** REQUIRED PLUGIN DEFINITION ******* @@ -99,7 +102,14 @@ module.exports = (app: ChartProviderApp): Plugin => { type: 'string', title: 'Map source / server type', default: 'tilelayer', - enum: ['tilelayer', 'S-57', 'WMS', 'WMTS', 'mapstyleJSON', 'tileJSON'], + enum: [ + 'tilelayer', + 'S-57', + 'WMS', + 'WMTS', + 'mapstyleJSON', + 'tileJSON' + ], description: 'Map data source type served by the supplied url. (Use tilelayer for xyz / tms tile sources.)' }, @@ -147,9 +157,8 @@ module.exports = (app: ChartProviderApp): Plugin => { name: 'Signal K Charts', schema: () => CONFIG_SCHEMA, uiSchema: () => CONFIG_UISCHEMA, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - start: (settings: any) => { - return doStartup(settings) // return required for tests + start: (settings: object) => { + return doStartup(settings as Config) // return required for tests }, stop: () => { app.setPluginStatus('stopped') @@ -158,12 +167,28 @@ module.exports = (app: ChartProviderApp): Plugin => { const doStartup = (config: Config) => { app.debug('** loaded config: ', config) + + // Do not register routes if plugin has been started once already + pluginStarted === false && registerRoutes() + pluginStarted = true + basePath = `${app.config.ssl ? 'https' : 'http'}://localhost:${ + 'getExternalPort' in app.config ? app.config.getExternalPort() : 3000 + }/${chartTilesPath}` + app.debug('**basePath**', basePath) + app.setPluginStatus('Started') + + return loadCharts(config) + } + + // Load chart files + const loadCharts = (config: Config) => { props = { ...config } const chartPaths = _.isEmpty(props.chartPaths) ? [defaultChartsPath] : resolveUniqueChartPaths(props.chartPaths, configBasePath) + // load from config const onlineProviders = _.reduce( props.onlineChartProviders, (result: { [key: string]: object }, data) => { @@ -173,21 +198,13 @@ module.exports = (app: ChartProviderApp): Plugin => { }, {} ) + app.debug( `Start charts plugin. Chart paths: ${chartPaths.join( ', ' )}, online charts: ${Object.keys(onlineProviders).length}` ) - // Do not register routes if plugin has been started once already - pluginStarted === false && registerRoutes() - pluginStarted = true - const urlBase = `${app.config.ssl ? 'https' : 'http'}://localhost:${ - 'getExternalPort' in app.config ? app.config.getExternalPort() : 3000 - }` - app.debug('**urlBase**', urlBase) - app.setPluginStatus('Started') - const loadProviders = bluebird .mapSeries(chartPaths, (chartPath: string) => findCharts(chartPath)) .then((list: ChartProvider[]) => @@ -214,7 +231,7 @@ module.exports = (app: ChartProviderApp): Plugin => { app.debug('** Registering API paths **') app.get( - `/signalk/:version(v[1-2])/api/resources/charts/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)`, + `/${chartTilesPath}/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)`, async (req: Request, res: Response) => { const { identifier, z, x, y } = req.params const provider = chartProviders[identifier] @@ -250,7 +267,7 @@ module.exports = (app: ChartProviderApp): Plugin => { app.debug('** Registering v1 API paths **') app.get( - apiRoutePrefix[1] + '/charts/:identifier', + '/signalk/v1/api/resources/charts/:identifier', (req: Request, res: Response) => { const { identifier } = req.params const provider = chartProviders[identifier] @@ -262,12 +279,15 @@ module.exports = (app: ChartProviderApp): Plugin => { } ) - app.get(apiRoutePrefix[1] + '/charts', (req: Request, res: Response) => { - const sanitized = _.mapValues(chartProviders, (provider) => - sanitizeProvider(provider) - ) - res.json(sanitized) - }) + app.get( + '/signalk/v1/api/resources/charts', + (req: Request, res: Response) => { + const sanitized = _.mapValues(chartProviders, (provider) => + sanitizeProvider(provider) + ) + res.json(sanitized) + } + ) // v2 routes if (serverMajorVersion === 2) { @@ -364,10 +384,10 @@ const sanitizeProvider = (provider: ChartProvider, version = 1) => { let v if (version === 1) { v = _.merge({}, provider.v1) - v.tilemapUrl = v.tilemapUrl.replace('~basePath~', apiRoutePrefix[1]) - } else if (version === 2) { + v.tilemapUrl = v.tilemapUrl.replace('~basePath~', basePath) + } else { v = _.merge({}, provider.v2) - v.url = v.url ? v.url.replace('~basePath~', apiRoutePrefix[2]) : '' + v.url = v.url ? v.url.replace('~basePath~', basePath) : '' } provider = _.omit(provider, [ '_filePath', diff --git a/src/types.ts b/src/types.ts index dd3e7da..9bdc2e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,10 @@ -type MapSourceType = 'tilelayer' | 'S-57' | 'WMS' | 'WMTS' | 'mapstyleJSON' | 'tileJSON' +type MapSourceType = + | 'tilelayer' + | 'S-57' + | 'WMS' + | 'WMTS' + | 'mapstyleJSON' + | 'tileJSON' export interface ChartProvider { _fileFormat?: 'mbtiles' | 'directory' From b34da1038cbf1e83bdc1cee575a01def2401270c Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:00:51 +0930 Subject: [PATCH 02/20] Added detection, processing and serving of mapbox style json files. --- CHANELOG.md | 2 ++ src/charts.ts | 37 +++++++++++++++++++++++++++++++++++-- src/index.ts | 37 +++++++++++++++++++++++++++++++++---- src/types.ts | 4 ++-- tsconfig.json | 4 ++-- 5 files changed, 74 insertions(+), 10 deletions(-) diff --git a/CHANELOG.md b/CHANELOG.md index 5be70f4..6b43df5 100644 --- a/CHANELOG.md +++ b/CHANELOG.md @@ -2,6 +2,8 @@ ### 3.1.0 +- **Added**: Detection, processing and serving of mapbox style json files. Files served from `/chart-styles` + - **Updated**: Moved the map tile url path to be out from under `resources/charts` to `/chart-tiles`. This better aligns with v2 multiple-provider support. - **Updated**: Tile url value is now a fully qualified url rather than a relative path. diff --git a/src/charts.ts b/src/charts.ts index b3022c8..c5c4c3e 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -14,10 +14,13 @@ export function findCharts(chartBaseDir: string) { const isMbtilesFile = file.name.match(/\.mbtiles$/i) const filePath = path.resolve(chartBaseDir, file.name) const isDirectory = file.isDirectory() + const isMbstylesFile = file.name.match(/\.json$/i) if (isMbtilesFile) { return openMbtilesFile(filePath, file.name) } else if (isDirectory) { return directoryToMapInfo(filePath, file.name) + } else if (isMbstylesFile) { + return openMbstylesFile(filePath, file.name) } else { return Promise.resolve(null) } @@ -80,13 +83,13 @@ function openMbtilesFile(file: string, filename: string) { type: 'tilelayer', scale: parseInt(res.metadata.scale) || 250000, v1: { - tilemapUrl: `~basePath~/${identifier}/{z}/{x}/{y}`, + tilemapUrl: `~basePath~/~tilePath~/${identifier}/{z}/{x}/{y}`, chartLayers: res.metadata.vector_layers ? parseVectorLayers(res.metadata.vector_layers) : [] }, v2: { - url: `~basePath~/${identifier}/{z}/{x}/{y}`, + url: `~basePath~/~tilePath~/${identifier}/{z}/{x}/{y}`, layers: res.metadata.vector_layers ? parseVectorLayers(res.metadata.vector_layers) : [] @@ -101,6 +104,36 @@ function openMbtilesFile(file: string, filename: string) { ) } +export function encStyleToId(filename: string) { + return filename.replace('.json', '').replaceAll(' ', '-').toLocaleLowerCase() +} + +async function openMbstylesFile(file: string, filename: string) { + const json = JSON.parse(await fs.readFile(file, 'utf8')) + const identifier = encStyleToId(filename) + return { + _flipY: false, + name: json.name, + description: '', + identifier, + bounds: undefined, + minzoom: undefined, + maxzoom: undefined, + format: undefined, + type: 'mapstyleJSON', + scale: 250000, + _filePath: file, + v1: { + tilemapUrl: `~basePath~/~stylePath~/${filename}`, + chartLayers: undefined + }, + v2: { + url: `~basePath~/~stylePath~/${filename}`, + layers: undefined + } + } +} + function parseVectorLayers(layers: Array<{ id: string }>) { return layers.map((l) => l.id) } diff --git a/src/index.ts b/src/index.ts index c89cfdf..87e8d28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import * as bluebird from 'bluebird' import path from 'path' import fs from 'fs' import * as _ from 'lodash' -import { findCharts } from './charts' +import { findCharts, encStyleToId } from './charts' import { ChartProvider, OnlineChartProvider } from './types' import { Request, Response, Application } from 'express' import { OutgoingHttpHeaders } from 'http' @@ -38,6 +38,7 @@ const MIN_ZOOM = 1 const MAX_ZOOM = 24 let basePath: string const chartTilesPath = 'chart-tiles' +const chartStylesPath = 'chart-styles' module.exports = (app: ChartProviderApp): Plugin => { let chartProviders: { [key: string]: ChartProvider } = {} @@ -173,7 +174,7 @@ module.exports = (app: ChartProviderApp): Plugin => { pluginStarted = true basePath = `${app.config.ssl ? 'https' : 'http'}://localhost:${ 'getExternalPort' in app.config ? app.config.getExternalPort() : 3000 - }/${chartTilesPath}` + }` app.debug('**basePath**', basePath) app.setPluginStatus('Started') @@ -230,6 +231,7 @@ module.exports = (app: ChartProviderApp): Plugin => { const registerRoutes = () => { app.debug('** Registering API paths **') + app.debug(`** Registering map tile path (${chartTilesPath} **`) app.get( `/${chartTilesPath}/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)`, async (req: Request, res: Response) => { @@ -264,6 +266,23 @@ module.exports = (app: ChartProviderApp): Plugin => { } ) + app.debug(`** Registering MapBox styles path (${chartStylesPath} **`) + app.get( + `/${chartStylesPath}/:style`, + async (req: Request, res: Response) => { + const { style } = req.params + const identifier = encStyleToId(style) + const provider = chartProviders[identifier] + res.sendFile(provider._filePath) + /*res.json({ + path: req.path, + style, + identifier, + file: provider._filePath + })*/ + } + ) + app.debug('** Registering v1 API paths **') app.get( @@ -384,10 +403,20 @@ const sanitizeProvider = (provider: ChartProvider, version = 1) => { let v if (version === 1) { v = _.merge({}, provider.v1) - v.tilemapUrl = v.tilemapUrl.replace('~basePath~', basePath) + v.tilemapUrl = v.tilemapUrl + ? v.tilemapUrl + .replace('~basePath~', basePath) + .replace('~stylePath~', chartStylesPath) + .replace('~tilePath~', chartTilesPath) + : '' } else { v = _.merge({}, provider.v2) - v.url = v.url ? v.url.replace('~basePath~', basePath) : '' + v.url = v.url + ? v.url + .replace('~basePath~', basePath) + .replace('~stylePath~', chartStylesPath) + .replace('~tilePath~', chartTilesPath) + : '' } provider = _.omit(provider, [ '_filePath', diff --git a/src/types.ts b/src/types.ts index 9bdc2e6..dad6bba 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,11 +19,11 @@ export interface ChartProvider { scale: number v1?: { tilemapUrl: string - chartLayers: string[] + chartLayers?: string[] } v2?: { url: string - layers: string[] + layers?: string[] } bounds?: number[] minzoom?: number diff --git a/tsconfig.json b/tsconfig.json index 8987f83..b16c5ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "ES2022", "module": "commonjs", "outDir": "./plugin", "esModuleInterop": true, @@ -21,7 +21,7 @@ "ignoreCompilerErrors": true, "excludePrivate": true, "excludeNotExported": true, - "target": "ES5", + "target": "ES2022", "moduleResolution": "node", "preserveConstEnums": true, "stripInternal": true, From dfe4cd4f78276f8f70f9bc5a57e0c7c5b9196aa9 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sat, 28 Sep 2024 15:51:53 +0930 Subject: [PATCH 03/20] TMS tiles path --- src/charts.ts | 4 ++-- src/index.ts | 38 ++++++++++++++++---------------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/charts.ts b/src/charts.ts index c5c4c3e..2c5e8d8 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -166,11 +166,11 @@ function directoryToMapInfo(file: string, identifier: string) { ;(info._fileFormat = 'directory'), (info._filePath = file), (info.v1 = { - tilemapUrl: `~basePath~/${identifier}/{z}/{x}/{y}`, + tilemapUrl: `~basePath~/~tilePath~/${identifier}/{z}/{x}/{y}`, chartLayers: [] }) info.v2 = { - url: `~basePath~/${identifier}/{z}/{x}/{y}`, + url: `~basePath~/~tilePath~/${identifier}/{z}/{x}/{y}`, layers: [] } diff --git a/src/index.ts b/src/index.ts index 87e8d28..f523959 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,14 +39,11 @@ const MAX_ZOOM = 24 let basePath: string const chartTilesPath = 'chart-tiles' const chartStylesPath = 'chart-styles' +let chartPaths: Array +let onlineProviders = {} module.exports = (app: ChartProviderApp): Plugin => { let chartProviders: { [key: string]: ChartProvider } = {} - let pluginStarted = false - let props: Config = { - chartPaths: [], - onlineChartProviders: [] - } const configBasePath = app.config.configPath const defaultChartsPath = path.join(configBasePath, '/charts') const serverMajorVersion = app.config.version @@ -158,8 +155,8 @@ module.exports = (app: ChartProviderApp): Plugin => { name: 'Signal K Charts', schema: () => CONFIG_SCHEMA, uiSchema: () => CONFIG_UISCHEMA, - start: (settings: object) => { - return doStartup(settings as Config) // return required for tests + start: (config: object) => { + return doStartup(config as Config) // return required for tests }, stop: () => { app.setPluginStatus('stopped') @@ -169,29 +166,19 @@ module.exports = (app: ChartProviderApp): Plugin => { const doStartup = (config: Config) => { app.debug('** loaded config: ', config) - // Do not register routes if plugin has been started once already - pluginStarted === false && registerRoutes() - pluginStarted = true + registerRoutes() basePath = `${app.config.ssl ? 'https' : 'http'}://localhost:${ 'getExternalPort' in app.config ? app.config.getExternalPort() : 3000 }` app.debug('**basePath**', basePath) app.setPluginStatus('Started') - return loadCharts(config) - } - - // Load chart files - const loadCharts = (config: Config) => { - props = { ...config } - - const chartPaths = _.isEmpty(props.chartPaths) + chartPaths = _.isEmpty(config.chartPaths) ? [defaultChartsPath] - : resolveUniqueChartPaths(props.chartPaths, configBasePath) + : resolveUniqueChartPaths(config.chartPaths, configBasePath) - // load from config - const onlineProviders = _.reduce( - props.onlineChartProviders, + onlineProviders = _.reduce( + config.onlineChartProviders, (result: { [key: string]: object }, data) => { const provider = convertOnlineProviderConfig(data) result[provider.identifier] = provider @@ -206,6 +193,13 @@ module.exports = (app: ChartProviderApp): Plugin => { )}, online charts: ${Object.keys(onlineProviders).length}` ) + return loadCharts() + } + + // Load chart files + const loadCharts = () => { + app.debug(`Loading Charts....`) + const loadProviders = bluebird .mapSeries(chartPaths, (chartPath: string) => findCharts(chartPath)) .then((list: ChartProvider[]) => From dd3123bbcf4a611aba018ee9e3712100f3a1cdce Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sat, 28 Sep 2024 16:47:54 +0930 Subject: [PATCH 04/20] Update tests. --- test/expected-charts.json | 6 +++--- test/plugin-test.js | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/test/expected-charts.json b/test/expected-charts.json index 5c8b180..46984ba 100644 --- a/test/expected-charts.json +++ b/test/expected-charts.json @@ -14,7 +14,7 @@ "minzoom": 3, "name": "MBTILES_19", "scale": 250000, - "tilemapUrl": "/signalk/v1/api/resources/charts/test/{z}/{x}/{y}", + "tilemapUrl": "http://localhost:3000/chart-tiles/test/{z}/{x}/{y}", "type": "tilelayer" }, "tms-tiles": { @@ -32,7 +32,7 @@ "minzoom": 4, "name": "Översikt Svenska Sjökort", "scale": 4000000, - "tilemapUrl": "/signalk/v1/api/resources/charts/tms-tiles/{z}/{x}/{y}", + "tilemapUrl": "http://localhost:3000/chart-tiles/tms-tiles/{z}/{x}/{y}", "type": "tilelayer" }, "unpacked-tiles": { @@ -50,7 +50,7 @@ "minzoom": 3, "name": "NOAA MBTiles test file", "scale": 250000, - "tilemapUrl": "/signalk/v1/api/resources/charts/unpacked-tiles/{z}/{x}/{y}", + "tilemapUrl": "http://localhost:3000/chart-tiles/unpacked-tiles/{z}/{x}/{y}", "type": "tilelayer" } } \ No newline at end of file diff --git a/test/plugin-test.js b/test/plugin-test.js index f9bcfa5..63a0027 100644 --- a/test/plugin-test.js +++ b/test/plugin-test.js @@ -109,7 +109,8 @@ describe('GET /resources/charts', () => { }) -describe('GET /resources/charts/:identifier/:z/:x/:y', () => { + +describe('GET /chart-tiles/:identifier/:z/:x/:y', () => { let plugin let testServer beforeEach(() => @@ -123,7 +124,7 @@ describe('GET /resources/charts/:identifier/:z/:x/:y', () => { it('returns correct tile from MBTiles file', () => { return plugin.start({}) - .then(() => get(testServer, '/signalk/v1/api/resources/charts/test/4/5/6')) + .then(() => get(testServer, '/chart-tiles/test/4/5/6')) .then(response => { // unpacked-tiles contains same tiles as the test.mbtiles file expectTileResponse(response, 'charts/unpacked-tiles/4/5/6.png', 'image/png') @@ -133,7 +134,7 @@ describe('GET /resources/charts/:identifier/:z/:x/:y', () => { it('returns correct tile from directory', () => { const expectedTile = fs.readFileSync(path.resolve(__dirname, 'charts/unpacked-tiles/4/4/6.png')) return plugin.start({}) - .then(() => get(testServer, '/signalk/v1/api/resources/charts/unpacked-tiles/4/4/6')) + .then(() => get(testServer, '/chart-tiles/unpacked-tiles/4/4/6')) .then(response => { expectTileResponse(response, 'charts/unpacked-tiles/4/4/6.png', 'image/png') }) @@ -143,7 +144,7 @@ describe('GET /resources/charts/:identifier/:z/:x/:y', () => { const expectedTile = fs.readFileSync(path.resolve(__dirname, 'charts/tms-tiles/5/17/21.png')) // Y-coordinate flipped return plugin.start({}) - .then(() => get(testServer, '/signalk/v1/api/resources/charts/tms-tiles/5/17/10')) + .then(() => get(testServer, '/chart-tiles/tms-tiles/5/17/10')) .then(response => { expectTileResponse(response, 'charts/tms-tiles/5/17/21.png', 'image/png') }) @@ -151,7 +152,7 @@ describe('GET /resources/charts/:identifier/:z/:x/:y', () => { it('returns 404 for missing tile', () => { return plugin.start({}) - .then(() => get(testServer, '/signalk/v1/api/resources/charts/tms-tiles/5/55/10')) + .then(() => get(testServer, '/chart-tiles/tms-tiles/5/55/10')) .catch(e => e.response) .then(response => { expect(response.status).to.equal(404) @@ -160,7 +161,7 @@ describe('GET /resources/charts/:identifier/:z/:x/:y', () => { it('returns 404 for wrong chart identifier', () => { return plugin.start({}) - .then(() => get(testServer, '/signalk/v1/api/resources/charts/foo/4/4/6')) + .then(() => get(testServer, '/chart-tiles/foo/4/4/6')) .catch(e => e.response) .then(response => { expect(response.status).to.equal(404) From 5b4f6518ca7bd8afc241a112f2a729d2be25eca0 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sat, 28 Sep 2024 17:28:06 +0930 Subject: [PATCH 05/20] Add MapBox auth token config option. This is appended to the url path to the mapbox style json file. --- src/index.ts | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index f523959..2d9e116 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { interface Config { chartPaths: string[] onlineChartProviders: OnlineChartProvider[] + accessToken: string } interface ChartProviderApp @@ -41,6 +42,7 @@ const chartTilesPath = 'chart-tiles' const chartStylesPath = 'chart-styles' let chartPaths: Array let onlineProviders = {} +let accessTokenGlobal = '' module.exports = (app: ChartProviderApp): Plugin => { let chartProviders: { [key: string]: ChartProvider } = {} @@ -56,6 +58,11 @@ module.exports = (app: ChartProviderApp): Plugin => { title: 'Signal K Charts', type: 'object', properties: { + accessToken: { + type: 'string', + title: 'MapBox Access Token (optional)', + description: `Token to append to mapbox style urls for authentication. e.g. "?access_token=xxxxx"` + }, chartPaths: { type: 'array', title: 'Chart paths', @@ -173,6 +180,8 @@ module.exports = (app: ChartProviderApp): Plugin => { app.debug('**basePath**', basePath) app.setPluginStatus('Started') + accessTokenGlobal = config.accessToken ?? '' + chartPaths = _.isEmpty(config.chartPaths) ? [defaultChartsPath] : resolveUniqueChartPaths(config.chartPaths, configBasePath) @@ -268,12 +277,6 @@ module.exports = (app: ChartProviderApp): Plugin => { const identifier = encStyleToId(style) const provider = chartProviders[identifier] res.sendFile(provider._filePath) - /*res.json({ - path: req.path, - style, - identifier, - file: provider._filePath - })*/ } ) @@ -393,20 +396,30 @@ const convertOnlineProviderConfig = (provider: OnlineChartProvider) => { return data } +const applyAccessToken = (uri: string) => { + if (uri.includes('access_token') || !uri.includes('~stylePath~')) { + return uri + } else { + return `${uri}?access_token=${accessTokenGlobal}` + } +} + const sanitizeProvider = (provider: ChartProvider, version = 1) => { let v if (version === 1) { v = _.merge({}, provider.v1) - v.tilemapUrl = v.tilemapUrl - ? v.tilemapUrl + const uri = applyAccessToken(v?.tilemapUrl) + v.tilemapUrl = uri + ? uri .replace('~basePath~', basePath) .replace('~stylePath~', chartStylesPath) .replace('~tilePath~', chartTilesPath) : '' } else { v = _.merge({}, provider.v2) - v.url = v.url - ? v.url + const uri = applyAccessToken(v?.url) + v.url = uri + ? uri .replace('~basePath~', basePath) .replace('~stylePath~', chartStylesPath) .replace('~tilePath~', chartTilesPath) From a0b490969baee54970bba5e18f9a5e7776e06daa Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:55:31 +0930 Subject: [PATCH 06/20] Revert to use of relative paths for map tiles url. Ensure operation behind proxy is not impacted. --- CHANELOG.md | 3 --- src/index.ts | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/CHANELOG.md b/CHANELOG.md index 6b43df5..e12082a 100644 --- a/CHANELOG.md +++ b/CHANELOG.md @@ -6,9 +6,6 @@ - **Updated**: Moved the map tile url path to be out from under `resources/charts` to `/chart-tiles`. This better aligns with v2 multiple-provider support. -- **Updated**: Tile url value is now a fully qualified url rather than a relative path. - - --- ### 3.0.0 diff --git a/src/index.ts b/src/index.ts index 2d9e116..9508105 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,7 +37,7 @@ interface ChartProviderApp const MIN_ZOOM = 1 const MAX_ZOOM = 24 -let basePath: string +const basePath = '' const chartTilesPath = 'chart-tiles' const chartStylesPath = 'chart-styles' let chartPaths: Array @@ -174,10 +174,6 @@ module.exports = (app: ChartProviderApp): Plugin => { app.debug('** loaded config: ', config) registerRoutes() - basePath = `${app.config.ssl ? 'https' : 'http'}://localhost:${ - 'getExternalPort' in app.config ? app.config.getExternalPort() : 3000 - }` - app.debug('**basePath**', basePath) app.setPluginStatus('Started') accessTokenGlobal = config.accessToken ?? '' From a037b1c0698163f4c604b0352f7548dc560161c8 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:57:31 +0930 Subject: [PATCH 07/20] Revert to use of relative paths for map tiles url. Ensure operation behind proxy is not impacted. --- test/expected-charts.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/expected-charts.json b/test/expected-charts.json index 46984ba..4da2e3a 100644 --- a/test/expected-charts.json +++ b/test/expected-charts.json @@ -14,7 +14,7 @@ "minzoom": 3, "name": "MBTILES_19", "scale": 250000, - "tilemapUrl": "http://localhost:3000/chart-tiles/test/{z}/{x}/{y}", + "tilemapUrl": "/chart-tiles/test/{z}/{x}/{y}", "type": "tilelayer" }, "tms-tiles": { @@ -32,7 +32,7 @@ "minzoom": 4, "name": "Översikt Svenska Sjökort", "scale": 4000000, - "tilemapUrl": "http://localhost:3000/chart-tiles/tms-tiles/{z}/{x}/{y}", + "tilemapUrl": "/chart-tiles/tms-tiles/{z}/{x}/{y}", "type": "tilelayer" }, "unpacked-tiles": { @@ -50,7 +50,7 @@ "minzoom": 3, "name": "NOAA MBTiles test file", "scale": 250000, - "tilemapUrl": "http://localhost:3000/chart-tiles/unpacked-tiles/{z}/{x}/{y}", + "tilemapUrl": "/chart-tiles/unpacked-tiles/{z}/{x}/{y}", "type": "tilelayer" } } \ No newline at end of file From e4aaa1167716baae8bb63f58bbb1bf8262038446 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:12:11 +0930 Subject: [PATCH 08/20] chore: doc --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 046e35a..2a2ec6a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,14 @@ Signal K Node server plugin to provide chart metadata, such as name, description and location of the actual chart tile data. -Supports both v1 and v2 Signal K resources api paths. +Chart metadata is derived from the following supported chart files: +- Mapbox Tiles _(.mbtiles)_ +- Mapbox Style _(.json)_ +- TMS _(tilemapresource.xml and tiles)_ + +Additionally chart metadata can be defined for other chart sources and types _(e.g. WMS, WMTS, S-57 tiles and tileJSON)_. + +Chart metadata made available to both v1 and v2 Signal K resources api paths. | Server Version | API | Path | |--- |--- |--- | From ae72a4a42560850e3f36b231a31cae87576413ab Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:01:31 +0930 Subject: [PATCH 09/20] Watch chart folders for changes and refresh providers. --- CHANELOG.md | 2 ++ src/index.ts | 44 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANELOG.md b/CHANELOG.md index e12082a..02206e1 100644 --- a/CHANELOG.md +++ b/CHANELOG.md @@ -4,6 +4,8 @@ - **Added**: Detection, processing and serving of mapbox style json files. Files served from `/chart-styles` +- **Added**: Watch chart folders for changes and refresh chart providers (#28) + - **Updated**: Moved the map tile url path to be out from under `resources/charts` to `/chart-tiles`. This better aligns with v2 multiple-provider support. --- diff --git a/src/index.ts b/src/index.ts index 9508105..c8ddd3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import * as bluebird from 'bluebird' import path from 'path' -import fs from 'fs' +import fs, { FSWatcher } from 'fs' import * as _ from 'lodash' import { findCharts, encStyleToId } from './charts' import { ChartProvider, OnlineChartProvider } from './types' @@ -43,6 +43,8 @@ const chartStylesPath = 'chart-styles' let chartPaths: Array let onlineProviders = {} let accessTokenGlobal = '' +let lastWatchEvent: number | undefined +const watchers: Array = [] module.exports = (app: ChartProviderApp): Plugin => { let chartProviders: { [key: string]: ChartProvider } = {} @@ -166,6 +168,7 @@ module.exports = (app: ChartProviderApp): Plugin => { return doStartup(config as Config) // return required for tests }, stop: () => { + watchers.forEach((w) => w.close()) app.setPluginStatus('stopped') } } @@ -192,6 +195,15 @@ module.exports = (app: ChartProviderApp): Plugin => { {} ) + chartPaths.forEach((p) => { + console.log('watching..', p) + watchers.push( + fs.watch(p, 'utf8', (eventType, filename) => + handleWatchEvent(eventType, filename) + ) + ) + }) + app.debug( `Start charts plugin. Chart paths: ${chartPaths.join( ', ' @@ -227,6 +239,21 @@ module.exports = (app: ChartProviderApp): Plugin => { }) } + const refreshProviders = async () => { + const td = Date.now() - (lastWatchEvent as number) + app.debug(`last watch event time elapsed = ${td}`) + if (lastWatchEvent && td > 10000) { + app.debug(`reloading Charts`) + lastWatchEvent = undefined + loadCharts() + } + } + + const handleWatchEvent = (eventType: string, filename: string) => { + console.log('***', eventType, filename) + lastWatchEvent = Date.now() + } + const registerRoutes = () => { app.debug('** Registering API paths **') @@ -235,10 +262,12 @@ module.exports = (app: ChartProviderApp): Plugin => { `/${chartTilesPath}/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)`, async (req: Request, res: Response) => { const { identifier, z, x, y } = req.params + await refreshProviders() const provider = chartProviders[identifier] if (!provider) { return res.sendStatus(404) } + switch (provider._fileFormat) { case 'directory': return serveTileFromFilesystem( @@ -271,6 +300,7 @@ module.exports = (app: ChartProviderApp): Plugin => { async (req: Request, res: Response) => { const { style } = req.params const identifier = encStyleToId(style) + await refreshProviders() const provider = chartProviders[identifier] res.sendFile(provider._filePath) } @@ -280,8 +310,9 @@ module.exports = (app: ChartProviderApp): Plugin => { app.get( '/signalk/v1/api/resources/charts/:identifier', - (req: Request, res: Response) => { + async (req: Request, res: Response) => { const { identifier } = req.params + await refreshProviders() const provider = chartProviders[identifier] if (provider) { return res.json(sanitizeProvider(provider)) @@ -293,7 +324,8 @@ module.exports = (app: ChartProviderApp): Plugin => { app.get( '/signalk/v1/api/resources/charts', - (req: Request, res: Response) => { + async (req: Request, res: Response) => { + await refreshProviders() const sanitized = _.mapValues(chartProviders, (provider) => sanitizeProvider(provider) ) @@ -315,18 +347,20 @@ module.exports = (app: ChartProviderApp): Plugin => { app.registerResourceProvider({ type: 'charts', methods: { - listResources: (params: { + listResources: async (params: { [key: string]: number | string | object | null }) => { app.debug(`** listResources()`, params) + await refreshProviders() return Promise.resolve( _.mapValues(chartProviders, (provider) => sanitizeProvider(provider, 2) ) ) }, - getResource: (id: string) => { + getResource: async (id: string) => { app.debug(`** getResource()`, id) + await refreshProviders() const provider = chartProviders[id] if (provider) { return Promise.resolve(sanitizeProvider(provider, 2)) From 46e834e824ee7af8ab74e7cc5f5c9de2811e15aa Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:07:34 +0930 Subject: [PATCH 10/20] remove watcher parameters --- src/index.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index c8ddd3a..1aa9556 100644 --- a/src/index.ts +++ b/src/index.ts @@ -197,11 +197,7 @@ module.exports = (app: ChartProviderApp): Plugin => { chartPaths.forEach((p) => { console.log('watching..', p) - watchers.push( - fs.watch(p, 'utf8', (eventType, filename) => - handleWatchEvent(eventType, filename) - ) - ) + watchers.push(fs.watch(p, 'utf8', () => handleWatchEvent())) }) app.debug( @@ -249,8 +245,7 @@ module.exports = (app: ChartProviderApp): Plugin => { } } - const handleWatchEvent = (eventType: string, filename: string) => { - console.log('***', eventType, filename) + const handleWatchEvent = () => { lastWatchEvent = Date.now() } From ba0c4bf3d08b0621e1ff6c37dbabf603ff8e628a Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:14:39 +0930 Subject: [PATCH 11/20] Fix test. --- test/plugin-test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/plugin-test.js b/test/plugin-test.js index 63a0027..421f19d 100644 --- a/test/plugin-test.js +++ b/test/plugin-test.js @@ -120,7 +120,10 @@ describe('GET /chart-tiles/:identifier/:z/:x/:y', () => { testServer = server }) ) - afterEach(done => testServer.close(() => done())) + afterEach(done => { + plugin.stop() + testServer.close(() => done()) + }) it('returns correct tile from MBTiles file', () => { return plugin.start({}) From 55f4775950d533c542972b2ff65fef836c80730c Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:45:22 +0930 Subject: [PATCH 12/20] chore: update package dependencies --- CHANELOG.md | 2 ++ package.json | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANELOG.md b/CHANELOG.md index 02206e1..c99c506 100644 --- a/CHANELOG.md +++ b/CHANELOG.md @@ -8,6 +8,8 @@ - **Updated**: Moved the map tile url path to be out from under `resources/charts` to `/chart-tiles`. This better aligns with v2 multiple-provider support. +- **Updated**: Updated package dependencies (#35) + --- ### 3.0.0 diff --git a/package.json b/package.json index 6c7e308..bf3e4aa 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,9 @@ "dependencies": { "@mapbox/mbtiles": "^0.12.1", "@signalk/server-api": "^2.0.0-beta.3", - "baconjs": "1.0.1", "bluebird": "3.5.1", "lodash": "^4.17.11", - "xml2js": "0.4.19" + "xml2js": "^0.6.2" }, "repository": { "type": "git", @@ -47,12 +46,12 @@ "@types/node": "^18.14.4", "@typescript-eslint/eslint-plugin": "^5.52.0", "@typescript-eslint/parser": "^5.52.0", - "body-parser": "1.18.2", + "body-parser": "^1.18.2", "chai": "4.1.2", "chai-http": "^4.2.1", "eslint": "^8.34.0", "eslint-config-prettier": "^8.6.0", - "express": "4.19.2", + "express": "^4.19.2", "mocha": "5.0.0", "prettier": "^2.8.4", "typescript": "^4.5.4" From 1491e4eb3cd6ec8f336abf3f356ccad940bdb2b3 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sun, 6 Oct 2024 16:13:53 +1030 Subject: [PATCH 13/20] use async / await in loadProviders --- src/index.ts | 51 ++++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1aa9556..e6b1b60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -173,7 +173,7 @@ module.exports = (app: ChartProviderApp): Plugin => { } } - const doStartup = (config: Config) => { + const doStartup = async (config: Config) => { app.debug('** loaded config: ', config) registerRoutes() @@ -196,7 +196,7 @@ module.exports = (app: ChartProviderApp): Plugin => { ) chartPaths.forEach((p) => { - console.log('watching..', p) + app.debug('watching folder..', p) watchers.push(fs.watch(p, 'utf8', () => handleWatchEvent())) }) @@ -210,38 +210,39 @@ module.exports = (app: ChartProviderApp): Plugin => { } // Load chart files - const loadCharts = () => { + const loadCharts = async () => { app.debug(`Loading Charts....`) - const loadProviders = bluebird - .mapSeries(chartPaths, (chartPath: string) => findCharts(chartPath)) - .then((list: ChartProvider[]) => - _.reduce(list, (result, charts) => _.merge({}, result, charts), {}) + try { + const plist = await bluebird.mapSeries(chartPaths, (chartPath: string) => + findCharts(chartPath) ) - - return loadProviders - .then((charts: { [key: string]: ChartProvider }) => { - app.debug( - `Chart plugin: Found ${ - _.keys(charts).length - } charts from ${chartPaths.join(', ')}.` - ) - chartProviders = _.merge({}, charts, onlineProviders) - }) - .catch((e: Error) => { - console.error(`Error loading chart providers`, e.message) - chartProviders = {} - app.setPluginError(`Error loading chart providers`) - }) + const charts = _.reduce( + plist, + (result, charts) => _.merge({}, result, charts), + {} + ) + app.debug( + `Chart plugin: Found ${ + _.keys(charts).length + } charts from ${chartPaths.join(', ')}.` + ) + chartProviders = _.merge({}, charts, onlineProviders) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + console.error(`Error loading chart providers`, e.message) + chartProviders = {} + app.setPluginError(`Error loading chart providers`) + } } const refreshProviders = async () => { const td = Date.now() - (lastWatchEvent as number) app.debug(`last watch event time elapsed = ${td}`) - if (lastWatchEvent && td > 10000) { - app.debug(`reloading Charts`) + if (lastWatchEvent && td > 5000) { + app.debug(`Reloading Charts...`) lastWatchEvent = undefined - loadCharts() + await loadCharts() } } From 5163475ec452df12d95bbd2d26606a33be70f84b Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sun, 20 Oct 2024 11:27:14 +1030 Subject: [PATCH 14/20] Align chart types --- src/charts.ts | 2 +- src/index.ts | 4 ++-- src/types.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/charts.ts b/src/charts.ts index 2c5e8d8..7a389d3 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -120,7 +120,7 @@ async function openMbstylesFile(file: string, filename: string) { minzoom: undefined, maxzoom: undefined, format: undefined, - type: 'mapstyleJSON', + type: 'mapboxstyle', scale: 250000, _filePath: file, v1: { diff --git a/src/index.ts b/src/index.ts index e6b1b60..9a6d70e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,8 +114,8 @@ module.exports = (app: ChartProviderApp): Plugin => { 'S-57', 'WMS', 'WMTS', - 'mapstyleJSON', - 'tileJSON' + 'mapboxstyle', + 'tilejson' ], description: 'Map data source type served by the supplied url. (Use tilelayer for xyz / tms tile sources.)' diff --git a/src/types.ts b/src/types.ts index dad6bba..f133bfb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,8 +3,8 @@ type MapSourceType = | 'S-57' | 'WMS' | 'WMTS' - | 'mapstyleJSON' - | 'tileJSON' + | 'mapboxstyle' + | 'tilejson' export interface ChartProvider { _fileFormat?: 'mbtiles' | 'directory' From 67ae54ed28eef88968d5de80c6965a2118c4f8d6 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sun, 20 Oct 2024 11:27:26 +1030 Subject: [PATCH 15/20] chore: docs --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2a2ec6a..ef62f5e 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ Signal K Node server plugin to provide chart metadata, such as name, description Chart metadata is derived from the following supported chart files: - Mapbox Tiles _(.mbtiles)_ -- Mapbox Style _(.json)_ +- Mapbox Styles _(.json)_ - TMS _(tilemapresource.xml and tiles)_ -Additionally chart metadata can be defined for other chart sources and types _(e.g. WMS, WMTS, S-57 tiles and tileJSON)_. +Additionally chart metadata can be defined for other chart sources and types _(e.g. WMS, WMTS, S-57 tiles and tilejson)_. Chart metadata made available to both v1 and v2 Signal K resources api paths. @@ -39,7 +39,10 @@ _Note: v2 resource paths will only be made available on Signal K server >= v2._ Online chart providers configuration +6. (Optional): Add Mapbox access token. + When provided, the access token will added to the url of Mapbox Styles _e.g. `?access_token=xyz123`_ + ![image](https://github.com/user-attachments/assets/b4d4d048-2ab1-4bf1-896b-2ca0031ec77f) _WMS example:_ @@ -53,8 +56,10 @@ _WMS example:_ - [Tuktuk Chart Plotter](https://www.npmjs.com/package/tuktuk-chart-plotter) ### Supported chart formats +pk.eyJ1IjoiYWRhbTIyMjIiLCJhIjoiY2l5dGJhaW96MDAwcDJ3bzM0MXk2aTB0bSJ9.kgHNRDiGEmq12toljp2-kA - [MBTiles](https://github.com/mapbox/mbtiles-spec) file +- [Mapbox Style](https://docs.mapbox.com/help/glossary/style/) JSON file _e.g. `bright-v9.json`_ - Directory with cached [TMS](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification) tiles and `tilemapresource.xml` - Directory with XYZ tiles and `metadata.json` - Online [TMS](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification) @@ -68,9 +73,46 @@ Publicly available MBTiles charts can be found from: Plugin adds support for `/resources/charts` endpoints described in [Signal K specification](http://signalk.org/specification/1.0.0/doc/otherBranches.html#resourcescharts): -- `GET /signalk/v1/api/resources/charts/` returns metadata for all available charts -- `GET /signalk/v1/api/resources/charts/${identifier}/` returns metadata for selected chart -- `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. +- Return metadata for all available charts + +```bash +# v1 API +GET /signalk/v1/api/resources/charts/` + +# v2 API +GET /signalk/v2/api/resources/charts/` +``` + +- Return metadata for selected chart + +```bash +# v1 API +GET /signalk/v1/api/resources/charts/${identifier}` + +# v2 API +GET /signalk/v2/api/resources/charts/${identifier}` +``` + +#### Chart Tiles +Chart tiles are retrieved using the url defined in the chart metadata. + +For chart files placed in the path(s) defined in the plugin configuration, the url will be: + +```bash +/chart-tiles/${identifier}/${z}/${x}/${y} +``` + +#### Mapbox Styles + +For Mapbox Styles JSON files the url returned in the metadata will be: + +```bash +/chart-styles/${mapboxstyle.json} + +# when access token is defined +/chart-styles/${mapboxstyle.json}?access_token=${token} +``` + License ------- From 4a8e51547e60fed73cf90a838ef7ddbc31ba2a48 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sun, 20 Oct 2024 11:29:11 +1030 Subject: [PATCH 16/20] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bf3e4aa..0ef7f08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@signalk/charts-plugin", - "version": "3.0.0", + "version": "3.1.0", "description": "Signal K plugin to provide chart support for Signal K server", "main": "plugin/index.js", "scripts": { From 0d82e9ae23020ee4ba1a7f2aab14fdda5ea34ee9 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sun, 20 Oct 2024 11:32:07 +1030 Subject: [PATCH 17/20] chore: docs --- CHANELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANELOG.md b/CHANELOG.md index c99c506..85b1deb 100644 --- a/CHANELOG.md +++ b/CHANELOG.md @@ -4,9 +4,11 @@ - **Added**: Detection, processing and serving of mapbox style json files. Files served from `/chart-styles` +- **Added**: Ability to provide a Mapbox access token in the plugin configuration. + - **Added**: Watch chart folders for changes and refresh chart providers (#28) -- **Updated**: Moved the map tile url path to be out from under `resources/charts` to `/chart-tiles`. This better aligns with v2 multiple-provider support. +- **Updated**: Move the serving of map tiles out from under `resources/charts` to `/chart-tiles` to better aligns with v2 multiple-provider support. - **Updated**: Updated package dependencies (#35) From d222c1bc58a65780efc6e4c641a8b6710b967045 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:20:40 +1030 Subject: [PATCH 18/20] Updates to address review comments --- CHANELOG.md | 20 -------------- README.md | 8 +++--- package.json | 2 +- src/charts.ts | 4 +-- src/index.ts | 57 ++++++++++++++++++++------------------- test/expected-charts.json | 6 ++--- test/plugin-test.js | 12 ++++----- 7 files changed, 46 insertions(+), 63 deletions(-) delete mode 100644 CHANELOG.md diff --git a/CHANELOG.md b/CHANELOG.md deleted file mode 100644 index 85b1deb..0000000 --- a/CHANELOG.md +++ /dev/null @@ -1,20 +0,0 @@ -## Change Log - -### 3.1.0 - -- **Added**: Detection, processing and serving of mapbox style json files. Files served from `/chart-styles` - -- **Added**: Ability to provide a Mapbox access token in the plugin configuration. - -- **Added**: Watch chart folders for changes and refresh chart providers (#28) - -- **Updated**: Move the serving of map tiles out from under `resources/charts` to `/chart-tiles` to better aligns with v2 multiple-provider support. - -- **Updated**: Updated package dependencies (#35) - ---- - -### 3.0.0 - -- **Added**: Signal K v2 Resources API support. -- **Updated**: Ported to Typescript. \ No newline at end of file diff --git a/README.md b/README.md index ef62f5e..ded4007 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Signal K Node server plugin to provide chart metadata, such as name, description and location of the actual chart tile data. Chart metadata is derived from the following supported chart files: -- Mapbox Tiles _(.mbtiles)_ +- MBTiles _(.mbtiles)_ - Mapbox Styles _(.json)_ - TMS _(tilemapresource.xml and tiles)_ @@ -99,7 +99,7 @@ Chart tiles are retrieved using the url defined in the chart metadata. For chart files placed in the path(s) defined in the plugin configuration, the url will be: ```bash -/chart-tiles/${identifier}/${z}/${x}/${y} +/signalk/chart-tiles/${identifier}/${z}/${x}/${y} ``` #### Mapbox Styles @@ -107,10 +107,10 @@ For chart files placed in the path(s) defined in the plugin configuration, the u For Mapbox Styles JSON files the url returned in the metadata will be: ```bash -/chart-styles/${mapboxstyle.json} +/signalk/chart-styles/${mapboxstyle.json} # when access token is defined -/chart-styles/${mapboxstyle.json}?access_token=${token} +/signalk/chart-styles/${mapboxstyle.json}?access_token=${token} ``` diff --git a/package.json b/package.json index 0ef7f08..bf3e4aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@signalk/charts-plugin", - "version": "3.1.0", + "version": "3.0.0", "description": "Signal K plugin to provide chart support for Signal K server", "main": "plugin/index.js", "scripts": { diff --git a/src/charts.ts b/src/charts.ts index 7a389d3..5ac7d73 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -20,7 +20,7 @@ export function findCharts(chartBaseDir: string) { } else if (isDirectory) { return directoryToMapInfo(filePath, file.name) } else if (isMbstylesFile) { - return openMbstylesFile(filePath, file.name) + return openMapboxStylesFile(filePath, file.name) } else { return Promise.resolve(null) } @@ -108,7 +108,7 @@ export function encStyleToId(filename: string) { return filename.replace('.json', '').replaceAll(' ', '-').toLocaleLowerCase() } -async function openMbstylesFile(file: string, filename: string) { +async function openMapboxStylesFile(file: string, filename: string) { const json = JSON.parse(await fs.readFile(file, 'utf8')) const identifier = encStyleToId(filename) return { diff --git a/src/index.ts b/src/index.ts index 9a6d70e..0f938b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,8 +38,8 @@ interface ChartProviderApp const MIN_ZOOM = 1 const MAX_ZOOM = 24 const basePath = '' -const chartTilesPath = 'chart-tiles' -const chartStylesPath = 'chart-styles' +const chartTilesPath = 'signalk/chart-tiles' +const chartStylesPath = 'signalk/chart-styles' let chartPaths: Array let onlineProviders = {} let accessTokenGlobal = '' @@ -47,7 +47,7 @@ let lastWatchEvent: number | undefined const watchers: Array = [] module.exports = (app: ChartProviderApp): Plugin => { - let chartProviders: { [key: string]: ChartProvider } = {} + let _chartProviders: { [key: string]: ChartProvider } = {} const configBasePath = app.config.configPath const defaultChartsPath = path.join(configBasePath, '/charts') const serverMajorVersion = app.config.version @@ -227,11 +227,11 @@ module.exports = (app: ChartProviderApp): Plugin => { _.keys(charts).length } charts from ${chartPaths.join(', ')}.` ) - chartProviders = _.merge({}, charts, onlineProviders) + _chartProviders = _.merge({}, charts, onlineProviders) // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { console.error(`Error loading chart providers`, e.message) - chartProviders = {} + _chartProviders = {} app.setPluginError(`Error loading chart providers`) } } @@ -246,6 +246,13 @@ module.exports = (app: ChartProviderApp): Plugin => { } } + const getChartProviders = async (): Promise<{ + [id: string]: ChartProvider + }> => { + await refreshProviders() + return _chartProviders + } + const handleWatchEvent = () => { lastWatchEvent = Date.now() } @@ -258,17 +265,16 @@ module.exports = (app: ChartProviderApp): Plugin => { `/${chartTilesPath}/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)`, async (req: Request, res: Response) => { const { identifier, z, x, y } = req.params - await refreshProviders() - const provider = chartProviders[identifier] - if (!provider) { + const providers = await getChartProviders() + if (!providers[identifier]) { return res.sendStatus(404) } - switch (provider._fileFormat) { + switch (providers[identifier]._fileFormat) { case 'directory': return serveTileFromFilesystem( res, - provider, + providers[identifier], parseInt(z), parseInt(x), parseInt(y) @@ -276,14 +282,14 @@ module.exports = (app: ChartProviderApp): Plugin => { case 'mbtiles': return serveTileFromMbtiles( res, - provider, + providers[identifier], parseInt(z), parseInt(x), parseInt(y) ) default: console.log( - `Unknown chart provider fileformat ${provider._fileFormat}` + `Unknown chart provider fileformat ${providers[identifier]._fileFormat}` ) res.status(500).send() } @@ -296,9 +302,8 @@ module.exports = (app: ChartProviderApp): Plugin => { async (req: Request, res: Response) => { const { style } = req.params const identifier = encStyleToId(style) - await refreshProviders() - const provider = chartProviders[identifier] - res.sendFile(provider._filePath) + const providers = await getChartProviders() + res.sendFile(providers[identifier]._filePath) } ) @@ -308,10 +313,9 @@ module.exports = (app: ChartProviderApp): Plugin => { '/signalk/v1/api/resources/charts/:identifier', async (req: Request, res: Response) => { const { identifier } = req.params - await refreshProviders() - const provider = chartProviders[identifier] - if (provider) { - return res.json(sanitizeProvider(provider)) + const providers = await getChartProviders() + if (providers[identifier]) { + return res.json(sanitizeProvider(providers[identifier])) } else { return res.status(404).send('Not found') } @@ -321,8 +325,8 @@ module.exports = (app: ChartProviderApp): Plugin => { app.get( '/signalk/v1/api/resources/charts', async (req: Request, res: Response) => { - await refreshProviders() - const sanitized = _.mapValues(chartProviders, (provider) => + const providers = await getChartProviders() + const sanitized = _.mapValues(providers, (provider) => sanitizeProvider(provider) ) res.json(sanitized) @@ -347,19 +351,18 @@ module.exports = (app: ChartProviderApp): Plugin => { [key: string]: number | string | object | null }) => { app.debug(`** listResources()`, params) - await refreshProviders() + const providers = await getChartProviders() return Promise.resolve( - _.mapValues(chartProviders, (provider) => + _.mapValues(providers, (provider) => sanitizeProvider(provider, 2) ) ) }, getResource: async (id: string) => { app.debug(`** getResource()`, id) - await refreshProviders() - const provider = chartProviders[id] - if (provider) { - return Promise.resolve(sanitizeProvider(provider, 2)) + const providers = await getChartProviders() + if (providers[id]) { + return Promise.resolve(sanitizeProvider(providers[id], 2)) } else { throw new Error('Chart not found!') } diff --git a/test/expected-charts.json b/test/expected-charts.json index 4da2e3a..7434814 100644 --- a/test/expected-charts.json +++ b/test/expected-charts.json @@ -14,7 +14,7 @@ "minzoom": 3, "name": "MBTILES_19", "scale": 250000, - "tilemapUrl": "/chart-tiles/test/{z}/{x}/{y}", + "tilemapUrl": "/signalk/chart-tiles/test/{z}/{x}/{y}", "type": "tilelayer" }, "tms-tiles": { @@ -32,7 +32,7 @@ "minzoom": 4, "name": "Översikt Svenska Sjökort", "scale": 4000000, - "tilemapUrl": "/chart-tiles/tms-tiles/{z}/{x}/{y}", + "tilemapUrl": "/signalk/chart-tiles/tms-tiles/{z}/{x}/{y}", "type": "tilelayer" }, "unpacked-tiles": { @@ -50,7 +50,7 @@ "minzoom": 3, "name": "NOAA MBTiles test file", "scale": 250000, - "tilemapUrl": "/chart-tiles/unpacked-tiles/{z}/{x}/{y}", + "tilemapUrl": "/signalk/chart-tiles/unpacked-tiles/{z}/{x}/{y}", "type": "tilelayer" } } \ No newline at end of file diff --git a/test/plugin-test.js b/test/plugin-test.js index 421f19d..00bc8b6 100644 --- a/test/plugin-test.js +++ b/test/plugin-test.js @@ -110,7 +110,7 @@ describe('GET /resources/charts', () => { }) -describe('GET /chart-tiles/:identifier/:z/:x/:y', () => { +describe('GET /signalk/chart-tiles/:identifier/:z/:x/:y', () => { let plugin let testServer beforeEach(() => @@ -127,7 +127,7 @@ describe('GET /chart-tiles/:identifier/:z/:x/:y', () => { it('returns correct tile from MBTiles file', () => { return plugin.start({}) - .then(() => get(testServer, '/chart-tiles/test/4/5/6')) + .then(() => get(testServer, '/signalk/chart-tiles/test/4/5/6')) .then(response => { // unpacked-tiles contains same tiles as the test.mbtiles file expectTileResponse(response, 'charts/unpacked-tiles/4/5/6.png', 'image/png') @@ -137,7 +137,7 @@ describe('GET /chart-tiles/:identifier/:z/:x/:y', () => { it('returns correct tile from directory', () => { const expectedTile = fs.readFileSync(path.resolve(__dirname, 'charts/unpacked-tiles/4/4/6.png')) return plugin.start({}) - .then(() => get(testServer, '/chart-tiles/unpacked-tiles/4/4/6')) + .then(() => get(testServer, '/signalk/chart-tiles/unpacked-tiles/4/4/6')) .then(response => { expectTileResponse(response, 'charts/unpacked-tiles/4/4/6.png', 'image/png') }) @@ -147,7 +147,7 @@ describe('GET /chart-tiles/:identifier/:z/:x/:y', () => { const expectedTile = fs.readFileSync(path.resolve(__dirname, 'charts/tms-tiles/5/17/21.png')) // Y-coordinate flipped return plugin.start({}) - .then(() => get(testServer, '/chart-tiles/tms-tiles/5/17/10')) + .then(() => get(testServer, '/signalk/chart-tiles/tms-tiles/5/17/10')) .then(response => { expectTileResponse(response, 'charts/tms-tiles/5/17/21.png', 'image/png') }) @@ -155,7 +155,7 @@ describe('GET /chart-tiles/:identifier/:z/:x/:y', () => { it('returns 404 for missing tile', () => { return plugin.start({}) - .then(() => get(testServer, '/chart-tiles/tms-tiles/5/55/10')) + .then(() => get(testServer, '/signalk/chart-tiles/tms-tiles/5/55/10')) .catch(e => e.response) .then(response => { expect(response.status).to.equal(404) @@ -164,7 +164,7 @@ describe('GET /chart-tiles/:identifier/:z/:x/:y', () => { it('returns 404 for wrong chart identifier', () => { return plugin.start({}) - .then(() => get(testServer, '/chart-tiles/foo/4/4/6')) + .then(() => get(testServer, '/signalk/chart-tiles/foo/4/4/6')) .catch(e => e.response) .then(response => { expect(response.status).to.equal(404) From 62bef44eb534ed7e68a4f8d2c4b29b1d0890c0ef Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:42:13 +1030 Subject: [PATCH 19/20] chore: Update README --- README.md | 68 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index ded4007..13f604c 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ Signal K Node server plugin to provide chart metadata, such as name, description and location of the actual chart tile data. -Chart metadata is derived from the following supported chart files: +Chart metadata is derived from the following supported chart file types: - MBTiles _(.mbtiles)_ - Mapbox Styles _(.json)_ - TMS _(tilemapresource.xml and tiles)_ -Additionally chart metadata can be defined for other chart sources and types _(e.g. WMS, WMTS, S-57 tiles and tilejson)_. +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)_. -Chart metadata made available to both v1 and v2 Signal K resources api paths. +Chart metadata is made available to both v1 and v2 Signal K `resources` api paths. | Server Version | API | Path | |--- |--- |--- | @@ -17,48 +17,64 @@ Chart metadata made available to both v1 and v2 Signal K resources api paths. | 2.x.x | v2 | `/signalk/v2/api/resources/charts` | -_Note: v2 resource paths will only be made available on Signal K server >= v2._ +_Note: Version 2 resource paths will only be made available on Signal K server v2.0.0 and later_ -### Usage +## Usage -1. Install "Signal K Charts" plugin from Signal K Appstore +1. Install `@signalk/signalk-charts` from the Signal K Server Appstore -2. Configure plugin in **Plugin Config** +2. Configure the plugin in the Admin UI _(**Server -> Plugin Config -> Signal K Charts**)_ -- Add "Chart paths" which are the paths to the folders where chart files are stored. Defaults to `${signalk-configuration-path}/charts` +3. Activate the plugin +Chart metadata will then be available to client apps via the resources api `/resources/charts` for example: +- [Freeboard SK](https://www.npmjs.com/package/@signalk/freeboard-sk) +- [Tuktuk Chart Plotter](https://www.npmjs.com/package/tuktuk-chart-plotter) + + +## Configuration + + +### Local Chart Files -3. Add "Chart paths" in plugin configuration. Defaults to `${signalk-configuration-path}/charts` +If you are using chart files stored on the Signal K Server you will need to add the locations where the chart files are stored so the plugin can generate the chart metadata. + +Do this by adding "Chart paths" and providing the path to each folder on the Signal K Server where chart files are stored. _(Defaults to `${signalk-configuration-path}/charts`)_ Chart paths configuration +When chart files are added to the folder(s) they will be processed by the plugin and the chart metadata will be available. -4. Put charts into selected paths +### Online chart providers -5. Add optional online chart providers +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. -Online chart providers configuration +You will need to provide the following information: +1. A chart name for client applications to display +2. The URL to the chart source +3. Select the chart image format +4. The minimum and maximum zoom levels where chart data is available. -6. (Optional): Add Mapbox access token. - When provided, the access token will added to the url of Mapbox Styles _e.g. `?access_token=xyz123`_ +You can also provide a description detailing the chart content. - ![image](https://github.com/user-attachments/assets/b4d4d048-2ab1-4bf1-896b-2ca0031ec77f) +Online chart providers configuration +For WMS & WMTS sources you can specify the layers you wish to display. -_WMS example:_ +Online chart provider layers -server type configuration +### Mapbox access token (Optional) -6. Activate plugin +If you are using chart sources that require a Mapbox access token then you can provide your in the space provided. + +![image](https://github.com/user-attachments/assets/b4d4d048-2ab1-4bf1-896b-2ca0031ec77f) + +--- -7. Use one of the client apps supporting Signal K charts, for example: -- [Freeboard SK](https://www.npmjs.com/package/@signalk/freeboard-sk) -- [Tuktuk Chart Plotter](https://www.npmjs.com/package/tuktuk-chart-plotter) ### Supported chart formats -pk.eyJ1IjoiYWRhbTIyMjIiLCJhIjoiY2l5dGJhaW96MDAwcDJ3bzM0MXk2aTB0bSJ9.kgHNRDiGEmq12toljp2-kA -- [MBTiles](https://github.com/mapbox/mbtiles-spec) file +- [MBTiles](https://github.com/mapbox/mbtiles-spec) files - [Mapbox Style](https://docs.mapbox.com/help/glossary/style/) JSON file _e.g. `bright-v9.json`_ - Directory with cached [TMS](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification) tiles and `tilemapresource.xml` - Directory with XYZ tiles and `metadata.json` @@ -69,11 +85,13 @@ Publicly available MBTiles charts can be found from: - [Finnish Transport Agency nautical charts](https://github.com/vokkim/rannikkokartat-mbtiles) - [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) +--- + ### API Plugin adds support for `/resources/charts` endpoints described in [Signal K specification](http://signalk.org/specification/1.0.0/doc/otherBranches.html#resourcescharts): -- Return metadata for all available charts +- List available charts ```bash # v1 API @@ -96,7 +114,7 @@ GET /signalk/v2/api/resources/charts/${identifier}` #### Chart Tiles Chart tiles are retrieved using the url defined in the chart metadata. -For chart files placed in the path(s) defined in the plugin configuration, the url will be: +For local chart files located in the Chart Path(s) defined in the plugin configuration, the url will be: ```bash /signalk/chart-tiles/${identifier}/${z}/${x}/${y} From 9233bb7e419e39652d002392d3d5c8539bfda0f8 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sat, 5 Jul 2025 16:44:56 +0930 Subject: [PATCH 20/20] remove mapboxstyle support --- README.md | 24 ++---------------- src/charts.ts | 41 +++---------------------------- src/index.ts | 68 +++++++++------------------------------------------ 3 files changed, 18 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 13f604c..597376d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ Signal K Node server plugin to provide chart metadata, such as name, description Chart metadata is derived from the following supported chart file types: - MBTiles _(.mbtiles)_ -- Mapbox Styles _(.json)_ - TMS _(tilemapresource.xml and tiles)_ 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)_. @@ -45,6 +44,7 @@ Do this by adding "Chart paths" and providing the path to each folder on the Sig When chart files are added to the folder(s) they will be processed by the plugin and the chart metadata will be available. + ### Online chart providers 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. @@ -63,19 +63,10 @@ For WMS & WMTS sources you can specify the layers you wish to display. Online chart provider layers -### Mapbox access token (Optional) - -If you are using chart sources that require a Mapbox access token then you can provide your in the space provided. - -![image](https://github.com/user-attachments/assets/b4d4d048-2ab1-4bf1-896b-2ca0031ec77f) - ---- - ### Supported chart formats - [MBTiles](https://github.com/mapbox/mbtiles-spec) files -- [Mapbox Style](https://docs.mapbox.com/help/glossary/style/) JSON file _e.g. `bright-v9.json`_ - Directory with cached [TMS](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification) tiles and `tilemapresource.xml` - Directory with XYZ tiles and `metadata.json` - Online [TMS](https://wiki.osgeo.org/wiki/Tile_Map_Service_Specification) @@ -85,6 +76,7 @@ Publicly available MBTiles charts can be found from: - [Finnish Transport Agency nautical charts](https://github.com/vokkim/rannikkokartat-mbtiles) - [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) + --- ### API @@ -120,18 +112,6 @@ For local chart files located in the Chart Path(s) defined in the plugin configu /signalk/chart-tiles/${identifier}/${z}/${x}/${y} ``` -#### Mapbox Styles - -For Mapbox Styles JSON files the url returned in the metadata will be: - -```bash -/signalk/chart-styles/${mapboxstyle.json} - -# when access token is defined -/signalk/chart-styles/${mapboxstyle.json}?access_token=${token} -``` - - License ------- Copyright 2018 Mikko Vesikkala diff --git a/src/charts.ts b/src/charts.ts index 5ac7d73..0351311 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -14,13 +14,10 @@ export function findCharts(chartBaseDir: string) { const isMbtilesFile = file.name.match(/\.mbtiles$/i) const filePath = path.resolve(chartBaseDir, file.name) const isDirectory = file.isDirectory() - const isMbstylesFile = file.name.match(/\.json$/i) if (isMbtilesFile) { return openMbtilesFile(filePath, file.name) } else if (isDirectory) { return directoryToMapInfo(filePath, file.name) - } else if (isMbstylesFile) { - return openMapboxStylesFile(filePath, file.name) } else { return Promise.resolve(null) } @@ -83,13 +80,13 @@ function openMbtilesFile(file: string, filename: string) { type: 'tilelayer', scale: parseInt(res.metadata.scale) || 250000, v1: { - tilemapUrl: `~basePath~/~tilePath~/${identifier}/{z}/{x}/{y}`, + tilemapUrl: `~tilePath~/${identifier}/{z}/{x}/{y}`, chartLayers: res.metadata.vector_layers ? parseVectorLayers(res.metadata.vector_layers) : [] }, v2: { - url: `~basePath~/~tilePath~/${identifier}/{z}/{x}/{y}`, + url: `~tilePath~/${identifier}/{z}/{x}/{y}`, layers: res.metadata.vector_layers ? parseVectorLayers(res.metadata.vector_layers) : [] @@ -104,36 +101,6 @@ function openMbtilesFile(file: string, filename: string) { ) } -export function encStyleToId(filename: string) { - return filename.replace('.json', '').replaceAll(' ', '-').toLocaleLowerCase() -} - -async function openMapboxStylesFile(file: string, filename: string) { - const json = JSON.parse(await fs.readFile(file, 'utf8')) - const identifier = encStyleToId(filename) - return { - _flipY: false, - name: json.name, - description: '', - identifier, - bounds: undefined, - minzoom: undefined, - maxzoom: undefined, - format: undefined, - type: 'mapboxstyle', - scale: 250000, - _filePath: file, - v1: { - tilemapUrl: `~basePath~/~stylePath~/${filename}`, - chartLayers: undefined - }, - v2: { - url: `~basePath~/~stylePath~/${filename}`, - layers: undefined - } - } -} - function parseVectorLayers(layers: Array<{ id: string }>) { return layers.map((l) => l.id) } @@ -166,11 +133,11 @@ function directoryToMapInfo(file: string, identifier: string) { ;(info._fileFormat = 'directory'), (info._filePath = file), (info.v1 = { - tilemapUrl: `~basePath~/~tilePath~/${identifier}/{z}/{x}/{y}`, + tilemapUrl: `~tilePath~/${identifier}/{z}/{x}/{y}`, chartLayers: [] }) info.v2 = { - url: `~basePath~/~tilePath~/${identifier}/{z}/{x}/{y}`, + url: `~tilePath~/${identifier}/{z}/{x}/{y}`, layers: [] } diff --git a/src/index.ts b/src/index.ts index 0f938b1..6c403e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,13 +2,13 @@ import * as bluebird from 'bluebird' import path from 'path' import fs, { FSWatcher } from 'fs' import * as _ from 'lodash' -import { findCharts, encStyleToId } from './charts' +import { findCharts } from './charts' import { ChartProvider, OnlineChartProvider } from './types' import { Request, Response, Application } from 'express' import { OutgoingHttpHeaders } from 'http' import { Plugin, - PluginServerApp, + ServerAPI, ResourceProviderRegistry } from '@signalk/server-api' @@ -19,14 +19,9 @@ interface Config { } interface ChartProviderApp - extends PluginServerApp, + extends ServerAPI, ResourceProviderRegistry, Application { - statusMessage?: () => string - error: (msg: string) => void - debug: (...msg: unknown[]) => void - setPluginStatus: (pluginId: string, status?: string) => void - setPluginError: (pluginId: string, status?: string) => void config: { ssl: boolean configPath: string @@ -37,12 +32,9 @@ interface ChartProviderApp const MIN_ZOOM = 1 const MAX_ZOOM = 24 -const basePath = '' -const chartTilesPath = 'signalk/chart-tiles' -const chartStylesPath = 'signalk/chart-styles' +const chartTilesPath = '/signalk/chart-tiles' let chartPaths: Array let onlineProviders = {} -let accessTokenGlobal = '' let lastWatchEvent: number | undefined const watchers: Array = [] @@ -60,11 +52,6 @@ module.exports = (app: ChartProviderApp): Plugin => { title: 'Signal K Charts', type: 'object', properties: { - accessToken: { - type: 'string', - title: 'MapBox Access Token (optional)', - description: `Token to append to mapbox style urls for authentication. e.g. "?access_token=xxxxx"` - }, chartPaths: { type: 'array', title: 'Chart paths', @@ -174,13 +161,11 @@ module.exports = (app: ChartProviderApp): Plugin => { } const doStartup = async (config: Config) => { - app.debug('** loaded config: ', config) + app.debug(`** loaded config: ${config}`) registerRoutes() app.setPluginStatus('Started') - accessTokenGlobal = config.accessToken ?? '' - chartPaths = _.isEmpty(config.chartPaths) ? [defaultChartsPath] : resolveUniqueChartPaths(config.chartPaths, configBasePath) @@ -196,7 +181,7 @@ module.exports = (app: ChartProviderApp): Plugin => { ) chartPaths.forEach((p) => { - app.debug('watching folder..', p) + app.debug(`watching folder.. ${p}`) watchers.push(fs.watch(p, 'utf8', () => handleWatchEvent())) }) @@ -262,7 +247,7 @@ module.exports = (app: ChartProviderApp): Plugin => { app.debug(`** Registering map tile path (${chartTilesPath} **`) app.get( - `/${chartTilesPath}/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)`, + `${chartTilesPath}/:identifier/:z([0-9]*)/:x([0-9]*)/:y([0-9]*)`, async (req: Request, res: Response) => { const { identifier, z, x, y } = req.params const providers = await getChartProviders() @@ -296,17 +281,6 @@ module.exports = (app: ChartProviderApp): Plugin => { } ) - app.debug(`** Registering MapBox styles path (${chartStylesPath} **`) - app.get( - `/${chartStylesPath}/:style`, - async (req: Request, res: Response) => { - const { style } = req.params - const identifier = encStyleToId(style) - const providers = await getChartProviders() - res.sendFile(providers[identifier]._filePath) - } - ) - app.debug('** Registering v1 API paths **') app.get( @@ -350,7 +324,7 @@ module.exports = (app: ChartProviderApp): Plugin => { listResources: async (params: { [key: string]: number | string | object | null }) => { - app.debug(`** listResources()`, params) + app.debug(`** listResources() ${params}`) const providers = await getChartProviders() return Promise.resolve( _.mapValues(providers, (provider) => @@ -359,7 +333,7 @@ module.exports = (app: ChartProviderApp): Plugin => { ) }, getResource: async (id: string) => { - app.debug(`** getResource()`, id) + app.debug(`** getResource() ${id}`) const providers = await getChartProviders() if (providers[id]) { return Promise.resolve(sanitizeProvider(providers[id], 2)) @@ -425,34 +399,16 @@ const convertOnlineProviderConfig = (provider: OnlineChartProvider) => { return data } -const applyAccessToken = (uri: string) => { - if (uri.includes('access_token') || !uri.includes('~stylePath~')) { - return uri - } else { - return `${uri}?access_token=${accessTokenGlobal}` - } -} - const sanitizeProvider = (provider: ChartProvider, version = 1) => { let v if (version === 1) { v = _.merge({}, provider.v1) - const uri = applyAccessToken(v?.tilemapUrl) - v.tilemapUrl = uri - ? uri - .replace('~basePath~', basePath) - .replace('~stylePath~', chartStylesPath) - .replace('~tilePath~', chartTilesPath) + v.tilemapUrl = v.tilemapUrl + ? v.tilemapUrl.replace('~tilePath~', chartTilesPath) : '' } else { v = _.merge({}, provider.v2) - const uri = applyAccessToken(v?.url) - v.url = uri - ? uri - .replace('~basePath~', basePath) - .replace('~stylePath~', chartStylesPath) - .replace('~tilePath~', chartTilesPath) - : '' + v.url = v.url ? v.url.replace('~tilePath~', chartTilesPath) : '' } provider = _.omit(provider, [ '_filePath',