UNPKG

gis-tools-ts

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

417 lines 16.9 kB
import { DrawType } from 's2-tilejson'; import { MultiMap } from '../../../dataStore'; import { compressStream } from '../../../util'; import { BaseVectorTile, writeMVTile, writeOVTile } from 'open-vector-tile'; import { PointCluster, PointGrid, Tile, TileStore } from '../../../dataStructures'; import { earclip, tesselate } from 'earclip'; import { idChildrenIJ, idFromFace, idToFaceIJ } from '../../../geometry'; /** Convert a vector feature to a collection of tiles and store each tile feature */ export default class TileWorker { id = 0; layerGuides = []; projection = 'S2'; encoding = 'none'; format = 'open-s2'; buildIndices = true; vectorStore = new MultiMap(); // Unique store for each layer that describes itself as a cluster source clusterStores = {}; rasterStores = {}; gridStores = {}; /** * Tile-ize input vector features and store them * @param event - the init message or a feature message */ onmessage(event) { this.handleMessage(event.data); } /** * Tile-ize input vector features and store them * @param message - the init message or a feature message */ handleMessage(message) { const { type } = message; if (type === 'init') { this.id = message.id; if (message.projection !== undefined) this.projection = message.projection; if (message.encoding !== undefined) this.encoding = message.encoding; if (message.format !== undefined) this.format = message.format; if (message.buildIndices !== undefined) this.buildIndices = message.buildIndices; this.#parseLayerGuides(message.layerGuides); self.postMessage({ type: 'ready' }); } else { this.storeFeature(message); } } /** Iterate through all the stores and sort/cluster as needed */ async sort() { for (const cluster of Object.values(this.clusterStores)) await cluster.buildClusters(); for (const raster of Object.values(this.rasterStores)) await raster.buildClusters(); for (const grid of Object.values(this.gridStores)) await grid.buildClusters(); } /** * Iterate through the stores and build tiles, compressing as we go if required * @yields - a built tile */ async *buildTiles() { const { format, layerGuides, projection, encoding } = this; const minzoom = getMinzoom(layerGuides); // three directions we can build data const tileCache = [idFromFace(0)]; if (projection === 'S2') tileCache.push(idFromFace(1), idFromFace(2), idFromFace(3), idFromFace(4), idFromFace(5)); while (tileCache.length > 0) { const id = tileCache.pop(); const tile = new Tile(id); const { face, zoom, i: x, j: y } = tile; const vectorTile = await this.#getVectorTile(id, tile); const rasterData = await this.#getRasterTile(id); const gridData = await this.#getGridTile(id); if (format === 'raster') { // RASTER CASE if (rasterData !== undefined) { const data = new Uint8Array(rasterData[0].image); yield { face, zoom, x, y, data }; // store 4 children tiles to ask for children features tileCache.push(...idChildrenIJ(face, zoom, x, y)); } else { // if we haven't reached the data yet, we store children if (minzoom > tile.zoom) tileCache.push(...idChildrenIJ(face, zoom, x, y)); } } else { // VECTOR CASE if (vectorTile === undefined && rasterData === undefined && gridData === undefined) { // if we haven't reached the data yet, we store children if (minzoom > tile.zoom) tileCache.push(...idChildrenIJ(face, zoom, x, y)); } else { // write to a buffer using the open-vector-tile spec let data = format === 'open-s2' ? writeOVTile(vectorTile, rasterData, gridData) : writeMVTile(vectorTile, format === 'mapbox'); // gzip if necessary if (encoding === 'gz') data = await compressStream(data, 'gzip'); // yield the buffer yield { face, zoom, x, y, data }; // store 4 children tiles to ask for children features tileCache.push(...idChildrenIJ(face, zoom, x, y)); } } } } /** * Store a feature across all appropriate zooms * @param message - the message to pull the feature and source info from */ storeFeature(message) { const { layerGuides } = this; const { feature, sourceName } = message; for (const layerGuide of layerGuides.filter((layer) => layer.sourceName === sourceName)) { const { onFeature, layerName } = layerGuide; const parsedFeature = onFeature !== undefined ? onFeature(feature) : feature; if (parsedFeature === undefined) return; if ('vectorGuide' in layerGuide) this.#storeVectorFeature(parsedFeature, layerGuide); else if ('clusterGuide' in layerGuide) this.clusterStores[layerName].insertFeature(parsedFeature); else if ('rasterGuide' in layerGuide) this.rasterStores[layerName].insertFeature(parsedFeature); else this.gridStores[layerName].insertFeature(parsedFeature); } } /** * Get vector/cluster features for a tile * @param id - the tile id * @param tile - the tile object to fill * @returns - a BaseVectorTile containing all the features if there are any */ async #getVectorTile(id, tile) { const { layerGuides, format, buildIndices } = this; if (format === 'raster') return; // store vector features const vectorFeatures = await this.vectorStore.get(id); if (vectorFeatures !== undefined) { if (buildIndices) earclipPolygons(vectorFeatures, tile.zoom, tile.extent); for (const feature of vectorFeatures) tile.addFeature(feature, feature.metadata?.layerName); } // store all cluster features for (const [layerName, cluster] of Object.entries(this.clusterStores)) { const layerClusterFeatures = await cluster.getTile(id); if (layerClusterFeatures === undefined) continue; for (const layer of Object.values(layerClusterFeatures.layers)) { for (const feature of layer.features) tile.addFeature(feature, layerName); } } if (!tile.isEmpty()) { // build the base vector tile layerguides => S2JSONLayerMap const vectorLayers = layerGuides.filter((layer) => 'vectorGuide' in layer); const maxzoom = getMaxzoom(layerGuides); tile.transform(0, maxzoom); return BaseVectorTile.fromS2JSONTile(tile, toLayerMap(vectorLayers)); } } /** * Get raster data for a tile * @param id - the tile id * @returns - a collection of GridInputs */ async #getRasterTile(id) { const res = []; // store all cluster features for (const raster of Object.values(this.rasterStores)) { const layerGrid = await raster.getTile(id); if (layerGrid === undefined) continue; const { name, size, data } = layerGrid; const image = data.flatMap(({ r, g, b, a }) => [r, g, b, a]); res.push({ name, type: 'raw', width: size, height: size, image: new Uint8Array(image), }); } if (res.length > 0) return res; } /** * Get gridded data for a tile * @param id - the tile id * @returns - a collection of ImageDataInputs */ async #getGridTile(id) { const res = []; // store all cluster features for (const [layerName, grid] of Object.entries(this.gridStores)) { const { extent } = this.layerGuides.filter((guide) => guide.layerName === layerName)[0]; const layerGrid = await grid.getTile(id); if (layerGrid === undefined) continue; const { name, size, data } = layerGrid; res.push({ name, size, data: data, extent, }); } if (res.length > 0) return res; } /** * Store a vector feature across all appropriate zooms * @param feature - the feature to store * @param vectorLayer - the layer guide to describe how to store the feature */ #storeVectorFeature(feature, vectorLayer) { const { projection } = this; const { vectorGuide, layerName, drawTypes } = vectorLayer; const minzoom = vectorGuide.minzoom ?? 0; if (!drawTypes.includes(toDrawType(feature))) return; // Setup a tileCache and dive down. Store the 4 children if data is found while storing data as we go const tileStore = new TileStore(feature, { projection, ...vectorGuide }); const tileCache = [idFromFace(0)]; if (projection === 'S2') tileCache.push(idFromFace(1), idFromFace(2), idFromFace(3), idFromFace(4), idFromFace(5)); while (tileCache.length > 0) { const id = tileCache.pop(); const [face, zoom, i, j] = idToFaceIJ(id); const tile = tileStore.getTile(id); if (minzoom > zoom) { // if we haven't reached the data yet, we store children tileCache.push(...idChildrenIJ(face, zoom, i, j)); } else if (tile !== undefined && !tile.isEmpty()) { // store feature with the associated layername for (const { features } of Object.values(tile.layers)) { for (const feature of features) { feature.metadata = { layer: layerName }; this.vectorStore.set(id, feature); } } // store 4 children tiles to ask for tileCache.push(...idChildrenIJ(face, zoom, i, j)); } } } /** * Convert a source guide to a parsed source guide (where onFeature is parsed back into a function) * @param sourceGuide - the source guide to parse */ #parseLayerGuides(sourceGuide) { const { projection } = this; // setup layerGuides this.layerGuides = sourceGuide.map((guide) => { return { ...guide, getValue: 'getValue' in guide && guide.getValue !== undefined ? new Function(guide.getValue)() : undefined, onFeature: guide.onFeature !== undefined ? new Function(guide.onFeature)() : undefined, }; }); // Setup layer stores for (const layer of this.layerGuides) { const { layerName } = layer; if ('vectorGuide' in layer) continue; else if ('clusterGuide' in layer) this.clusterStores[layerName] = new PointCluster(undefined, { projection, layerName, ...layer.clusterGuide, }); else if ('rasterGuide' in layer) this.rasterStores[layerName] = new PointGrid({ projection, layerName, ...layer.rasterGuide, }); else this.gridStores[layerName] = new PointGrid({ projection, layerName, ...layer.gridGuide }); } } } /** * Get the absolute minzoom from the layer guides * @param layerGuides - the user defined guide on building the vector tiles * @returns the absolute minzoom */ export function getMinzoom(layerGuides) { return Math.min(...layerGuides.map((layer) => { if ('vectorGuide' in layer) return layer.vectorGuide.minzoom ?? 0; else if ('clusterGuide' in layer) return layer.clusterGuide.minzoom ?? 0; else if ('rasterGuide' in layer) return layer.rasterGuide.minzoom ?? 0; else return layer.gridGuide.minzoom ?? 0; })); } /** * Get the absolute maxzoom from the layer guides * @param layerGuides - the user defined guide on building the vector tiles * @returns the absolute maxzoom */ export function getMaxzoom(layerGuides) { return Math.max(...layerGuides.map((layer) => { if ('vectorGuide' in layer) return layer.vectorGuide.maxzoom ?? 14; else if ('clusterGuide' in layer) return layer.clusterGuide.maxzoom ?? 14; else if ('rasterGuide' in layer) return layer.rasterGuide.maxzoom ?? 14; else return layer.gridGuide.maxzoom ?? 14; })); } /** * Convert vector layer guides to S2JSONLayerMap to store in the open-vector-tile schema * @param layerGuides - the user defined guide on building the vector tiles * @returns the S2JSONLayerMap */ function toLayerMap(layerGuides) { const res = {}; for (const { layerName, extent, shape, mShape } of layerGuides) res[layerName] = { extent, shape, mShape }; return res; } /** * Check if a feature is included by draw types defined by the layer guide * @param feature - the feature to find the associating draw type for * @returns - the associating draw type for the feature */ function toDrawType(feature) { const { geometry: { type, is3D }, } = feature; if (type === 'Point' || type === 'MultiPoint') return is3D ? DrawType.Points3D : DrawType.Points; else if (type === 'LineString' || type === 'MultiLineString') return is3D ? DrawType.Lines3D : DrawType.Lines; else if (type === 'Polygon' || type === 'MultiPolygon') return is3D ? DrawType.Polys3D : DrawType.Polys; else if (type === 'Point3D' || type === 'MultiPoint3D') return DrawType.Points3D; else if (type === 'LineString3D' || type === 'MultiLineString3D') return DrawType.Lines3D; else if (type === 'Polygon3D' || type === 'MultiPolygon3D') return DrawType.Polys3D; else if (type === 'Raster') return DrawType.Raster; else if (type === 'Grid') return DrawType.Grid; else return DrawType.Points; } /** * Pre-earclip polygons for faster processing in the tile * @param features - the features to be stored in the tile * @param zoom - the tile zoom * @param extent - the tile extent */ function earclipPolygons(features, zoom, extent) { const { max, min, floor, fround } = Math; for (const feature of features) { const { geometry } = feature; const { type } = geometry; if (type !== 'Polygon' && type !== 'MultiPolygon') continue; // prep polys const polys = []; // prep for processing if (type === 'MultiPolygon') { for (const poly of geometry.coordinates) polys.push(poly); } else { polys.push(geometry.coordinates); } let offset = 0; const verts = []; const indices = []; for (const poly of polys) { // create triangle mesh const data = earclip(poly, Infinity, fround(offset / 2)); // update vertex position offset += data.vertices.length; verts.push(...data.vertices); // store indices for (let i = 0, il = data.indices.length; i < il; i++) indices.push(data.indices[i]); } const tessPos = verts.length; const level = 1 << max(min(floor(zoom / 2), 4), 0); const division = 16 / level; if (division > 1) tesselate(verts, indices, extent / division, 2); const tessPoints = verts.slice(tessPos).map((n) => fround(n)); // store geometry.indices = indices; geometry.tesselation = tessPoints; } } //# sourceMappingURL=tileWorker.js.map