@mapwhit/glyph-server 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +56 -0
- package/index.js +25 -0
- package/lib/find-fonts.js +35 -0
- package/lib/get-fonts-pbf.js +51 -0
- package/lib/map-glyph-server.js +66 -0
- package/package.json +45 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[![NPM version][npm-image]][npm-url]
|
|
2
|
+
[![Build Status][build-image]][build-url]
|
|
3
|
+
[![Dependency Status][deps-image]][deps-url]
|
|
4
|
+
|
|
5
|
+
# map-glyph-server
|
|
6
|
+
|
|
7
|
+
Server for glyph/font files compatible with [mapbox-gl-js], [maplibre-gl-js], [mapwhit-gl-js]
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
$ npm install --global @mapwhit/glyph-server
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Environment
|
|
16
|
+
|
|
17
|
+
The following environment variables are used by `@mapwhit/glyph-server`
|
|
18
|
+
|
|
19
|
+
- `MAP_GLYPH_SERVER_FONT_PATH` - needs to specify the location of the fonts files
|
|
20
|
+
- `MAP_GLYPH_SERVER_PORT` - local port to which `@mapwhit/glyph-server` binds to default: *3060*
|
|
21
|
+
- `MAP_GLYPH_SERVER_CACHE_MAX_AGE` - if specified used as a `max-age` value of the
|
|
22
|
+
[`Cache-Control` header][cache-control] when serving font/pbf files
|
|
23
|
+
(maximum amount of seconds a resource is considered fresh)
|
|
24
|
+
|
|
25
|
+
`/etc/default/map-glyph-server` can be used to set environment variables
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Prior to starting the server glyphs should be located in `MAP_GLYPH_SERVER_FONT_PATH` directory. This can
|
|
30
|
+
be achieved using [openmaptiles/fonts] or a similar utility.
|
|
31
|
+
|
|
32
|
+
In order to use the fonts edit [Style JSON][glyphs-doc] and change the endpoint for glyphs:
|
|
33
|
+
|
|
34
|
+
"glyphs": "https://fonts.example.com/fonts/{fontstack}/{range}.pbf",
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT © [Damian Krzeminski](https://furkot.com)
|
|
39
|
+
|
|
40
|
+
[npm-image]: https://img.shields.io/npm/v/@mapwhit/glyph-server
|
|
41
|
+
[npm-url]: https://npmjs.org/package/@mapwhit/glyph-server
|
|
42
|
+
|
|
43
|
+
[build-url]: https://github.com/mapwhit/glyph-server/actions/workflows/check.yaml
|
|
44
|
+
[build-image]: https://img.shields.io/github/actions/workflow/status/mapwhit/glyph-server/check.yaml?branch=main
|
|
45
|
+
|
|
46
|
+
[deps-image]: https://img.shields.io/librariesio/release/npm/@mapwhit/glyph-server
|
|
47
|
+
[deps-url]: https://libraries.io/npm/@mapwhit%2Fglyph-server
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
[openmaptiles/fonts]: https://github.com/openmaptiles/fonts
|
|
51
|
+
[cache-control]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
|
52
|
+
[glyphs-doc]: https://docs.mapbox.com/mapbox-gl-js/style-spec/glyphs/
|
|
53
|
+
|
|
54
|
+
[mapbox-gl-js]: https://github.com/mapbox/mapbox-gl-js
|
|
55
|
+
[maplibre-gl-js]: https://github.com/maplibre/maplibre-gl-js
|
|
56
|
+
[mapwhit-gl-js]: https://github.com/mapwhit/mapwhit-gl-js
|
package/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
require('dotenv').config({ path: '/etc/default/map-glyph-server' });
|
|
4
|
+
|
|
5
|
+
const connect = require('@pirxpilot/connect');
|
|
6
|
+
const router = require('./lib/map-glyph-server');
|
|
7
|
+
|
|
8
|
+
const PORT = process.env.MAP_GLYPH_SERVER_PORT || 3060;
|
|
9
|
+
const FONT_PATH = process.env.MAP_GLYPH_SERVER_FONT_PATH;
|
|
10
|
+
|
|
11
|
+
if (!FONT_PATH) {
|
|
12
|
+
console.error('Please configure MAP_GLYPH_SERVER_FONT_PATH');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const app = connect();
|
|
17
|
+
|
|
18
|
+
app.use('/fonts', router(FONT_PATH));
|
|
19
|
+
|
|
20
|
+
module.exports = app;
|
|
21
|
+
|
|
22
|
+
if (!module.parent) {
|
|
23
|
+
app.listen(PORT);
|
|
24
|
+
console.log('Listening on port', PORT);
|
|
25
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const { readdir, stat } = require('node:fs/promises');
|
|
2
|
+
const { join } = require('node:path');
|
|
3
|
+
|
|
4
|
+
const debug = require('debug')('map-glyph-server');
|
|
5
|
+
|
|
6
|
+
module.exports = findFonts;
|
|
7
|
+
|
|
8
|
+
const memo = Object.create(null);
|
|
9
|
+
|
|
10
|
+
function findFonts(fontPath) {
|
|
11
|
+
let p = memo[fontPath];
|
|
12
|
+
if (!p) {
|
|
13
|
+
memo[fontPath] = p = findFontsImplementation(fontPath);
|
|
14
|
+
}
|
|
15
|
+
return p;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function findFontsImplementation(fontPath) {
|
|
19
|
+
debug('Looking for fonts in:', fontPath);
|
|
20
|
+
const dirs = await readdir(fontPath, { withFileTypes: true });
|
|
21
|
+
const fonts = {};
|
|
22
|
+
await Promise.all(
|
|
23
|
+
dirs.filter(d => d.isDirectory() || d.isSymbolicLink()).map(checkDir)
|
|
24
|
+
);
|
|
25
|
+
return fonts;
|
|
26
|
+
|
|
27
|
+
async function checkDir({ name, parentPath }) {
|
|
28
|
+
const filename = join(parentPath, name, '0-255.pbf');
|
|
29
|
+
const st = await stat(filename).catch(() => null);
|
|
30
|
+
if (st?.isFile()) {
|
|
31
|
+
debug('Found font', name);
|
|
32
|
+
fonts[name] = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { join } = require('node:path');
|
|
2
|
+
const { readFile } = require('node:fs/promises');
|
|
3
|
+
|
|
4
|
+
const { combine } = require('@furkot/glyph-pbf-composite');
|
|
5
|
+
|
|
6
|
+
const debug = require('debug')('map-glyph-server');
|
|
7
|
+
|
|
8
|
+
module.exports = getFontsPbf;
|
|
9
|
+
|
|
10
|
+
async function getFontPbf(fontPath, choices, range) {
|
|
11
|
+
debug('Looking for', choices[0], range);
|
|
12
|
+
const checked = Object.create(null);
|
|
13
|
+
|
|
14
|
+
for (const name of choices) {
|
|
15
|
+
const pbf = await loadFont(name);
|
|
16
|
+
if (pbf) {
|
|
17
|
+
return pbf;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function loadFont(name) {
|
|
22
|
+
const filename = join(fontPath, name, `${range}.pbf`);
|
|
23
|
+
|
|
24
|
+
if (checked[filename]) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const data = await readFile(filename);
|
|
29
|
+
debug('Font found:', name);
|
|
30
|
+
return data;
|
|
31
|
+
} catch {
|
|
32
|
+
debug('Font not found:', name);
|
|
33
|
+
checked[filename] = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function getFontsPbf(fontPath, fonts, range, fallbacks) {
|
|
39
|
+
let pbfs = await Promise.all(
|
|
40
|
+
fonts.map(font => getFontPbf(fontPath, [font, ...fallbacks], range))
|
|
41
|
+
);
|
|
42
|
+
// only non-empty fonts are OK
|
|
43
|
+
pbfs = pbfs.filter(p => p?.length);
|
|
44
|
+
debug(pbfs);
|
|
45
|
+
if (pbfs.length < fonts.length) {
|
|
46
|
+
console.log('here');
|
|
47
|
+
throw 404;
|
|
48
|
+
}
|
|
49
|
+
const result = pbfs.length > 1 ? combine(pbfs) : pbfs[0];
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const Router = require('@pirxpilot/router');
|
|
2
|
+
|
|
3
|
+
const findFonts = require('./find-fonts');
|
|
4
|
+
const getFontsPbf = require('./get-fonts-pbf');
|
|
5
|
+
const entityTag = require('etag');
|
|
6
|
+
const fresh = require('fresh');
|
|
7
|
+
|
|
8
|
+
const CACHE_MAX_AGE = process.env.MAP_GLYPH_SERVER_CACHE_MAX_AGE;
|
|
9
|
+
|
|
10
|
+
async function find(req, res, next) {
|
|
11
|
+
const fonts = await findFonts(req.fontPath);
|
|
12
|
+
req.fonts = fonts;
|
|
13
|
+
req.fontList = Object.keys(fonts);
|
|
14
|
+
next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sendFontsList(req, res) {
|
|
18
|
+
const fontList = req.fontList.sort();
|
|
19
|
+
const payload = JSON.stringify(fontList);
|
|
20
|
+
res.setHeader('Content-Type', 'application/json');
|
|
21
|
+
res.setHeader('Content-Length', Buffer.byteLength(payload));
|
|
22
|
+
return res.end(payload);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function sendFontsPbf(req, res) {
|
|
26
|
+
const { fontPath } = req;
|
|
27
|
+
const { fontstack, range } = req.params;
|
|
28
|
+
|
|
29
|
+
const fontstacks = decodeURI(fontstack).split(',');
|
|
30
|
+
|
|
31
|
+
const pbf = await getFontsPbf(fontPath, fontstacks, range, req.fontList);
|
|
32
|
+
if (pbf.length === 0) {
|
|
33
|
+
return res.writeHeader(204).end();
|
|
34
|
+
}
|
|
35
|
+
if (CACHE_MAX_AGE) {
|
|
36
|
+
const etag = entityTag(pbf);
|
|
37
|
+
res.setHeader('Cache-Control', `public, max-age=${CACHE_MAX_AGE}`);
|
|
38
|
+
res.setHeader('ETag', etag);
|
|
39
|
+
if (fresh(req.headers, { etag })) {
|
|
40
|
+
return res.writeHeader(304).end();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
res.setHeader('Content-Type', 'application/x-protobuf');
|
|
44
|
+
res.setHeader('Content-Length', pbf.length);
|
|
45
|
+
res.end(pbf);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = function (fontPath) {
|
|
49
|
+
const router = new Router({
|
|
50
|
+
strict: true,
|
|
51
|
+
caseSensitive: true
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
router.use(function (req, res, next) {
|
|
55
|
+
req.fontPath = fontPath;
|
|
56
|
+
next();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
router.get(
|
|
60
|
+
/^\/(?<fontstack>[^/]+)\/(?<range>\d+-\d+)\.pbf$/,
|
|
61
|
+
find,
|
|
62
|
+
sendFontsPbf
|
|
63
|
+
);
|
|
64
|
+
router.get('/.json', find, sendFontsList);
|
|
65
|
+
return router;
|
|
66
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mapwhit/glyph-server",
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "Server for glyph/font files compatible with mapbox-gl-js",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Damian Krzeminski",
|
|
7
|
+
"email": "pirxpilot@furkot.com",
|
|
8
|
+
"url": "https://furkot.com"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/mapwhit/glyph-server.git"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"map-glyph-server",
|
|
17
|
+
"map",
|
|
18
|
+
"mapbox",
|
|
19
|
+
"font",
|
|
20
|
+
"vector tiles"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"map-glyph-server": "index.js"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@furkot/glyph-pbf-composite": "~1",
|
|
27
|
+
"@pirxpilot/connect": "~4",
|
|
28
|
+
"@pirxpilot/router": "~1",
|
|
29
|
+
"debug": "~4",
|
|
30
|
+
"dotenv": "~16",
|
|
31
|
+
"etag": "~1",
|
|
32
|
+
"fresh": "~2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "^1.9.4",
|
|
36
|
+
"supertest-fetch": "~2"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "make check"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"index.js",
|
|
43
|
+
"lib"
|
|
44
|
+
]
|
|
45
|
+
}
|