import convert from './convert'; // GeoJSON conversion and preprocessing import clip from './clip'; // stripe clipping algorithm import wrap from './wrap'; // date line processing import transform from './transform'; // coordinate transformation import createTile from './tile'; // final simplified tile generation export default 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'); if (options.maxZoom < 0 || options.maxZoom > 24) throw new Error('maxZoom should be in the 0-24 range'); if (options.promoteId && options.generateId) throw new Error('promoteId and generateId cannot be used together.'); var features = convert(data, options); this.tiles = {}; this.tileCoords = []; if (debug) { console.timeEnd('preprocess data'); console.log('index: maxZoom: %d, maxPoints: %d', options.indexMaxZoom, options.indexMaxPoints); console.time('generate tiles'); this.stats = {}; this.total = 0; } features = wrap(features, options); // start slicing from the top tile down if (features.length) this.splitTile(features, 0, 0, 0); if (debug) { if (features.length) console.log('features: %d, points: %d', this.tiles[0].numFeatures, this.tiles[0].numPoints); console.timeEnd('generate tiles'); console.log('tiles generated:', this.total, JSON.stringify(this.stats)); } } GeoJSONVT.prototype.options = { maxZoom: 14, // max zoom to preserve detail on indexMaxZoom: 5, // max zoom in the tile index indexMaxPoints: 100000, // max number of points per tile in the tile index tolerance: 3, // simplification tolerance (higher means simpler) extent: 4096, // tile extent buffer: 64, // tile buffer on each side lineMetrics: false, // whether to calculate line metrics promoteId: null, // name of a feature property to be promoted to feature.id generateId: false, // whether to generate feature ids. Cannot be used with promoteId debug: 0 // logging level (0, 1 or 2) }; GeoJSONVT.prototype.splitTile = function (features, z, x, y, cz, cx, cy) { var stack = [features, z, x, y], options = this.options, debug = options.debug; // avoid recursion by using a processing queue while (stack.length) { y = stack.pop(); x = stack.pop(); z = stack.pop(); features = stack.pop(); var z2 = 1 << z, id = toID(z, x, y), tile = this.tiles[id]; if (!tile) { if (debug > 1) console.time('creation'); tile = this.tiles[id] = createTile(features, z, x, y, options); this.tileCoords.push({z: z, x: x, y: y}); 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'); } var key = 'z' + z; this.stats[key] = (this.stats[key] || 0) + 1; this.total++; } } // save reference to original geometry in tile so that we can drill down later if we stop now tile.source = features; // if it's the first-pass tiling if (!cz) { // stop tiling if we reached max zoom, or if the tile is too simple if (z === options.indexMaxZoom || tile.numPoints <= options.indexMaxPoints) continue; // if a drilldown to a specific tile } else { // stop tiling if we reached base zoom or our target tile zoom if (z === options.maxZoom || z === cz) continue; // stop tiling if it's not an ancestor of the target tile var m = 1 << (cz - z); if (x !== Math.floor(cx / m) || y !== Math.floor(cy / m)) continue; } // if we slice further down, no need to keep source geometry tile.source = null; if (features.length === 0) continue; if (debug > 1) console.time('clipping'); // values we'll use for clipping var k1 = 0.5 * options.buffer / options.extent, k2 = 0.5 - k1, k3 = 0.5 + k1, k4 = 1 + k1, tl, bl, tr, br, left, right; tl = bl = tr = br = null; left = clip(features, z2, x - k1, x + k3, 0, tile.minX, tile.maxX, options); right = clip(features, z2, x + k2, x + k4, 0, tile.minX, tile.maxX, options); features = null; if (left) { tl = clip(left, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options); bl = clip(left, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options); left = null; } if (right) { tr = clip(right, z2, y - k1, y + k3, 1, tile.minY, tile.maxY, options); br = clip(right, z2, y + k2, y + k4, 1, tile.minY, tile.maxY, options); right = null; } if (debug > 1) console.timeEnd('clipping'); stack.push(tl || [], z + 1, x * 2, y * 2); stack.push(bl || [], z + 1, x * 2, y * 2 + 1); stack.push(tr || [], z + 1, x * 2 + 1, y * 2); stack.push(br || [], z + 1, x * 2 + 1, y * 2 + 1); } }; GeoJSONVT.prototype.getTile = function (z, x, y) { var options = this.options, extent = options.extent, debug = options.debug; if (z < 0 || z > 24) return null; var z2 = 1 << z; x = ((x % z2) + z2) % z2; // wrap tile x coordinate var id = toID(z, x, y); if (this.tiles[id]) return transform(this.tiles[id], extent); 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 (!parent || !parent.source) return null; // if we found a parent tile containing the original geometry, we can drill down from it if (debug > 1) console.log('found parent tile z%d-%d-%d', z0, x0, y0); if (debug > 1) console.time('drilling down'); this.splitTile(parent.source, z0, x0, y0, z, x, y); if (debug > 1) console.timeEnd('drilling down'); return this.tiles[id] ? transform(this.tiles[id], extent) : null; }; function toID(z, x, y) { return (((1 << z) * y + x) * 32) + z; } function extend(dest, src) { for (var i in src) dest[i] = src[i]; return dest; }