tileblaster 0.2.6 → 0.3.2

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.
@@ -1,14 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  var argv = require('minimist')(process.argv.slice(2));
3
- require("../lib/tileblaster.js")(
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")(
4
6
  (function(config){
5
7
  ["socket","tiles","queue","id"].forEach(function(n){
6
8
  if (!!argv[n]) config[n] = argv[n];
7
9
  });
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
+ }
18
+ });
8
19
  return config;
9
20
  })((function(){
10
21
  try {
11
- return (!!process.argv[2]) ? require(require("path").resolve(process.cwd(), process.argv[2])) : require("../config.js");
22
+ return require(configfile);
12
23
  } catch (err) {
13
24
  console.error("usage: tileblaster <config.js> [--socket tileblaster.sock] [--tiles /path/to/tiles] [--queue 100] [--id mytileblaster]");
14
25
  process.exit(1);
package/config.js.dist CHANGED
@@ -56,6 +56,12 @@ module.exports = {
56
56
  // * .png with `optipng`
57
57
  // * .jpg with `mozjpeg`
58
58
  "optimize": true,
59
+
60
+ // compress tiles (only makes sense for non-rastered tiles like json, pbf, mvt)
61
+ "compress": [ "gz", "br" ],
62
+
63
+ // write tiles to disk
64
+ "cache": true,
59
65
 
60
66
  // minimum time tiles are kept
61
67
  "expires": "1d",
@@ -5,6 +5,7 @@ var url = require("url");
5
5
  var path = require("path");
6
6
  var http = require("http");
7
7
  var util = require("util");
8
+ var zlib = require("zlib");
8
9
  var stream = require("stream");
9
10
 
10
11
  var debug = require("debug")("tileblaster");
@@ -17,6 +18,7 @@ var dur = require("dur");
17
18
  // optional dependencies; if only
18
19
  try { var pnck = require("pnck"); } catch (err) { var pnck = null; }
19
20
  try { var jpck = require("jpck"); } catch (err) { var jpck = null; }
21
+ try { var zopfli = require("node-zopfli"); } catch (err) { var zopfli = null; }
20
22
 
21
23
  // load package
22
24
  var pckg = require("../package.json");
@@ -47,6 +49,9 @@ tileblaster.prototype.mime = {
47
49
  arcamf: 'application/octet-stream',
48
50
  };
49
51
 
52
+ // compression formats
53
+ tileblaster.prototype.comp = [ "gz", "br" ];
54
+
50
55
  // initialize
51
56
  tileblaster.prototype.init = function(config){
52
57
  var self = this;
@@ -56,6 +61,36 @@ tileblaster.prototype.init = function(config){
56
61
  // server
57
62
  self.srvr = null;
58
63
 
64
+ // statistics
65
+ self.statistics = {
66
+ hits: 0,
67
+ phits: 0,
68
+ served: 0,
69
+ last: Date.now()
70
+ };
71
+
72
+ // compression queue
73
+ self.cqueue = queue(1);
74
+
75
+ // have maps with active expires?
76
+ self.expiration = false;
77
+
78
+ // reconfigure
79
+ self.reconfigure(config);
80
+
81
+ return this;
82
+
83
+ };
84
+
85
+ // reconfigure
86
+ tileblaster.prototype.reconfigure = function(config){
87
+ var self = this;
88
+
89
+ debug("<init> reconfiguring");
90
+
91
+ // cache
92
+ self.errcache = {};
93
+
59
94
  // keep config
60
95
  self.config = config || {};
61
96
 
@@ -74,26 +109,12 @@ tileblaster.prototype.init = function(config){
74
109
  // queue size
75
110
  self.config.queue = Math.max(parseInt(self.config.queue,10)||100,1);
76
111
 
77
- // cache
78
- self.errcache = {};
79
-
80
- // statistics
81
- self.statistics = {
82
- hits: 0,
83
- phits: 0,
84
- served: 0,
85
- last: Date.now()
86
- };
87
-
88
112
  // extend mime types
89
113
  if (!!self.config.mime) Object.keys(self.config.mime).map(function(ext){ self.mime[ext] = self.config.mime[ext] });
90
114
 
91
115
  // queue
92
116
  self.queue = queue(self.config.queue);
93
-
94
- // have maps with active expires?
95
- self.expiration = false;
96
-
117
+
97
118
  // check configured maps
98
119
  self.maps = Object.keys(self.config.maps).reduce(function(maps, id){
99
120
 
@@ -112,10 +133,10 @@ tileblaster.prototype.init = function(config){
112
133
  }
113
134
 
114
135
  // resolutions
115
- if (!map.res) map.res = [];
136
+ if (!map.res) map.res = [ "" ];
116
137
  if (!(map.res instanceof Array)) map.res = [ map.res ];
117
138
  map.res = map.res.filter(function(res){
118
- return /^@[1-9][0-9]*(\.[0-9]+)?x$/.test(res);
139
+ return (res === "") || /^@[1-9][0-9]*(\.[0-9]+)?x$/.test(res);
119
140
  });
120
141
 
121
142
  // precalculate bbox for zoom levels
@@ -151,6 +172,13 @@ tileblaster.prototype.init = function(config){
151
172
  // expires
152
173
  if (!!map.expires) map.expires = dur(map.expires);
153
174
  if (!!map.expires) self.expiration = true;
175
+ if (!map.expires) map.expires = Infinity;
176
+
177
+ // cache
178
+ map.cache = (!map.hasOwnProperty("cache")) ? true : (!!map.cache);
179
+
180
+ // compression
181
+ map.compress = map.compress.filter(function(c){ return (self.comp.indexOf(c) >= 0) });
154
182
 
155
183
  return map;
156
184
 
@@ -159,13 +187,13 @@ tileblaster.prototype.init = function(config){
159
187
  },{});
160
188
 
161
189
  // build user agent
162
- if (!self.config.useragent) self.config.useragent = util.format("%s/%s (+)", pckg.name, pckg.version, pckg.homepage);
190
+ if (!self.config.useragent) self.config.useragent = util.format("%s/%s (+%s)", pckg.name, pckg.version, pckg.homepage);
163
191
 
164
192
  // cleanup timer
165
193
  self.config.cleanup = (!!self.config.cleanup) ? dur(self.config.cleanup) : false;
194
+ if (!!self.cleaner) clearInterval(self.cleaner);
166
195
  if (self.config.cleanup && self.expiration) self.cleaner = setInterval(function(){ self.cleanup(); }, self.config.cleanup).unref();
167
-
168
- return this;
196
+
169
197
  };
170
198
 
171
199
  // return server
@@ -179,32 +207,40 @@ tileblaster.prototype.server = function(){
179
207
  // check http method
180
208
  if (req.method !== "GET") return debug("<server> invalid method: %s", req.method), (!!self.config.hints&&res.setHeader("x-err-hint","invalid method")), res.statusCode = 405, res.end();
181
209
 
182
- // only tile requests are allowed FIXME optimize
183
- if (!(/\/(([a-z0-9\-\_\.]+)\/([0-9]+)\/([0-9]+)\/([0-9]+)(@[0-9]+(\.[0-9]+)?x)?\.([a-z0-9]+))$/.exec(url.parse(req.url).pathname.toLowerCase()))) return debug("<server> invalid request: %s", url.parse(req.url).pathname), (!!self.config.hints&&res.setHeader("x-err-hint","invalid request")), res.statusCode = 404, res.end();
210
+ // parse request
211
+ var t = self._tilepath(url.parse(req.url).pathname.toLowerCase());
212
+ if (!t) return debug("<server> invalid request: %s", url.parse(req.url).pathname), (!!self.config.hints&&res.setHeader("x-err-hint","invalid request")), res.statusCode = 404, res.end();
184
213
 
185
- // asselmble information
186
- var p = RegExp.$1;
187
- var mapid = RegExp.$2;
188
-
189
214
  // check if map exists
190
- if (!self.maps.hasOwnProperty(mapid)) return debug("<server> requested invalid map: %s", mapid), (!!self.config.hints&&res.setHeader("x-err-hint","invalid map")), res.statusCode = 404, res.end();
215
+ if (!self.maps.hasOwnProperty(t.mapid)) return debug("<server> requested invalid map: %s", t.mapid), (!!self.config.hints&&res.setHeader("x-err-hint","invalid map")), res.statusCode = 404, res.end();
216
+
217
+ // deliver file if requested
218
+ if (t.type === "file") {
219
+ switch (t.file) {
220
+ case "tile.json":
221
+ res.writeHead(200, { "Content-Type": "application/json" });
222
+ res.end(self._tilejson(t.mapid, ((req.headers["x-https"] === "on") ? "https://" : "http://")+req.headers["host"]));
223
+ break;
224
+ default:
225
+ return debug("<server> requested invalid file: %s", t.p), (!!self.config.hints&&res.setHeader("x-err-hint","invalid file")), res.statusCode = 404, res.end();
226
+ break;
227
+ }
228
+ return;
229
+ }
191
230
 
192
- var z = parseInt(RegExp.$3,10);
193
- var x = parseInt(RegExp.$4,10);
194
- var y = parseInt(RegExp.$5,10);
195
- var r = ((self.maps[mapid].res.indexOf(RegExp.$6) >= 0) ? RegExp.$6 : false);
196
- var ext = RegExp.$8;
231
+ // res
232
+ t.r = (!!t.r && (self.maps[t.mapid].res.indexOf(t.r) >= 0) ? t.r : false);
197
233
 
198
- debug("<server> [%s] requested", p);
234
+ debug("<server> [%s] requested", t.p);
199
235
 
200
- self.tile(mapid, z, x, y, r, ext, function(err, stream, meta){
201
- if (err) return debug("<server> [%s] error: %s", p, err), (!!self.config.hints&&res.setHeader("x-err-hint",err.toString())), res.statusCode = 204, res.end();
236
+ self.tile(t.mapid, t.z, t.x, t.y, t.r, t.ext, function(err, stream, meta){
237
+ if (err) return debug("<server> [%s] error: %s", t.p, err.toString()), (!!self.config.hints&&res.setHeader("x-err-hint",err.toString())), res.statusCode = 204, res.end();
202
238
 
203
239
  // send headers
204
240
  res.writeHead(200, { "Content-Type": meta['content-type'] });
205
241
 
206
242
  stream.on("end", function(){
207
- debug("<server> [%s] done", p);
243
+ debug("<server> [%s] done", t.p);
208
244
  self.statistics.served++;
209
245
  });
210
246
 
@@ -300,8 +336,16 @@ tileblaster.prototype.heartbeat = function(){
300
336
  // delete expired tiles
301
337
  tileblaster.prototype.cleanup = function(){
302
338
  var self = this;
339
+
340
+ // clean up error cache
341
+ var d = Date.now();
342
+ self.errcache = self.errcache.filter(function(e){
343
+ return (e.until > d);
344
+ });
345
+
346
+ // FIXME: extra process
303
347
 
304
- //
348
+ // clean up map tiles
305
349
  Object.keys(self.maps).filter(function(mapid){
306
350
  return (!!self.maps[mapid].expires);
307
351
  }).forEach(function(mapid){
@@ -357,7 +401,7 @@ tileblaster.prototype.tile = function(mapid, z, x, y, r, e, fn){
357
401
  debug("<tile> [%s] valid", tilefile);
358
402
 
359
403
  // check for cached 404 tiles
360
- if (!!self.errcache[tilefile] && self.errcache[tilefile] > (Date.now()-self.config.expires)) return fn(new Error("Known bad tile"), null);
404
+ if (!!self.errcache[tilefile] && (self.errcache[tilefile].until > Date.now())) return fn(new Error("Known bad tile: "+self.errcache[tilefile].err), null);
361
405
 
362
406
  // resolve tile path
363
407
  var tilepath = path.resolve(self.config.tiles, tilefile);
@@ -367,23 +411,31 @@ tileblaster.prototype.tile = function(mapid, z, x, y, r, e, fn){
367
411
 
368
412
  self.fetchtile(tileurl, mapid, function(err, stream, meta){
369
413
 
370
- // locally cache 404 and 204
371
- if (!!err && (stream === 404 || stream === 204)) self.errcache[tilefile] = Date.now();
414
+ if (err) {
415
+ if (stream !== null) {
416
+ // cache error tile
417
+ self.errcache[tilefile] = { until: (Date.now()+self.config.maps[mapid].expires), err: err, code: stream };
372
418
 
373
- // error or no stream
374
- if (!!err || !stream || typeof stream === 'number') return fn(err, stream);
419
+ // mark tile as erronous in file system
420
+ fs.writeFile(tilepath+".err", JSON.stringify(self.errcache[tilefile]), function(err){
421
+ if (err) return debug("<tile> [%s.err] error: %s", tilefile, err.toString());
422
+ });
423
+ }
424
+ return debug("<tile> [%s] error: %s", tilefile, err.toString()), fn(err, stream);
425
+ }
375
426
 
376
427
  debug("<tile> [%s] fetched", tilefile);
377
428
 
378
- // strem mux
379
- stream.pipe(self.optimize(mapid, e)).pipe(self._mux(function(stream){
429
+ var streams = [];
380
430
 
431
+ streams.push(function(stream){
381
432
  // call back with stream
382
433
  fn(null, stream, {
383
434
  'content-type': (self.mime[e])
384
435
  });
385
-
386
- }, function(stream){
436
+ });
437
+
438
+ if (!!self.config.maps[mapid].cache) streams.push(function(stream){
387
439
 
388
440
  // don't overwrite if tile exists
389
441
  fs.access(tilepath, fs.constants.F_OK, function(err){
@@ -394,6 +446,10 @@ tileblaster.prototype.tile = function(mapid, z, x, y, r, e, fn){
394
446
  // save to tmp file, rename when done
395
447
  stream.pipe(fs.createWriteStream(tilepath+".tmp").on('finish', function(){
396
448
  fs.rename(tilepath+".tmp", tilepath, function(){
449
+
450
+ // compress
451
+ if (self.config.maps[mapid].compress instanceof Array && self.config.maps[mapid].compress.length > 0) self.compress(tilepath, self.config.maps[mapid].compress);
452
+
397
453
  debug("<tile> [%s] saved", tilefile);
398
454
  });
399
455
  }));
@@ -404,8 +460,10 @@ tileblaster.prototype.tile = function(mapid, z, x, y, r, e, fn){
404
460
 
405
461
  });
406
462
 
463
+ });
407
464
 
408
- }));
465
+ // stream mux
466
+ stream.pipe(self.optimize(mapid, e)).pipe(self._mux.apply(self, streams));
409
467
 
410
468
  });
411
469
 
@@ -434,7 +492,7 @@ tileblaster.prototype.fetchtile = function(tileurl, mapid, fn){
434
492
  // check mime type, status code, content-length, FIXME: cache!
435
493
  if (resp.statusCode !== 200) return fn(new Error("status code "+resp.statusCode), resp.statusCode);
436
494
  if (!resp.headers['content-type']||(!!self.maps[mapid].mime&&self.maps[mapid].mime.indexOf(resp.headers['content-type'])<0)) return fn(new Error("invalid content type "+resp.headers['content-type']), resp.statusCode);
437
- if (!!resp.headers['content-length']&&parseInt(resp.headers['content-type'],10)===0) return fn(new Error("no content"), resp.statusCode);
495
+ if (!!resp.headers['content-length']&&parseInt(resp.headers['content-length'],10)===0) return fn(new Error("no content"), resp.statusCode);
438
496
 
439
497
  // signal queue when read stream has finished
440
498
  this.once('end', function(){ (!d++)&&done(); });
@@ -444,7 +502,7 @@ tileblaster.prototype.fetchtile = function(tileurl, mapid, fn){
444
502
  return fn(null, this, {
445
503
  date: (new Date(resp.headers.date||Date.now()).valueOf()),
446
504
  size: (parseInt(resp.headers['content-length'],10)||null),
447
- mime: (resp.headers['content-type']||'apllication/octet-stream'),
505
+ mime: (resp.headers['content-type']||'application/octet-stream'),
448
506
  });
449
507
 
450
508
  }).on('error', function(err){
@@ -485,6 +543,54 @@ tileblaster.prototype.optimize = function(mapid, ext){
485
543
 
486
544
  };
487
545
 
546
+ // compression
547
+ tileblaster.prototype.compress = function(file, comp){
548
+ var self = this;
549
+ comp.forEach(function(c){
550
+ self.cqueue.push(function(done){
551
+ switch (c) {
552
+ case "br":
553
+ // use builtin brotli
554
+ fs.createReadStream(file).pipe(zlib.createBrotliCompress({
555
+ level: 6
556
+ })).pipe(fs.createWriteStream(file+".br.tmp")).on("finish", function(){
557
+ fs.rename(file+".br.tmp", file+".br", function(){
558
+ debug("<tile> [%s] compressed with brotli", file);
559
+ done();
560
+ });
561
+ });
562
+ break;
563
+ case "gz":
564
+ if (zopfli !== null) {
565
+ // use zopfli
566
+ fs.createReadStream(file).pipe(zopfli.createGzip({
567
+ numiterations: 5, // don't block the queue too long
568
+ })).pipe(fs.createWriteStream(file+".gz.tmp")).on("finish", function(){
569
+ fs.rename(file+".gz.tmp", file+".gz", function(){
570
+ debug("<tile> [%s] compressed with zopfli", file);
571
+ done();
572
+ });
573
+ });
574
+ } else {
575
+ // use builtin gzip
576
+ fs.createReadStream(file).pipe(zlib.createGzip({
577
+ level: 6
578
+ })).pipe(fs.createWriteStream(file+".gz.tmp")).on("finish", function(){
579
+ fs.rename(file+".gz.tmp", file+".gz", function(){
580
+ debug("<tile> [%s] compressed with gzip", file);
581
+ done();
582
+ });
583
+ });
584
+ }
585
+ break;
586
+ default:
587
+ done();
588
+ break;
589
+ }
590
+ });
591
+ });
592
+ };
593
+
488
594
  // stream multiplexer
489
595
  tileblaster.prototype._mux = function(fn){
490
596
  var self = this;
@@ -550,7 +656,8 @@ tileblaster.prototype._checktile = function(mapid, z, x, y, r, ext, fn){
550
656
  // check extension
551
657
  if (!!self.maps[mapid].ext && self.maps[mapid].ext.indexOf(ext) < 0) return debug("<check> disallowed extension '%s' for map '%s'", ext, mapid), fn(new Error("Disallowed extension"));
552
658
 
553
- // FIXME: check retina support?
659
+ // check density
660
+ if (!!r && self.maps[mapid].res.indexOf(r) < 0) return debug("<check> disallowed density '%s' for map '%s'", res, mapid), fn(new Error("Disallowed density"));
554
661
 
555
662
  // check zoom level
556
663
  var zf = parseFloat(z,10);
@@ -584,4 +691,44 @@ tileblaster.prototype._tilefile = function(mapid, z, x, y, r, e){
584
691
  return (mapid+"/"+z.toFixed(0)+"/"+x.toFixed(0)+"/"+y.toFixed(0)+((!!r)?r:"")+"."+((e) ? e : ""));
585
692
  };
586
693
 
694
+ // path parsing regular expression
695
+ tileblaster.prototype._pathregx = /\/(([a-z0-9\-\_\.]+)\/((tile\.json)|([0-9]+)\/([0-9]+)\/([0-9]+)(@([0-9]+(\.[0-9]+)?)x)?\.([a-z0-9\.]+)))$/;
696
+
697
+ // transform path to parameters
698
+ tileblaster.prototype._tilepath = function(p) {
699
+ var r = (this._pathregx.exec(p));
700
+ if (!r) return false;
701
+ if (!r[4]) {
702
+ return {
703
+ type: "tile",
704
+ p: r[1],
705
+ mapid: r[2],
706
+ z: parseInt(r[5],10),
707
+ x: parseInt(r[6],10),
708
+ y: parseInt(r[7],10),
709
+ r: r[8],
710
+ ext: r[11],
711
+ };
712
+ } else {
713
+ return {
714
+ type: "file",
715
+ p: r[1],
716
+ mapid: r[2],
717
+ file: r[4],
718
+ };
719
+ }
720
+ };
721
+
722
+ // assemble tilejson (good enough)
723
+ tileblaster.prototype._tilejson = function(id, base) {
724
+ var self = this;
725
+ return JSON.stringify({
726
+ tilejson: "2.2.0",
727
+ minzoom: self.config.maps[id].zoom[0],
728
+ maxzoom: self.config.maps[id].zoom[1],
729
+ bounds: self.config.maps[id].bbox,
730
+ tiles: [ (self.config.base||base)+"/"+id+"/{z}/{x}/{y}"+(self.config.maps[id].res[0]||"")+"."+self.config.maps[id].ext[0] ],
731
+ });
732
+ };
733
+
587
734
  module.exports = tileblaster;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tileblaster",
3
- "version": "0.2.6",
4
- "description": "pretty fast optimizing tile caching proxy",
3
+ "version": "0.3.2",
4
+ "description": "pretty fast optimizing & compressing tile caching proxy",
5
5
  "main": "lib/tileblaster.js",
6
6
  "bin": {
7
7
  "tileblaster": "bin/tileblaster.js"
@@ -17,17 +17,19 @@
17
17
  },
18
18
  "homepage": "https://github.com/yetzt/tileblaster#readme",
19
19
  "dependencies": {
20
- "debug": "^4",
21
- "dur": "^0",
22
- "glob": "^7.1",
20
+ "debug": "^4.3.3",
21
+ "dur": "^0.0.3",
22
+ "glob": "^7.2.0",
23
23
  "minimist": "^1.2.5",
24
24
  "mkdirp": "^1",
25
+ "node-watch": "^0.7.3",
25
26
  "nsa": "^0.2",
26
- "quu": "^0.3",
27
+ "quu": "^0.4.1",
27
28
  "request": "^2.88"
28
29
  },
29
30
  "optionalDependencies": {
30
31
  "jpck": "^1.0.2",
32
+ "node-zopfli": "^2.1.4",
31
33
  "pnck": "^1.0.1"
32
34
  }
33
35
  }
package/readme.md CHANGED
@@ -28,6 +28,13 @@ upstream upstream_tileblaster {
28
28
  server {
29
29
  listen 80;
30
30
  server_name tileblaster;
31
+
32
+ gzip_static on;
33
+ # brotli_static on; # if ngx_brotli is available
34
+
35
+ if (-f $document_root/$uri.err) {
36
+ return 204;
37
+ }
31
38
 
32
39
  location / {
33
40
  root /path/to/tileblaster/tiles;
@@ -48,3 +55,4 @@ get the tiles via `http://server/<mapid>/<z>/<x>/<y>[<d>].<ext>`
48
55
  * `<z>`, `<x>` and `<z>` are the tile coorinates
49
56
  * `<d>` is the optional pixel density marker, for example `@2x`
50
57
  * `<ext>` is the extension, for example `png`, `geojson` or `mvt`
58
+
package/.gitignore~ DELETED
@@ -1,4 +0,0 @@
1
- config.js
2
- node_modules
3
- data
4
- tileblaster.sock
package/renovate.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "extends": [
3
- "config:base"
4
- ]
5
- }