UNPKG

s2maps-gpu

Version:

S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.

351 lines (350 loc) 12.9 kB
/** * # Style Engine * * The Style Engine is responsible for: * - PRE) If style is a string (url), ship it off to Source Worker to fetch the style * - 1) Build workflows necessary to render the style * - 2) Build out the layers * - 3) Ship off appropriate style params to Tile Workers so they know how to build the tiles * - 4) Build the layers for the painter */ export default class Style { camera; apiKey; urlMap; maskLayers = []; layers = []; interactive = false; dirty = true; /** * @param camera - render camera * @param options - map options */ constructor(camera, options) { const { apiKey, urlMap } = options; this.camera = camera; this.apiKey = apiKey; this.urlMap = urlMap; } /** * Build the style, preparing it for rendering and tile/source processing * @param style - style definition * @param ignorePosition - if true, we want to leave the position of the camera unchanged. * @returns true if we can start rendering */ async buildStyle(style, ignorePosition = false) { const { camera } = this; const { painter, projector } = camera; if (typeof style === 'string') { this.#requestStyle(style); return false; } if (typeof style !== 'object') throw Error('style must be an object'); this.dirty = true; // inform the projection painter.context.setProjection(style.projection ?? 'S2'); // set the clear color if present if (style.clearColor !== undefined) { painter.context.setClearColor(style.clearColor); } // build workflows that don't exist yet (depends on projection) await this.#buildWorkflows(style); // build layer definitions this.#buildLayers(style.layers); // built layers let us know if we have an interactive layer or not (depends on layers) painter.context.setInteractive(this.interactive); // build time series if exists if (style.timeSeries !== undefined) camera.buildTimeCache(style.timeSeries); // ship to Tile Workers this.#sendStyleDataToWorkers(style); // update the projector with our style projector.setStyleParameters(style, ignorePosition); // return we can start rendering return true; } /** * Inject mask layers * @param tile - tile to inject the mask layers for */ injectMaskLayers(tile) { const { maskLayers, camera } = this; for (const maskLayer of maskLayers) { const workflow = camera.painter.workflows[maskLayer.type]; workflow?.buildMaskFeature(maskLayer, tile); } } /** * Request the style if the input style was an href pointing to the style object * @param style - style href */ #requestStyle(style) { const { apiKey, urlMap, camera } = this; const { id, webworker } = camera; const analytics = this.#buildAnalytics(); if (webworker) { const message = { mapID: id, analytics, type: 'requestStyle', style, apiKey, urlMap, }; postMessage(message); } else { window.S2WorkerPool.requestStyle(id, style, analytics, apiKey, urlMap); } } /** * Build workflows * @param style - input style object */ async #buildWorkflows(style) { const { camera, urlMap } = this; const { painter } = camera; const { skybox, wallpaper, layers } = style; const workflows = new Set(['fill']); // setup appropriate background if it exists if (skybox !== undefined) workflows.add('skybox'); if (wallpaper !== undefined) workflows.add('wallpaper'); // iterate layers and add workflows if (Array.isArray(layers)) { for (const layer of layers) { if (layer.type !== undefined) workflows.add(layer.type); } } // build workflows await painter.buildWorkflows(workflows); // inject styles into workflows for (const [name, workflow] of Object.entries(painter.workflows)) { if (name === 'background') continue; if ('updateStyle' in workflow) workflow.updateStyle(style, camera, urlMap); } } /** * Build layer definitions * - 1) ensure "bad" layers are removed (missing important keys or subkeys) * - 2) ensure the order is correct for when WebGL eventually parses the encodings * @param layers - layers to build */ #buildLayers(layers = []) { const layerDefinitions = []; let layerIndex = 0; for (const layerStyle of layers) { const layerDefinition = this.#buildLayer(layerStyle, layerIndex); if (layerDefinition !== undefined) { if (layerDefinition.source === 'mask' && (layerDefinition.type === 'fill' || layerDefinition.type === 'shade')) this.maskLayers.push(layerDefinition); layerDefinitions.push(layerDefinition); if (layerDefinition.interactive === true) this.interactive = true; layerIndex++; } } this.layers = layerDefinitions; } /** * Build layer definition * @param layerStyle - layer style to build * @param layerIndex - position of the layer in the style layers array * @returns a layer definition if successful */ #buildLayer(layerStyle, layerIndex) { const { workflows } = this.camera.painter; // grab variables const { type, name, source, layer, minzoom, maxzoom, filter, lch, visible } = layerStyle; if (type === undefined || name === undefined || source === undefined) { console.warn('Skipping layer: "', layerStyle, '" because it is missing "type", "name" and/or "source"'); return; } // prepare layer base const base = { type, name, source, layerIndex, layer: layer ?? 'default', minzoom: minzoom ?? 0, maxzoom: maxzoom ?? 20, filter, lch: lch ?? false, visible: visible ?? true, }; // store the layer definition const layerDefinition = workflows[type]?.buildLayerDefinition?.(base, layerStyle); if (layerDefinition !== undefined) return layerDefinition; } /** * Request tiles. The projector will forward requests throug this class so it can build the * tile requests with style specific data before forwarding it to the worker pool * @param tiles - tiles to request data for */ requestTiles(tiles) { if (tiles.length === 0) return; const { id, webworker } = this.camera; const tileRequests = []; tiles.forEach((tile) => { // grab request values const { id, face, i, j, zoom, type, bbox, division } = tile; // build tileRequests tileRequests.push({ id, face, i, j, zoom, type, bbox, division }); }); // send the tiles over to the worker pool manager to split the workload if (webworker) { postMessage({ mapID: id, type: 'tilerequest', tiles: tileRequests }); } else { window.S2WorkerPool.tileRequest(id, tileRequests); } } /** * Send style data to workers * @param style - style definition to forward */ #sendStyleDataToWorkers(style) { const { apiKey, urlMap, layers } = this; const { id, webworker, painter, projector } = this.camera; const { type } = painter.context; const { tileSize } = projector; const { projection, sources, glyphs, fonts, icons, sprites, images, minzoom, maxzoom, experimental, } = style; const analytics = this.#buildAnalytics(); // now that we have various source data, package up the style objects we need and send it off: const stylePackage = { projection: projection ?? 'S2', gpuType: type, sources: sources ?? {}, glyphs: glyphs ?? {}, fonts: fonts ?? {}, icons: icons ?? {}, sprites: sprites ?? {}, images: images ?? {}, layers: layers.filter((l) => l.source !== 'mask'), minzoom: minzoom ?? 0, maxzoom: maxzoom ?? 20, tileSize, analytics, apiKey, urlMap, experimental: experimental ?? false, }; // If the map engine is running on the main thread, directly send the stylePackage to the worker pool. // Otherwise perhaps this map instance is a web worker and has a global instance of postMessage if (webworker) { postMessage({ mapID: id, type: 'style', style: stylePackage }); } else { window.S2WorkerPool.injectStyle(id, stylePackage); } } /** * This is a helper function to build the analytics object. Useful for servers to know what kind * of GPU or limitations this browser has * @returns the analytics object */ #buildAnalytics() { const { context } = this.camera.painter; const { renderer, type, presentation } = context; const { width, height } = presentation; return { gpu: renderer, context: type, language: navigator.language.split('-')[0] ?? 'en', width, height, }; } } // addLayer (layer: Layer, nameIndex?: number | string, tileRequests: Array<TileRequest>) { // const { painter } = this.map // const workflows = new Set() // // prebuild & convert nameIndex to index // const index = this.#findLayerIndex(nameIndex) // this._prebuildLayer(layer, index) // // let the workers know // if (this.webworker) { // postMessage({ mapID: this.map.id, type: 'addLayer', layer, index, tileRequests }) // } else { // window.S2WorkerPool.addLayer(this.map.id, layer, index, tileRequests) // } // // insert layer into layers, updating positions of other layers as necessary // const { layers } = this // layers.splice(index, 0, layer) // for (let i = index + 1, ll = layers.length; i < ll; i++) { // const layer = layers[i] // layer.layerIndex++ // layer.depthPos++ // } // // build layer // this._buildLayer(layer, index + 1, workflows) // // tell the painter that we might be using a new workflow // painter.buildWorkflows(workflows) // // let the renderer know the style is dirty // this.dirty = true // } // deleteLayer (nameIndex?: number | string): number { // // grab the index // const index = this.#findLayerIndex(nameIndex) // // let the workers know // if (this.webworker) { // postMessage({ mapID: this.map.id, type: 'deleteLayer', index }) // } else { // window.S2WorkerPool.deleteLayer(this.map.id, index) // } // // remove index from layers and update layerIndex & depthPos // const { layers } = this // layers.splice(index, 1) // for (let i = index, ll = layers.length; i < ll; i++) { // const layer = layers[i] // layer.layerIndex-- // layer.depthPos-- // } // // let the renderer know the style is dirty // this.dirty = true // return index // } // reorderLayers (layerChanges: { [string | number]: number }) { // const { layers } = this // const newLayers = [] // // move the layer to its new position // for (const [from, to] of Object.entries(layerChanges)) { // const layer = layers[+from] // layer.layerIndex = to // layer.depthPos = to + 1 // newLayers[to] = layer // } // // store the new layers // this.layers = newLayers // // let the webworkers know about the reorder // if (this.webworker) { // postMessage({ mapID: this.map.id, type: 'reorderLayers', layerChanges }) // } else { // window.S2WorkerPool.reorderLayers(this.map.id, layerChanges) // } // // let the renderer know the style is dirty // this.dirty = true // } // export function findLayerIndex (layers: LayerDefinition[], nameIndex: number | string): number { // const length = layers.length // if (typeof nameIndex === 'number') { // return nameIndex // } else if (typeof nameIndex === 'string') { // for (let i = 0; i < length; i++) { // const layer = layers[i] // if (layer.name === nameIndex) { // return i // } // } // } // return length // }