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.
package/lib/purge.js ADDED
@@ -0,0 +1,109 @@
1
+ // purge cache cache
2
+
3
+ const worker = require("node:worker_threads");
4
+
5
+ if (worker.isMainThread) {
6
+
7
+ const debug = require("./debug");
8
+
9
+ const purge = module.exports = function purge(){
10
+
11
+ // create cleanup worker
12
+ const debug = require("./debug");
13
+ debug.info("Creating Purge Worker");
14
+ new worker.Worker(__filename, {
15
+ argv: process.argv.slice(2),
16
+ env: process.env
17
+ }).on("message", function(message){
18
+ if (message.hasOwnProperty("purged")) debug.info("Purged %d files", message.purged);
19
+ }).unref();
20
+
21
+ };
22
+
23
+ // run cleanup if called directly
24
+ if (require.main === module) purge();
25
+
26
+ } else { // cleanup worker
27
+
28
+ const dur = require("dur");
29
+ const config = require("./config");
30
+ const klaw = require("klaw");
31
+ const path = require("node:path");
32
+ const fs = require("node:fs");
33
+
34
+ // determine paths
35
+ if (!config.paths) config.paths = {};
36
+ if (!config.paths.work) config.paths.work = path.resolve(os.homedir(), "tileblaster");
37
+ if (!config.paths.data) config.paths.data = path.resolve(config.paths.work, "data");
38
+
39
+ const caches = Object.entries(config.maps).reduce(function(caches,[map,tasks]){
40
+
41
+ // find cache config fore map
42
+ let cache = tasks.find(function(task){
43
+ return task.builtin === "cache" && task.hasOwnProperty("expires");
44
+ });
45
+
46
+ // no cache here
47
+ if (!cache) return caches;
48
+
49
+ // parse expires, lifted from the builtin
50
+ let expires;
51
+ switch (typeof cache.expires) {
52
+ case "boolean":
53
+ expires = cache.expires;
54
+ break;
55
+ case "number":
56
+ expires = ((Number.isFinite(cache.expires)) ? Math.max(0,Math.round(cache.expires*1000)) : (cache.expires > 0)) || true;
57
+ break;
58
+ case "string":
59
+ expires = dur(cache.expires, true);
60
+ break;
61
+ default:
62
+ expires = true; // expires immediately
63
+ break;
64
+ }
65
+
66
+ if (!expires) return caches;
67
+ if (expires === true) expires = 0;
68
+
69
+ caches.push({
70
+ dir: path.join(config.paths.data, map),
71
+ expires: Date.now()+expires,
72
+ });
73
+
74
+ return caches;
75
+
76
+ },[]);
77
+
78
+ if (caches.length === 0) {
79
+ worker.parentPort.postMessage({ purged: 0 });
80
+ } else {
81
+
82
+ // create tasks
83
+ let purged = 0;
84
+ Promise.allSettled(caches.map(function(cache){
85
+ return new Promise(function(resolve,reject){
86
+
87
+ let deletable = [];
88
+
89
+ klaw(cache.dir).on("data", function(file){
90
+ if (file.stats.mtimeMs < cache.expires) deletable.push(file.path);
91
+ }).on("end", function(){
92
+ Promise.allSettled(deletable.map(function(file){
93
+ return new Promise(function(resolve,reject){
94
+ fs.unlink(file, function(err){
95
+ if (err) return reject(err);
96
+ purged++;
97
+ resolve();
98
+ });
99
+ });
100
+ })).then(resolve).catch(reject);
101
+ });
102
+ });
103
+ })).then(function(){
104
+ worker.parentPort.postMessage({ purged });
105
+ });
106
+
107
+ };
108
+
109
+ };
@@ -0,0 +1,15 @@
1
+ const phin = require("phin");
2
+
3
+ // prepare agents with keepalive enabled
4
+ const agents = {
5
+ http: new require("node:http").Agent({ keepAlive: true }),
6
+ https: new require("node:https").Agent({ keepAlive: true }),
7
+ };
8
+
9
+ // agentify phin
10
+ const retrieve = module.exports = async function retrieve(opts, fn) {
11
+ if (typeof opts === "string") opts = { url: opts }; // ensure opts is object
12
+ if (!opts.hasOwnProperty("core")) opts.core = {}; // ensure core is present
13
+ if (!opts.core.hasOwnProperty("agent")) opts.core.agent = agents[opts.url.slice(0,opts.url.indexOf(":"))]; // set agent by protocol
14
+ return phin(opts, fn);
15
+ };
@@ -0,0 +1,9 @@
1
+ // convert a date object into a rfc 822 date
2
+
3
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
4
+ const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
5
+
6
+ module.exports = function(date){
7
+ let tzo = date.getTimezoneOffset();
8
+ return days[date.getDay()] + ", " + date.getDate().toString().padStart(2,"0") + " " + months[date.getMonth()] + " " + date.getFullYear() + " " + date.getHours().toString().padStart(2,"0") + ":" + date.getMinutes().toString().padStart(2,"0") + ":" + date.getSeconds().toString().padStart(2,"0") + " " + (tzo > 0 ? "-" : "+") + Math.abs(Math.floor(tzo/60)).toString().padStart(2,"0") + Math.abs(tzo%60).toString().padStart(2,"0");
9
+ };
package/lib/router.js ADDED
@@ -0,0 +1,95 @@
1
+ const normalize = require("node:path").normalize;
2
+ const tasks = require("./tasks");
3
+
4
+ // unfancy router, good enough for tileblaster
5
+ const router = module.exports = function router({ mountpoint }) {
6
+ if (!(this instanceof router)) return new router(...arguments);
7
+ const self = this;
8
+
9
+ self.routes = {};
10
+ self.uses = [];
11
+
12
+ // mountpoint, without trailing slash
13
+ self.mountpoint = self.fixpath(mountpoint || "");
14
+
15
+ // default default route, very bare bones
16
+ self.routes[""] = function(req, res){
17
+ res.statusCode = 404, res.end();
18
+ };
19
+
20
+ return self;
21
+ };
22
+
23
+ // route handler, pass to server
24
+ router.prototype.serve = function(req, res) {
25
+ const self = this
26
+
27
+ // prepare
28
+ let path = req.url
29
+
30
+ // if url get path only
31
+ if (path.charCodeAt(0) !== 47) path = path.replace(/^https?:\/\/.*?\//,"/");
32
+
33
+ // trim query string and fragment
34
+ if (path.includes("?")) path = path.slice(0, path.indexOf("?"));
35
+ if (path.includes("#")) path = path.slice(0, path.indexOf("#"));
36
+
37
+ // trim mountpoint
38
+ if (self.mountpoint && path.slice(0,self.mountpoint.length) === self.mountpoint) path = path.slice(self.mountpoint.length);
39
+
40
+ // fix path
41
+ path = self.fixpath(path);
42
+
43
+ // FIXME some header prep? other things?
44
+
45
+ // find route
46
+ let route = path;
47
+ // while (!self.routes.hasOwnProperty(route)) route = route.slice(0, route.lastIndexOf("/")); // in case we need it later
48
+ if (!self.routes.hasOwnProperty(route)) route = ""; // good for now
49
+
50
+ // apply to req
51
+ req.path = path;
52
+ req.route = route;
53
+
54
+ // apply uses
55
+ tasks(self.uses).run(req, res, function(err, req, res){
56
+
57
+ // very unglamourous server error
58
+ if (err) return res.statusCode = 500, res.end();
59
+
60
+ // call route
61
+ self.routes[req.route](req, res);
62
+
63
+ });
64
+
65
+ };
66
+
67
+ // add route
68
+ router.prototype.route = function(path, route) { // function(req, res){}
69
+ return this.routes[path] = route, this;
70
+ };
71
+
72
+ // add middleware
73
+ router.prototype.use = function(use) { // function(req, res, next){}
74
+ return this.uses.push(use), this;
75
+ };
76
+
77
+ // set default route
78
+ router.prototype.default = function(route) {
79
+ return this.routes[""] = route, this;
80
+ };
81
+
82
+ // fix path
83
+ router.prototype.fixpath = function(path) {
84
+
85
+ // ensure leadin slash
86
+ if (path.charCodeAt(0) !== 47) path = "/"+path;
87
+
88
+ // remove trailing slashes
89
+ while (path.length > 1 && path.charCodeAt(path.length-1) === 47) path = path.slice(0, -1);
90
+
91
+ // normalize path
92
+ path = normalize(path);
93
+
94
+ return path;
95
+ };
package/lib/store.js ADDED
@@ -0,0 +1,161 @@
1
+ // store files to disk, use xattr to keep metadata
2
+
3
+ const fs = require("node:fs");
4
+ const dur = require("dur");
5
+ const path = require("node:path");
6
+ const xattr = require("fs-xattr");
7
+ const rfc822date = require("./rfc822date");
8
+
9
+ const store = module.exports = function store({ root }){
10
+ if (!(this instanceof store)) return new store(...arguments);
11
+
12
+ this.root = path.resolve(process.cwd(), root);
13
+ this.cache = {}; // FIXME use lru cache
14
+
15
+ return this;
16
+ };
17
+
18
+ // find best file on disk
19
+ store.prototype.find = function(file, extensions, fn){
20
+ const self = this;
21
+
22
+ // find all files in question
23
+ Promise.allSettled([ "", ...extensions.map(function(e){
24
+ return e[0] === "." ? e : "."+e;
25
+ })].map(function(ext){
26
+ return new Promise(function(resolve, reject){
27
+ const filepath = path.join(self.root, file+ext);
28
+ fs.stat(filepath, function(err, stats){
29
+ if (err) reject();
30
+ // get extended attributes
31
+ xattr.get(filepath, "user.tileblaster").then(function(attr){
32
+
33
+ try {
34
+ attr = JSON.parse(attr);
35
+ } catch (err) {
36
+ return reject(err);
37
+ }
38
+
39
+ // check if expired
40
+ if (attr.expires && (attr.expires === true || attr.expires < Date.now())) return reject();
41
+
42
+ resolve({
43
+ file: filepath,
44
+ stats: stats,
45
+ attr: attr,
46
+ });
47
+
48
+ }).catch(function(err){
49
+ reject(err);
50
+ });
51
+ })
52
+ });
53
+ })).then(function(found){
54
+
55
+ // extract
56
+ found = found.filter(function(f){
57
+ return f.status === "fulfilled";
58
+ }).map(function(f){
59
+ return f.value;
60
+ })
61
+
62
+ switch (found.length) {
63
+ case 0: return fn(null, null); break;
64
+ case 1: return fn(null, found[0]); break;
65
+ default:
66
+ // find the smallest size
67
+ return fn(null, Array.from(found).sort(function(a,b){
68
+ return a.stats.size - b.stats.size;
69
+ }).shift());
70
+ break;
71
+ }
72
+ });
73
+
74
+ return this;
75
+ };
76
+
77
+ // store file and attributes to disk
78
+ store.prototype.put = function(tile, fn){
79
+ const self = this;
80
+
81
+ const destfile = path.join(self.root, tile.path);
82
+ const tmpfile = destfile+".tmp";
83
+
84
+ // check if exists and still valid?
85
+ self.check(destfile, function(err, isValid){
86
+ if (!err && isValid) return fn(null); // end if file is still valid
87
+
88
+ // ensure dest dir
89
+ fs.mkdir(path.dirname(tmpfile), { recursive: true }, function(err){
90
+ if (err) return fn(err);
91
+
92
+ // write tmp file
93
+ fs.writeFile(tmpfile, tile.buffer, function(err){
94
+ if (err) return fn(err);
95
+
96
+ // set etag and rfc822-date
97
+ fs.stat(tmpfile, function(err, stats){
98
+ if (err) return fn(err);
99
+
100
+ tile.headers.etag = '"'+Math.floor(stats.mtimeMs/1000).toString(16)+'-'+(stats.size).toString(16)+'"';
101
+ tile.headers["last-modified"] = rfc822date(stats.mtime);
102
+ tile.headers["expires"] = rfc822date(new Date(tile.expires));
103
+
104
+ // store anything but buffer in xattr
105
+ const attr = Object.entries(tile).reduce(function(attr, [ k, v ]){
106
+ if (k !== "buffer") attr[k] = v;
107
+ return attr;
108
+ },{});
109
+
110
+ // set attributes
111
+ xattr.set(tmpfile, "user.tileblaster", JSON.stringify(attr)).then(function(){
112
+
113
+ // switch tmp → file
114
+ fs.rename(tmpfile, destfile, function(err){
115
+ if (err) return fn(err), fs.unlink(tmpfile, function(){}); // attempt unlinking, no feedback
116
+ fn(null);
117
+
118
+ });
119
+
120
+ }).catch(function(err){
121
+ return fn(err);
122
+ });
123
+
124
+ });
125
+
126
+ });
127
+
128
+ });
129
+
130
+ });
131
+
132
+ return this;
133
+ };
134
+
135
+ store.prototype.check = function(file, fn) { // err, isValid
136
+ const self = this;
137
+
138
+ fs.stat(file, function(err, stat){
139
+ if (err && err.code === "ENOENT") return fn(null, false); // does not exist
140
+ if (err) return fn(err);
141
+
142
+ xattr.get(file, "user.tileblaster").then(function(attr){
143
+
144
+ // try parse xattr
145
+ try {
146
+ attr = JSON.parse(attr);
147
+ } catch (err) {
148
+ return fn(err);
149
+ }
150
+
151
+ // file is valid when no expires property or expires < now
152
+ return fn(null, (!attr.expires || attr.expires < Date.now()));
153
+
154
+ }).catch(function(err){
155
+ return fn(err);
156
+ });
157
+
158
+ });
159
+
160
+ return this;
161
+ };
package/lib/strtpl.js ADDED
@@ -0,0 +1,15 @@
1
+ // minimalistic string tempate, no regex
2
+ const types = { string: true, number: true, boolean: true };
3
+ const strtpl = module.exports = function strtpl(str, params){
4
+ let out = "", j = 0;
5
+ for (let i = 0; i < str.length; i++) {
6
+ j = i+1;
7
+ if (str[i] === "{" && str[i+2] === "}" && str[j] >= "a" && str[j] <= "z") {
8
+ if (types[typeof params[str[j]]]) out += params[str[j]];
9
+ i+= 2;
10
+ } else {
11
+ out += str[i];
12
+ }
13
+ }
14
+ return out;
15
+ };
package/lib/tasks.js ADDED
@@ -0,0 +1,50 @@
1
+ // recursive task queue
2
+ const tasks = module.exports = function tasks(jobs){
3
+ if (!(this instanceof tasks)) return new tasks(...arguments);
4
+ const self = this;
5
+ self.stack = [];
6
+ if (jobs) self.push(jobs);
7
+ return this;
8
+ };
9
+
10
+ // add jobs to stack
11
+ tasks.prototype.push = function(jobs){
12
+ const self = this;
13
+ if (!Array.isArray(jobs)) jobs = [ jobs ];
14
+ jobs.forEach(function(fn){
15
+ if (typeof fn === "function") self.stack.push(fn);
16
+ });
17
+ return this;
18
+ };
19
+
20
+ // recursively run stack until empty
21
+ tasks.prototype.run = function(){
22
+ const self = this;
23
+ let args = Array.from(arguments);
24
+ let fn = args.pop(); // callback should always be last argument
25
+ if (typeof fn !== "function") throw new Error("tasks.run needs a function argument");
26
+ if (typeof args === "function") fn = args, args = {};
27
+ if (self.stack.length === 0) return fn(null, ...args);
28
+ try {
29
+ self.stack.shift()(...args, function(err){
30
+ if (err) return fn(err, ...args);
31
+ return self.run(...args, fn);
32
+ }, function(label){ // skip
33
+ if (label) {
34
+ // skip stack until found
35
+ let nextlabel = self.stack.findIndex((f)=>(f.label === label));
36
+ if (nextlabel >= 0) {
37
+ self.stack = self.stack.slice(nextlabel);
38
+ return self.run(...args, fn);
39
+ } else {
40
+ return fn(new Error("Tasks: Unable to find '"+label+"'"), ...args);
41
+ }
42
+ } else {
43
+ return fn(null, ...args); // end execution
44
+ }
45
+ });
46
+ } catch (err) {
47
+ return fn(err, ...args);
48
+ }
49
+ return this;
50
+ };
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "tileblaster",
3
- "version": "0.4.9",
4
- "description": "pretty fast optimizing & compressing tile caching proxy",
5
- "main": "lib/tileblaster.js",
3
+ "version": "1.0.0",
4
+ "description": "a quick and versatile map tile caching proxy",
5
+ "main": "tileblaster.js",
6
6
  "bin": {
7
7
  "tileblaster": "bin/tileblaster.js"
8
8
  },
9
+ "scripts": {
10
+ "dev": "DEBUG=tileblaster node bin/tileblaster-dev.js"
11
+ },
9
12
  "repository": {
10
13
  "type": "git",
11
14
  "url": "git+https://github.com/yetzt/tileblaster.git"
@@ -17,25 +20,34 @@
17
20
  },
18
21
  "homepage": "https://github.com/yetzt/tileblaster#readme",
19
22
  "dependencies": {
20
- "dur": "^0.0.3",
21
- "klaw": "^3.0.0",
23
+ "colrz": "^0.0.5",
24
+ "dur": "^1.0.1",
25
+ "fs-xattr": "^0.3.1",
26
+ "klaw": "^4.1.0",
22
27
  "minimist": "^1.2.8",
23
28
  "node-watch": "^0.7.3",
24
29
  "phin": "^3.7.0",
25
30
  "quu": "^0.4.3"
26
31
  },
27
32
  "optionalDependencies": {
28
- "debug": "^4.3.4",
29
- "jpck": "^1.0.2",
30
- "node-zopfli": "^2.1.4",
31
- "nsa": "^0.2",
32
- "pnck": "^1.0.1",
33
- "versatiles": "^0.3.0"
33
+ "fzstd": "^0.1.0",
34
+ "js-mozjpeg": "^0.1.2",
35
+ "mbg": "^0.0.2",
36
+ "node-liblzma": "^1.1.9",
37
+ "optipng-js": "^0.1.2",
38
+ "pmtiles": "^2.7.1",
39
+ "sharp": "^0.32.0",
40
+ "versatiles": "^0.3.1",
41
+ "vtt": "^0.0.3"
34
42
  },
35
43
  "engines": {
36
- "node": ">= 10"
44
+ "node": ">=14"
37
45
  },
38
46
  "os": [
39
- "!win32"
47
+ "darwin",
48
+ "linux"
49
+ ],
50
+ "keywords": [
51
+ "tileblaster", "tileserver", "map", "tiles", "tile", "mbtiles", "versatiles", "pmtiles", "server", "proxy", "cache", "vectortiles", "rastertiles", "mvt", "pbf"
40
52
  ]
41
53
  }
