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
JavaScript
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;
}
}