├── .eslintrc ├── .gitignore ├── LICENSE.txt ├── README.md ├── bench.js ├── index.js ├── package.json ├── test.js └── tile-generator /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "mourner", 3 | "rules": { 4 | "strict": [2, "never"], 5 | "indent": [2, 2, {"SwitchCase": 1}], 6 | "no-unreachable": [0], 7 | "camelcase": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | ISC License 3 | 4 | Copyright (c) 2017, Mapbox 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Generate a stream of XYZ tiles from a bounding box. 2 | 3 | ### install 4 | ```sh 5 | npm install tile-generator 6 | ``` 7 | 8 | ### usage 9 | 10 | ```js 11 | var tileGenerator = require('tile-generator'); 12 | 13 | var tiles = []; 14 | tileGenerator(12, [0, 30, 5, 35]) 15 | .on('data', function (tile) { 16 | tiles.push(tile); 17 | }) 18 | .on('end', function () { 19 | console.log(JSON.stringify(tiles); 20 | }); 21 | ``` 22 | 23 | tile-generator can also generate a stream of tiles for the whole world (`[-180, -85, 180, 85]`): 24 | 25 | ```js 26 | var tileGenerator = require('tile-generator'); 27 | 28 | tileGenerator(12) 29 | .on('data', function (tile) { 30 | console.log(tile); 31 | }); 32 | ``` 33 | 34 | ### cli 35 | 36 | ```sh 37 | tile-generator --minzoom 0 --maxzoom 10 --bbox -180,-85,180,85 38 | ``` 39 | -------------------------------------------------------------------------------- /bench.js: -------------------------------------------------------------------------------- 1 | var Benchmark = require('benchmark'); 2 | var tileGenerator = require('./'); 3 | var tileCover = require('tile-cover'); 4 | var bboxPolygon = require('turf-bbox-polygon'); 5 | 6 | var bbox = [ 7 | -77.16110229492188, 8 | 38.785133984236815, 9 | -76.89056396484375, 10 | 38.997841307500714 11 | ]; 12 | var bboxGeometry = bboxPolygon(bbox).geometry; 13 | 14 | var suite = new Benchmark.Suite('tile-generator'); 15 | suite 16 | .add('tile-generator#tile-cover control', function () { 17 | var tiles = tileCover.tiles(bboxGeometry, {min_zoom: 18, max_zoom: 18}); 18 | }) 19 | .add('tile-generator#tile-generator', function (deferred) { 20 | var tiles = []; 21 | tileGenerator(bbox, 18) 22 | .on('data', function (dt) { 23 | tiles.push(dt); 24 | }) 25 | .on('end', function () { 26 | deferred.resolve(); 27 | }); 28 | }, {defer: true}) 29 | .on('cycle', function (event) { 30 | console.log(String(event.target)); 31 | }) 32 | .on('complete', function () { 33 | 34 | }) 35 | .run(); 36 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Readable = require('readable-stream').Readable; 2 | var util = require('util'); 3 | 4 | var d2r = Math.PI / 180; 5 | 6 | function pointToTileFrac(lon, lat, z) { 7 | var sin = Math.sin(lat * d2r), 8 | z2 = Math.pow(2, z), 9 | x = ~~(z2 * (lon / 360 + 0.5)), 10 | y = ~~(z2 * (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI)); 11 | if (x === z2) x = z2 - 1; 12 | if (y === z2) y = z2 - 1; 13 | return [x, y, z]; 14 | } 15 | 16 | function TileGenerator(zoom, bbox, options) { 17 | options = options || {}; 18 | options.objectMode = true; 19 | if (!bbox) bbox = [-180, -85, 180, 85]; 20 | 21 | Readable.call(this, options); 22 | 23 | this.zoom = zoom; 24 | var sw = pointToTileFrac(bbox[0], bbox[1], zoom); 25 | var ne = pointToTileFrac(bbox[2], bbox[3], zoom); 26 | 27 | this.startX = sw[0]; 28 | this.endX = ne[0]; 29 | this.startY = ne[1]; 30 | this.endY = sw[1]; 31 | this.currX = this.startX - 1; 32 | this.currY = this.startY; 33 | } 34 | util.inherits(TileGenerator, Readable); 35 | 36 | TileGenerator.prototype.nextValue = function () { 37 | this.currX++; 38 | if (this.currX > this.endX) { 39 | this.currX = this.startX; 40 | this.currY++; 41 | } 42 | if (this.currY > this.endY) return null; 43 | 44 | return [this.currX, this.currY, this.zoom]; 45 | }; 46 | 47 | TileGenerator.prototype._read = function () { 48 | var next = this.nextValue(); 49 | return this.push(next); 50 | }; 51 | 52 | module.exports = function (zoom, bbox, options) { 53 | return new TileGenerator(zoom, bbox, options); 54 | }; 55 | 56 | module.exports.TileGenerator = TileGenerator; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tile-generator", 3 | "version": "1.1.0", 4 | "description": "Generate a stream of tiles in a bbox", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/mapbox/tile-generator.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/mapbox/tile-generator/issues" 12 | }, 13 | "scripts": { 14 | "pretest": "npm run lint", 15 | "lint": "eslint index.js test.js", 16 | "test": "tap -Rspec test.js" 17 | }, 18 | "bin": "tile-generator", 19 | "author": "Tim Channell", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "benchmark": "^1.0.0", 23 | "eslint": "^1.10.3", 24 | "eslint-config-mourner": "^1.0.1", 25 | "tap": "^2.3.4", 26 | "tile-cover": "^3.0.1", 27 | "turf-bbox-polygon": "^1.0.2" 28 | }, 29 | "dependencies": { 30 | "readable-stream": "^2.0.5", 31 | "yargs": "^11.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tileGenerator = require('./'), 2 | bboxPolygon = require('turf-bbox-polygon'), 3 | tileCover = require('tile-cover'), 4 | test = require('tap').test; 5 | 6 | var bbox = [ 7 | -77.16110229492188, 8 | 38.785133984236815, 9 | -76.89056396484375, 10 | 38.997841307500714 11 | ]; 12 | 13 | var sortFn = function (a, b) { 14 | if (a[0] === b[0]) { 15 | return a[1] - b[1]; 16 | } 17 | return a[0] - b[0]; 18 | }; 19 | 20 | [0, 10, 15, 18].map(function (zoom) { 21 | test('tile-generator -- DC at z' + zoom, function (t) { 22 | var cover = tileCover.tiles(bboxPolygon(bbox).geometry, {min_zoom: zoom, max_zoom: zoom}); 23 | var tiles = []; 24 | tileGenerator(zoom, bbox) 25 | .on('data', function (tile) { 26 | tiles.push(tile); 27 | }) 28 | .on('end', function () { 29 | cover.sort(sortFn); 30 | tiles.sort(sortFn); 31 | t.deepEqual(cover, tiles, 'should be the same as tile-cover'); 32 | t.equal(cover.length, tiles.length, 'has the same length as tile-cover - ' + tiles.length); 33 | t.end(); 34 | }); 35 | }); 36 | }); 37 | 38 | test('tile-generator -- no bbox runs the world', function (t) { 39 | var tiles = []; 40 | tileGenerator(1) 41 | .on('data', function (tile) { 42 | tiles.push(tile); 43 | }) 44 | .on('end', function () { 45 | t.equal(tiles.length, 4, 'world at z1 is 4 tiles'); 46 | t.deepEqual(tiles, [ 47 | [0, 0, 1], 48 | [1, 0, 1], 49 | [0, 1, 1], 50 | [1, 1, 1] 51 | ], 'should contain all z1 tiles'); 52 | t.end(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tile-generator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const tileGenerator = require('./'); 4 | const assert = require('assert'); 5 | const argv = require('yargs') 6 | .usage("$0 --minzoom 0 --maxzoom 10 --bbox west,south,east,north") 7 | .option('minzoom', { 8 | alias: 'z', 9 | type: 'number', 10 | requiresArg: true, 11 | }) 12 | .option('maxzoom', { 13 | alias: 'Z', 14 | type: 'number', 15 | requiresArg: true, 16 | }) 17 | .option('bbox', { 18 | type: 'string', 19 | requiresArg: true 20 | }) 21 | .help('h', 'Show help').alias('h', 'help') 22 | .argv; 23 | 24 | const min_zoom = argv.minzoom; 25 | const max_zoom = argv.maxzoom; 26 | const bbox = argv.bbox.split(',').map((coord) => { return Number(coord); }); 27 | 28 | assert(min_zoom <= max_zoom, 'min_zoom must be less than or equal to max_zoom'); 29 | assert(min_zoom >= 0, 'min_zoom must be greater than or equal to 0'); 30 | assert(bbox.length === 4, 'bbox must be 4 comma separated coordinates'); 31 | assert(bbox[0] < bbox[2], 'bbox west must be less than bbox east'); 32 | assert(bbox[1] < bbox[3], 'bbox south must be less than bbox north'); 33 | 34 | tilesForZoom(min_zoom); 35 | 36 | function tilesForZoom(z) { 37 | tileGenerator(z, bbox) 38 | .on('data', function (tile) { 39 | console.log(`[${tile[0]}, ${tile[1]}, ${tile[2]}]`); 40 | }) 41 | .on('end', function () { 42 | if (z < max_zoom) { 43 | tilesForZoom(z + 1); 44 | } 45 | }); 46 | } 47 | --------------------------------------------------------------------------------