maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
199 lines (168 loc) • 6.94 kB
text/typescript
import {loadGlyphRange} from '../style/load_glyph_range';
import TinySDF from '@mapbox/tiny-sdf';
import {AlphaImage} from '../util/image';
import type {StyleGlyph} from '../style/style_glyph';
import type {RequestManager} from '../util/request_manager';
import type {GetGlyphsResponse} from '../util/actor_messages';
type Entry = {
// null means we've requested the range, but the glyph wasn't included in the result.
glyphs: {
[id: number]: StyleGlyph | null;
};
requests: {
[range: number]: Promise<{[_: number]: StyleGlyph | null}>;
};
ranges: {
[range: number]: boolean | null;
};
tinySDF?: TinySDF;
};
export class GlyphManager {
requestManager: RequestManager;
localIdeographFontFamily: string | false;
entries: {[stack: string]: Entry};
url: string;
// exposed as statics to enable stubbing in unit tests
static loadGlyphRange = loadGlyphRange;
static TinySDF = TinySDF;
constructor(requestManager: RequestManager, localIdeographFontFamily?: string | false) {
this.requestManager = requestManager;
this.localIdeographFontFamily = localIdeographFontFamily;
this.entries = {};
}
setURL(url?: string | null) {
this.url = url;
}
async getGlyphs(glyphs: {[stack: string]: Array<number>}): Promise<GetGlyphsResponse> {
const glyphsPromises: Promise<{stack: string; id: number; glyph: StyleGlyph}>[] = [];
for (const stack in glyphs) {
for (const id of glyphs[stack]) {
glyphsPromises.push(this._getAndCacheGlyphsPromise(stack, id));
}
}
const updatedGlyphs = await Promise.all(glyphsPromises);
const result: GetGlyphsResponse = {};
for (const {stack, id, glyph} of updatedGlyphs) {
if (!result[stack]) {
result[stack] = {};
}
// Clone the glyph so that our own copy of its ArrayBuffer doesn't get transferred.
result[stack][id] = glyph && {
id: glyph.id,
bitmap: glyph.bitmap.clone(),
metrics: glyph.metrics
};
}
return result;
}
async _getAndCacheGlyphsPromise(stack: string, id: number): Promise<{stack: string; id: number; glyph: StyleGlyph}> {
let entry = this.entries[stack];
if (!entry) {
entry = this.entries[stack] = {
glyphs: {},
requests: {},
ranges: {}
};
}
let glyph = entry.glyphs[id];
if (glyph !== undefined) {
return {stack, id, glyph};
}
glyph = this._tinySDF(entry, stack, id);
if (glyph) {
entry.glyphs[id] = glyph;
return {stack, id, glyph};
}
const range = Math.floor(id / 256);
if (range * 256 > 65535) {
throw new Error('glyphs > 65535 not supported');
}
if (entry.ranges[range]) {
return {stack, id, glyph};
}
if (!this.url) {
throw new Error('glyphsUrl is not set');
}
if (!entry.requests[range]) {
const promise = GlyphManager.loadGlyphRange(stack, range, this.url, this.requestManager);
entry.requests[range] = promise;
}
const response = await entry.requests[range];
for (const id in response) {
if (!this._doesCharSupportLocalGlyph(+id)) {
entry.glyphs[+id] = response[+id];
}
}
entry.ranges[range] = true;
return {stack, id, glyph: response[id] || null};
}
_doesCharSupportLocalGlyph(id: number): boolean {
// The CJK Unified Ideographs blocks and Hangul Syllables blocks are
// spread across many glyph PBFs and are typically accessed very
// randomly. Preferring local rendering for these blocks reduces
// wasteful bandwidth consumption. For visual consistency within CJKV
// text, also include any other CJKV or siniform ideograph or hangul,
// hiragana, or katakana character.
return !!this.localIdeographFontFamily &&
/\p{Ideo}|\p{sc=Hang}|\p{sc=Hira}|\p{sc=Kana}/u.test(String.fromCodePoint(id));
}
_tinySDF(entry: Entry, stack: string, id: number): StyleGlyph {
const fontFamily = this.localIdeographFontFamily;
if (!fontFamily) {
return;
}
if (!this._doesCharSupportLocalGlyph(id)) {
return;
}
// Client-generated glyphs are rendered at 2x texture scale,
// because CJK glyphs are more detailed than others.
const textureScale = 2;
let tinySDF = entry.tinySDF;
if (!tinySDF) {
let fontWeight = '400';
if (/bold/i.test(stack)) {
fontWeight = '900';
} else if (/medium/i.test(stack)) {
fontWeight = '500';
} else if (/light/i.test(stack)) {
fontWeight = '200';
}
tinySDF = entry.tinySDF = new GlyphManager.TinySDF({
fontSize: 24 * textureScale,
buffer: 3 * textureScale,
radius: 8 * textureScale,
cutoff: 0.25,
fontFamily,
fontWeight
});
}
const char = tinySDF.draw(String.fromCharCode(id));
/**
* TinySDF's "top" is the distance from the alphabetic baseline to the top of the glyph.
* Server-generated fonts specify "top" relative to an origin above the em box (the origin
* comes from FreeType, but I'm unclear on exactly how it's derived)
* ref: https://github.com/mapbox/sdf-glyph-foundry
*
* Server fonts don't yet include baseline information, so we can't line up exactly with them
* (and they don't line up with each other)
* ref: https://github.com/mapbox/node-fontnik/pull/160
*
* To approximately align TinySDF glyphs with server-provided glyphs, we use this baseline adjustment
* factor calibrated to be in between DIN Pro and Arial Unicode (but closer to Arial Unicode)
*/
const topAdjustment = 27.5;
const leftAdjustment = 0.5;
return {
id,
bitmap: new AlphaImage({width: char.width || 30 * textureScale, height: char.height || 30 * textureScale}, char.data),
metrics: {
width: char.glyphWidth / textureScale || 24,
height: char.glyphHeight / textureScale || 24,
left: (char.glyphLeft / textureScale + leftAdjustment) || 0,
top: char.glyphTop / textureScale - topAdjustment || -8,
advance: char.glyphAdvance / textureScale || 24,
isDoubleResolution: true
}
};
}
}