s2maps-gpu
Version:
S2 Maps GPU - An open source, high-performance, and GPU-accelerated map engine for rendering large-scale, interactive maps.
235 lines (234 loc) • 8.62 kB
JavaScript
/**
* # Family Source
*
* Maintain a store of the Font/Icon family, it's glyphs, ligatures, icons, and colors associated
* with it. Make requests to the source worker for missing glyphs.
*/
export default class FamilySource {
name;
extent = 0;
defaultAdvance = 0;
glyphSet = new Set(); // existing glyphs
ligatures = {};
// cache system
glyphCache = new Map(); // glyphs we have built already
iconCache = new Map();
// track missing glyphs for future requests to the source worker
glyphRequestList = new Map();
isIcon = false;
/**
* @param name - the name of the family
* @param metadata - the raw metadata to unpack
*/
constructor(name, metadata) {
this.name = name;
if (metadata === undefined)
return;
const meta = new DataView(metadata);
this.extent = meta.getUint16(0, true);
this.defaultAdvance = meta.getUint16(8, true) / this.extent;
const glyphCount = meta.getUint16(10, true);
const iconMapSize = meta.getUint32(12, true);
const colorBufSize = meta.getUint16(16, true) * 4;
const substituteSize = meta.getUint32(18, true);
this.isIcon = iconMapSize > 0;
// store glyphSet
const glyphEnd = 30 + glyphCount * 2;
const gmdv = new DataView(metadata, 30, glyphCount * 2);
for (let i = 0; i < glyphCount; i++) {
this.glyphSet.add(String(gmdv.getUint16(i * 2, true)));
}
// build icon metadata
const iconMap = this.#buildIconMap(iconMapSize, new DataView(metadata, glyphEnd, iconMapSize));
const colors = this.#buildColorMap(colorBufSize, new DataView(metadata, glyphEnd + iconMapSize, colorBufSize));
// store the icon
for (const [name, pieces] of Object.entries(iconMap)) {
this.iconCache.set(name, pieces.map((piece) => {
return { glyphID: piece.glyphID, color: colors[piece.colorID] };
}));
}
this.#buildSubstituteMap(substituteSize, new DataView(metadata, glyphEnd + iconMapSize + colorBufSize, substituteSize));
// store space (32)
if (!this.isIcon)
this.glyphCache.set('32', {
code: '32',
texX: 0,
texY: 0,
texW: 0,
texH: 0,
xOffset: 0,
yOffset: 0,
width: 0,
height: 0,
advanceWidth: this.defaultAdvance,
});
}
/**
* Given an image metadata input, build a FamilySource
* @param imageSource - the image metadata
* @returns a new FamilySource
*/
static FromImageMetadata(imageSource) {
const { name, metadata } = imageSource;
const fs = new FamilySource(name);
fs.addMetadata(metadata);
return fs;
}
/**
* Check if the Family Source has an existing glyph/icon
* @param code - glyph code
* @returns true if the glyph/icon exists
*/
has(code) {
return this.glyphSet.has(code);
}
/**
* Check if this source is missing a glyph/icon
* @param code - the code of the glyph/icon
* @returns true if the glyph/icon is missing
*/
missingGlyph(code) {
const { isIcon, glyphSet, glyphCache } = this;
if (isIcon)
return !glyphCache.has(code);
return glyphSet.has(code) && !glyphCache.has(code);
}
/**
* Add image metadata to the source
* @param metadata - the image metadata
*/
addMetadata(metadata) {
for (const [code, glyph] of Object.entries(metadata)) {
this.glyphSet.add(code);
this.glyphCache.set(code, glyph);
if (!this.iconCache.has(code))
this.iconCache.set(code, [{ glyphID: code, color: [0, 0, 0, 0] }]);
}
}
/**
* Add a glyph request to be processed
* @param tileID - the id of the tile that requested the glyph
* @param code - the code of the glyph/icon
*/
addGlyphRequest(tileID, code) {
if (!this.glyphRequestList.has(tileID))
this.glyphRequestList.set(tileID, new Set());
const requests = this.glyphRequestList.get(tileID);
requests?.add(code);
}
/**
* Get the glyph requests for a tile
* @param tileID - the id of the tile that requested the glyph/icon
* @returns the list of glyph/icon requests
*/
getRequests(tileID) {
const glyphList = this.glyphRequestList.get(tileID) ?? new Set();
// cleanup requests that we are pulling from the cache
this.glyphRequestList.delete(tileID);
return [...glyphList];
}
/**
* Build a collection of icons and their associated glyphs & colors
* @param iconMapSize - the size of the icon map
* @param dv - the data view to read from
* @returns the icon map
*/
#buildIconMap(iconMapSize, dv) {
const iconMap = {};
let pos = 0;
while (pos < iconMapSize) {
const nameLength = dv.getUint8(pos);
const mapLength = dv.getUint8(pos + 1);
pos += 2;
const id = [];
for (let i = 0; i < nameLength; i++)
id.push(dv.getUint8(pos + i));
const name = id.map((n) => String.fromCharCode(n)).join('');
pos += nameLength;
const map = [];
for (let i = 0; i < mapLength; i++) {
map.push({
glyphID: String(dv.getUint16(pos, true)),
colorID: dv.getUint16(pos + 2, true),
});
pos += 4;
}
iconMap[name] = map;
}
return iconMap;
}
/**
* Build a collection of colors
* @param colorSize - the size of the color map
* @param dv - the data view to read from
* @returns the color map
*/
#buildColorMap(colorSize, dv) {
const colors = [];
for (let i = 0; i < colorSize; i += 4) {
colors.push([dv.getUint8(i), dv.getUint8(i + 1), dv.getUint8(i + 2), dv.getUint8(i + 3)]);
}
return colors;
}
/**
* Build a collection of ligature substitutions
* @param substituteSize - the size of the ligature map
* @param dv - the data view to read from
*/
#buildSubstituteMap(substituteSize, dv) {
let pos = 0;
while (pos < substituteSize) {
const type = dv.getUint8(pos);
if (type === 4) {
// LIGATURE TYPE
const count = dv.getUint8(pos + 1);
const components = [];
for (let j = 0; j < count; j++) {
components.push(String(dv.getUint16(pos + 2 + j * 2, true)));
}
const substitute = components.join('.');
this.glyphSet.add(substitute);
let tree = this.ligatures;
for (const component of components) {
const unicode = Number(component);
if (tree[unicode] === undefined)
tree[unicode] = {};
tree = tree[unicode];
}
tree.substitute = substitute;
pos += 2 + count * 2;
}
else {
throw new Error(`Unknown substitute type: ${type}`);
}
}
}
/**
* Zero Width Joiner pass goes first
* @param strCodes - array of codes to parse
* @param zwjPass - true if we are in the zero width joiner pass
*/
parseLigatures(strCodes, zwjPass = false) {
// iterate through the unicodes and follow the tree, if we find a substitute,
// replace the unicodes with the substitute, but don't stop diving down the tree until we don't find
// a substitute. This is because we want to find the longest ligature match possible.
for (let i = 0; i < strCodes.length; i++) {
let code = Number(strCodes[i]);
let tree = this.ligatures;
let j = i;
let zwj = false;
while (tree[code] !== undefined) {
if (code === 8205)
zwj = true;
tree = tree[code];
if (tree.substitute !== undefined && (zwjPass ? zwj : true)) {
strCodes.splice(i, j - i + 1, tree.substitute);
}
else {
j++;
}
code = Number(strCodes[j]);
}
}
}
}