s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
249 lines • 10.8 kB
JavaScript
import { DrawType } from 's2-tilejson';
import { MultiMap } from '../../../dataStore';
import { compressStream } from '../../../util';
import { BaseVectorTile, writeOVTile } from 'open-vector-tile';
import { PointCluster, PointIndex, Tile, TileStore } from '../../../dataStructures';
import { childrenIJ, convert, fromFace, toFaceIJ } from '../../../geometry';
/** Convert a vector feature to a collection of tiles and store each tile feature */
export default class VectorTileWorker {
id = 0;
layerGuides = [];
scheme = 'fzxy';
encoding = 'none';
vectorStore = new MultiMap();
// Unique store for each layer that describes itself as a cluster source
clusterStores = {};
rasterStore = new PointIndex();
elevationStore = new PointIndex();
/**
* 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.layerGuides = parseLayerGuides(message.layerGuides);
this.id = message.id;
if (message.scheme !== undefined)
this.scheme = message.scheme;
if (message.encoding !== undefined)
this.encoding = message.encoding;
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();
await this.rasterStore.sort();
await this.elevationStore.sort();
}
/**
* Iterate through the stores and build tiles, gzip compressing as we go
* @yields - a built tile
*/
async *buildTiles() {
const { layerGuides, scheme, encoding } = this;
const minzoom = getMinzoom(layerGuides);
// three directions we can build data
const tileCache = [fromFace(0)];
if (scheme === 'fzxy')
tileCache.push(fromFace(1), fromFace(2), fromFace(3), fromFace(4), fromFace(5));
while (tileCache.length > 0) {
const id = tileCache.pop();
const tile = new Tile(id);
// store vector features
const vectorFeatures = await this.vectorStore.get(id);
if (vectorFeatures !== undefined) {
for (const feature of vectorFeatures)
tile.addFeature(feature);
}
// 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 feature of layerClusterFeatures.layers.default.features) {
tile.addFeature(feature, layerName);
}
}
// TODO: Request raster tile if it exists
// if tile is not empty we build a vector tile
if (!tile.isEmpty()) {
// build the base vector tile layerguides => S2JSONLayerMap
const vectorTile = BaseVectorTile.fromS2JSONTile(tile, toLayerMap(layerGuides));
// write to a buffer using the open-vector-tile spec
let vectorTileBuffer = writeOVTile(vectorTile);
// gzip if necessary
if (encoding === 'gz') {
vectorTileBuffer = await compressStream(vectorTileBuffer, 'gzip');
}
// yield the buffer
yield { face: tile.face, zoom: tile.zoom, x: tile.i, y: tile.j, data: vectorTileBuffer };
// store 4 children tiles to ask for children features
tileCache.push(...childrenIJ(tile.face, tile.zoom, tile.i, tile.j));
}
else if (minzoom > tile.zoom) {
// if we haven't reached the data yet, we store children
tileCache.push(...childrenIJ(tile.face, tile.zoom, tile.i, tile.j));
}
}
}
/**
* 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, metadata: { drawTypes }, } = layerGuide;
const parsedFeature = onFeature !== undefined ? onFeature(feature) : feature;
if (parsedFeature === undefined)
return;
if (drawTypes.length === 0 || drawTypes.includes(toDrawType(feature)))
return;
if ('tileGuide' in layerGuide)
this.#storeVectorFeature(parsedFeature, layerGuide);
else if ('clusterGuide' in layerGuide)
this.#storeClusterFeature(parsedFeature, layerGuide);
// TODO raster source storing
}
}
/**
* Store a cluster feature in the correct point cluster
* @param feature - the feature to store
* @param clusterLayer - the layer guide to describe how to store the feature
*/
#storeClusterFeature(feature, clusterLayer) {
if (feature.geometry.type !== 'Point' && feature.geometry.type !== 'MultiPoint')
return;
const { scheme } = this;
const { clusterGuide, layerName, metadata: { maxzoom }, } = clusterLayer;
const projection = clusterGuide.projection ?? (scheme === 'fzxy' ? 'S2' : 'WM');
if (this.clusterStores[layerName] === undefined) {
this.clusterStores[layerName] = new PointCluster(undefined, clusterGuide);
}
const vectorFeature = convert(projection, feature, false, undefined, maxzoom, false)[0];
const { face, geometry: { type, coordinates }, properties, } = vectorFeature;
if (type === 'Point') {
const { x, y } = coordinates;
if (projection === 'S2')
this.clusterStores[layerName].insertFaceST(face ?? 0, x, y, properties);
else
this.clusterStores[layerName].insertLonLat(x, y, vectorFeature.properties);
}
else if (type === 'MultiPoint') {
for (const point of coordinates) {
const { x, y } = point;
if (projection === 'S2')
this.clusterStores[layerName].insertFaceST(face ?? 0, x, y, properties);
else
this.clusterStores[layerName].insertLonLat(x, y, vectorFeature.properties);
}
}
}
/**
* 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 { tileGuide, layerName, metadata: { minzoom }, } = vectorLayer;
// NOTE: Don't store above minzoom
// three directions we can build data
const tileStore = new TileStore(feature, tileGuide);
const tileCache = [fromFace(0)];
if (tileStore.projection === 'S2')
tileCache.push(fromFace(1), fromFace(2), fromFace(3), fromFace(4), fromFace(5));
while (tileCache.length > 0) {
const id = tileCache.pop();
const [face, zoom, i, j] = toFaceIJ(id);
const tile = tileStore.getTile(id);
if (minzoom > zoom) {
// if we haven't reached the data yet, we store children
tileCache.push(...childrenIJ(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(...childrenIJ(tile.face, tile.zoom, tile.i, tile.j));
}
}
}
}
/**
* Get the absolute minzoom from the layer guides
* @param layerGuides - the user defined guide on building the vector tiles
* @returns the absolute minzoom
*/
function getMinzoom(layerGuides) {
return Math.min(...layerGuides.map((layer) => layer.metadata.minzoom));
}
/**
* Convert 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 layer of layerGuides) {
if (!('extent' in layer))
continue;
const { shape, mShape } = layer.metadata;
res[layer.layerName] = { extent: layer.extent, shape, mShape };
}
return res;
}
/**
* Convert a source guide to a parsed source guide (where onFeature is parsed back into a function)
* @param sourceGuide - the source guide to parse
* @returns the parsed source guide
*/
function parseLayerGuides(sourceGuide) {
return sourceGuide.map((guide) => {
return {
...guide,
onFeature: guide.onFeature !== undefined ? new Function(guide.onFeature)() : undefined,
};
});
}
/**
* 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 }, } = feature;
if (type === 'Point' || type === 'MultiPoint')
return DrawType.Points;
else if (type === 'Point3D' || type === 'MultiPoint3D')
return DrawType.Points3D;
else if (type === 'LineString' || type === 'MultiLineString')
return DrawType.Lines;
else if (type === 'Polygon' || type === 'MultiPolygon')
return DrawType.Polys;
else if (type === 'LineString3D' || type === 'MultiLineString3D')
return DrawType.Lines3D;
else if (type === 'Polygon3D' || type === 'MultiPolygon3D')
return DrawType.Polys3D;
else
return DrawType.Points;
}
//# sourceMappingURL=tileWorker.js.map