UNPKG

s2maps-gpu

Version:

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

443 lines (442 loc) 18.2 kB
import { adjustURL } from '../util/index.js'; import { GlyphSource, ImageSource, JSONSource, LocalSource, MarkerSource, S2PMTilesSource, S2TilesSource, Session, Source, SpriteSource, TexturePack, } from './source/index.js'; /** * # SOURCE WORKER * * The source worker builds the appropriate module defined * by the style "sources" object. All tile requests are forwarded to the source * worker, where the worker properly builds the requests. Upon creation of a request, * the request string is passed to a tile worker to run the fetch and then consequently * process. * * GEOJSON / S2JSON is a unique case where the source worker just builds the json * locally to avoid processing the same json multiple times per tile worker. * * The glyph map is processed by the source worker. All data is localized to * the source worker. When a tile worker requests glyph data, the source will * scale up the map and send off the x-y position along with the width-height of * each glyph requested. * * SOURCE TYPES * S2Tile - a compact s2tiles file where we request tiles of many file types and compression types * GeoJSON - a json file containing geo-spatial data * S2JSON - a json file modeled much like geojson * Glyph - either a font or icon file stored in a pbf structure * LocalSource -> local build tile information * default -> assumed the location has a metadata. json at root with a "s2cellid.ext" file structure * * SESSION TOKEN * This is a pre-approved JWT token for any calls made to api.opens2.com or api.s2maps.io * when building requests, we take the current time, run a black box digest to hash, and return: * { h: 'first-five-chars', t: '1620276149967' } // h -> hash ; t -> timestamp ('' + Date.now()) * (now - timestamp) / 1000 = seconds passed */ export default class SourceWorker { workers = []; session = new Session(); /** { mapID: Map } */ maps = {}; /** * Handle source worker's messages * @param msg - incoming message */ onMessage(msg) { const { data, ports } = msg; const { type } = data; if (type === 'port') this.#loadWorkerPort(ports[0], ports[1], data.id); else { const { mapID } = data; if (type === 'requestStyle') this.#requestStyle(mapID, data.style, data.analytics, data.apiKey, data.urlMap); else if (type === 'style') this.#loadStyle(mapID, data.style); else if (type === 'tilerequest') void this.#requestTile(mapID, data.tiles, data.sources); else if (type === 'timerequest') void this.#requestTime(mapID, data.tiles, data.sourceNames); else if (type === 'glyphrequest') this.#glyphRequest(mapID, data.workerID, data.reqID, data.glyphList); else if (type === 'addMarkers') this.#addMarkers(mapID, data.markers, data.sourceName); else if (type === 'deleteMarkers') this.#deleteMarkers(mapID, data.ids, data.sourceName); else if (type === 'deleteSource') this.#deleteSource(mapID, data.sourceNames); else if (type === 'addLayer') this.#addLayer(mapID, data.layer, data.index, data.tileRequest); else if (type === 'deleteLayer') this.#deleteLayer(mapID, data.index); else if (type === 'reorderLayers') this.#reorderLayers(mapID, data.layerChanges); } } /** * Load worker. First message that comes in upon creation of this worker * @param messagePort - the communication port to talk listen to a tile worker's messages * @param postPort - the communication port to send messages to the tile worker * @param id - the id of the tile worker */ #loadWorkerPort(messagePort, postPort, id) { this.workers[id] = postPort; messagePort.onmessage = this.onMessage.bind(this); this.session.loadWorker(messagePort, postPort, id); } /** * Request map style given an href * @param mapID - the id of the map asking for the style * @param style - the href of the style * @param analytics - basic analytics to know what this browser can handle * @param apiKey - the api key * @param urlMap - the url map */ #requestStyle(mapID, style, analytics, apiKey, urlMap) { // build maps session this.session.loadStyle(mapID, analytics, apiKey); // request style void this.session.requestStyle(mapID, style, urlMap); } /** * Load a style object for a map * @param mapID - the id of the map to load the style for * @param style - the style */ #loadStyle(mapID, style) { // pull style data const { projection, gpuType, minzoom, maxzoom, layers, analytics, experimental, apiKey, urlMap, } = style; const texturePack = new TexturePack(); this.maps[mapID] = { projection, gpuType, minzoom, maxzoom, analytics, experimental, sources: {}, layers, glyphs: {}, sprites: {}, urls: urlMap ?? {}, texturePack, images: new ImageSource('__images', '', texturePack, this.session), }; // create a session with the style this.session.loadStyle(mapID, analytics, apiKey); // now build sources void this.#buildSources(mapID, style); } /** * Add a style layer to a map * @param mapID - the id of the map to add the layer to * @param layer - the style layer * @param index - the index to add the layer at * @param _tileRequest - the list of tiles of all existing tiles in the map already to adjust */ #addLayer(mapID, layer, index, _tileRequest) { // add the layer to the tile const { layers } = this.maps[mapID]; if (index === undefined) layers.push(layer); else layers.splice(index, 0, layer); if (index === undefined) index = layers.length; for (let i = index + 1, ll = layers.length; i < ll; i++) { const layer = layers[i]; layer.layerIndex++; } // tell the correct source to request the tiles and build the layer of interest // const source = this.sources[mapID][layer.source] // for (const tile of tiles) source.tileRequest(mapID, { ...tile }, [index]) } /** * Delete a style layer * @param mapID - the id of the map to delete the layer from * @param index - the index to delete the layer from */ #deleteLayer(mapID, index) { const { layers } = this.maps[mapID]; layers.splice(index, 1); for (let i = index, ll = layers.length; i < ll; i++) { const layer = layers[i]; layer.layerIndex--; } } /** * Reorder style layers * @param mapID - the id of the map to reorder the layers * @param layerChanges - the layer changes to make */ #reorderLayers(mapID, layerChanges) { const { layers } = this.maps[mapID]; const newLayers = []; // move the layer to its new position for (const [from, to] of Object.entries(layerChanges)) { const layer = layers[Number(from)]; layer.layerIndex = to; newLayers[to] = layer; } // because other classes depend upon the current array, we just update array items for (let i = 0; i < layers.length; i++) layers[i] = newLayers[i]; } /** * Build sources for a map given an style object * @param mapID - the id of the map * @param style - the style object to pull source from */ async #buildSources(mapID, style) { const { urls, images: mapImages } = this.maps[mapID]; const { sources, layers, fonts, icons, glyphs, sprites, images } = style; // sources for (const [name, source] of Object.entries(sources)) { this.#createSource(mapID, name, source, layers.filter((layer) => layer.source === name)); } // fonts & icons const glyphAwaits = []; for (const [name, source] of Object.entries({ ...fonts, ...icons, ...glyphs })) { glyphAwaits.push(this.#createGlyphSource(mapID, name, source)); } // sprites const imageAwaits = []; for (const [name, source] of Object.entries(sprites)) { if (typeof source === 'object') { const path = adjustURL(source.path, urls); imageAwaits.push(this.#createSpriteSheet(mapID, name, path, source.fileType)); } else { imageAwaits.push(this.#createSpriteSheet(mapID, name, source)); } } // images for (const [name, href] of Object.entries(images)) { const path = adjustURL(href, urls); imageAwaits.push(mapImages.addImage(mapID, name, path)); } // ship the glyph metadata const glyphMetadata = await Promise.all(glyphAwaits); const filteredMetadata = glyphMetadata.filter((m) => m?.metadata !== undefined); const imageMetadata = await Promise.all(imageAwaits); const filteredImageMetadata = imageMetadata.filter((m) => m !== undefined); for (const worker of this.workers) { const message = { mapID, type: 'glyphmetadata', glyphMetadata: filteredMetadata, imageMetadata: filteredImageMetadata, }; worker.postMessage(message); } } /** * Create a source * @param mapID - the id of the map * @param name - the name of the source * @param input - the path to the source * @param layers - the layers that use this source */ #createSource(mapID, name, input, layers) { const { maps, session } = this; const { urls, projection, sources } = maps[mapID]; // prepare variables to build appropriate source type let metadata; let ext; if (typeof input === 'object') { metadata = input; ext = 'extension' in input ? input.extension : input.type; input = 'path' in input ? (input.path ?? '') : ''; } if (ext === undefined) ext = (input.split('.').pop() ?? '').toLowerCase(); const needsToken = session.hasAPIKey(mapID); const path = adjustURL(input, urls); // create the proper source type let source; if (ext === 's2pmtiles' || ext === 'pmtiles') { source = new S2PMTilesSource(name, projection, layers, path, needsToken, session); } else if (ext === 's2tiles') { source = new S2TilesSource(name, projection, layers, path, needsToken, session); } else if (ext === 'json' || ext === 's2json' || ext === 'geojson') { source = new JSONSource(name, projection, layers, path, needsToken, session); } else if (input === '_local') { source = new LocalSource(name, session, layers); } else if (input === '_markers') { source = new MarkerSource(name, session, projection, layers); } else source = new Source(name, projection, layers, path, needsToken, session); // default -> folder structure // store & build sources[name] = source; void source.build(mapID, metadata); } /** * Create a glyph source and build * @param mapID - the id of the map * @param name - the name of the source * @param input - the path to the source * @returns a list of glyph metadata that's yet to be parsed */ async #createGlyphSource(mapID, name, input) { const { maps, session } = this; const { urls, texturePack, glyphs } = maps[mapID]; // check if already exists if (glyphs[name] !== undefined) return; const source = new GlyphSource(name, adjustURL(input, urls), texturePack, session); glyphs[name] = source; return await source.build(mapID); } /** * Create a sprite sheet given an input source * @param mapID - the id of the map to build the source for * @param name - the name of the source sprite * @param input - the path to the source * @param fileType - the file type (extension) * @returns the image metadata if successful */ async #createSpriteSheet(mapID, name, input, fileType) { const { maps, session } = this; const { urls, texturePack, sprites } = maps[mapID]; // check if already exists if (sprites[name] !== undefined) return; const source = new SpriteSource(name, adjustURL(input, urls), texturePack, session, fileType); // store & build sprites[name] = source; return await source.build(mapID); } /** * Given a tile request, ship out requests for data from all sources * @param mapID - the id of the map * @param tiles - the tile requests * @param sources - the sources to modify if needed. */ async #requestTile(mapID, tiles, sources = []) { const { sources: mapSources } = this.maps[mapID]; const newHrefs = sources.filter((s) => s[1] !== undefined); const sourceNames = sources.map((s) => s[0]); // if new hrefs update sources for (const [sourceName, href] of newHrefs) { const source = mapSources[sourceName]; if (source !== undefined) { // steal the layer data and rebuild this.#createSource(mapID, sourceName, href, source.styleLayers); } } // build requests for (const tile of tiles) { const flush = { type: 'flush', from: 'source', mapID, tileID: tile.id, layersToBeLoaded: new Set(), }; for (const source of Object.values(mapSources)) { if (sourceNames.length > 0 && !sourceNames.includes(source.name)) continue; if (source.isTimeFormat === true) continue; await source.tileRequest(mapID, { ...tile }, flush); } postMessage(flush); } } /** * Request temporal source data * @param mapID - the id of the map * @param tiles - the tile requests with time stamps * @param sourceNames - the sources to fetch data for */ async #requestTime(mapID, tiles, sourceNames = []) { const { sources: mapSources } = this.maps[mapID]; // build requests for (const tile of tiles) { const flush = { type: 'flush', from: 'source', mapID, tileID: tile.id, layersToBeLoaded: new Set(), }; for (const source of Object.values(mapSources)) { if (source.isTimeFormat !== true || !sourceNames.includes(source.name)) continue; await source.tileRequest(mapID, { ...tile }, flush); } postMessage(flush); } } /** * Request glyph data * @param mapID - the id of the map that needs the glyphs * @param workerID - the Tile ID making the request * @param reqID - the request id, an encoded string tracking metadata about the request * @param sourceGlyphs - the glyphs to request, their sources, etc. */ #glyphRequest(mapID, workerID, reqID, sourceGlyphs) { // prep const { maps, workers } = this; const { glyphs } = maps[mapID]; // iterate the glyph sources for the unicodes for (const [name, codes] of Object.entries(sourceGlyphs)) { void glyphs[name].glyphRequest(codes, mapID, reqID, workers[workerID]); } } /** * Add marker(s) to the map * @param mapID - the id of the map to add marker(s) to * @param markers - the marker(s) to add * @param sourceName - the name of the source to add the marker(s) to */ #addMarkers(mapID, markers, sourceName) { const markerSource = this.#getMarkerSource(mapID, sourceName); if (markerSource === undefined) return; for (const marker of markers) markerSource.addMarker(marker); } /** * Delete marker(s) from the map * @param mapID - the id of the map to delete marker(s) from * @param ids - the id(s) of the marker(s) to delete * @param sourceName - the name of the source to delete the marker(s) from */ #deleteMarkers(mapID, ids, sourceName) { const markerSource = this.#getMarkerSource(mapID, sourceName); markerSource?.deleteMarkers(ids); } /** * Get the marker source * @param mapID - the id of the map * @param sourceName - the name of the source * @returns the marker source if found */ #getMarkerSource(mapID, sourceName) { const { sources, layers, projection } = this.maps[mapID]; if (sources === undefined) throw new Error(`Map ${mapID} does not exist`); if (sources[sourceName] === undefined) sources[sourceName] = new MarkerSource(sourceName, this.session, projection, layers); return sources[sourceName]; } /** * Delete source(s) from the map * @param mapID - the id of the map to delete the source from * @param sourceNames - the name(s) of the source(s) to delete */ #deleteSource(mapID, sourceNames) { for (const sourceName of sourceNames) { // @ts-expect-error - we are deleting the source, this is ok this.sources[mapID][sourceName] = undefined; } } } // create the tileworker const sourceWorker = new SourceWorker(); // bind the onmessage function self.onmessage = sourceWorker.onMessage.bind(sourceWorker);