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 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/)
@@ -1,29 +1,79 @@
1
1
  #!/usr/bin/env node
2
- var argv = require('minimist')(process.argv.slice(2));
3
- var watch = require('node-watch');
4
- var configfile = require("path").resolve.apply(global, (!!argv._[0]) ? [ process.cwd(), argv._[0] ] : [ "../config.js" ]);
5
- var tb = require("../lib/tileblaster.js")(
6
- (function(config){
7
- ["socket","tiles","queue","id","port"].forEach(function(n){
8
- if (!!argv[n]) config[n] = argv[n];
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
- // watch changes in config file
11
- watch(configfile, function(evt,f){
12
- if (evt === "update") try {
13
- delete require.cache[require.resolve(configfile)];
14
- tb.reconfigure(require(configfile));
15
- } catch (err) {
16
- console.error("Unable to read changed config file", err);
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
+ };
@@ -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
+ };
@@ -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
+ };
@@ -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
+ };