tileblaster 0.4.8 → 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 -787
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// modernize
|
|
2
|
+
module.exports = function modernize({ opts, data }, next){
|
|
3
|
+
const sharp = this.lib.sharp;
|
|
4
|
+
const debug = this.lib.debug;
|
|
5
|
+
|
|
6
|
+
// check if sharp is available
|
|
7
|
+
if (typeof sharp === "undefined") {
|
|
8
|
+
debug.warn("Modernize: Sharp dependency is missing.");
|
|
9
|
+
return next();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// check if tile should be optimized
|
|
13
|
+
if (!["png","jpeg","jpg","gif"].includes(data.tile.type)) {
|
|
14
|
+
debug.warn("Modernize: Unsupported file type: %s", data.tile.type);
|
|
15
|
+
return next();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
Promise.allSettled(["avif","webp"].map(function(method){
|
|
19
|
+
return new Promise(function(resolve, reject) {
|
|
20
|
+
if (!opts[method]) return reject();
|
|
21
|
+
|
|
22
|
+
// copy primary tile
|
|
23
|
+
const tile = { ...data.tile };
|
|
24
|
+
|
|
25
|
+
// fixme: find suitable tile in tiles
|
|
26
|
+
sharp(tile.buffer).toFormat(method, opts[method]).toBuffer(function(err, buffer, info){
|
|
27
|
+
if (err) {
|
|
28
|
+
debug.error("Modernize: %s", err);
|
|
29
|
+
return reject();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// fix tile
|
|
33
|
+
tile.buffer = buffer;
|
|
34
|
+
tile.path = tile.path+"."+method;
|
|
35
|
+
tile.type = method;
|
|
36
|
+
tile.mimetype = "image/"+method;
|
|
37
|
+
tile.headers = { ...tile.headers };
|
|
38
|
+
|
|
39
|
+
// add to tile stack
|
|
40
|
+
data.tiles.unshift(tile);
|
|
41
|
+
|
|
42
|
+
// set primary if client supports
|
|
43
|
+
if (data.req.supports[method] && buffer.length < data.tile.buffer.length) {
|
|
44
|
+
debug.info("Modernized '%s': -%db", tile.path.magenta, data.tile.buffer.length-buffer.length);
|
|
45
|
+
data.tile = tile;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
resolve();
|
|
49
|
+
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
});
|
|
53
|
+
})).then(function(){
|
|
54
|
+
next();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
};
|
package/builtins/noop.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const worker = require("node:worker_threads");
|
|
2
|
+
const stream = require("node:stream");
|
|
3
|
+
|
|
4
|
+
if (worker.isMainThread) {
|
|
5
|
+
|
|
6
|
+
const cache = {};
|
|
7
|
+
|
|
8
|
+
// optimize
|
|
9
|
+
module.exports = function modernize({ opts, data }, next){
|
|
10
|
+
const debug = this.lib.debug;
|
|
11
|
+
const load = this.lib.load;
|
|
12
|
+
|
|
13
|
+
if (!cache.optipng) cache.optipng = load.exists("optipng-js");
|
|
14
|
+
if (!cache.mozjpeg) cache.mozjpeg = load.exists("js-mozjpeg");
|
|
15
|
+
|
|
16
|
+
if (!cache.hasOwnProperty(data.map)) cache[data.map] = {
|
|
17
|
+
png: (!opts.png) ? false : (opts.png === true) ? {} : opts.png,
|
|
18
|
+
jpeg: (!opts.jpeg) ? false : (opts.jpeg === true) ? {} : opts.jpeg,
|
|
19
|
+
};
|
|
20
|
+
opts = cache[data.map];
|
|
21
|
+
|
|
22
|
+
const jobs = [];
|
|
23
|
+
|
|
24
|
+
// find all pngs
|
|
25
|
+
if (opts.png && cache.optipng) /*[ data.tile, ...data.tiles ]*/data.tiles.filter(function(tile){
|
|
26
|
+
return (tile.type === "png" && !tile.compression && tile.buffer.length > 0) // uncompressed png only
|
|
27
|
+
}).forEach(function(tile){
|
|
28
|
+
jobs.push(new Promise(function(resolve,reject){
|
|
29
|
+
|
|
30
|
+
// create png worker
|
|
31
|
+
const w = new worker.Worker(__filename, {
|
|
32
|
+
workerData: { type: "png", opts: opts.png },
|
|
33
|
+
stdin: true, stdout: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// get result from worker
|
|
37
|
+
let result = [];
|
|
38
|
+
w.stdout.on("data", function(chunk){
|
|
39
|
+
result.push(chunk);
|
|
40
|
+
}).on("end", function(){
|
|
41
|
+
result = Buffer.concat(result);
|
|
42
|
+
if (result && result.length > 0 && result.length < tile.buffer.length) {
|
|
43
|
+
debug.info("Optimized PNG '%s': -%db", tile.path.magenta, tile.buffer.length-result.length);
|
|
44
|
+
tile.buffer = result;
|
|
45
|
+
}
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// stream buffer to worker
|
|
50
|
+
stream.Readable.from(tile.buffer).pipe(w.stdin);
|
|
51
|
+
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// find all jpegs
|
|
56
|
+
if (opts.jpeg && cache.mozjpeg) [ data.tile, ...data.tiles ].filter(function(tile){
|
|
57
|
+
return ((tile.type === "jpeg" || tile.type === "jpg") && !tile.compression && tile.buffer.length > 0) // uncompressed jpeg only
|
|
58
|
+
}).forEach(function(tile){
|
|
59
|
+
jobs.push(new Promise(function(resolve,reject){
|
|
60
|
+
|
|
61
|
+
// create jpeg worker
|
|
62
|
+
const w = new worker.Worker(__filename, {
|
|
63
|
+
workerData: { type: "jpeg", opts: opts.jpeg },
|
|
64
|
+
stdin: true, stdout: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// get result from worker
|
|
68
|
+
let result = [];
|
|
69
|
+
w.stdout.on("data", function(chunk){
|
|
70
|
+
result.push(chunk);
|
|
71
|
+
}).on("end", function(){
|
|
72
|
+
result = Buffer.concat(result);
|
|
73
|
+
if (result && result.length > 0 && result.length < tile.buffer.length) {
|
|
74
|
+
debug.info("Optimized JPEG '%s': -%db", tile.path.magenta, tile.buffer.length-result.length);
|
|
75
|
+
tile.buffer = result;
|
|
76
|
+
}
|
|
77
|
+
resolve();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// stream buffer to worker
|
|
81
|
+
stream.Readable.from(tile.buffer).pipe(w.stdin);
|
|
82
|
+
|
|
83
|
+
}));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// check if there are jobs
|
|
87
|
+
if (jobs.length === 0) return debug.warn("Nothing to optimize for '%s': -%db", data.tile.path.magenta), next();
|
|
88
|
+
|
|
89
|
+
// execute
|
|
90
|
+
Promise.allSettled(jobs).then(function(){
|
|
91
|
+
next();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
} else {
|
|
97
|
+
let buf = [];
|
|
98
|
+
process.stdin.on("data", function(chunk){
|
|
99
|
+
buf.push(chunk);
|
|
100
|
+
}).on("end", function() {
|
|
101
|
+
buf = Buffer.concat(buf);
|
|
102
|
+
if (buf.length > 0) switch (worker.workerData.type) {
|
|
103
|
+
case "png":
|
|
104
|
+
const optipng = require("optipng-js");
|
|
105
|
+
const outputpng = optipng(buf, { o: "3", i: "0", fix: true, quiet: true, ...worker.workerData.opts });
|
|
106
|
+
if (outputpng.data && outputpng.data.length > 0 && outputpng.data.length < buf.length) buf = Buffer.from(outputpng.data);
|
|
107
|
+
break;
|
|
108
|
+
case "jpg":
|
|
109
|
+
case "jpeg":
|
|
110
|
+
const jpegtran = require("js-mozjpeg").jpegtran;
|
|
111
|
+
const outputjpeg = jpegtran(buf, { optimize: true, maxmemory: "4m", copy: "none", ...(worker.workerData.opts||{}) });
|
|
112
|
+
if (outputjpeg.data && outputjpeg.data.length > 0 && outputjpeg.data.length < buf.length) buf = Buffer.from(outputjpeg.data);
|
|
113
|
+
break;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
stream.Readable.from(buf).pipe(process.stdout);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// parse client request
|
|
2
|
+
|
|
3
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
4
|
+
|
|
5
|
+
data.req = {};
|
|
6
|
+
|
|
7
|
+
data.req.path = req.path;
|
|
8
|
+
|
|
9
|
+
// cache things
|
|
10
|
+
data.req.etag = req.headers["if-none-match"] || null;
|
|
11
|
+
data.req.last = req.headers.hasOwnProperty("if-modified-since") ? Date.parse(req.headers["if-modified-since"]).valueOf() : null;
|
|
12
|
+
|
|
13
|
+
// examine some headers for capabilities
|
|
14
|
+
data.req.supports = {
|
|
15
|
+
webp: (req.headers.accept||"").includes("image/webp"),
|
|
16
|
+
avif: (req.headers.accept||"").includes("image/avif"),
|
|
17
|
+
br: (req.headers["accept-encoding"]||"").includes("br"),
|
|
18
|
+
gz: (req.headers["accept-encoding"]||"").includes("gzip"),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// languages
|
|
22
|
+
data.req.lang = ((!!req.headers["accept-language"]) ? req.headers["accept-language"].split(",").map(function(lang){
|
|
23
|
+
return lang.split(";").shift().trim();
|
|
24
|
+
}) : []).map(function(lang){
|
|
25
|
+
return lang.slice(0,2).toLowerCase();
|
|
26
|
+
}).filter(function(lang,i,languages){
|
|
27
|
+
return languages.indexOf(lang) === i;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// get params from steps
|
|
31
|
+
data.req.params = {
|
|
32
|
+
m: data.map,
|
|
33
|
+
x: parseInt(req.steps[2],10), // lon
|
|
34
|
+
y: parseInt(req.steps[3],10), // lat
|
|
35
|
+
z: parseInt(req.steps[1],10), // zoom
|
|
36
|
+
r: req.steps[3].includes("@") ? req.steps[3].slice(req.steps[3].indexOf("@"), req.steps[3].indexOf("x")+1) : "", // raw density marker ("@2x")
|
|
37
|
+
e: req.steps[3].includes(".") ? req.steps[3].slice(req.steps[3].indexOf(".", Math.max(0,req.steps[3].indexOf("x")))+1) : null, // extension
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// density options
|
|
41
|
+
data.req.params.d = data.req.params.r ? parseFloat(data.req.params.r.slice(1,-1)) : 1;
|
|
42
|
+
data.req.params.w = Math.round(data.req.params.d * 256);
|
|
43
|
+
|
|
44
|
+
// patch in override parse function
|
|
45
|
+
if (opts.hasOwnProperty("parse") && typeof opts.parse === "function") return opts.parse(req, function(err, params){
|
|
46
|
+
if (err) return next(err);
|
|
47
|
+
data.req.params = { ...data.req.params, params };
|
|
48
|
+
next();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
next();
|
|
52
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const load = require("../lib/load");
|
|
2
|
+
const pmtiles = load("pmtiles");
|
|
3
|
+
|
|
4
|
+
// map pmtiles compression id to method
|
|
5
|
+
const pmcompression = [ null, null, "gz", "br", "zstd" ];
|
|
6
|
+
|
|
7
|
+
const cache = {};
|
|
8
|
+
|
|
9
|
+
// pmtiles backend
|
|
10
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
11
|
+
const mime = this.lib.mime;
|
|
12
|
+
const debug = this.lib.debug;
|
|
13
|
+
const decompress = this.lib.decompress;
|
|
14
|
+
|
|
15
|
+
if (!pmtiles) return next(new Error("Dependency 'pmtiles' is missing."));
|
|
16
|
+
|
|
17
|
+
const run = function(){
|
|
18
|
+
const pm = cache[data.map];
|
|
19
|
+
|
|
20
|
+
debug.info("Fetching %s/%s/%s", data.req.params.z, data.req.params.x, data.req.params.y);
|
|
21
|
+
pm.tiles.getZxy(data.req.params.z, data.req.params.x, data.req.params.y).then(function(result){
|
|
22
|
+
|
|
23
|
+
// undefined result means tile does not exists
|
|
24
|
+
if (!result) return next(new Error("PMTiles: tile does not exist"));
|
|
25
|
+
|
|
26
|
+
// pm.tiles.decompress(Buffer.from(result.data), pm.header.tileCompression).then(function(buf){
|
|
27
|
+
// pmtiles spec allows gzip, brotli and zstd, decompress function only supports gzip. use own implementation?
|
|
28
|
+
decompress(Buffer.from(result.data), pmcompression[pm.header.tileCompression]).then(function(buf){
|
|
29
|
+
|
|
30
|
+
const tile = {
|
|
31
|
+
buffer: buf,
|
|
32
|
+
type: pm.type,
|
|
33
|
+
status: ((buf.length > 0) ? 200 : 204),
|
|
34
|
+
mimetype: mime.mimetype([pm.type]),
|
|
35
|
+
// defaults
|
|
36
|
+
path: data.req.path,
|
|
37
|
+
headers: {},
|
|
38
|
+
compression: false,
|
|
39
|
+
language: null,
|
|
40
|
+
expires: true, // default policy
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// add to tile stack, set primary tile
|
|
44
|
+
data.tiles.unshift(tile);
|
|
45
|
+
data.tile = tile;
|
|
46
|
+
|
|
47
|
+
next();
|
|
48
|
+
|
|
49
|
+
}).catch(function(err){
|
|
50
|
+
return next(err);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
}).catch(function(err){
|
|
54
|
+
return next(err);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (cache.hasOwnProperty(data.map)) return run();
|
|
60
|
+
|
|
61
|
+
cache[data.map] = {};
|
|
62
|
+
cache[data.map].tiles = new pmtiles.PMTiles(opts.url);
|
|
63
|
+
|
|
64
|
+
cache[data.map].tiles.getHeader().then(function(header){
|
|
65
|
+
|
|
66
|
+
cache[data.map].header = header;
|
|
67
|
+
|
|
68
|
+
switch (header.tileType) {
|
|
69
|
+
case 0: cache[data.map].type = "bin"; break;
|
|
70
|
+
case 1: cache[data.map].type = "pbf"; break;
|
|
71
|
+
case 2: cache[data.map].type = "png"; break;
|
|
72
|
+
case 3: cache[data.map].type = "jpeg"; break;
|
|
73
|
+
case 4: cache[data.map].type = "webp"; break;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
run();
|
|
77
|
+
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
// sharp image manipulation
|
|
3
|
+
module.exports = function sharp({ opts, data }, next){
|
|
4
|
+
const sharp = this.lib.sharp;
|
|
5
|
+
const debug = this.lib.debug;
|
|
6
|
+
|
|
7
|
+
// check if sharp is available
|
|
8
|
+
if (typeof sharp === "undefined") {
|
|
9
|
+
debug.warn("sharp: `sharp` dependency is missing.");
|
|
10
|
+
return next();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// check if image can be edited
|
|
14
|
+
if (!["png","jpeg","jpg","gif","webp","avif","tif","tiff"].includes(data.tile.type)) {
|
|
15
|
+
debug.warn("sharp: Unsupported file type: %s", data.tile.type);
|
|
16
|
+
return next();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let img = sharp(data.tile.buffer);
|
|
20
|
+
let error = null;
|
|
21
|
+
|
|
22
|
+
Object.entries(opts).filter(function([method, param]){
|
|
23
|
+
return (typeof img[method] === "function");
|
|
24
|
+
}).forEach(function([method, param]){
|
|
25
|
+
if (error) return;
|
|
26
|
+
try {
|
|
27
|
+
img = img[method](param);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
debug.error("sharp: %s failed", method, err);
|
|
30
|
+
img = null, error = err;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// end on previous error
|
|
35
|
+
if (error) return next(error);
|
|
36
|
+
|
|
37
|
+
// write buffer to tile
|
|
38
|
+
img.toBuffer().then(function(buf){
|
|
39
|
+
data.tile.buffer = buf;
|
|
40
|
+
next();
|
|
41
|
+
}).catch(function(err){
|
|
42
|
+
debug.error("sharp: creating image buffer failed", err);
|
|
43
|
+
return next(err);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const cache = {};
|
|
2
|
+
|
|
3
|
+
// tileserver backend
|
|
4
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
5
|
+
const mime = this.lib.mime;
|
|
6
|
+
const debug = this.lib.debug;
|
|
7
|
+
const strtpl = this.lib.strtpl;
|
|
8
|
+
const retrieve = this.lib.retrieve;
|
|
9
|
+
|
|
10
|
+
// cache opts
|
|
11
|
+
if (!cache.hasOwnProperty(data.map)) {
|
|
12
|
+
cache[data.map] = {};
|
|
13
|
+
|
|
14
|
+
cache[data.map].url = opts.url;
|
|
15
|
+
|
|
16
|
+
// success codes, convert to int, filter crap, default 200
|
|
17
|
+
cache[data.map].status = (opts.hasOwnProperty("status")) ? Array.isArray(opts.status) ? opts.status : [ opts.status ] : [ 200 ];
|
|
18
|
+
cache[data.map].status.map(function(status){ return parseInt(status,10); }).filter(function(status){ return !isNaN(status); });
|
|
19
|
+
if (cache[data.map].status.length === 0) cache[data.map].status.push(200);
|
|
20
|
+
|
|
21
|
+
// headers
|
|
22
|
+
cache[data.map].headers = opts.headers || {};
|
|
23
|
+
|
|
24
|
+
// mime types, make array
|
|
25
|
+
cache[data.map].mimetypes = (opts.hasOwnProperty("mimetypes")) ? Array.isArray(opts.mimetypes) ? opts.mimetypes : [ opts.mimetypes ] : false; // FIXME tolowercase
|
|
26
|
+
|
|
27
|
+
// tms
|
|
28
|
+
cache[data.map].tms = opts.tms === true;
|
|
29
|
+
|
|
30
|
+
// subdomains
|
|
31
|
+
cache[data.map].subdomains = (opts.hasOwnProperty("subdomains")) ? Array.isArray(opts.subdomains) ? opts.subdomains : [ opts.subdomains ] : false;
|
|
32
|
+
|
|
33
|
+
// headers
|
|
34
|
+
cache[data.map].headers = (opts.hasOwnProperty("headers")) ? opts.headers : {};
|
|
35
|
+
|
|
36
|
+
};
|
|
37
|
+
opts = cache[data.map];
|
|
38
|
+
|
|
39
|
+
// clone params for request
|
|
40
|
+
const params = { ...data.req.params };
|
|
41
|
+
|
|
42
|
+
// flip y for tms maps
|
|
43
|
+
if (opts.tms) params.y = Math.pow(2,params.z)-params.y-1;
|
|
44
|
+
|
|
45
|
+
// set subdomain if configured
|
|
46
|
+
if (opts.subdomains) params.s = opts.subdomains[ Date.now() % opts.subdomains.length ];
|
|
47
|
+
|
|
48
|
+
const tileurl = strtpl(opts.url, params);
|
|
49
|
+
|
|
50
|
+
// FIXME check fails cache
|
|
51
|
+
|
|
52
|
+
debug.info("Fetching %s", tileurl);
|
|
53
|
+
|
|
54
|
+
// request
|
|
55
|
+
retrieve({
|
|
56
|
+
url: tileurl,
|
|
57
|
+
headers: {
|
|
58
|
+
...opts.headers,
|
|
59
|
+
},
|
|
60
|
+
followRedirects: true,
|
|
61
|
+
compression: true,
|
|
62
|
+
timeout: 10000,
|
|
63
|
+
}).then(function(resp){
|
|
64
|
+
|
|
65
|
+
// FIXME cache fails
|
|
66
|
+
|
|
67
|
+
// check response status code
|
|
68
|
+
if (opts.status && !opts.status.includes(resp.statusCode)) return next(new Error("Source Tileserver responded with statusCode "+resp.statusCode)); // FIXME set response status?
|
|
69
|
+
|
|
70
|
+
// get respose media type
|
|
71
|
+
const contenttype = (resp.headers["content-type"]||"application/octet-stream").trim().toLowerCase();
|
|
72
|
+
const mediatype = contenttype.includes(";") ? contenttype.slice(0, contenttype.indexOf(";")) : contenttype;
|
|
73
|
+
|
|
74
|
+
// check response mime type
|
|
75
|
+
if (opts.mimetypes && !opts.mimetypes.includes(mediatype)) return next(new Error("Source Tileserver responded with disallowed mime-type "+mediatype));
|
|
76
|
+
|
|
77
|
+
// set tile
|
|
78
|
+
const tile = {};
|
|
79
|
+
tile.path = data.req.path;
|
|
80
|
+
tile.buffer = resp.body;
|
|
81
|
+
tile.mimetype = opts.mimetype || mediatype; // override via opts
|
|
82
|
+
tile.type = opts.filetype || mime.filetype(opts.mimetype || mediatype, data.req.params.e);
|
|
83
|
+
|
|
84
|
+
// http response
|
|
85
|
+
tile.status = (tile.buffer.length > 0) ? 200 : 204;
|
|
86
|
+
|
|
87
|
+
// defaults
|
|
88
|
+
tile.headers = {};
|
|
89
|
+
tile.compression = false; // http client always delivers uncompressed
|
|
90
|
+
tile.language = null;
|
|
91
|
+
tile.expires = true; // default policy
|
|
92
|
+
|
|
93
|
+
// keep around? FIXME
|
|
94
|
+
// data.tile.sourceHeaders = resp.headers;
|
|
95
|
+
|
|
96
|
+
// add to tile stack, set primary tile
|
|
97
|
+
data.tiles.unshift(tile);
|
|
98
|
+
data.tile = tile;
|
|
99
|
+
|
|
100
|
+
next();
|
|
101
|
+
|
|
102
|
+
}).catch(function(err){
|
|
103
|
+
next(err);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const load = require("../lib/load");
|
|
2
|
+
const versatiles = load("versatiles");
|
|
3
|
+
|
|
4
|
+
const cache = {};
|
|
5
|
+
|
|
6
|
+
// versatiles backend
|
|
7
|
+
module.exports = function({ req, res, opts, data }, next){
|
|
8
|
+
const mime = this.lib.mime;
|
|
9
|
+
const debug = this.lib.debug;
|
|
10
|
+
|
|
11
|
+
if (!versatiles) return next(new Error("Dependency 'versatiles' is missing."));
|
|
12
|
+
|
|
13
|
+
// cache versatiles instance
|
|
14
|
+
if (!cache.hasOwnProperty(data.map)) {
|
|
15
|
+
cache[data.map] = new versatiles(opts.url, {
|
|
16
|
+
tms: (!!opts.tms),
|
|
17
|
+
headers: ((opts.hasOwnProperty("headers")) ? opts.headers : {}),
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
const vt = cache[data.map];
|
|
21
|
+
|
|
22
|
+
debug.info("Fetching %s/%s/%s", data.req.params.z, data.req.params.x, data.req.params.y);
|
|
23
|
+
vt.getTile(data.req.params.z, data.req.params.x, data.req.params.y, function(err, buf){
|
|
24
|
+
if (err) return next(err);
|
|
25
|
+
|
|
26
|
+
// if precompressed, keep in tile stack
|
|
27
|
+
/* TODO evaluate side effects
|
|
28
|
+
if (vt.header.tile_precompression) data.tiles.unshift({
|
|
29
|
+
buffer: buf,
|
|
30
|
+
type: vt.header.tile_format,
|
|
31
|
+
status: ((buf.length > 0) ? 200 : 204),
|
|
32
|
+
mimetype: vt.mimetypes[vt.header.tile_format],
|
|
33
|
+
// defaults
|
|
34
|
+
path: data.req.path,
|
|
35
|
+
headers: {},
|
|
36
|
+
compression: vt.header.tile_precompression,
|
|
37
|
+
language: null,
|
|
38
|
+
expires: true, // default policy
|
|
39
|
+
}); */
|
|
40
|
+
|
|
41
|
+
// decompress tile
|
|
42
|
+
vt.decompress(vt.header.tile_precompression, buf, function(err, buf){
|
|
43
|
+
if (err) return next(err);
|
|
44
|
+
|
|
45
|
+
const tile = {
|
|
46
|
+
buffer: buf,
|
|
47
|
+
type: vt.header.tile_format,
|
|
48
|
+
status: ((buf.length > 0) ? 200 : 204),
|
|
49
|
+
mimetype: vt.mimetypes[vt.header.tile_format],
|
|
50
|
+
// defaults
|
|
51
|
+
path: data.req.path,
|
|
52
|
+
headers: {},
|
|
53
|
+
compression: false,
|
|
54
|
+
language: null,
|
|
55
|
+
expires: true, // default policy
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// add to tile stack, set primary tile
|
|
59
|
+
data.tiles.unshift(tile);
|
|
60
|
+
data.tile = tile;
|
|
61
|
+
|
|
62
|
+
next();
|
|
63
|
+
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
};
|
package/config.dist.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const config = module.exports = {
|
|
2
|
+
version: 1, // config file format version
|
|
3
|
+
|
|
4
|
+
id: "tileblaster", // id of the tileblaster instance, in case you want to run more than one; default: tileblaster
|
|
5
|
+
|
|
6
|
+
treads: 1, // number of worker threads in cluster, default: 1
|
|
7
|
+
queue: 10, // number of parallel tile processes per worker, default: 12
|
|
8
|
+
|
|
9
|
+
server: {
|
|
10
|
+
url: "https://tileserver/tiles", // public url including subdirectory
|
|
11
|
+
mount: "/tiles", // mountpoint override, default: pathname from ${config.paths.base}
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
paths: {
|
|
15
|
+
work: "/path/to/stuff", // the base directory where files (sockets, logs, plugins, ...) go; default: ~/tileblaster
|
|
16
|
+
data: "/path/to/tiles", // the directory in which cached tiles are saved; default: ${config.paths.work}/data
|
|
17
|
+
logs: "/path/to/logs", // the directory in which cached tiles are saved; default: ${config.paths.work}/logs
|
|
18
|
+
plugins: "/path/to/plugins", // the directory from which plugins ar loaded; default: ${config.paths.work}/plugins
|
|
19
|
+
sockets: "/path/to/sockets", // the directory i nwhich sockets are created; default: ${config.paths.work}/sockets
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// listen
|
|
23
|
+
listen: [{
|
|
24
|
+
port: 8080, // required
|
|
25
|
+
host: "localhost", // default localhost
|
|
26
|
+
},{
|
|
27
|
+
socket: "test.socket", // required, absolute path or relative to ${config.paths.socket}
|
|
28
|
+
mode: 0o660, // change socket mode to this id
|
|
29
|
+
group: 1000, // change socket group to this is
|
|
30
|
+
}],
|
|
31
|
+
|
|
32
|
+
// plugins, relative or absolute paths for local files, npm module names otherwise
|
|
33
|
+
plugins: {
|
|
34
|
+
resize: "./resize.js", // path relative to `config.paths.plugins`, starting with ./
|
|
35
|
+
convert: "/path/to/convert.js", // absolute path
|
|
36
|
+
optimize: "someplugin", // npm module
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
maps: {
|
|
40
|
+
example: [{
|
|
41
|
+
// cors headers
|
|
42
|
+
// they are only useful for standalone servers
|
|
43
|
+
builtin: "cors",
|
|
44
|
+
origins: [ "https://example.org/" ],
|
|
45
|
+
},{
|
|
46
|
+
builtin: "parse", // /z/x/y@r.ext, other formats via parse function
|
|
47
|
+
parse: function(req, next){ // override parse function, req is the raw request
|
|
48
|
+
|
|
49
|
+
// req.url=/foo/bar/{z}.{x}.{y}
|
|
50
|
+
let p = req.url.split("/").pop().split(".");
|
|
51
|
+
|
|
52
|
+
// do things to get parameters from path
|
|
53
|
+
next(null, {
|
|
54
|
+
z: p[0],
|
|
55
|
+
x: p[1],
|
|
56
|
+
y: p[2],
|
|
57
|
+
r: "",
|
|
58
|
+
w: 256,
|
|
59
|
+
d: 1,
|
|
60
|
+
e: ".png"
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
},{
|
|
64
|
+
builtin: "check",
|
|
65
|
+
zoom: [ 0, 22 ], // min, max
|
|
66
|
+
bbox: [ -180, -90, 180, 90 ], // west, south, east, north
|
|
67
|
+
extensions: [ "png", "jpeg" ], // allowed extensions
|
|
68
|
+
density: [ "", "@2x", "@3x" ], // allowed density markeers
|
|
69
|
+
check: function(params, fn) { // override check function, params from parse
|
|
70
|
+
fn(new Error("Check failed")); // deliver error if check failed
|
|
71
|
+
}
|
|
72
|
+
},{ // get from cache, skip to `skipto` if successful
|
|
73
|
+
builtin: "cache",
|
|
74
|
+
skipto: "deliver",
|
|
75
|
+
},{
|
|
76
|
+
builtin: "noop", // does nothing
|
|
77
|
+
},{
|
|
78
|
+
builtin: "tileserver",
|
|
79
|
+
url: "https://{s}.tileserver.example/test/{z}/{x}/{y}{r}.{e}",
|
|
80
|
+
subdomains: [ "a", "b", "x" ], // random chosen and passed in as {s}
|
|
81
|
+
tms: true, // y coordinate is inverted
|
|
82
|
+
headers: {}, // additional headers sent to the backend tileserver
|
|
83
|
+
status: [ 200 ], // expected status code(s)
|
|
84
|
+
mimetypes: [ "image/png", "image/jpeg" ], // expected mime types
|
|
85
|
+
mimetype: "image/png", // overwrite mime type from server
|
|
86
|
+
},
|
|
87
|
+
/* alternative:
|
|
88
|
+
{
|
|
89
|
+
// get tile from versatiles container
|
|
90
|
+
builtin: "versatiles",
|
|
91
|
+
url: "https://cdn.example/planet.versatiles",
|
|
92
|
+
headers: { // headers sent to versatiles server
|
|
93
|
+
"X-Tileblaster": "True",
|
|
94
|
+
},
|
|
95
|
+
},{
|
|
96
|
+
// get tiles from pmtiles container
|
|
97
|
+
builtin: "pmtiles",
|
|
98
|
+
url: "https://cdn.example/planet.pmtiles"
|
|
99
|
+
},{
|
|
100
|
+
// get tiles from local mbtiles database
|
|
101
|
+
builtin: "mbtiles",
|
|
102
|
+
file: "/path/to/planet.mbtiles"
|
|
103
|
+
},
|
|
104
|
+
*/
|
|
105
|
+
{
|
|
106
|
+
// edit vectortile
|
|
107
|
+
builtin: "edit",
|
|
108
|
+
edit: function(layers){
|
|
109
|
+
|
|
110
|
+
// remove unused layer
|
|
111
|
+
layers = layers.filter(function(layer){
|
|
112
|
+
return (layer.name !== "unused-layer");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return layers;
|
|
116
|
+
}
|
|
117
|
+
},{
|
|
118
|
+
// use sharp for image manipulation
|
|
119
|
+
plugin: "sharp",
|
|
120
|
+
resize: { width: 512, height: 512 }, // sharp.resize()
|
|
121
|
+
},{
|
|
122
|
+
plugin: "optimize",
|
|
123
|
+
png: { o: 4 }, // true or opts for optipng
|
|
124
|
+
jpeg: true, // true or opts for mozjpeg
|
|
125
|
+
},{
|
|
126
|
+
// convert raster tiles to webp and/or avif
|
|
127
|
+
builtin: "modernize",
|
|
128
|
+
webp: {
|
|
129
|
+
quality: 90,
|
|
130
|
+
effort: 4,
|
|
131
|
+
},
|
|
132
|
+
avif: {
|
|
133
|
+
quality: 90,
|
|
134
|
+
effort: 5,
|
|
135
|
+
},
|
|
136
|
+
},{
|
|
137
|
+
builtin: "compress",
|
|
138
|
+
brotli: 8, // true or <level> or {opts}
|
|
139
|
+
gzip: true, // true or <level> or {opts}
|
|
140
|
+
},{
|
|
141
|
+
builtin: "cache",
|
|
142
|
+
expires: "30d",
|
|
143
|
+
},{
|
|
144
|
+
// debug output
|
|
145
|
+
builtin: "dump",
|
|
146
|
+
},{
|
|
147
|
+
builtin: "deliver", // deliver best matching tile for client
|
|
148
|
+
headers: {}, // additional http headers
|
|
149
|
+
}],
|
|
150
|
+
// more maps
|
|
151
|
+
othermap: [],
|
|
152
|
+
}
|
|
153
|
+
};
|