UNPKG

s2maps-gpu

Version:

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

277 lines (276 loc) 10.5 kB
import FamilySource from './glyph/familySource.js'; import { NULL_GLYPH } from './glyph/buildGlyphQuads.js'; /** Map of Glyph/Icon Sources, their requests and their resolves */ export class MapGlyphSource extends Map { /** resolve mechanic to ensure the glyph/icon source is built */ resolve = () => { }; ready = new Promise((resolve) => { this.resolve = resolve; }); // track requests per tile glyphRequestTracker = new Map(); /** * Get a glyph/icon family or list of glyph/icon families * @param family - the name(s) of the glyph/icon family * @returns the glyph/icon family(ies) */ getFamily(family) { if (Array.isArray(family)) { const families = []; for (const name of family) { const glyphStore = this.get(name); if (glyphStore !== undefined) families.push(glyphStore); } return families; } return this.get(family); } } /** * Image Store * * Manages the Glyph/Icon sources, Sprites, and Images (like fill pattern images) */ export default class ImageStore { // mapId: GlyphSourceMap EX 'map1': GlyphSourceMap glyphSources = new Map(); // worker properties idGen; sourceWorker; /** * Setup the image store * @param idGen - id generator * @param sourceWorker - the source worker to send requests to */ setup(idGen, sourceWorker) { this.idGen = idGen; this.sourceWorker = sourceWorker; } /** * Setup a glyph/icon source * @param mapID - the id of the map to setup the glyph/icon source for */ setupMap(mapID) { this.glyphSources.set(mapID, new MapGlyphSource()); } /** * Wait for the glyph/icon source to be ready * @param mapID - the id of the map to await the glyph/icon source for */ async getReady(mapID) { const glyphSource = this.glyphSources.get(mapID); if (glyphSource !== undefined) await glyphSource.ready; } /** * Get the glyph/icon source * @param mapID - the id of the map to get the glyph/icon source for * @returns the glyph/icon source */ getGlyphSource(mapID) { const store = this.glyphSources.get(mapID); if (store === undefined) throw new Error('GlyphSource not setup'); return store; } /** * Get a glyph/icon family * @param mapID - the id of the map to get the glyph/icon source for * @param family - the name of the glyph/icon family * @returns the glyph/icon family */ getFamilyMap(mapID, family) { const glyphSource = this.getGlyphSource(mapID); const glyphStore = glyphSource.get(family); if (glyphStore === undefined) throw new Error('GlyphSource not setup'); return glyphStore; } /** * Parse specific ligatures * @param mapID - the id of the map * @param families - the name(s) of the glyph/icon family * @param glyphs - the ligature codes */ parseLigatures(mapID, families, glyphs) { // split the glyphs string[] into pieces everytime we see a space or line break characters const splitGlyphs = []; let current = []; for (const glyph of glyphs) { if (glyph === '32' || glyph === '10' || glyph === '13') { splitGlyphs.push({ glyphs: current, splitValue: glyph }); current = []; } else { current.push(glyph); } } splitGlyphs.push({ glyphs: current }); // next we check each "word" for ligatures; if a family source contains the codes // we move on, otherwise we might accidentally use two fonts for a single word for (const splitGlyph of splitGlyphs) { for (const family of families) { const familySource = this.getFamilyMap(mapID, family); familySource.parseLigatures(splitGlyph.glyphs, true); familySource.parseLigatures(splitGlyph.glyphs); if (familySource.has(splitGlyph.glyphs[0])) break; } } // rejoin the splitGlyphs back into the glyphs array glyphs.splice(0, glyphs.length); for (const splitGlyph of splitGlyphs) { for (const glyph of splitGlyph.glyphs) glyphs.push(glyph); if (splitGlyph.splitValue !== undefined) glyphs.push(splitGlyph.splitValue); } } /** * Add missing glyphs * @param mapID - the id of the map * @param tileID - the id of the tile * @param glyphCodes - the codes of the glyphs * @param families - the name(s) of the glyph/icon family * @returns true if there are missing glyphs */ addMissingGlyph(mapID, tileID, glyphCodes, families) { let missing = false; for (const code of glyphCodes) { for (const family of families) { const familySource = this.getFamilyMap(mapID, family); if (familySource.missingGlyph(code)) { familySource.addGlyphRequest(tileID, code); missing = true; } } } return missing; } /** * Process metadata for a collection of glyph/icon/sprite/image metadatas * NOTE: This function is called from the source thread ONLY ONCE per mapID before anything is processed * @param mapID - the id of the map to process the metadatas for * @param glyphMetadata - the glyph/icon metadatas * @param imageMetadata - the sprite/image metadatas */ processMetadata(mapID, glyphMetadata, imageMetadata) { const glyphSource = this.glyphSources.get(mapID); if (glyphSource === undefined) return; for (const { name, metadata } of glyphMetadata) { glyphSource.set(name, new FamilySource(name, metadata)); } for (const metadata of imageMetadata) { const imageSource = glyphSource.get(metadata.name); if (imageSource !== undefined) imageSource.addMetadata(metadata.metadata); else glyphSource.set(metadata.name, FamilySource.FromImageMetadata(metadata)); } // let any glyph based work know the metadata is ready glyphSource.resolve(); } /** * Process missing data * @param mapID - the id of the map to process the missing data for * @param tileID - the id of the tile that has missing data * @param sourceName - the name of the source that has missing data */ async processMissingData(mapID, tileID, sourceName) { const { idGen, sourceWorker } = this; const glyphSource = this.getGlyphSource(mapID); const { glyphRequestTracker } = glyphSource; const { workerID } = idGen; // build glyphRequestList to ship to the source thread const glyphList = {}; let glyphFamilyCount = 0; for (const [familyName, familySource] of glyphSource) { const list = familySource.getRequests(tileID); if (list.length > 0) { glyphList[familyName] = list; glyphFamilyCount++; } } if (glyphFamilyCount > 0) { // random string of numbers and letters 7 characters long const reqID = `${mapID}:${sourceName}:${Math.random().toString(36).substring(2, 9)}`; // send off and prep for response const requestMessage = { type: 'glyphrequest', mapID, workerID, reqID, glyphList, }; sourceWorker.postMessage(requestMessage); const self = { promise: undefined }; self.promise = new Promise((resolve) => { glyphRequestTracker.set(reqID, { glyphFamilyCount, processed: 0, resolve, self }); }); await self.promise; } else if (glyphRequestTracker.size > 0) { // a seperate tile request for the same source may be in the process of building glyphs shared with this request. We need to wait for those to finish await Promise.all([...glyphRequestTracker.values()].map(async ({ self }) => { await self.promise; })); } else { await new Promise((resolve) => { resolve(); }); } } /** * Process a response from the source thread * @param mapID - the id of the map * @param reqID - the id of the request * @param glyphMetadata - the glyph metadata * @param familyName - the name of the family */ processGlyphResponse(mapID, reqID, glyphMetadata, familyName) { // pull in the features and delete the reference const glyphSource = this.getGlyphSource(mapID); const { glyphRequestTracker } = glyphSource; const store = glyphRequestTracker.get(reqID); if (store === undefined) return; store.processed++; // store our response glyphs this.importGlyphs(mapID, familyName, glyphMetadata); // If we have all data, we now process the built glyphs if (store.glyphFamilyCount === store.processed) { glyphRequestTracker.delete(reqID); store.resolve(); } } /** * a response from the sourceThread for glyph data * @param mapID - the id of the map to process the response for * @param familyName - the name of the family * @param glyphs - the glyphs to import */ importGlyphs(mapID, familyName, glyphs) { const familyMap = this.getFamilyMap(mapID, familyName); for (const glyph of glyphs) { const { code } = glyph; familyMap.glyphCache.set(code, glyph); } } /** * Get an image pattern (used by fills) * @param mapID - the id of the map * @param familyName - the name of the family * @param name - the name of the pattern * @returns the pattern guide */ getPattern(mapID, familyName, name) { if (name === undefined) return NULL_GLYPH; const familyMap = this.getFamilyMap(mapID, familyName); const glyph = familyMap.glyphCache.get(name); return glyph ?? NULL_GLYPH; } }