tileblaster 0.4.9 → 1.0.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.
@@ -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
+ };
@@ -0,0 +1,5 @@
1
+ // bare bones example builtin
2
+ module.exports = function({ req, res, opts, data }, next){
3
+ console.log("I'm a plugin! my opts are: %s", JSON.stringify(opts));
4
+ next();
5
+ };
@@ -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
+ threads: 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
+ };