UNPKG

@mapbox/tile-cover

Version:

generate the minimum number of tiles to cover a geojson geometry

254 lines (213 loc) 7.73 kB
'use strict'; var tilebelt = require('@mapbox/tilebelt'); /** * Given a geometry, create cells and return them in a format easily readable * by any software that reads GeoJSON. * * @alias geojson * @param {Object} geom GeoJSON geometry * @param {Object} limits an object with min_zoom and max_zoom properties * specifying the minimum and maximum level to be tiled. * @returns {Object} FeatureCollection of cells formatted as GeoJSON Features */ exports.geojson = function (geom, limits) { return { type: 'FeatureCollection', features: getTiles(geom, limits).map(tileToFeature) }; }; function tileToFeature(t) { return { type: 'Feature', geometry: tilebelt.tileToGeoJSON(t), properties: {} }; } /** * Given a geometry, create cells and return them in their raw form, * as an array of cell identifiers. * * @alias tiles * @param {Object} geom GeoJSON geometry * @param {Object} limits an object with min_zoom and max_zoom properties * specifying the minimum and maximum level to be tiled. * @returns {Array<Array<number>>} An array of tiles given as [x, y, z] arrays */ exports.tiles = getTiles; /** * Given a geometry, create cells and return them as * [quadkey](http://msdn.microsoft.com/en-us/library/bb259689.aspx) indexes. * * @alias indexes * @param {Object} geom GeoJSON geometry * @param {Object} limits an object with min_zoom and max_zoom properties * specifying the minimum and maximum level to be tiled. * @returns {Array<String>} An array of tiles given as quadkeys. */ exports.indexes = function (geom, limits) { return getTiles(geom, limits).map(tilebelt.tileToQuadkey); }; function getTiles(geom, limits) { var i, tile, coords = geom.coordinates, maxZoom = limits.max_zoom, tileHash = {}, tiles = []; if (geom.type === 'Point') { return [tilebelt.pointToTile(coords[0], coords[1], maxZoom)]; } else if (geom.type === 'MultiPoint') { for (i = 0; i < coords.length; i++) { tile = tilebelt.pointToTile(coords[i][0], coords[i][1], maxZoom); tileHash[toID(tile[0], tile[1], tile[2])] = true; } } else if (geom.type === 'LineString') { lineCover(tileHash, coords, maxZoom); } else if (geom.type === 'MultiLineString') { for (i = 0; i < coords.length; i++) { lineCover(tileHash, coords[i], maxZoom); } } else if (geom.type === 'Polygon') { polygonCover(tileHash, tiles, coords, maxZoom); } else if (geom.type === 'MultiPolygon') { for (i = 0; i < coords.length; i++) { polygonCover(tileHash, tiles, coords[i], maxZoom); } } else { throw new Error('Geometry type not implemented'); } if (limits.min_zoom !== maxZoom) { // sync tile hash and tile array so that both contain the same tiles var len = tiles.length; appendHashTiles(tileHash, tiles); for (i = 0; i < len; i++) { var t = tiles[i]; tileHash[toID(t[0], t[1], t[2])] = true; } return mergeTiles(tileHash, tiles, limits); } appendHashTiles(tileHash, tiles); return tiles; } function mergeTiles(tileHash, tiles, limits) { var mergedTiles = []; for (var z = limits.max_zoom; z > limits.min_zoom; z--) { var parentTileHash = {}; var parentTiles = []; for (var i = 0; i < tiles.length; i++) { var t = tiles[i]; if (t[0] % 2 === 0 && t[1] % 2 === 0) { var id2 = toID(t[0] + 1, t[1], z), id3 = toID(t[0], t[1] + 1, z), id4 = toID(t[0] + 1, t[1] + 1, z); if (tileHash[id2] && tileHash[id3] && tileHash[id4]) { tileHash[toID(t[0], t[1], t[2])] = false; tileHash[id2] = false; tileHash[id3] = false; tileHash[id4] = false; var parentTile = [t[0] / 2, t[1] / 2, z - 1]; if (z - 1 === limits.min_zoom) mergedTiles.push(parentTile); else { parentTileHash[toID(t[0] / 2, t[1] / 2, z - 1)] = true; parentTiles.push(parentTile); } } } } for (i = 0; i < tiles.length; i++) { t = tiles[i]; if (tileHash[toID(t[0], t[1], t[2])]) mergedTiles.push(t); } tileHash = parentTileHash; tiles = parentTiles; } return mergedTiles; } function polygonCover(tileHash, tileArray, geom, zoom) { var intersections = []; for (var i = 0; i < geom.length; i++) { var ring = []; lineCover(tileHash, geom[i], zoom, ring); for (var j = 0, len = ring.length, k = len - 1; j < len; k = j++) { var m = (j + 1) % len; var y = ring[j][1]; // add interesction if it's not local extremum or duplicate if ((y > ring[k][1] || y > ring[m][1]) && // not local minimum (y < ring[k][1] || y < ring[m][1]) && // not local maximum y !== ring[m][1]) intersections.push(ring[j]); } } intersections.sort(compareTiles); // sort by y, then x for (i = 0; i < intersections.length; i += 2) { // fill tiles between pairs of intersections y = intersections[i][1]; for (var x = intersections[i][0] + 1; x < intersections[i + 1][0]; x++) { var id = toID(x, y, zoom); if (!tileHash[id]) { tileArray.push([x, y, zoom]); } } } } function compareTiles(a, b) { return (a[1] - b[1]) || (a[0] - b[0]); } function lineCover(tileHash, coords, maxZoom, ring) { var prevX, prevY; for (var i = 0; i < coords.length - 1; i++) { var start = tilebelt.pointToTileFraction(coords[i][0], coords[i][1], maxZoom), stop = tilebelt.pointToTileFraction(coords[i + 1][0], coords[i + 1][1], maxZoom), x0 = start[0], y0 = start[1], x1 = stop[0], y1 = stop[1], dx = x1 - x0, dy = y1 - y0; if (dy === 0 && dx === 0) continue; var sx = dx > 0 ? 1 : -1, sy = dy > 0 ? 1 : -1, x = Math.floor(x0), y = Math.floor(y0), tMaxX = dx === 0 ? Infinity : Math.abs(((dx > 0 ? 1 : 0) + x - x0) / dx), tMaxY = dy === 0 ? Infinity : Math.abs(((dy > 0 ? 1 : 0) + y - y0) / dy), tdx = Math.abs(sx / dx), tdy = Math.abs(sy / dy); if (x !== prevX || y !== prevY) { tileHash[toID(x, y, maxZoom)] = true; if (ring && y !== prevY) ring.push([x, y]); prevX = x; prevY = y; } while (tMaxX < 1 || tMaxY < 1) { if (tMaxX < tMaxY) { tMaxX += tdx; x += sx; } else { tMaxY += tdy; y += sy; } tileHash[toID(x, y, maxZoom)] = true; if (ring && y !== prevY) ring.push([x, y]); prevX = x; prevY = y; } } if (ring && y === ring[0][1]) ring.pop(); } function appendHashTiles(hash, tiles) { var keys = Object.keys(hash); for (var i = 0; i < keys.length; i++) { tiles.push(fromID(+keys[i])); } } function toID(x, y, z) { var dim = 2 * (1 << z); return ((dim * y + x) * 32) + z; } function fromID(id) { var z = id % 32, dim = 2 * (1 << z), xy = ((id - z) / 32), x = xy % dim, y = ((xy - x) / dim) % dim; return [x, y, z]; }