geojson-vt
Version:
Slice GeoJSON data into vector tiles efficiently
204 lines (153 loc) • 5.91 kB
JavaScript
'use strict';
module.exports = geojsonvt;
var clip = require('./clip'),
convert = require('./convert'),
createTile = require('./tile'),
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) {
return new GeoJSONVT(data, options);
}
function GeoJSONVT(data, options) {
options = this.options = extend(Object.create(this.options), options);
var debug = options.debug;
if (debug) console.time('preprocess data');
var z2 = 1 << options.baseZoom,
features = convert(data, options.tolerance / (z2 * extent));
this.tiles = {};
if (debug) {
console.timeEnd('preprocess data');
console.time('generate tiles up to z' + options.maxZoom);
this.stats = [];
this.total = 0;
}
this.splitTile(features, 0, 0, 0);
if (debug) {
console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints);
console.timeEnd('generate tiles up to z' + options.maxZoom);
console.log('tiles generated:', this.total, this.stats);
}
}
GeoJSONVT.prototype.options = {
maxZoom: 4,
baseZoom: 14,
maxPoints: 100,
tolerance: 3,
debug: 0
};
GeoJSONVT.prototype.splitTile = function (features, z, x, y, cz, cx, cy) {
var stack = [features, z, x, y],
options = this.options,
debug = options.debug;
while (stack.length) {
features = stack.shift();
z = stack.shift();
x = stack.shift();
y = stack.shift();
var z2 = 1 << z,
id = toID(z, x, y),
tile = this.tiles[id],
tileTolerance = z === options.baseZoom ? 0 : options.tolerance / (z2 * extent);
if (!tile) {
if (debug > 1) console.time('creation');
tile = this.tiles[id] = createTile(features, z2, x, y, tileTolerance, extent, z === options.baseZoom);
if (debug) {
if (debug > 1) {
console.log('tile z%d-%d-%d (features: %d, points: %d, simplified: %d)',
z, x, y, tile.numFeatures, tile.numPoints, tile.numSimplified);
console.timeEnd('creation');
}
this.stats[z] = (this.stats[z] || 0) + 1;
this.total++;
}
}
if (!cz && (z === options.maxZoom || tile.numPoints <= options.maxPoints ||
isClippedSquare(tile.features)) || z === options.baseZoom || z === cz) {
tile.source = features;
continue; // stop tiling
}
if (cz) tile.source = features;
else tile.source = null;
if (debug > 1) console.time('clipping');
var k1 = 0.5 * padding,
k2 = 0.5 - k1,
k3 = 0.5 + k1,
k4 = 1 + k1,
tl, bl, tr, br, left, right,
m, goLeft, goTop;
if (cz) { // if we have a specific tile to drill down to, calculate where to go
m = 1 << (cz - z);
goLeft = cx / m - x < 0.5;
goTop = cy / m - y < 0.5;
}
tl = bl = tr = br = left = right = null;
if (!cz || goLeft) left = clip(features, z2, x - k1, x + k3, 0, intersectX);
if (!cz || !goLeft) right = clip(features, z2, x + k2, x + k4, 0, intersectX);
if (left) {
if (!cz || goTop) tl = clip(left, z2, y - k1, y + k3, 1, intersectY);
if (!cz || !goTop) bl = clip(left, z2, y + k2, y + k4, 1, intersectY);
}
if (right) {
if (!cz || goTop) tr = clip(right, z2, y - k1, y + k3, 1, intersectY);
if (!cz || !goTop) br = clip(right, z2, y + k2, y + k4, 1, intersectY);
}
if (debug > 1) console.timeEnd('clipping');
if (tl) stack.push(tl, z + 1, x * 2, y * 2);
if (bl) stack.push(bl, z + 1, x * 2, y * 2 + 1);
if (tr) stack.push(tr, z + 1, x * 2 + 1, y * 2);
if (br) stack.push(br, z + 1, x * 2 + 1, y * 2 + 1);
}
};
GeoJSONVT.prototype.getTile = function (z, x, y) {
var id = toID(z, x, y);
if (this.tiles[id]) return this.tiles[id];
var debug = this.options.debug;
if (debug > 1) console.log('drilling down to z%d-%d-%d', z, x, y);
var z0 = z,
x0 = x,
y0 = y,
parent;
while (!parent && z0 > 0) {
z0--;
x0 = Math.floor(x0 / 2);
y0 = Math.floor(y0 / 2);
parent = this.tiles[toID(z0, x0, y0)];
}
if (debug > 1) console.log('found parent tile z%d-%d-%d', z0, x0, y0);
if (parent.source) {
if (isClippedSquare(parent.features)) return parent;
if (debug) console.time('drilling down');
this.splitTile(parent.source, z0, x0, y0, z, x, y);
if (debug) console.timeEnd('drilling down');
}
return this.tiles[id];
};
// checks whether a tile is a whole-area fill after clipping; if it is, there's no sense slicing it further
function isClippedSquare(features) {
if (features.length !== 1) return false;
var feature = features[0];
if (feature.type !== 3 || feature.geometry.length > 1) return false;
for (var i = 0; i < feature.geometry[0].length; i++) {
var p = feature.geometry[0][i];
if ((p[0] !== minPx && p[0] !== maxPx) ||
(p[1] !== minPx && p[1] !== maxPx)) return false;
}
return true;
}
function toID(z, x, y) {
return (((1 << z) * y + x) * 32) + z;
}
function intersectX(a, b, x) {
return [x, (x - a[0]) * (b[1] - a[1]) / (b[0] - a[0]) + a[1], 1];
}
function intersectY(a, b, y) {
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];
}
return dest;
}