@@ -0,0 +1,22 @@
1
+ // bare bones example plugin
2
+ module.exports = function({ req, res, opts, data }, next, skip){
3
+
4
+ // next() calls the next task
5
+ // skip(name) skips all the next tasks until a plugin or builtin calles `name`, or until the end
6
+
7
+ // req and res are your http connection.
8
+ // if you consume res (e.g. send a resonse and call `res.edn()`),
9
+ // then set `res.used = true;` and use skip() to end processing
10
+
11
+ // opts contains everything from the config
12
+
13
+ // data contains all the data that's passed along
14
+ // data.tile for the "main" tile
15
+ // data.tiles[] for alternative versions
16
+
17
+ // let's so something:
18
+ this.lib.debug.info("I'm a plugin!");
19
+
20
+ // continue
21
+ next();
22
+ };
package/readme.md CHANGED
@@ -1,58 +1,91 @@
1
1
  # tileblaster
2
2
 
3
- tileblaster is a map tile caching (and optimizing) proxy, designed to run with nginx.
3
+ ![tileblaster](docs/tileblaster.png)
4
4
 
5
- ## install
5
+ tileblaster is a versatile caching proxy server for map tiles. it can handle many different tile sources and file formats
6
+ and can optimise tiles on the fly and speed up delivery by acting as a cache.
6
7
 
