geojson-vt
Advanced tools
Comparing version 1.0.1 to 1.1.0
{ | ||
"name": "geojson-vt", | ||
"version": "1.0.1", | ||
"version": "1.1.0", | ||
"description": "Slice GeoJSON data into vector tiles efficiently", | ||
@@ -20,25 +20,49 @@ "homepage": "https://github.com/mapbox/geojson-vt", | ||
"benchmark": "^1.0.0", | ||
"browserify": "^7.0.0", | ||
"eslint": "^0.15.1", | ||
"faucet": "0.0.1", | ||
"jshint": "^2.5.10", | ||
"tape": "^3.0.3", | ||
"uglify-js": "^2.4.16", | ||
"watchify": "^2.1.1" | ||
}, | ||
"scripts": { | ||
"test": "jshint src/*.js test/test-*.js && tape test/test-*.js | faucet", | ||
"build-min": "browserify src/index.js -s geojsonvt -o geojson-vt.js", | ||
"build-dev": "browserify -d src/index.js -s geojsonvt -o debug/geojson-vt-dev.js", | ||
"watch": "watchify -v -d src/index.js -s geojsonvt -o debug/geojson-vt-dev.js" | ||
"test": "eslint src/*.js test/test-*.js && tape test/test-*.js | faucet", | ||
"build-min": "browserify src/index.js -s geojsonvt | uglifyjs -c -m -o geojson-vt.js", | ||
"build-dev": "browserify -d src/index.js -s geojsonvt -o geojson-vt-dev.js", | ||
"watch": "watchify -v -d src/index.js -s geojsonvt -o geojson-vt-dev.js" | ||
}, | ||
"jshintConfig": { | ||
"undef": true, | ||
"unused": true, | ||
"trailing": true, | ||
"eqeqeq": true, | ||
"indent": 4, | ||
"node": true, | ||
"latedef": "nofunc", | ||
"strict": true, | ||
"globalstrict": true, | ||
"quotmark": true | ||
"eslintConfig": { | ||
"rules": { | ||
"no-use-before-define": [ | ||
2, | ||
"nofunc" | ||
], | ||
"camelcase": 2, | ||
"space-after-function-name": 2, | ||
"space-in-parens": 2, | ||
"space-before-blocks": 2, | ||
"space-after-keywords": 2, | ||
"comma-style": 2, | ||
"no-lonely-if": 2, | ||
"no-else-return": 2, | ||
"new-cap": 2, | ||
"no-empty": 2, | ||
"no-new": 2, | ||
"key-spacing": 2, | ||
"no-multi-spaces": 0, | ||
"space-in-brackets": 2, | ||
"brace-style": 2, | ||
"indent": 2, | ||
"quotes": [ | ||
2, | ||
"single" | ||
], | ||
"curly": 0, | ||
"no-constant-condition": 0 | ||
}, | ||
"env": { | ||
"node": true, | ||
"browser": true | ||
} | ||
} | ||
} |
@@ -1,11 +0,32 @@ | ||
### GeoJSON Vector Tiles | ||
### geojson-vt — GeoJSON Vector Tiles | ||
A highly efficient JavaScript library for slicing GeoJSON data | ||
into [vector tiles](https://github.com/mapbox/vector-tile-spec/) | ||
(or rather their JSON equivalent) on the fly, | ||
primarily for rendering purposes. | ||
A highly efficient JavaScript library for **slicing GeoJSON data into vector tiles on the fly**, | ||
primarily designed to enable rendering and interacting with large geospatial datasets | ||
on the browser side (without a server). | ||
Created to power GeoJSON rendering in [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js), | ||
but can be useful for other data visualization purposes. | ||
Created to power GeoJSON in [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js), | ||
but can be useful in other visualization platforms | ||
like [Leaflet](https://github.com/Leaflet/Leaflet) and [d3](https://github.com/mbostock/d3). | ||
It can also be easily used on the server as well. | ||
Resulting tiles conform to the JSON equivalent | ||
of the [vector tile specification](https://github.com/mapbox/vector-tile-spec/). | ||
To make data rendering and interaction fast, the tiles are simplified, | ||
retaining the minimum level of detail appropriate for each zoom level | ||
(simplifying shapes, filtering out tiny polygons and polylines). | ||
#### Demo | ||
Here's **geojson-vt** action in [Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js), | ||
dynamically loading a 100Mb US zip codes GeoJSON with 5.4 million points: | ||
![](https://cloud.githubusercontent.com/assets/25395/5360312/86028d8e-7f91-11e4-811f-87f24acb09ca.gif) | ||
There's a convenient debug page to test out **geojson-vt** on different data. | ||
Make sure you have the [dev version built](#browser-builds); | ||
open `debug/index.html` in your browser, | ||
and drag any GeoJSON on the page, watching the console. | ||
![](https://cloud.githubusercontent.com/assets/25395/5363235/41955c6e-7fa8-11e4-9575-a66ef54cb6d9.gif) | ||
#### Usage | ||
@@ -15,8 +36,3 @@ | ||
// build an initial index of tiles | ||
var tileIndex = geojsonvt(geoJSON, { | ||
baseZoom: 14, // max zoom to preserve detail on | ||
maxZoom: 14, // zoom to slice down on first pass | ||
maxPoints: 100, // during first pass, stop slicing each tile below this number of points | ||
debug: 0 // debug level: 1 = some timing info; 2 = individual tiles timing; | ||
}); | ||
var tileIndex = geojsonvt(geoJSON); | ||
@@ -27,8 +43,25 @@ // request a particular tile | ||
#### Demo | ||
#### Options | ||
To see a **geojson-vt** in action, run `npm run build-dev`, | ||
then open `debug/index.html` in your browser and drag any GeoJSON on the page. | ||
It was tested on files up to 100Mb: | ||
You can fine-tune the results with an options object, | ||
although the defaults are sensible and work well for most use cases. | ||
![](https://cloud.githubusercontent.com/assets/25395/5328953/4edebdac-7d64-11e4-8e99-dddfd00851fb.gif) | ||
```js | ||
var tileIndes = geojsonvt(data, { | ||
baseZoom: 14, // max zoom to preserve detail on | ||
maxZoom: 4, // zoom to slice down to on first pass | ||
maxPoints: 100, // stop slicing each tile below this number of points | ||
tolerance: 3, // simplification tolerance (higher means simpler) | ||
extent: 4096, // tile extent (both width and height) | ||
buffer: 64, // tile buffer on each side | ||
debug: 0 // logging level (0 to disable, 1 or 2) | ||
}); | ||
``` | ||
#### Browser builds | ||
```bash | ||
npm install | ||
npm run build-dev # development build, used by the debug page | ||
npm run build-min # minified production build | ||
``` |
@@ -41,2 +41,4 @@ 'use strict'; | ||
if (slices.length) { | ||
// if a feature got clipped, it will likely get clipped on the next zoom level as well, | ||
// so there's no need to recalculate bboxes | ||
clipped.push({ | ||
@@ -120,2 +122,3 @@ geometry: slices, | ||
// add the last point | ||
a = points[len - 1]; | ||
@@ -125,6 +128,4 @@ ak = a[axis]; | ||
var sliceLen = slice.length; | ||
// close the polygon if its endpoints are not the same after clipping | ||
if (closed && slice[0] !== slice[sliceLen - 1]) slice.push(slice[0]); | ||
if (closed && slice[0] !== slice[slice.length - 1]) slice.push(slice[0]); | ||
@@ -140,4 +141,7 @@ // add the final slice | ||
if (slice.length) { | ||
// we don't recalculate the area/length of the unclipped geometry because the case where it goes | ||
// below the visibility threshold as a result of clipping is rare, so we avoid doing unnecessary work | ||
slice.area = area; | ||
slice.dist = dist; | ||
slices.push(slice); | ||
@@ -144,0 +148,0 @@ } |
@@ -7,3 +7,3 @@ 'use strict'; | ||
// converts GeoJSON feature into an intermediate JSON vector format with projection & simplification | ||
// converts GeoJSON feature into an intermediate projected JSON vector format with simplification data | ||
@@ -21,2 +21,3 @@ function convert(data, tolerance) { | ||
} else { | ||
// single geometry or a geometry collection | ||
convertFeature(features, {geometry: data}, tolerance); | ||
@@ -77,4 +78,4 @@ } | ||
tags: tags || null, | ||
min: [1, 1], | ||
max: [0, 0] | ||
min: [1, 1], // initial bbox values; | ||
max: [0, 0] // note that all coords are in [0..1] range | ||
}; | ||
@@ -106,11 +107,15 @@ calcBBox(feature); | ||
function calcSize(points) { | ||
var sum = 0, | ||
var area = 0, | ||
dist = 0; | ||
for (var i = 0, a, b; i < points.length - 1; i++) { | ||
a = b || points[i]; | ||
b = points[i + 1]; | ||
sum += a[0] * b[1] - b[0] * a[1]; | ||
dist += Math.abs(b[0] - a[0]) + Math.abs(b[1] - a[1]); // Manhattan distance | ||
area += a[0] * b[1] - b[0] * a[1]; | ||
// use Manhattan distance instead of Euclidian one to avoid expensive square root computation | ||
dist += Math.abs(b[0] - a[0]) + Math.abs(b[1] - a[1]); | ||
} | ||
points.area = Math.abs(sum / 2); | ||
points.area = Math.abs(area / 2); | ||
points.dist = dist; | ||
@@ -125,13 +130,9 @@ } | ||
if (feature.type === 1) { | ||
calcRingBBOX(min, max, geometry); | ||
} else { | ||
for (var i = 0; i < geometry.length; i++) { | ||
calcRingBBOX(min, max, geometry[i]); | ||
} | ||
} | ||
if (feature.type === 1) calcRingBBox(min, max, geometry); | ||
else for (var i = 0; i < geometry.length; i++) calcRingBBox(min, max, geometry[i]); | ||
return feature; | ||
} | ||
function calcRingBBOX(min, max, points) { | ||
function calcRingBBox(min, max, points) { | ||
for (var i = 0, p; i < points.length; i++) { | ||
@@ -138,0 +139,0 @@ p = points[i]; |
@@ -5,13 +5,7 @@ 'use strict'; | ||
var clip = require('./clip'), | ||
convert = require('./convert'), | ||
createTile = require('./tile'), | ||
var convert = require('./convert'), // GeoJSON conversion and preprocessing | ||
clip = require('./clip'), // stripe clipping algorithm | ||
createTile = require('./tile'); // final simplified tile generation | ||
extent = 4096, | ||
padding = 8 / 512, // padding on each side of the tile (in percentage of extent) | ||
minPx = Math.round(-padding * extent), | ||
maxPx = Math.round((1 + padding) * extent); | ||
function geojsonvt(data, options) { | ||
@@ -28,4 +22,4 @@ return new GeoJSONVT(data, options); | ||
var z2 = 1 << options.baseZoom, | ||
features = convert(data, options.tolerance / (z2 * extent)); | ||
var z2 = 1 << options.baseZoom, // 2^z | ||
features = convert(data, options.tolerance / (z2 * options.extent)); | ||
@@ -37,6 +31,7 @@ this.tiles = {}; | ||
console.time('generate tiles up to z' + options.maxZoom); | ||
this.stats = []; | ||
this.stats = {}; | ||
this.total = 0; | ||
} | ||
// start slicing from the top tile down | ||
this.splitTile(features, 0, 0, 0); | ||
@@ -47,3 +42,3 @@ | ||
console.timeEnd('generate tiles up to z' + options.maxZoom); | ||
console.log('tiles generated:', this.total, this.stats); | ||
console.log('tiles generated:', this.total, JSON.stringify(this.stats)); | ||
} | ||
@@ -53,7 +48,9 @@ } | ||
GeoJSONVT.prototype.options = { | ||
maxZoom: 4, | ||
baseZoom: 14, | ||
maxPoints: 100, | ||
tolerance: 3, | ||
debug: 0 | ||
baseZoom: 14, // max zoom to preserve detail on | ||
maxZoom: 4, // zoom to slice down to on first pass | ||
maxPoints: 100, // stop slicing a tile below this number of points | ||
tolerance: 3, // simplification tolerance (higher means simpler) | ||
extent: 4096, // tile extent | ||
buffer: 64, // tile buffer on each side | ||
debug: 0 // logging level (0, 1 or 2) | ||
}; | ||
@@ -65,4 +62,7 @@ | ||
options = this.options, | ||
debug = options.debug; | ||
debug = options.debug, | ||
extent = options.extent, | ||
buffer = options.buffer; | ||
// avoid recursion by using a processing queue | ||
while (stack.length) { | ||
@@ -90,3 +90,4 @@ features = stack.shift(); | ||
} | ||
this.stats[z] = (this.stats[z] || 0) + 1; | ||
var key = 'z' + z + ':'; | ||
this.stats[key] = (this.stats[key] || 0) + 1; | ||
this.total++; | ||
@@ -97,3 +98,3 @@ } | ||
if (!cz && (z === options.maxZoom || tile.numPoints <= options.maxPoints || | ||
isClippedSquare(tile.features)) || z === options.baseZoom || z === cz) { | ||
isClippedSquare(tile.features, extent, buffer)) || z === options.baseZoom || z === cz) { | ||
tile.source = features; | ||
@@ -108,3 +109,4 @@ continue; // stop tiling | ||
var k1 = 0.5 * padding, | ||
// values we'll use for clipping | ||
var k1 = 0.5 * buffer / extent, | ||
k2 = 0.5 - k1, | ||
@@ -151,3 +153,4 @@ k3 = 0.5 + k1, | ||
var debug = this.options.debug; | ||
var options = this.options, | ||
debug = options.debug; | ||
@@ -170,4 +173,5 @@ if (debug > 1) console.log('drilling down to z%d-%d-%d', z, x, y); | ||
// if we found a parent tile containing the original geometry, we can drill down from it | ||
if (parent.source) { | ||
if (isClippedSquare(parent.features)) return parent; | ||
if (isClippedSquare(parent.features, options.extent, options.buffer)) return parent; | ||
@@ -183,3 +187,3 @@ if (debug) console.time('drilling down'); | ||
// checks whether a tile is a whole-area fill after clipping; if it is, there's no sense slicing it further | ||
function isClippedSquare(features) { | ||
function isClippedSquare(features, extent, buffer) { | ||
if (features.length !== 1) return false; | ||
@@ -192,4 +196,4 @@ | ||
var p = feature.geometry[0][i]; | ||
if ((p[0] !== minPx && p[0] !== maxPx) || | ||
(p[1] !== minPx && p[1] !== maxPx)) return false; | ||
if ((p[0] !== -buffer && p[0] !== extent + buffer) || | ||
(p[1] !== -buffer && p[1] !== extent + buffer)) return false; | ||
} | ||
@@ -206,3 +210,2 @@ return true; | ||
} | ||
function intersectY(a, b, y) { | ||
@@ -213,6 +216,4 @@ return [(y - a[1]) * (b[0] - a[0]) / (b[1] - a[1]) + a[0], y, 1]; | ||
function extend(dest, src) { | ||
for (var i in src) { | ||
dest[i] = src[i]; | ||
} | ||
for (var i in src) dest[i] = src[i]; | ||
return dest; | ||
} |
@@ -16,5 +16,7 @@ 'use strict'; | ||
// always retain the endpoints (1 is the max value) | ||
points[first][2] = 1; | ||
points[last][2] = 1; | ||
// avoid recursion by using a stack | ||
while (last) { | ||
@@ -34,3 +36,3 @@ | ||
if (maxSqDist > sqTolerance) { | ||
points[index][2] = maxSqDist; | ||
points[index][2] = maxSqDist; // save the point importance in squared pixels as a z coordinate | ||
stack.push(first, index, index, last); | ||
@@ -37,0 +39,0 @@ } |
@@ -52,3 +52,3 @@ 'use strict'; | ||
p = ring[j]; | ||
// keep points with significance > tolerance and points introduced by clipping | ||
// keep points with importance > tolerance | ||
if (noSimplify || p[2] > sqTolerance) { | ||
@@ -55,0 +55,0 @@ transformedRing.push(transformPoint(p, z2, tx, ty, extent)); |
@@ -6,2 +6,3 @@ 'use strict'; | ||
/*eslint comma-spacing:0*/ | ||
@@ -26,10 +27,10 @@ function intersectX(p0, p1, x) { | ||
var expected = [ | ||
{geometry:[ | ||
{geometry: [ | ||
[[10,0],[40,0]], | ||
[[40,10],[20,10],[20,20],[30,20],[30,30],[40,30]], | ||
[[40,40],[25,40],[25,50],[10,50]], | ||
[[10,60],[25,60]]], type:2,tags:1}, | ||
{geometry:[ | ||
[[10,60],[25,60]]], type: 2, tags: 1}, | ||
{geometry: [ | ||
[[10,0],[40,0]], | ||
[[40,10],[10,10]]], type:2,tags:2}]; | ||
[[40,10],[10,10]]], type: 2, tags: 2}]; | ||
@@ -53,5 +54,5 @@ t.equal(JSON.stringify(clipped), JSON.stringify(expected)); | ||
var expected = [ | ||
{geometry:[[[10,0],[40,0],[40,10],[20,10],[20,20],[30,20],[30,30],[40,30], | ||
[40,40],[25,40],[25,50],[10,50],[10,60],[25,60],[10,24],[10,0]]],type:3,tags:1}, | ||
{geometry:[[[10,0],[40,0],[40,10],[10,10],[10,0]]],type:3,tags:2} | ||
{geometry: [[[10,0],[40,0],[40,10],[20,10],[20,20],[30,20],[30,30],[40,30], | ||
[40,40],[25,40],[25,50],[10,50],[10,60],[25,60],[10,24],[10,0]]], type: 3, tags: 1}, | ||
{geometry: [[[10,0],[40,0],[40,10],[10,10],[10,0]]], type: 3, tags: 2} | ||
]; | ||
@@ -71,5 +72,5 @@ | ||
t.same(clipped, [{geometry:[[20,10],[20,20],[30,20],[30,30],[25,40],[25,50],[25,60]],type:1,tags:1}]); | ||
t.same(clipped, [{geometry: [[20,10],[20,20],[30,20],[30,30],[25,40],[25,50],[25,60]], type: 1, tags: 1}]); | ||
t.end(); | ||
}); |
@@ -6,2 +6,4 @@ 'use strict'; | ||
/*eslint comma-spacing:0, no-shadow: 0*/ | ||
var points = [ | ||
@@ -8,0 +10,0 @@ [0.22455,0.25015],[0.22691,0.24419],[0.23331,0.24145],[0.23498,0.23606], |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
1507315
6424
66
7