s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
200 lines (199 loc) • 8.54 kB
JavaScript
import parseFeature from 's2/style/parseFeature.js';
import parseFilter from 'style/parseFilter.js';
import VectorWorker, { colorFunc, idToRGB } from './vectorWorker.js';
import { drawLine, featureSort, scaleShiftClipLines } from './util/index.js';
/** Worker for processing line data */
export default class LineWorker extends VectorWorker {
featureStore = new Map(); // tileID -> features
/**
* Setup the worker layer to process future line data
* @param lineLayer - input style layer guide
* @returns the worker layer to process future line vector data
*/
setupLayer(lineLayer) {
const { name, layerIndex, source, layer, minzoom, maxzoom, filter, dashed, geoFilter, interactive, cursor, lch, cap, join, color, opacity, width, gapwidth, } = lineLayer;
// build feature code design
// color -> opacity -> width -> gapwidth
const design = [[color, colorFunc(lch)], [opacity], [width], [gapwidth]];
return {
type: 'line',
name,
layerIndex,
source,
layer,
minzoom,
maxzoom,
cap: parseFeature(cap),
join: parseFeature(join),
filter: parseFilter(filter),
getCode: this.buildCode(design),
dashed,
geoFilter,
interactive,
cursor,
};
}
/**
* Build a line feature
* @param tile - the tile request
* @param extent - the tile extent
* @param feature - the vector tile feature
* @param lineLayer - the line worker layer
* @param mapID - the map id to ship the data back to
* @param sourceName - the name of the source the data belongs to
* @returns true if the feature was built
*/
buildFeature(tile, extent, feature, lineLayer, mapID, sourceName) {
const { gpuType } = this;
const { zoom, division } = tile;
const { properties } = feature;
const { getCode, layerIndex, geoFilter } = lineLayer;
const type = feature.geoType();
if (type === 'Point' || type === 'MultiPoint')
return false;
if (geoFilter.includes('line') && (type === 'LineString' || type === 'MultiLineString'))
return false;
if (geoFilter.includes('poly') && (type === 'Polygon' || type === 'MultiPolygon'))
return false;
// load geometry
const [geometry] = feature.loadLines() ?? [];
if (geometry === undefined)
return false;
const cap = lineLayer.cap([], properties, zoom);
const vertices = [];
const lengthSoFar = [];
// create multiplier
const multiplier = 1 / extent;
// find a max distance to modify lines too large (round off according to the sphere)
const maxDistance = division === 1 ? 0 : extent / division;
// preprocess geometry
const geo = scaleShiftClipLines(geometry, extent, tile);
// draw
for (const lineString of geo) {
// build the vertex, normal, and index data
const { prev, curr, next, lengthSoFar: _lsf } = drawLine(lineString, cap, maxDistance);
for (let i = 0, vc = curr.length; i < vc; i += 2) {
vertices.push(prev[i] * multiplier, prev[i + 1] * multiplier, curr[i] * multiplier, curr[i + 1] * multiplier, next[i] * multiplier, next[i + 1] * multiplier);
}
for (const l of _lsf)
lengthSoFar.push(l * multiplier);
}
// skip empty geometry
if (vertices.length === 0)
return false;
const id = this.idGen.getNum();
const [gl1Code, gl2Code] = getCode(zoom, properties);
const lineFeature = {
cap,
vertices,
lengthSoFar,
layerIndex,
code: gpuType === 1 ? gl1Code : gl2Code,
gl2Code,
idRGB: idToRGB(id),
};
const storeID = `${mapID}:${tile.id}:${sourceName}`;
if (!this.featureStore.has(storeID))
this.featureStore.set(storeID, []);
const store = this.featureStore.get(storeID);
store?.push(lineFeature);
return true;
}
/**
* Flush a tile-request's result line data to the render thread
* @param mapID - id of the map to ship the data back to
* @param tile - tile request
* @param sourceName - name of the source the data belongs to
*/
async flush(mapID, tile, sourceName) {
const storeID = await `${mapID}:${tile.id}:${sourceName}`;
const features = this.featureStore.get(storeID) ?? [];
if (features.length === 0)
return;
this.#flush(mapID, sourceName, tile.id, features);
this.featureStore.delete(storeID);
}
/**
* Flush a tile-request's result line data to the render thread
* @param mapID - id of the map to ship the data back to
* @param sourceName - name of the source the data belongs to
* @param tileID - the id of the tile that requested the lines
* @param features - the features to flush to the render thread
*/
#flush(mapID, sourceName, tileID, features) {
// 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 lengthSoFar = [];
const featureGuide = [];
let encodings = features[0].code;
let indexCount = 0;
let indexOffset = 0;
let curFeatureCode = encodings.toString();
let curlayerIndex = features[0].layerIndex;
let curCap = 0;
for (const { layerIndex, code, cap, vertices: _vertices, lengthSoFar: _lsf } of features) {
// 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(curCap, 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]);
for (const l of _lsf)
lengthSoFar.push(l);
// update previous layerIndex
curlayerIndex = layerIndex;
// store the cap type
curCap = encodeCap(cap);
// increment indexCount
indexCount += fl / 6;
}
// store the very last featureGuide batch if not yet stored
if (indexCount > 0) {
featureGuide.push(curCap, 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 lengthSoFarBuffer = new Float32Array(lengthSoFar).buffer;
const featureGuideBuffer = new Float32Array(featureGuide).buffer;
// ship the vector data.
const data = {
mapID,
type: 'line',
sourceName,
tileID,
vertexBuffer,
lengthSoFarBuffer,
featureGuideBuffer,
};
postMessage(data, [vertexBuffer, featureGuideBuffer]);
}
}
/**
* Encode the cap type
* @param cap - the cap type
* @returns the encoded cap
*/
function encodeCap(cap) {
if (cap === 'butt')
return 0;
else if (cap === 'square')
return 1;
else
return 2; // round
}