7
- `npm i tileblaster -g`
8
+ ## Awesome things you can do with tileblaster
8
9
 
9
- use `--no-optional` if you don't want tile optimization or [versatiles](https://github.com/versatiles-org/versatiles-spec) support.
10
+ * Serve tiles from any ZXY/TMS tileserver, [VersaTiles container](https://versatiles.org/), pmtiles container or mbtiles database.
11
+ * Edit vector tiles on the fly with [Vector Tile Transformer](https://www.npmjs.com/package/vtt)
12
+ * Optimize raster tiles with `mozjpeg` / `optipng`, convert them to `webp` / `aviv` format on the fly or edit them with [sharp](https://www.npmjs.com/package/sharp).
13
+ * Precompress tiles with `gzip` and `brotli`
14
+ * Cache remote files locally
10
15
 
11
- ## run
16
+ and much more
12
17
 
13
- `tileblaster /path/to/config.js`
18
+ ## What tileblaster isn't
14
19
 
15
- Use [pm2](https://npmjs.com/package/pm2), [nodemon](https://npmjs.com/package/nodemon), [forever](https://npmjs.com/package/forever) or similar to run tileblaster as service;
20
+ tileblaster is not a tileserver, it does not read raw OpenStreetMap data or create map tiles from scratch; you need to
21
+ have a source for map tiles. You can of course use tools like [tilemaker](https://tilemaker.org/) to create your own
22
+ tilesets, use freely available ready-made tiles from [Versatiles](https://versatiles.org/) or use another tileserver
23
+ if you're allowed to do so.
16
24
 
17
- ## configuration
25
+ ## Install
18
26
 
19
- see [config.js.dist](config.js.dist)
27
+ `npm i -g tileblaster`
20
28
 
21
- ### nginx configuration
29
+ ## Usage
30
+
31
+ `tileblaster [options] [-c] config.js`
32
+
33
+ ### Options
34
+
35
+ * `-c` `--config <config.js>` - load config file
36
+ * `-p` `--port <[host:]port>` - listen on this port (overrides config)
37
+ * `-s` `--socket <socket[,mode,gid]>` - on this socket (overrides config)
38
+ * `-t` `--threads <num>` - number of threads (overrides config)
39
+ * `-h` `--help` - print help screen
40
+ * `-v` `--verbose` - enable debug output
41
+ * `-q` `--quiet` - disable debug output
42
+
43
+ ## Configuration
44
+
45
+ See [Configuration](docs/config.md) and [Examples](docs/examples.md)
46
+
47
+ ### Plugins
48
+
49
+ tileblaster supports plugins. They work just like builtins, but you can specify them in `config.plugins`
50
+
51
+ [Example Plugin](plugins/example.js)
52
+
53
+ ### Nginx
54
+
55
+ tileblaster is easy to use with nginx acting as a reverse proxy. Here is a simple example:
22
56
 
23
57
  ```
24
- upstream upstream_tileblaster {
25
- server unix:/path/to/tileblaster.sock;
58
+ upstream tileblaster {
59
+ server 127.0.0.1:28897;
60
+ # server unix:/path/to/tileblaster.socket; # ← if you use sockets
26
61
  }
27
62
 
28
63
  server {
29
- listen 80;
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
- }
38
64
 
39
- location / {
40
- root /path/to/tileblaster/tiles;
41
- try_files $uri $uri/ @tileblaster;
65
+ # ...
66
+
67
+ location /tileblaster { # ← set config.server.mount to the same path
68
+ proxy_set_header Host $host;
69
+ proxy_set_header Origin $http_origin;
70
+ proxy_set_header Accept-Encoding $http_accept_encoding;
71
+ proxy_set_header Accept-Language $http_accept_language;
72
+ proxy_set_header Accept $http_accept;
73
+ proxy_set_header If-Modified-Since $http_if_modified_since
74
+ proxy_set_header If-None-Match $http_if_none_match
75
+ proxy_http_version 1.1;
76
+ proxy_pass http://tileblaster;
42
77
  }
43
78
 
44
- location @tileblaster {
45
- proxy_pass http://upstream_tileblaster;
46
- }
47
79
  }
80
+
48
81
  ```
49
82
 
50
- ## usage
83
+ ## Optional Dependencies
84
+
85
+ tileblaster has a few optional dependencies, that are mostly used for image manilulation and optimisation (Sharp, MozJPEG, OptiPNG) or more complex tile sources (Versatiles, PMTiles, MBTiles).
51
86
 
52
- get the tiles via `http://server/<mapid>/<z>/<x>/<y>[<d>].<ext>`
87
+ If you don't need them, install tileblaster with `npm i -g tileblaster --no-optional`
53
88
 
54
- * `<mapid>` is the map id specified in your `config.js`
55
- * `<z>`, `<x>` and `<z>` are the tile coorinates
56
- * `<d>` is the optional pixel density marker, for example `@2x`
57
- * `<ext>` is the extension, for example `png`, `geojson` or `pbf`
89
+ ## License
58
90
 
91
+ [Unlicense](./UNLICENSE.md)