tileblaster 0.4.9 → 1.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.
- package/UNLICENSE.md +9 -0
- package/bin/tileblaster.js +75 -25
- package/builtins/cache.js +160 -0
- package/builtins/check.js +115 -0
- package/builtins/compress.js +91 -0
- package/builtins/cors.js +33 -0
- package/builtins/deliver.js +34 -0
- package/builtins/dump.js +19 -0
- package/builtins/edit.js +30 -0
- package/builtins/mbtiles.js +52 -0
- package/builtins/modernize.js +57 -0
- package/builtins/noop.js +5 -0
- package/builtins/optimize.js +119 -0
- package/builtins/parse.js +52 -0
- package/builtins/pmtiles.js +80 -0
- package/builtins/sharp.js +46 -0
- package/builtins/tileserver.js +106 -0
- package/builtins/versatiles.js +68 -0
- package/config.dist.js +153 -0
- package/docs/config.md +343 -0
- package/docs/examples.md +257 -0
- package/docs/tileblaster.png +0 -0
- package/docs/tileblaster.svg +261 -0
- package/docs/todo.md +140 -0
- package/lib/debug.js +68 -0
- package/lib/decompress.js +44 -0
- package/lib/load.js +26 -0
- package/lib/mime.js +51 -0
- package/lib/purge.js +109 -0
- package/lib/retrieve.js +15 -0
- package/lib/rfc822date.js +9 -0
- package/lib/router.js +95 -0
- package/lib/store.js +161 -0
- package/lib/strtpl.js +15 -0
- package/lib/tasks.js +50 -0
- package/package.json +25 -13
- package/plugins/example.js +22 -0
- package/readme.md +66 -33
- package/tileblaster.js +445 -0
- package/LICENSE +0 -21
- package/config.js.dist +0 -81
- package/lib/tileblaster.js +0 -789
package/UNLICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
**This is free and unencumbered software released into the public domain.**
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
|
4
|
+
|
|
5
|
+
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
8
|
+
|
|
9
|
+
For more information, please refer to [unlicense.org](https://unlicense.org/)
|
package/bin/tileblaster.js
CHANGED
|
@@ -1,29 +1,79 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
(
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
|
|
3
|
+
// run only if called directly
|
|
4
|
+
if (require.main === module) {
|
|
5
|
+
|
|
6
|
+
const cluster = require("node:cluster");
|
|
7
|
+
const config = require("../lib/config");
|
|
8
|
+
const debug = require("../lib/debug");
|
|
9
|
+
|
|
10
|
+
if (cluster.isPrimary) { // main
|
|
11
|
+
|
|
12
|
+
// shutdown state
|
|
13
|
+
let shuttingdown = false;
|
|
14
|
+
|
|
15
|
+
// pool management
|
|
16
|
+
let pool = [];
|
|
17
|
+
|
|
18
|
+
// recursive fork helper
|
|
19
|
+
function fork(n){
|
|
20
|
+
worker = cluster.fork({ ...process.env, workerid: n });
|
|
21
|
+
debug.info("Started Worker #%d".bold.white, n);
|
|
22
|
+
worker.on('disconnect', function(){
|
|
23
|
+
debug.warn("Disconnect from Worker #%d".bold.white, n);
|
|
24
|
+
if (shuttingdown) { // remove worker from pool
|
|
25
|
+
pool[n] = null;
|
|
26
|
+
if (pool.filter(w=>!!w).length === 0) { // no more workers left, shutdowm
|
|
27
|
+
debug.info("All Workers terminated. Exiting.".bold.white);
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
} else { // fork new worker
|
|
31
|
+
pool[n] = fork(n);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return worker;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// initial fork for all worker threads
|
|
38
|
+
pool = Array(config.threads).fill().map(function(_,n){
|
|
39
|
+
return fork(n);
|
|
9
40
|
});
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
41
|
+
|
|
42
|
+
const shutdown = function shutdown(){
|
|
43
|
+
shuttingdown = true;
|
|
44
|
+
debug.info("Shutdown initiated".white.bold);
|
|
45
|
+
pool.forEach(function(worker,n){
|
|
46
|
+
debug.info("Sending shutdown message to Worker #%d".white.bold, n);
|
|
47
|
+
worker.send("shutdown");
|
|
48
|
+
});
|
|
49
|
+
setTimeout(function(){
|
|
50
|
+
debug.warn("Exiting Non-Graceful".white.bold);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
},2900);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
process.on("SIGINT", shutdown);
|
|
56
|
+
process.on("SIGTERM", shutdown);
|
|
57
|
+
|
|
58
|
+
// watch config file for changes, reload workers on change
|
|
59
|
+
const watch = require("node-watch");
|
|
60
|
+
watch(config._file, {}).on("change", function(evt){
|
|
61
|
+
debug.info("Config file change detected, restarting Workers".white.bold);
|
|
62
|
+
pool.forEach(function(worker,n){
|
|
63
|
+
worker.send("shutdown");
|
|
64
|
+
});
|
|
18
65
|
});
|
|
19
|
-
return config;
|
|
20
|
-
})((function(){
|
|
21
|
-
try {
|
|
22
|
-
return require(configfile);
|
|
23
|
-
} catch (err) {
|
|
24
|
-
console.error("usage: tileblaster <config.js> [--socket tileblaster.sock] [--tiles /path/to/tiles] [--queue 100] [--id mytileblaster]");
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
})())
|
|
28
|
-
).server().listen();
|
|
29
66
|
|
|
67
|
+
// create purge worker
|
|
68
|
+
const purge = require("../lib/purge");
|
|
69
|
+
setInterval(purge, 3600000).unref(); // regularly every 1h; FIXME: make configurable
|
|
70
|
+
setTimeout(purge, 300000).unref(); // run once after 5 minutes of uptime
|
|
71
|
+
|
|
72
|
+
} else { // worker
|
|
73
|
+
|
|
74
|
+
const tileblaster = require("../tileblaster");
|
|
75
|
+
const instance = tileblaster({ ...config, id: process.env.workerid });
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const dur = require("dur");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const xattr = require("fs-xattr");
|
|
5
|
+
|
|
6
|
+
const cache = {};
|
|
7
|
+
|
|
8
|
+
// file cache
|
|
9
|
+
module.exports = function({ opts, data, res }, next, skip){
|
|
10
|
+
const debug = this.lib.debug;
|
|
11
|
+
const config = this.config;
|
|
12
|
+
|
|
13
|
+
// cache opts
|
|
14
|
+
if (!cache.hasOwnProperty(data.map)) {
|
|
15
|
+
|
|
16
|
+
// store instance
|
|
17
|
+
cache.store = this.lib.store({ root: config.paths.data });
|
|
18
|
+
|
|
19
|
+
// merge opts of all uses of the cache builtin in a map config
|
|
20
|
+
opts = config.maps[data.map].filter(function(use){
|
|
21
|
+
return use.builtin === "cache";
|
|
22
|
+
}).reduce(function(opts, useopts){
|
|
23
|
+
return Object.entries(useopts).reduce(function(opts, [k,v]){
|
|
24
|
+
return opts[k]=v,opts;
|
|
25
|
+
}, opts);
|
|
26
|
+
},{});
|
|
27
|
+
|
|
28
|
+
cache[data.map] = {};
|
|
29
|
+
|
|
30
|
+
// skipto
|
|
31
|
+
cache[data.map].skipto = opts.skipto || false; // FIXME check valid label?
|
|
32
|
+
|
|
33
|
+
// true: expires instantly, false: expires never, number: seconds, string: parse
|
|
34
|
+
if (opts.hasOwnProperty("expires")) {
|
|
35
|
+
switch (typeof opts.expires) {
|
|
36
|
+
case "boolean":
|
|
37
|
+
cache[data.map].expires = opts.expires;
|
|
38
|
+
break;
|
|
39
|
+
case "number":
|
|
40
|
+
cache[data.map].expires = ((Number.isFinite(opts.expires)) ? Math.max(0,Math.round(opts.expires*1000)) : (opts.expires > 0)) || true;
|
|
41
|
+
break;
|
|
42
|
+
case "string":
|
|
43
|
+
cache[data.map].expires = dur(opts.expires, true);
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
cache[data.map].expires = true; // expires immediately
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
cache[data.map].expires = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
};
|
|
54
|
+
opts = cache[data.map];
|
|
55
|
+
const store = cache.store;
|
|
56
|
+
|
|
57
|
+
// if tile length is empty, retrieve. otherwise save
|
|
58
|
+
if (data.tiles.length === 0) {
|
|
59
|
+
|
|
60
|
+
// find extensions
|
|
61
|
+
let extensions = Object.entries(data.req.supports).reduce(function(extensions, [ext, supports]){
|
|
62
|
+
if (supports) extensions.push(ext);
|
|
63
|
+
return extensions;
|
|
64
|
+
},[]);
|
|
65
|
+
|
|
66
|
+
// get from storage
|
|
67
|
+
return store.find(data.req.path, extensions, function(err, tile){
|
|
68
|
+
if (err) return debug.error("Cache: Could not find %s: %s", data.req.path, err), next();
|
|
69
|
+
if (!tile) return next();
|
|
70
|
+
|
|
71
|
+
// check etag
|
|
72
|
+
if (data.req.etag && tile.attr.headers && data.req.etag === tile.attr.headers.etag) {
|
|
73
|
+
res.statusCode = 304;
|
|
74
|
+
res.end();
|
|
75
|
+
res.used = true;
|
|
76
|
+
return skip(); // skip rest of jobs
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// check last-modified
|
|
80
|
+
if (data.req.last && tile.stats && data.req.last < tile.stats.mtimeMs) {
|
|
81
|
+
res.statusCode = 304;
|
|
82
|
+
res.end();
|
|
83
|
+
res.used = true;
|
|
84
|
+
return skip(); // skip rest of jobs
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// if skipto is set, build tile data from cache and skip
|
|
88
|
+
if (opts.skipto) {
|
|
89
|
+
|
|
90
|
+
data.tile = tile.attr;
|
|
91
|
+
fs.readFile(tile.file, function(err, buf){
|
|
92
|
+
if (err) return debug.error("Cache: Could not read %s: %s", tile.file, err), next();
|
|
93
|
+
|
|
94
|
+
data.tile.buffer = buf;
|
|
95
|
+
data.tiles.push(data.tile);
|
|
96
|
+
|
|
97
|
+
skip(opts.skipto);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
} else { // send tile to client
|
|
101
|
+
|
|
102
|
+
// if tile buffer is empty, return 204
|
|
103
|
+
if (tile.stats.size === 0) {
|
|
104
|
+
res.statusCode = 204;
|
|
105
|
+
res.used = true;
|
|
106
|
+
res.end();
|
|
107
|
+
return skip(); // skip rest of jobs
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// set status and content-type
|
|
111
|
+
res.statusCode = tile.attr.status;
|
|
112
|
+
|
|
113
|
+
// set headers
|
|
114
|
+
Object.entries({
|
|
115
|
+
...tile.attr.headers, // tile-specific
|
|
116
|
+
"content-type": tile.attr.mimetype,
|
|
117
|
+
"content-length": tile.stats.size,
|
|
118
|
+
}).forEach(function([ k, v ]){
|
|
119
|
+
res.setHeader(k, v);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// send as stream
|
|
123
|
+
fs.createReadStream(tile.file).pipe(res);
|
|
124
|
+
res.used = true;
|
|
125
|
+
|
|
126
|
+
skip();
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
} else {
|
|
133
|
+
|
|
134
|
+
// set expires on all tiles
|
|
135
|
+
if (opts.expires !== false) {
|
|
136
|
+
let expires = Date.now()+opts.expires;
|
|
137
|
+
data.tile.expires = expires;
|
|
138
|
+
data.tiles.forEach(function(tile){
|
|
139
|
+
tile.expires = expires;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// no need to wait for files to get stored
|
|
144
|
+
next();
|
|
145
|
+
|
|
146
|
+
// skip storing if tiles expire immediately
|
|
147
|
+
if (opts.expires === true) return debug.warn("Cache: Skipping due to immediate expiration in map %s", data.map.magenta);
|
|
148
|
+
|
|
149
|
+
// store all tiles if not stored
|
|
150
|
+
Promise.allSettled(data.tiles.map(function(tile){
|
|
151
|
+
return new Promise(function(reject, resolve){
|
|
152
|
+
store.put(tile, function(err){
|
|
153
|
+
if (err) return debug.warn("Cache: Error storing %s: %s", tile.path.magenta, err), reject(err);
|
|
154
|
+
return debug.info("Cache: Stored %s", tile.path.magenta), resolve();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
};
|
|
160
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// check data.params against constraints
|
|
2
|
+
|
|
3
|
+
const cache = {};
|
|
4
|
+
|
|
5
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
6
|
+
|
|
7
|
+
// fill cache for map
|
|
8
|
+
if (!cache.hasOwnProperty(data.map)) {
|
|
9
|
+
|
|
10
|
+
cache[data.map] = {};
|
|
11
|
+
|
|
12
|
+
// assume reasonable default if no zoom level was specified
|
|
13
|
+
cache[data.map].zoom = (!opts.zoom || opts.zoom.length === 0) ? [ 0, 24 ] : opts.zoom;
|
|
14
|
+
|
|
15
|
+
// find min and max, clamp to reasonable levels
|
|
16
|
+
cache[data.map].minZoom = Math.max(0, Math.min(...cache[data.map].zoom));
|
|
17
|
+
cache[data.map].maxZoom = Math.min(24, Math.max(...cache[data.map].zoom));
|
|
18
|
+
|
|
19
|
+
// expand to array of zoom levels
|
|
20
|
+
cache[data.map].zoomLevels = Array(cache[data.map].maxZoom-cache[data.map].minZoom+1).fill().map(function(v,i){
|
|
21
|
+
return i+cache[data.map].minZoom;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (opts.bbox && Array.isArray(opts.bbox) && opts.bbox.length === 4) {
|
|
25
|
+
|
|
26
|
+
// clamp bbox to planet and sort latitude
|
|
27
|
+
// longitude might be inverted when bounds span the antimeridian
|
|
28
|
+
cache[data.map].bbox = [
|
|
29
|
+
Math.max(-180, opts.bbox[0]),
|
|
30
|
+
Math.max( -90, Math.min(opts.bbox[1], opts.bbox[3])),
|
|
31
|
+
Math.min( 180, opts.bbox[2]),
|
|
32
|
+
Math.min( 90, Math.max(opts.bbox[1], opts.bbox[3])),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// precalculate tile index bounds per zoom level, clamp to planet
|
|
36
|
+
// hint: tile indexes increment north → south
|
|
37
|
+
// hint: https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
|
|
38
|
+
|
|
39
|
+
// clamp tile index to max extent of zoom level
|
|
40
|
+
function clamp(v,z) {
|
|
41
|
+
return Math.max(0,Math.min(Math.pow(2,z)-1,v));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// tile x of zoom level from longitude
|
|
45
|
+
function lon(x,z) {
|
|
46
|
+
return clamp(Math.floor((x+180)/360*Math.pow(2,z)),z);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// tile y of zoom level from latitude
|
|
50
|
+
function lat(y,z){
|
|
51
|
+
return clamp(Math.floor((1-Math.log(Math.tan(y*Math.PI/180)+1/Math.cos(y*Math.PI/180))/Math.PI)/2*Math.pow(2,z)),z);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
cache[data.map].bounds = cache[data.map].zoomLevels.reduce(function(b,z){
|
|
55
|
+
return b[z] = [
|
|
56
|
+
lon(cache[data.map].bbox[0],z), // west
|
|
57
|
+
lat(cache[data.map].bbox[3],z), // north
|
|
58
|
+
lon(cache[data.map].bbox[2],z), // east
|
|
59
|
+
lat(cache[data.map].bbox[1],z), // south
|
|
60
|
+
],b;
|
|
61
|
+
},[]);
|
|
62
|
+
|
|
63
|
+
} else {
|
|
64
|
+
|
|
65
|
+
cache[data.map].bbox = false;
|
|
66
|
+
cache[data.map].bounds = false;
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// allowed extensions
|
|
71
|
+
cache[data.map].extensions = ((opts.extensions) ? ((Array.isArray(opts.extensions)) ? opts.extensions : ((typeof opts.extensions === "string") ? [ opts.extensions ] : [])) : []);
|
|
72
|
+
|
|
73
|
+
// density marker
|
|
74
|
+
if (opts.hasOwnProperty("density")) {
|
|
75
|
+
cache[data.map].density = (Array.isArray(opts.density) ? opts.density : [ opts.density ]).map(function(density){
|
|
76
|
+
return (!density) ? null : density;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (cache[data.map].density.length === 0) cache[data.map].density = false;
|
|
80
|
+
|
|
81
|
+
} else {
|
|
82
|
+
cache[data.map].density = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
};
|
|
86
|
+
opts = cache[data.map];
|
|
87
|
+
|
|
88
|
+
// check for NaNs
|
|
89
|
+
if (isNaN(data.params.z) || isNaN(data.params.x) || isNaN(data.params.y)) return next(new Error("illegal zxy."));
|
|
90
|
+
|
|
91
|
+
// check zoom
|
|
92
|
+
if (data.params.z < opts.minZoom || data.params.z > opts.maxZoom) return next(new Error("illegal zoom."));
|
|
93
|
+
|
|
94
|
+
// check bounds
|
|
95
|
+
if (opts.bounds) {
|
|
96
|
+
if (opts.bounds[data.params.z][0] < opts.bounds[data.params.z][2]) { // check for bounds spanning antimeridian
|
|
97
|
+
// bounds don't span antimeridian
|
|
98
|
+
if (data.params.x < opts.bounds[data.params.z][0] || data.params.x > opts.bounds[data.params.z][2]) return next(new Error("x is out of bounds."));
|
|
99
|
+
} else {
|
|
100
|
+
// bounds span antimeridian
|
|
101
|
+
if (data.params.x > opts.bounds[data.params.z][0] && data.params.x < opts.bounds[data.params.z][2]) return next(new Error("x is out of bounds, bounds span antimeridian"));
|
|
102
|
+
}
|
|
103
|
+
if (data.params.y < opts.bounds[data.params.z][1] || data.params.y > opts.bounds[data.params.z][3]) return next(new Error("y is out of bounds."));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// check extension
|
|
107
|
+
if (opts.extensions.length > 0 && !opts.extensions.includes(data.params.e) && !opts.extensions.includes(data.params.f)) return next(new Error("illegal extension."));
|
|
108
|
+
|
|
109
|
+
// check density
|
|
110
|
+
if (opts.density && !opts.density.includes(data.params.d) && !opts.density.includes(data.params.f)) return next(new Error("illegal density marker."));
|
|
111
|
+
|
|
112
|
+
// all passed
|
|
113
|
+
next();
|
|
114
|
+
|
|
115
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const zlib = require("node:zlib");
|
|
2
|
+
const promisify = require("node:util").promisify;
|
|
3
|
+
|
|
4
|
+
const brotli = promisify(zlib.brotliCompress);
|
|
5
|
+
const gzip = promisify(zlib.gzip);
|
|
6
|
+
|
|
7
|
+
const cache = {};
|
|
8
|
+
|
|
9
|
+
// compress tiles
|
|
10
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
11
|
+
const debug = this.lib.debug;
|
|
12
|
+
|
|
13
|
+
if (!cache.hasOwnProperty(data.map)) cache[data.map] = {
|
|
14
|
+
brotli: (!opts.brotli) ? false :
|
|
15
|
+
(opts.brotli === true) ? { level: 9 } : // reasonable
|
|
16
|
+
(typeof opts.brotli === "number") ? { level: opts.brotli } :
|
|
17
|
+
(typeof opts.brotli === "object") ? opts.brotli : {},
|
|
18
|
+
gzip: (!opts.gzip) ? false :
|
|
19
|
+
(opts.gzip === true) ? { level: 9 } : // reasonable
|
|
20
|
+
(typeof opts.gzip === "number") ? { level: opts.gzip } :
|
|
21
|
+
(typeof opts.gzip === "object") ? opts.gzip : {},
|
|
22
|
+
};
|
|
23
|
+
opts = cache[data.map];
|
|
24
|
+
|
|
25
|
+
// pass through if nothing to do, but complain
|
|
26
|
+
if (!opts.brotli && !opts.gzip) {
|
|
27
|
+
debug.warn("No compression enabled for map '%s'", data.map);
|
|
28
|
+
return next();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ensure unique tiles FIXME
|
|
32
|
+
|
|
33
|
+
// compress all uncompressed tiles
|
|
34
|
+
Promise.allSettled(data.tiles.filter(function(tile){
|
|
35
|
+
return (!tile.compression && tile.buffer.length > 0) // filter already compressed or empty tiles
|
|
36
|
+
}).reduce(function(promises, tile){
|
|
37
|
+
|
|
38
|
+
// FIXME: roll this up:
|
|
39
|
+
if (opts.brotli) promises.push(new Promise(function(resolve, reject) {
|
|
40
|
+
brotli(tile.buffer, opts.brotli).then(function(compressed){
|
|
41
|
+
if (compressed.length > tile.buffer.length) {
|
|
42
|
+
debug.warn("Discarding useless Brotli compression for %s: +%db", data.path.magenta, compressed.length-tile.buffer.length);
|
|
43
|
+
return resolve();
|
|
44
|
+
};
|
|
45
|
+
data.tiles.push({
|
|
46
|
+
...tile,
|
|
47
|
+
buffer: compressed,
|
|
48
|
+
compression: "br",
|
|
49
|
+
headers: { ...(tile.headers||{}), "content-encoding": "br" },
|
|
50
|
+
params: { ...(tile.params||{}), c: ".br" },
|
|
51
|
+
});
|
|
52
|
+
resolve();
|
|
53
|
+
}).catch(function(err){
|
|
54
|
+
debug.error("Brotli failed for %s: %s", data.path.magenta, err);
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
if (opts.gzip) promises.push(new Promise(function(resolve, reject) {
|
|
60
|
+
gzip(tile.buffer, opts.gzip).then(function(compressed){
|
|
61
|
+
if (compressed.length > tile.buffer.length) {
|
|
62
|
+
debug.warn("Discarding useless Gzip compression for %s: +%db", data.path.magenta, compressed.length-tile.buffer.length);
|
|
63
|
+
return resolve();
|
|
64
|
+
}
|
|
65
|
+
data.tiles.push({
|
|
66
|
+
...tile,
|
|
67
|
+
buffer: compressed,
|
|
68
|
+
compression: "gzip",
|
|
69
|
+
headers: { ...(tile.headers||{}), "content-encoding": "gzip" },
|
|
70
|
+
params: { ...(tile.params||{}), c: ".gz" },
|
|
71
|
+
});
|
|
72
|
+
resolve();
|
|
73
|
+
}).catch(function(err){
|
|
74
|
+
debug.error("Gzip failed for %s: %s", data.path.magenta, err);
|
|
75
|
+
reject(err);
|
|
76
|
+
});
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
return promises;
|
|
80
|
+
}, [])).then(function(){
|
|
81
|
+
|
|
82
|
+
// set tile to best compressed tile client accepts FIXME find by buffer size?
|
|
83
|
+
const bestCompression = (opts.brotli && data.capabilities.br) ? "br" : (opts.gzip && data.capabilities.gz) ? "gzip" : null;
|
|
84
|
+
if (bestCompression) data.tile = data.tiles.find(function(tile){
|
|
85
|
+
return tile.compression === bestCompression && tile.mimetype === data.tile.mimetype && tile.filetype === data.tile.filetype;
|
|
86
|
+
}) || data.tile;
|
|
87
|
+
|
|
88
|
+
next();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
};
|
package/builtins/cors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// send cors headers.
|
|
2
|
+
|
|
3
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
4
|
+
|
|
5
|
+
// only if origin header is set
|
|
6
|
+
if (!req.headers.hasOwnProperty("origin") || !req.headers.origin) return next();
|
|
7
|
+
|
|
8
|
+
// check origin in opts
|
|
9
|
+
if (!opts.hasOwnProperty("origins")) return next();
|
|
10
|
+
|
|
11
|
+
// ensure opts.origins is array of strings
|
|
12
|
+
if (!Array.isArray(opts.origins)) opts.origins = [ opts.origins ].filter(o=>typeof o === "string");
|
|
13
|
+
|
|
14
|
+
// check if origin is allowed cors
|
|
15
|
+
if (!opts.origins.includes("*") && opts.origins.includes(req.headers.origin)) return next();
|
|
16
|
+
|
|
17
|
+
// send common headers
|
|
18
|
+
res.setHeader("Access-Control-Allow-Origin", req.headers.origin||"*");
|
|
19
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
20
|
+
res.setHeader("Access-Control-Allow-Headers", "DNT,If-None-Match,If-Modified-Since,Cache-Control,Content-Type,Range,Accept-Encoding");
|
|
21
|
+
res.setHeader("Access-Control-Expose-Headers", "Content-Length,Content-Range,Etag,Last-Modified,Content-Encoding");
|
|
22
|
+
|
|
23
|
+
// if method is OPTGIONS, set Access-Control-Max-Age and bail
|
|
24
|
+
if (req.method === "OPTIONS") {
|
|
25
|
+
res.setHeader("Access-Control-Max-Age", 1728000);
|
|
26
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
27
|
+
res.setHeader("Content-Length", 0);
|
|
28
|
+
res.statusCode = 204;
|
|
29
|
+
res.end();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
next();
|
|
33
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const stream = require("node:stream");
|
|
2
|
+
|
|
3
|
+
// deliver tile
|
|
4
|
+
module.exports = function({ res, opts, data }, next){
|
|
5
|
+
|
|
6
|
+
// do nothing if response stream has been used
|
|
7
|
+
if (res.used || res.headersSent || !res.writable || res.destroyed || res.finished || res.closed || res.piped) return next();
|
|
8
|
+
|
|
9
|
+
// if tile buffer is empty, return 204
|
|
10
|
+
if (data.tile.buffer.length === 0) {
|
|
11
|
+
res.statusCode = 204;
|
|
12
|
+
res.end();
|
|
13
|
+
return next();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// set status and content-type
|
|
17
|
+
res.statusCode = data.tile.status;
|
|
18
|
+
|
|
19
|
+
// set headers
|
|
20
|
+
Object.entries({
|
|
21
|
+
...data.tile.headers, // tile-specific
|
|
22
|
+
...opts.headers, // from opts
|
|
23
|
+
"content-type": data.tile.mimetype,
|
|
24
|
+
"content-length": data.tile.buffer.length,
|
|
25
|
+
}).forEach(function([ k, v ]){
|
|
26
|
+
res.setHeader(k, v);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// send as stream
|
|
30
|
+
stream.Readable.from(data.tile.buffer).pipe(res);
|
|
31
|
+
res.used = true;
|
|
32
|
+
|
|
33
|
+
next();
|
|
34
|
+
};
|
package/builtins/dump.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const util = require("node:util");
|
|
2
|
+
|
|
3
|
+
// a debuging module: dump all the information and end the request
|
|
4
|
+
module.exports = function({ res, data }, next, skip){
|
|
5
|
+
const dump = util.inspect(data, false, 3);
|
|
6
|
+
|
|
7
|
+
console.error(dump);
|
|
8
|
+
|
|
9
|
+
// do nothing if response stream has been used
|
|
10
|
+
if (res.used || res.headersSent || !res.writable || res.destroyed || res.finished || res.closed || res.piped) return;
|
|
11
|
+
|
|
12
|
+
res.statusCode = 200;
|
|
13
|
+
res.setHeader("content-type", "text/plain");
|
|
14
|
+
res.end(dump);
|
|
15
|
+
|
|
16
|
+
res.used = true;
|
|
17
|
+
skip();
|
|
18
|
+
|
|
19
|
+
};
|
package/builtins/edit.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const load = require("../lib/load");
|
|
2
|
+
const vtt = load("vtt");
|
|
3
|
+
|
|
4
|
+
const cache = {};
|
|
5
|
+
|
|
6
|
+
// versatiles backend
|
|
7
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
8
|
+
const debug = this.lib.debug;
|
|
9
|
+
|
|
10
|
+
if (!vtt) return next(new Error("Dependency 'vtt' is missing."));
|
|
11
|
+
|
|
12
|
+
if (!opts.edit || typeof opts.edit !== "function") {
|
|
13
|
+
debug.warn("No edit function specified, skipping");
|
|
14
|
+
return next();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// apply to all tiles
|
|
18
|
+
data.tiles.filter(function(tile){
|
|
19
|
+
return tile.type === "pbf";
|
|
20
|
+
}).forEach(function(tile){
|
|
21
|
+
try {
|
|
22
|
+
tile.buffer = vtt.pack(opts.edit(vtt.unpack(tile.buffer)));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
debug.error("Editing vector tile failed:", err);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
next();
|
|
29
|
+
|
|
30
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const load = require("../lib/load");
|
|
2
|
+
const mbg = load("mbg");
|
|
3
|
+
|
|
4
|
+
const cache = {};
|
|
5
|
+
|
|
6
|
+
// mbtiles backend
|
|
7
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
8
|
+
const mime = this.lib.mime;
|
|
9
|
+
const debug = this.lib.debug;
|
|
10
|
+
const decompress = this.lib.decompress;
|
|
11
|
+
|
|
12
|
+
if (!mbg) return next(new Error("Dependency 'mbg' is missing."));
|
|
13
|
+
|
|
14
|
+
// cache mbg instance
|
|
15
|
+
if (!cache.hasOwnProperty(data.map)) cache[data.map] = new mbg(opts.file);
|
|
16
|
+
const mb = cache[data.map];
|
|
17
|
+
|
|
18
|
+
debug.info("Fetching %s/%s/%s", data.req.params.z, data.req.params.x, data.req.params.y);
|
|
19
|
+
mb.get(data.req.params.z, data.req.params.x, data.req.params.y, function(err, buf, info){
|
|
20
|
+
if (err) return next(err);
|
|
21
|
+
|
|
22
|
+
// decompress tile
|
|
23
|
+
decompress(buf, info.compression, function(err, buf){
|
|
24
|
+
if (err) return next(err);
|
|
25
|
+
|
|
26
|
+
// handle non-existant tiles
|
|
27
|
+
if (buf === null) return next(new Error("MBTiles: tile does not exist"));
|
|
28
|
+
|
|
29
|
+
const tile = {
|
|
30
|
+
buffer: buf,
|
|
31
|
+
type: mime.filetype(info.mimetype),
|
|
32
|
+
status: ((buf.length > 0) ? 200 : 204),
|
|
33
|
+
mimetype: info.mimetype,
|
|
34
|
+
// defaults
|
|
35
|
+
path: data.req.path,
|
|
36
|
+
headers: {},
|
|
37
|
+
compression: false,
|
|
38
|
+
language: null,
|
|
39
|
+
expires: true, // default policy
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// add to tile stack, set primary tile
|
|
43
|
+
data.tiles.unshift(tile);
|
|
44
|
+
data.tile = tile;
|
|
45
|
+
|
|
46
|
+
next();
|
|
47
|
+
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
};
|