s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
240 lines (239 loc) • 10 kB
JavaScript
import parseFeature from 's2/style/parseFeature.js';
import parseFilter from 'style/parseFilter.js';
import VectorWorker, { colorFunc, idToRGB } from './vectorWorker.js';
import { featureSort, scaleShiftClipPoints } from './util/index.js';
/** Worker for processing point data */
export default class PointWorker extends VectorWorker {
featureStore = new Map(); // tileID -> features
/**
* Setup a layer for future data processing
* @param layerDefinition - layer definition
* @returns the pre-processed layer
*/
setupLayer(layerDefinition) {
const { type, name, layerIndex, source, layer, minzoom, maxzoom, filter, geoFilter, radius, opacity, lch, } = layerDefinition;
// build featureCode design
// heatmap: radius -> opacity -> intensity
// point: radius -> opacity -> color -> stroke -> strokeWidth
const design = [[radius], [opacity]];
if (type === 'point') {
const { color, stroke, strokeWidth } = layerDefinition;
design.push([color, colorFunc(lch)], [stroke, colorFunc(lch)], [strokeWidth]);
}
else {
const { intensity } = layerDefinition;
design.push([intensity]);
}
const base = {
name,
layerIndex,
source,
layer,
minzoom,
maxzoom,
filter: parseFilter(filter),
getCode: this.buildCode(design),
};
if (type === 'point') {
const { interactive, cursor } = layerDefinition;
return { type, geoFilter, interactive, cursor, ...base };
}
else {
const weight = parseFeature(layerDefinition.weight);
return { type, geoFilter, weight, ...base };
}
}
/**
* Build a point feature
* @param tile - the tile request
* @param extent - the tile extent
* @param feature - the vector tile feature
* @param layer - the layer definition
* @param mapID - the map id to ship the data back to
* @param sourceName - the source name for the data to belong to
* @returns true if the feature was built
*/
buildFeature(tile, extent, feature, layer, mapID, sourceName) {
const { gpuType } = this;
const { zoom } = tile;
const { properties } = feature;
const { type, getCode, layerIndex, geoFilter } = layer;
const featureType = feature.geoType();
if (geoFilter.includes('poly') && (featureType === 'Polygon' || featureType === 'MultiPolygon'))
return false;
if (geoFilter.includes('line') &&
(featureType === 'LineString' || featureType === 'MultiLineString'))
return false;
if (geoFilter.includes('point') && (featureType === 'Point' || featureType === 'MultiPoint'))
return false;
// load geometry
const points = feature.loadPoints();
if (points === undefined)
return false;
// preprocess geometry
const clip = scaleShiftClipPoints(points, extent, tile);
if (clip === undefined)
return false;
const vertices = [];
const weights = [];
const isHeatmap = type === 'heatmap';
const weight = isHeatmap ? layer.weight([], properties, zoom) : 0;
// create multiplier
const multiplier = 1 / extent;
// if weight, then it is a heatmap and we add weight data
for (const point of clip) {
vertices.push(point.x * multiplier, point.y * multiplier);
if (isHeatmap)
weights.push(weight);
}
// skip empty geometry
if (vertices.length === 0)
return false;
const codeLoBoth = getCode(zoom, properties);
const codeLo = codeLoBoth[gpuType === 1 ? 0 : 1];
const gl2Code = codeLoBoth[1];
const codeHi = getCode(zoom + 1, properties)[gpuType === 1 ? 0 : 1];
const typeFeature = {
vertices,
layerIndex,
code: type === 'point' ? codeLo : [...codeLo, ...codeHi],
gl2Code,
};
const storeID = `${mapID}:${tile.id}:${sourceName}`;
if (!this.featureStore.has(storeID))
this.featureStore.set(storeID, { point: [], heatmap: [] });
const store = this.featureStore.get(storeID);
if (type === 'point') {
const id = this.idGen.getNum();
store?.point.push({
type: 'point',
idRGB: idToRGB(id),
...typeFeature,
});
// if interactive, store interactive properties
if (layer.interactive)
this._addInteractiveFeature(id, properties, layer);
}
else {
store?.heatmap.push({
type: 'heatmap',
weights,
...typeFeature,
});
}
return true;
}
/**
* Flush the feature store (all processed data is sent back to the main thread)
* @param mapID - the map id to ship the data back to
* @param tile - the tile request
* @param sourceName - the source name the data to belongs to
* @param wait - this promise must be resloved before flushing.
*/
async flush(mapID, tile, sourceName, wait) {
this.#flush(mapID, sourceName, tile.id, 'point');
this.#flush(mapID, sourceName, tile.id, 'heatmap');
this.featureStore.delete(`${mapID}:${tile.id}:${sourceName}`);
await super.flush(mapID, tile, sourceName, wait);
}
/**
* Internal flush function
* @param mapID - the map id to ship the data back to
* @param sourceName - the source name the data to belongs to
* @param tileID - the tile id
* @param type - the feature type (point or heatmap)
*/
#flush(mapID, sourceName, tileID, type) {
const features = (this.featureStore.get(`${mapID}:${tileID}:${sourceName}`) ?? {
point: [],
heatmap: [],
})[type];
if (features.length === 0)
return;
// Step 1: Sort by layerIndex, than sort by feature code.
features.sort(featureSort);
// step 2: Run through all features and bundle into the fewest featureBatches. Caveats:
// 1) don't store VAO set larger than index size (we use an extension for WebGL1, so we will probably never go over 1 << 32)
// 2) don't store any feature code larger than MAX_FEATURE_BATCH_SIZE
const vertices = [];
const weights = [];
const featureGuide = [];
const ids = [];
let encodings = features[0].code;
let indexCount = 0;
let indexOffset = 0;
let curFeatureCode = encodings.toString();
let curlayerIndex = features[0].layerIndex;
for (const feature of features) {
const { type: featureType, layerIndex, code, vertices: _vertices } = feature;
// on layer change or max feature code change, we have to setup a new featureGuide
if (indexCount > 0 && (curlayerIndex !== layerIndex || curFeatureCode !== code.toString())) {
// store the current feature
featureGuide.push(curlayerIndex, indexCount, indexOffset, encodings.length, ...encodings); // layerIndex, count, offset, encoding size, encodings
// update indexOffset
indexOffset += indexCount;
// reset indexCount
indexCount = 0;
// update to new encoding set
encodings = code;
// update what the current encoding is
curFeatureCode = encodings.toString();
}
// NOTE: Spreader functions on large arrays are failing in chrome right now -_-
// so we just do a for loop. Store vertices and feature code for each vertex set
const fl = _vertices.length;
for (let f = 0; f < fl; f++) {
vertices.push(_vertices[f]);
if (featureType === 'point' && f % 2 === 0)
ids.push(...feature.idRGB);
}
// build weights if heatmap
if (featureType === 'heatmap') {
const { weights: _weights } = feature;
const wl = _weights.length;
for (let f = 0; f < wl; f++)
weights.push(_weights[f]);
}
// store
// update previous layerIndex
curlayerIndex = layerIndex;
// increment indexCount
indexCount += fl / 2;
}
// store the very last featureGuide batch if not yet stored
if (indexCount > 0) {
featureGuide.push(curlayerIndex, indexCount, indexOffset, encodings.length, ...encodings); // layerIndex, count, offset, encoding size, encodings
}
// Upon building the batches, convert to buffers and ship.
const vertexBuffer = new Float32Array(vertices).buffer;
const weightBuffer = new Float32Array(weights).buffer;
const idBuffer = new Uint8ClampedArray(ids).buffer; // pre-store each id as an rgb value
const featureGuideBuffer = new Float32Array(featureGuide).buffer;
// ship the vector data.
if (type === 'point') {
const data = {
mapID,
type,
sourceName,
tileID,
vertexBuffer,
idBuffer,
featureGuideBuffer,
};
postMessage(data, [vertexBuffer, idBuffer, featureGuideBuffer]);
}
else {
const data = {
mapID,
type,
sourceName,
tileID,
vertexBuffer,
weightBuffer,
idBuffer,
featureGuideBuffer,
};
postMessage(data, [vertexBuffer, weightBuffer, idBuffer, featureGuideBuffer]);
}
}
}