tileblaster 0.4.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,789 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- var fs = require("fs");
4
- var url = require("url");
5
- var path = require("path");
6
- var http = require("http");
7
- var util = require("util");
8
- var zlib = require("zlib");
9
- var stream = require("stream");
10
-
11
- var phin = require("phin");
12
- var queue = require("quu");
13
- var walk = require("klaw");
14
- var dur = require("dur");
15
-
16
- // optional dependencies;
17
- try { var pnck = require("pnck"); } catch (err) { var pnck = null; }
18
- try { var jpck = require("jpck"); } catch (err) { var jpck = null; }
19
- try { var zopfli = require("node-zopfli"); } catch (err) { var zopfli = null; }
20
- try { var versatiles = require("versatiles"); } catch (err) { var versatiles = null; }
21
- try { var debug = require("debug")("tileblaster"); } catch (err) { var debug = function(){ if (process.env.DEBUG) console.error(...arguments); }}
22
-
23
- // load package
24
- var pckg = require("../package.json");
25
-
26
- function tileblaster(config){
27
- return (this instanceof tileblaster) ? this.init(config) : new tileblaster(config);
28
- };
29
-
30
- // mime types
31
- tileblaster.prototype.mime = {
32
- // raster
33
- png: 'image/png',
34
- jpg: 'image/jpeg',
35
- jpeg: 'image/jpeg',
36
- gif: 'image/gif',
37
- // vector
38
- svg: 'image/svg+xml',
39
- mvt: 'application/vnd.mapbox-vector-tile',
40
- pbf: 'application/x-protobuf',
41
- // data
42
- json: 'text/json',
43
- geojson: 'text/json',
44
- topojson: 'text/json',
45
- // obscure ones:
46
- arcjson: 'text/json',
47
- geobson: 'application/octet-stream',
48
- geoamf: 'application/octet-stream',
49
- arcamf: 'application/octet-stream',
50
- };
51
-
52
- // compression formats
53
- tileblaster.prototype.comp = [ "gz", "br" ];
54
-
55
- // initialize
56
- tileblaster.prototype.init = function(config){
57
- var self = this;
58
-
59
- debug("<init> inizializing");
60
-
61
- // server
62
- self.srvr = null;
63
-
64
- // statistics
65
- self.statistics = {
66
- hits: 0,
67
- phits: 0,
68
- served: 0,
69
- last: Date.now()
70
- };
71
-
72
- // http agents
73
- self.agents = {};
74
-
75
- // compression queue
76
- self.cqueue = queue(1);
77
-
78
- // have maps with active expires?
79
- self.expiration = false;
80
-
81
- // reconfigure
82
- self.reconfigure(config);
83
-
84
- return this;
85
-
86
- };
87
-
88
- // reconfigure
89
- tileblaster.prototype.reconfigure = function(config){
90
- var self = this;
91
-
92
- debug("<init> reconfiguring");
93
-
94
- // cache
95
- self.errcache = {};
96
-
97
- // keep config
98
- self.config = config || {};
99
-
100
- // id
101
- self.config.id = (!!self.config.id) ? self.config.id : "tileblaster";
102
-
103
- // socket
104
- self.config.socket = path.resolve(self.config.socket || "./"+self.config.id+".socket");
105
-
106
- // tile path
107
- self.config.tiles = path.resolve(self.config.tiles || "./tiles");
108
-
109
- // expires (default: never)
110
- self.config.expires = (dur(self.config.expires) || Infinity);
111
-
112
- // queue size
113
- self.config.queue = Math.max(parseInt(self.config.queue,10)||100,1);
114
-
115
- // extend mime types
116
- if (!!self.config.mime) Object.keys(self.config.mime).map(function(ext){ self.mime[ext] = self.config.mime[ext] });
117
-
118
- // queue
119
- self.queue = queue(self.config.queue);
120
-
121
- // check configured maps
122
- self.maps = Object.keys(self.config.maps).reduce(function(maps, id){
123
-
124
- return maps[id] = (function(map,id){
125
-
126
- // default zoom
127
- if (!map.zoom) map.zoom = [0,20];
128
-
129
- // check for subdomain feature
130
- if (map.url.indexOf("{s}") >= 0) {
131
- if (!map.sub) throw new Error("no subdomains configured for map "+id);
132
- if (typeof map.sub === "string") map.sub = map.sub.split("");
133
- if (!(map.sub instanceof Array)) throw new Error("invalid 'sub' option for map "+id);
134
- } else {
135
- map.sub = false;
136
- }
137
-
138
- // resolutions
139
- if (!map.res) map.res = [ "" ];
140
- if (!(map.res instanceof Array)) map.res = [ map.res ];
141
- map.res = map.res.filter(function(res){
142
- return (res === "") || /^@[1-9][0-9]*(\.[0-9]+)?x$/.test(res);
143
- });
144
-
145
- // precalculate bbox for zoom levels
146
- map.bounds = false;
147
- if (!!map.bbox) {
148
-
149
- // check if bbox and zoom are valid
150
- if (!(map.bbox instanceof Array) || map.bbox.length !== 4 || !self._checklnglat([map.bbox[0],map.bbox[1]]) || !self._checklnglat([map.bbox[2],map.bbox[3]])) throw new Error("invalid bounding box: "+JSON.stringify(map.bbox));
151
-
152
- // sort bbox to ensure wsen
153
- map.bbox = [
154
- Math.min(map.bbox[0], map.bbox[2]),
155
- Math.min(map.bbox[1], map.bbox[3]),
156
- Math.max(map.bbox[0], map.bbox[2]),
157
- Math.max(map.bbox[1], map.bbox[3]),
158
- ];
159
-
160
- map.bounds = [];
161
- self._range(map.zoom).forEach(function(z){
162
- map.bounds[z] = {
163
- "w": self._lngid(map.bbox[0],z),
164
- "s": self._latid(map.bbox[1],z),
165
- "e": self._lngid(map.bbox[2],z),
166
- "n": self._latid(map.bbox[3],z)
167
- };
168
- });
169
-
170
- };
171
-
172
- // ensure extension list is array
173
- map.ext = (!!map.ext) ? (map.ext instanceof Array) ? map.ext : [map.ext] : false;
174
-
175
- // expires
176
- if (!!map.expires) map.expires = dur(map.expires);
177
- if (!!map.expires) self.expiration = true;
178
- if (!map.expires) map.expires = Infinity;
179
-
180
- // cache
181
- map.cache = (!map.hasOwnProperty("cache")) ? true : (!!map.cache);
182
-
183
- // compression
184
- map.compress = map.compress.filter(function(c){ return (self.comp.indexOf(c) >= 0) });
185
-
186
- // is versatiles
187
- map.versatiles = (map.cloudtiles === true || map.versatiles === true);
188
-
189
- return map;
190
-
191
- })(self.config.maps[id],id), maps;
192
-
193
- },{});
194
-
195
- // build user agent
196
- if (!self.config.useragent) self.config.useragent = util.format("%s/%s (+%s)", pckg.name, pckg.version, pckg.homepage);
197
-
198
- // cleanup timer
199
- self.config.cleanup = (!!self.config.cleanup) ? dur(self.config.cleanup) : false;
200
- if (!!self.cleaner) clearInterval(self.cleaner);
201
- if (self.config.cleanup && self.expiration) self.cleaner = setInterval(function(){ self.cleanup(); }, self.config.cleanup).unref(), self.cleanup();
202
-
203
- };
204
-
205
- // return server
206
- tileblaster.prototype.server = function(){
207
- var self = this;
208
-
209
- self.srvr = http.createServer(function (req, res) {
210
-
211
- self.statistics.hits++;
212
-
213
- // check http method
214
- 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();
215
-
216
- // parse request
217
- var t = self._tilepath(url.parse(req.url).pathname.toLowerCase());
218
- 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();
219
-
220
- // check if map exists
221
- 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();
222
-
223
- // deliver file if requested
224
- if (t.type === "file") {
225
- switch (t.file) {
226
- case "tile.json":
227
- res.writeHead(200, { "Content-Type": "application/json" });
228
- res.end(self._tilejson(t.mapid, ((req.headers["x-https"] === "on") ? "https://" : "http://")+req.headers["host"]));
229
- break;
230
- default:
231
- return debug("<server> requested invalid file: %s", t.p), (!!self.config.hints&&res.setHeader("x-err-hint","invalid file")), res.statusCode = 404, res.end();
232
- break;
233
- }
234
- return;
235
- }
236
-
237
- // res
238
- t.r = (!!t.r && (self.maps[t.mapid].res.indexOf(t.r) >= 0) ? t.r : false);
239
-
240
- debug("<server> [%s] requested", t.p);
241
-
242
- self.tile(t.mapid, t.z, t.x, t.y, t.r, t.ext, function(err, stream, meta){
243
- 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();
244
-
245
- // send headers
246
- res.writeHead(200, { "Content-Type": meta['content-type'] });
247
-
248
- stream.on("end", function(){
249
- debug("<server> [%s] done", t.p);
250
- self.statistics.served++;
251
- });
252
-
253
- // pipe stream to http client
254
- stream.pipe(res);
255
-
256
- });
257
-
258
- });
259
-
260
- // in case self.listen() was called before self.server();
261
- if (self.listentome) self.listen();
262
- if (!!self.config.heartbeat) self.heartbeat();
263
-
264
- return this;
265
-
266
- };
267
-
268
- // listen on socket
269
- tileblaster.prototype.listen = function(){
270
- var self = this;
271
-
272
- // wait for server to be ready
273
- if (!self.srvr) return (self.listentome = true), this;
274
-
275
- // omit socket if port specified
276
- if (self.config.port) return self.srvr.listen(self.config.port, function(err){
277
- if (err) debug("<listen> unable to listen on port %d", self.config.port), process.exit(1);
278
- debug("<listen> listening on port %d", self.config.port);
279
- }), self;
280
-
281
- // listen on socket
282
- (function(fn){
283
- (function(next){
284
- fs.exists(self.config.socket, function(x){
285
- if (!x) return next();
286
- fs.unlink(self.config.socket, function(err){
287
- if (err) return fn(err);
288
- next();
289
- });
290
- });
291
- })(function(){
292
- self.srvr.listen(self.config.socket, function(err) {
293
- if (err) return fn(err);
294
- fs.chmod(self.config.socket, 0777, fn);
295
- });
296
- });
297
- })(function(err){
298
- if (err) debug("<listen> unable to listen on socket %s", self.config.socket), process.exit(1);
299
- debug("<listen> listening on socket %s", self.config.socket);
300
- });
301
-
302
- return this;
303
-
304
- };
305
-
306
- // start heartbeat server
307
- tileblaster.prototype.heartbeat = function(){
308
- var self = this;
309
-
310
- if (!self.config.heartbeat) return;
311
-
312
- // check if nsa dependency is installed
313
- try {
314
- var nsa = require("nsa");
315
- } catch (err) {
316
- return debug("<heartbeat> missing dependency: nsa");
317
- }
318
-
319
- self.nsa = nsa({
320
- server: self.config.heartbeat,
321
- service: self.config.id,
322
- interval: "10s"
323
- }).start(function(){
324
- // send stats every five minutes
325
- setInterval(function(){
326
-
327
- var h = self.statistics.hits - self.statistics.phits;
328
- var t = Date.now() - self.statistics.last;
329
- self.statistics.phits = self.statistics.hits;
330
- self.statistics.last = Date.now();
331
- var s = {
332
- "req/s": ((h/t)*1000).toFixed(2),
333
- "served": self.statistics.served
334
- };
335
- self.nsa.send(s);
336
- debug("<stat> %s req/s, %s served", s["req/s"], s.served);
337
- },60000).unref();
338
-
339
- });
340
-
341
- (function(terminate){
342
-
343
- process.on("SIGTERM", function(){ terminate("SIGTERM"); });
344
- process.on("SIGINT", function(){ terminate("SIGINT"); });
345
-
346
- })(function(signal){
347
- debug("<terminate> %s", signal);
348
- debug("<heartbeat> statistics: %j", self.statistics);
349
- self.nsa.end(function(){
350
- process.exit(0);
351
- });
352
- });
353
-
354
- return this;
355
- };
356
-
357
- // delete expired tiles
358
- tileblaster.prototype.cleanup = function(){
359
- var self = this;
360
-
361
- // clean up error cache
362
- var d = Date.now();
363
- self.errcache = Object.entries(self.errcache).reduce(function(errcache, [ key, value ]){
364
- if (value.until < d) errcache[key] = value;
365
- return errcache;
366
- },{});
367
-
368
- // delete queue
369
- var deletequeue = queue(1000);
370
- var now = Date.now();
371
-
372
- // clean up map tiles
373
- Object.keys(self.maps).filter(function(mapid){
374
- return (!!self.maps[mapid].expires);
375
- }).forEach(function(mapid){
376
- self.queue.push(function(done){
377
- debug("<cleanup> start: %s", mapid);
378
- walk(path.resolve(self.config.tiles, mapid)).on("data", function(f){
379
- if (f.stats.isFile() && f.stats.mtimeMs+self.maps[mapid].expires < now) deletequeue.push(function(done){
380
- fs.unlink(f.path, done);
381
- });
382
- }).on("error", function(err){
383
- if (err.code !== "ENOENT") debug("<cleanup> err: %s", err);
384
- else debug("<cleanup> done: %s", mapid);
385
- done();
386
- }).on("end", function(){
387
- debug("<cleanup> done: %s", mapid);
388
- done();
389
- });
390
- });
391
- });
392
-
393
- return this;
394
- };
395
-
396
- // get tile
397
- tileblaster.prototype.tile = function(mapid, z, x, y, r, e, fn){
398
- var self = this;
399
-
400
- // optionalize r and e
401
- if (typeof r === 'function') var e = r, r = false;
402
- if (typeof e === 'function') var fn = e, e = null;
403
-
404
- // generate tile filename
405
- var tilefile = self._tilefile(mapid, z, x, y, r, e);
406
-
407
- debug("<tile> [%s] requested", tilefile);
408
-
409
- // check tile
410
- self._checktile(mapid, z, x, y, r, e, function(err){
411
- if (err) return fn(err);
412
-
413
- debug("<tile> [%s] valid", tilefile);
414
-
415
- // check for cached 404 tiles
416
- if (!!self.errcache[tilefile] && (self.errcache[tilefile].until > Date.now())) return fn(new Error("Known bad tile: "+self.errcache[tilefile].err), null);
417
-
418
- // resolve tile path
419
- var tilepath = path.resolve(self.config.tiles, tilefile);
420
-
421
- // construct upstream tile url
422
- var tileurl = self._tileurl(mapid, z, x, y, r, e);
423
-
424
- (function(next){
425
-
426
- // versatiles branch
427
- if (self.maps[mapid].versatiles) return self.versatile(tileurl, mapid, z, x, y, next);
428
- self.fetchtile(tileurl, mapid, next);
429
-
430
- })(function(err, tilestream){
431
-
432
- if (err) {
433
- if (stream !== null) {
434
- // cache error tile
435
- self.errcache[tilefile] = { until: (Date.now()+self.config.maps[mapid].expires), err: err, code: stream };
436
-
437
- // mark tile as erronous in file system
438
- fs.writeFile(tilepath+".err", JSON.stringify(self.errcache[tilefile]), function(err){
439
- if (err) return debug("<tile> [%s.err] error: %s", tilefile, err.toString());
440
- });
441
- }
442
- return debug("<tile> [%s] error: %s", tilefile, err.toString()), fn(err, stream);
443
- }
444
-
445
- debug("<tile> [%s] fetched", tilefile);
446
-
447
- var streams = [];
448
-
449
- streams.push(function(tilestream){
450
- // call back with stream
451
- fn(null, tilestream, {
452
- 'content-type': (self.mime[e])
453
- });
454
- });
455
-
456
- if (!!self.config.maps[mapid].cache) streams.push(function(tilestream){
457
-
458
- // don't overwrite if tile exists
459
- fs.access(tilepath, fs.constants.F_OK, function(err){
460
- if (!err) return debug("<tile> [%s] exists", tilefile);
461
-
462
- fs.mkdir(path.dirname(tilepath), { recursive: true }, function(err){
463
- if (err) return debug("<tile> [%s] -- %s", tilefile, err);
464
-
465
- // save to tmp file, rename when done
466
- tilestream.pipe(fs.createWriteStream(tilepath+".tmp").on('finish', function(){
467
- fs.rename(tilepath+".tmp", tilepath, function(){
468
-
469
- // compress
470
- if (self.config.maps[mapid].compress instanceof Array && self.config.maps[mapid].compress.length > 0) self.compress(tilepath, self.config.maps[mapid].compress);
471
-
472
- debug("<tile> [%s] saved", tilefile);
473
- });
474
- }));
475
-
476
- });
477
-
478
- });
479
-
480
- });
481
-
482
- // stream mux
483
- tilestream.pipe(self.optimize(mapid, e)).pipe(self._mux.apply(self, streams));
484
-
485
- });
486
-
487
- });
488
-
489
- return this;
490
- };
491
-
492
- // fetch tile from remote
493
- tileblaster.prototype.fetchtile = function(tileurl, mapid, fn){
494
- var self = this;
495
-
496
- self.queue.push(function(done){
497
-
498
- // create agent
499
- var proto = tileurl.substr(0,tileurl.indexOf(":"));
500
- if (!self.agents.hasOwnProperty(proto)) self.agents[proto] = new require(proto).Agent({ keepAlive: true });
501
-
502
- debug("<fetchtile> [%s] requested", tileurl);
503
- var d = 0;
504
-
505
- phin({
506
- url: tileurl,
507
- headers: {
508
- 'user-agent': self.config.useragent, // be nice and tell who we are
509
- ...(self.config.maps[mapid].headers||{}), // extra headers from config
510
- },
511
- parse: "none",
512
- stream: true,
513
- followRedirects: true,
514
- compression: true,
515
- timeout: 10000,
516
- core: { agent: self.agents[proto] },
517
- }).then(function(resp){
518
-
519
- // check mime type, status code, content-length, FIXME: cache!
520
- if (resp.statusCode !== 200) return resp.stream.destroy(), fn(new Error("status code "+resp.statusCode), resp.statusCode);
521
-
522
- // parse raw headers if not set
523
- if (!resp.headers) {
524
- let rawHeaders = [ ...resp.rawHeaders ];
525
- resp.headers = {};
526
- while (rawHeaders.length > 0) resp.headers[ rawHeaders.shift().toLowerCase() ] = rawHeaders.shift();
527
- };
528
-
529
- // check headers
530
- if (!resp.headers['content-type']||(!!self.maps[mapid].mime&&self.maps[mapid].mime.indexOf(resp.headers['content-type'])<0)) return resp.stream.destroy(), fn(new Error("invalid content type "+resp.headers['content-type']), resp.statusCode);
531
- if (!!resp.headers['content-length']&&parseInt(resp.headers['content-length'],10)===0) return resp.stream.destroy(), fn(new Error("no content"), resp.statusCode);
532
-
533
- // signal queue when read stream has finished
534
- resp.stream.once('end', function(){ (!d++)&&done(); });
535
-
536
- debug("<fetchtile> [%s] received", tileurl);
537
-
538
- return fn(null, resp.stream, {
539
- date: (new Date(resp.headers.date||Date.now()).valueOf()),
540
- size: (parseInt(resp.headers['content-length'],10)||null),
541
- mime: (resp.headers['content-type']||'application/octet-stream'),
542
- });
543
-
544
- }).catch(function(err){
545
- return debug("<fetchtile> error fetching '%s': %s", tileurl, err), fn(err, null), done();
546
- });
547
-
548
- });
549
-
550
- };
551
-
552
- // get versatile
553
- tileblaster.prototype.versatile = function(tileurl, mapid, z, x, y, fn) {
554
- const self = this;
555
- if (!versatiles) return fn(new Error("Missing dependency: versatiles"));
556
- if (!self.maps[mapid].c) self.maps[mapid].c = versatiles(tileurl, { tms: !!self.maps[mapid].tms });
557
- self.maps[mapid].c.getTile(z,x,y, function(err, buf){
558
- if (err) return fn(err);
559
- self.maps[mapid].c.decompress(self.maps[mapid].c.header.tile_precompression, buf, function(err, buf){
560
- if (err) return fn(err);
561
- var strm = new stream.PassThrough;
562
- strm.write(buf);
563
- strm.end();
564
- return fn(null, strm);
565
- });
566
- });
567
- return self;
568
- };
569
-
570
- // optimization
571
- tileblaster.prototype.optimize = function(mapid, ext){
572
- var self = this;
573
-
574
- var strm = new stream.PassThrough;
575
-
576
- if (self.maps[mapid].optimize) switch (ext) {
577
- case "png":
578
- if (!pnck) break;
579
- debug("<optimize> [%s] png", mapid);
580
- return strm.pipe(pnck(['-o7','-zc8','-zm8','-f5','-quiet','-fix']));
581
- break;
582
- case "jpg":
583
- case "jpeg":
584
- if (!jpck) break;
585
- debug("<optimize> [%s] jpg", mapid);
586
- strm = strm.pipe(jpck({
587
- optimize: true,
588
- copy: "none",
589
- fastcrush: true,
590
- limit: 102400
591
- }));
592
- break;
593
- }
594
-
595
- return strm;
596
-
597
- };
598
-
599
- // compression
600
- tileblaster.prototype.compress = function(file, comp){
601
- var self = this;
602
- comp.forEach(function(c){
603
- self.cqueue.push(function(done){
604
- switch (c) {
605
- case "br":
606
- // use builtin brotli
607
- fs.createReadStream(file).pipe(zlib.createBrotliCompress({
608
- level: 6
609
- })).pipe(fs.createWriteStream(file+".br.tmp")).on("finish", function(){
610
- fs.rename(file+".br.tmp", file+".br", function(){
611
- debug("<tile> [%s] compressed with brotli", file);
612
- done();
613
- });
614
- });
615
- break;
616
- case "gz":
617
- if (zopfli !== null) {
618
- // use zopfli
619
- fs.createReadStream(file).pipe(zopfli.createGzip({
620
- numiterations: 5, // don't block the queue too long
621
- })).pipe(fs.createWriteStream(file+".gz.tmp")).on("finish", function(){
622
- fs.rename(file+".gz.tmp", file+".gz", function(){
623
- debug("<tile> [%s] compressed with zopfli", file);
624
- done();
625
- });
626
- });
627
- } else {
628
- // use builtin gzip
629
- fs.createReadStream(file).pipe(zlib.createGzip({
630
- level: 6
631
- })).pipe(fs.createWriteStream(file+".gz.tmp")).on("finish", function(){
632
- fs.rename(file+".gz.tmp", file+".gz", function(){
633
- debug("<tile> [%s] compressed with gzip", file);
634
- done();
635
- });
636
- });
637
- }
638
- break;
639
- default:
640
- done();
641
- break;
642
- }
643
- });
644
- });
645
- };
646
-
647
- // stream multiplexer
648
- tileblaster.prototype._mux = function(){
649
-
650
- // create a passthrough stream for every callback argument
651
- // call back with created stream
652
- var streams = Array.from(arguments).map(function(f){
653
- return (function(s,f){
654
- return f(s),s;
655
- })(new stream.PassThrough,f);
656
- });
657
-
658
- // multiplex to streams
659
- return (new stream.Writable({
660
- write: function(chunk, encoding, done) {
661
- streams.forEach(function(stream){
662
- stream.write(chunk, encoding);
663
- });
664
- done();
665
- },
666
- final: function(done) {
667
- streams.forEach(function(stream){
668
- stream.end();
669
- });
670
- done();
671
- }
672
- }));
673
-
674
- };
675
-
676
- // get all integer steps including start and end
677
- tileblaster.prototype._range = function(z){
678
- var zooms = [], z = Array.from(z.sort(function(a,b){ return a-b; })); // ensure order and deref
679
- while (z[0]<=z[1]) zooms.push(z[0]++);
680
- return zooms;
681
- };
682
-
683
- // convert lng to tile x
684
- tileblaster.prototype._lngid = function(lng,z){
685
- return (Math.floor((lng+180)/360*Math.pow(2,z)));
686
- };
687
-
688
- // convert lat to tile y
689
- tileblaster.prototype._latid = function(lat,z){
690
- return (Math.floor((1-Math.log(Math.tan(lat*Math.PI/180) + 1/Math.cos(lat*Math.PI/180))/Math.PI)/2 *Math.pow(2,z)));
691
- };
692
-
693
- // check lonlat
694
- tileblaster.prototype._checklnglat = function(lnglat) {
695
- if (!(lnglat instanceof Array) || lnglat.length !== 2) return false;
696
- lnglat = lnglat.map(parseFloat);
697
- return (!isNaN(lnglat[0]) && !isNaN(lnglat[1]) && lnglat[0] >= -180 && lnglat[0] <= 180 && lnglat[1] >= -90 && lnglat[1] <= 90);
698
- };
699
-
700
- // check if a tile meets specifications
701
- tileblaster.prototype._checktile = function(mapid, z, x, y, r, ext, fn){
702
- var self = this;
703
-
704
- // check map identifier
705
- if (!/^[A-Za-z0-9\-\_]+$/.test(mapid)) return debug("<check> invalid map '%s'", mapid), fn(new Error("Invalid map identifier"));
706
- if (!self.maps[mapid]) return debug("<check> unknown map '%s'", mapid), fn(new Error("Unknown map identifier"));
707
-
708
- // check extension
709
- 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"));
710
-
711
- // check density
712
- 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"));
713
-
714
- // check zoom level
715
- var zf = parseFloat(z,10);
716
- if (zf%1!==0) return debug("<check> invalid zoom float %d", zf), fn(new Error("Disallowed zoom factor"));
717
- if (zf < self.maps[mapid].zoom[0]) return debug("<check> invalid zoom %d < %d for map '%s'", zf, self.maps[mapid].zoom[0], mapid), fn(new Error("Disallowed zoom factor"));
718
- if (zf > self.maps[mapid].zoom[1]) return debug("<check> invalid zoom %d > %d for map '%s'", zf, self.maps[mapid].zoom[1], mapid), fn(new Error("Disallowed zoom factor"));
719
-
720
- // check bbox
721
- if (!!self.maps[mapid].bbox && (x < self.maps[mapid].bounds[z].w || x > self.maps[mapid].bounds[z].e)) return debug("<check> invalid tile x %d <> [%d-%d@%d] for map '%s'", x, self.maps[mapid].bounds[z].e, self.maps[mapid].bounds[z].w, z, mapid), fn(new Error("Disallowed tile x"));
722
- if (!!self.maps[mapid].bbox && (y < self.maps[mapid].bounds[z].n || y > self.maps[mapid].bounds[z].s)) return debug("<check> invalid tile y %d <> [%d-%d@%d] for map '%s'", y, self.maps[mapid].bounds[z].n, self.maps[mapid].bounds[z].s, z, mapid), fn(new Error("Disallowed tile y"));
723
-
724
- fn(null);
725
-
726
- };
727
-
728
- // transform parameters to url
729
- tileblaster.prototype._tileurl = function(mapid, z, x, y, r, e){
730
- var self = this;
731
-
732
- // when backend uses tms
733
- if (!!self.maps[mapid].tms) y = Math.pow(2,z)-y-1;
734
-
735
- return self.maps[mapid].url
736
- .replace("{s}", (self.maps[mapid].sub !== false) ? self.maps[mapid].sub[Math.floor(Math.random()*self.maps[mapid].sub.length)] : "")
737
- .replace("{x}", x.toFixed(0))
738
- .replace("{y}", y.toFixed(0))
739
- .replace("{z}", z.toFixed(0))
740
- .replace("{r}", (!!r) ? r : "")
741
- .replace("{e}", (e) ? e : "");
742
- };
743
-
744
- // transform parameters to filename
745
- tileblaster.prototype._tilefile = function(mapid, z, x, y, r, e){
746
- return (mapid+"/"+z.toFixed(0)+"/"+x.toFixed(0)+"/"+y.toFixed(0)+((!!r)?r:"")+"."+((e) ? e : ""));
747
- };
748
-
749
- // path parsing regular expression
750
- tileblaster.prototype._pathregx = /\/(([a-z0-9\-\_\.]+)\/((tile\.json)|([0-9]+)\/([0-9]+)\/([0-9]+)(@([0-9]+(\.[0-9]+)?)x)?\.([a-z0-9\.]+)))$/;
751
-
752
- // transform path to parameters
753
- tileblaster.prototype._tilepath = function(p) {
754
- var r = (this._pathregx.exec(p));
755
- if (!r) return false;
756
- if (!r[4]) {
757
- return {
758
- type: "tile",
759
- p: r[1],
760
- mapid: r[2],
761
- z: parseInt(r[5],10),
762
- x: parseInt(r[6],10),
763
- y: parseInt(r[7],10),
764
- r: r[8],
765
- ext: r[11],
766
- };
767
- } else {
768
- return {
769
- type: "file",
770
- p: r[1],
771
- mapid: r[2],
772
- file: r[4],
773
- };
774
- }
775
- };
776
-
777
- // assemble tilejson (good enough)
778
- tileblaster.prototype._tilejson = function(id, base) {
779
- var self = this;
780
- return JSON.stringify({
781
- tilejson: "2.2.0",
782
- minzoom: self.config.maps[id].zoom[0],
783
- maxzoom: self.config.maps[id].zoom[1],
784
- bounds: self.config.maps[id].bbox,
785
- tiles: [ (self.config.base||base)+"/"+id+"/{z}/{x}/{y}"+(self.config.maps[id].res[0]||"")+"."+self.config.maps[id].ext[0] ],
786
- });
787
- };
788
-
789
- module.exports = tileblaster;