Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flexible font handling without Google Fonts default #851

Merged
merged 1 commit into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,28 @@ Note that this low-level API does not create a source for the layer, and extra w

### Font handling

Only commonly available system fonts and [Google Fonts](https://developers.google.com/fonts/) will automatically be available for any `text-font` defined in the Mapbox Style object. It is the responsibility of the application to load other fonts. Because `ol-mapbox-style` uses system and web fonts instead of PBF/SDF glyphs, the [font stack](https://www.mapbox.com/help/manage-fontstacks/) is treated a little different: style and weight are taken from the primary font (i.e. the first one in the font stack). Subsequent fonts in the font stack are only used if the primary font is not available/loaded, and they will be used with the style and weight of the primary font.
`ol-mapbox-style` cannot use PBF/SDF glyphs for `text-font` layout property, as defined in the Mapbox Style specification. Instead, it relies on web fonts. A `ol-webfonts` metadata property can be set on the root of the Style object to specify a location for webfonts, e.g.
```js
{
"version": 8,
"metadata": {
"ol-webfonts": "https://my.server/fonts/{font-family}/{fontweight}{-fontstyle}.css"
}
// ...
}
```

The following placeholders can be used in the `ol-webfonts` url:

* `{font-family}`: CSS font family converted to lowercase, blanks replaced with -, e.g. noto-sans
* `{Font+Family}`: CSS font family in original case, blanks replaced with +, e.g. Noto+Sans
* `{fontweight}`: CSS font weight (numeric), e.g. 400, 700
* `{fontstyle}`: CSS font style, e.g. normal, italic
* `{-fontstyle}`: CSS font style other than normal, e.g. -italic or empty string for normal

If no `metadata['ol-webfonts']` property is available on the Style object, [Fontsource Fonts](https://fontsource.org/fonts) will be used. It is also possible for the application to load other fonts. If a font is already available in the browser, `ol-mapbox-style` will not load it.

Because of this difference, the [font stack](https://www.mapbox.com/help/manage-fontstacks/) is treated a little different than defined in the spec: style and weight are taken from the primary font (i.e. the first one in the font stack). Subsequent fonts in the font stack are only used if the primary font is not available/loaded, and they will be used with the style and weight of the primary font.

## Building the library

Expand Down
4 changes: 4 additions & 0 deletions examples/maptiler-hillshading.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ fetch(`https://api.maptiler.com/maps/outdoor-v2/style.json?key=${key}`)
Object.assign({}, style, {
center: [13.783578, 47.609499],
zoom: 11,
metadata: Object.assign(style.metadata, {
'ol:webfonts':
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}',
}),
})
);
});
12 changes: 9 additions & 3 deletions src/stylefunction.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,9 @@ export const styleFunctionArgs = {};
* @param {string} spriteImageUrl Sprite image url for the sprite
* specified in the Mapbox Style object's `sprite` property. Only required if a
* `sprite` property is specified in the Mapbox Style object.
* @param {function(Array<string>):Array<string>} getFonts Function that
* receives a font stack as arguments, and returns a (modified) font stack that
* @param {function(Array<string>, string=):Array<string>} getFonts Function that
* receives a font stack and the url template from the GL style's `metadata['ol:webfonts']`
* property (if set) as arguments, and returns a (modified) font stack that
* is available. Font names are the names used in the Mapbox Style object. If
* not provided, the font stack will be used as-is. This function can also be
* used for loading web fonts.
Expand Down Expand Up @@ -1145,7 +1146,12 @@ export function stylefunction(
featureState
);
font = mb2css(
getFonts ? getFonts(fontArray) : fontArray,
getFonts
? getFonts(
fontArray,
glStyle.metadata ? glStyle.metadata['ol:webfonts'] : undefined
)
: fontArray,
textSize,
textLineHeight
);
Expand Down
33 changes: 20 additions & 13 deletions src/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,42 +151,49 @@ const processedFontFamilies = {};

/**
* @param {Array} fonts Fonts.
* @param {string} [templateUrl] Template URL.
* @return {Array} Processed fonts.
* @private
*/
export function getFonts(fonts) {
export function getFonts(
fonts,
templateUrl = 'https://cdn.jsdelivr.net/npm/@fontsource/{font-family}/{fontweight}{-fontstyle}.css'
) {
const fontsKey = fonts.toString();
if (fontsKey in processedFontFamilies) {
return processedFontFamilies[fontsKey];
}
const googleFontDescriptions = [];
const fontDescriptions = [];
for (let i = 0, ii = fonts.length; i < ii; ++i) {
fonts[i] = fonts[i].replace('Arial Unicode MS', 'Arial');
const font = fonts[i];
const cssFont = mb2css(font, 1);
registerFont(cssFont);
const parts = cssFont.split(' ');
googleFontDescriptions.push([
fontDescriptions.push([
parts.slice(3).join(' ').replace(/"/g, ''),
parts[1],
parts[0],
]);
}
for (let i = 0, ii = googleFontDescriptions.length; i < ii; ++i) {
const googleFontDescription = googleFontDescriptions[i];
const family = googleFontDescription[0];
for (let i = 0, ii = fontDescriptions.length; i < ii; ++i) {
const fontDescription = fontDescriptions[i];
const family = fontDescription[0];
if (!hasFontFamily(family)) {
if (
checkedFonts.get(
`${googleFontDescription[2]}\n${googleFontDescription[1]} \n${family}`
`${fontDescription[2]}\n${fontDescription[1]} \n${family}`
) !== 100
) {
const fontUrl =
'https://fonts.googleapis.com/css?family=' +
family.replace(/ /g, '+') +
':' +
googleFontDescription[1] +
googleFontDescription[2];
const fontUrl = templateUrl
.replace('{font-family}', family.replace(/ /g, '-').toLowerCase())
.replace('{Font+Family}', family.replace(/ /g, '+'))
.replace('{fontweight}', fontDescription[1])
.replace(
'{-fontstyle}',
fontDescription[2].replace('normal', '').replace(/(.+)/, '-$1')
)
.replace('{fontstyle}', fontDescription[2]);
if (!document.querySelector('link[href="' + fontUrl + '"]')) {
const markup = document.createElement('link');
markup.href = fontUrl;
Expand Down
54 changes: 54 additions & 0 deletions test/apply.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -900,4 +900,58 @@ describe('ol-mapbox-style', function () {
.catch(done);
});
});

describe('Font loading', function () {
let target;
beforeEach(function () {
target = document.createElement('div');
});

it('loads fonts from a style', function (done) {
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
stylesheets.forEach(function (stylesheet) {
stylesheet.remove();
});
apply(target, {
version: 8,
metadata: {
'ol:webfonts':
'https://fonts.openmaptiles.org/{font-family}/{fontweight}{-fontstyle}.css',
},
sources: {
test: {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
},
},
layers: [
{
id: 'test',
type: 'symbol',
source: 'test',
layout: {
'text-field': 'test',
'text-font': ['Open Sans Regular'],
},
},
],
})
.then(function (map) {
const getStyle = map.getAllLayers()[0].getStyle();
getStyle(new Feature(new Point([0, 0])), 1);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(1);
should(stylesheets.item(0).href).eql(
'https://fonts.openmaptiles.org/open-sans/400.css'
);
done();
})
.catch(function (err) {
done(err);
});
});
});
});
56 changes: 43 additions & 13 deletions test/text.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('text', function () {
});

describe('getFonts', function () {
beforeEach(function () {
before(function () {
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
stylesheets.forEach(function (stylesheet) {
stylesheet.remove();
Expand All @@ -54,14 +54,16 @@ describe('text', function () {
should(stylesheets.length).eql(0);
});

it('loads fonts from fonts.google.com', function () {
let stylesheets;
getFonts([
'Noto Sans Bold',
'Noto Sans Regular Italic',
'Averia Sans Libre Bold',
]);
stylesheets = document.querySelectorAll('link[rel=stylesheet]');
it('loads fonts with a template using {Font+Family} and {fontstyle}', function () {
getFonts(
[
'Noto Sans Bold',
'Noto Sans Regular Italic',
'Averia Sans Libre Bold',
],
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}'
);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(3);
should(stylesheets.item(0).href).eql(
'https://fonts.googleapis.com/css?family=Noto+Sans:700normal'
Expand All @@ -72,11 +74,39 @@ describe('text', function () {
should(stylesheets.item(2).href).eql(
'https://fonts.googleapis.com/css?family=Averia+Sans+Libre:700normal'
);
});

// already loaded family, no additional link
getFonts(['Noto Sans Bold']);
stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(3);
it('loads fonts with a template using {font-family} and {-fontstyle}', function () {
getFonts(
['Noto Sans Regular', 'Averia Sans Libre Bold Italic'],
'./fonts/{font-family}/{fontweight}{-fontstyle}.css'
);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(5);
should(stylesheets.item(3).href).eql(
location.origin + '/fonts/noto-sans/400.css'
);
should(stylesheets.item(4).href).eql(
location.origin + '/fonts/averia-sans-libre/700-italic.css'
);
});

it('does not load fonts twice', function () {
getFonts(
['Noto Sans Bold'],
'https://fonts.googleapis.com/css?family={Font+Family}:{fontweight}{fontstyle}'
);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(5);
});

it('uses the default template if none is provided', function () {
getFonts(['Averia Sans Libre']);
const stylesheets = document.querySelectorAll('link[rel=stylesheet]');
should(stylesheets.length).eql(6);
should(stylesheets.item(5).href).eql(
'https://cdn.jsdelivr.net/npm/@fontsource/averia-sans-libre/400.css'
);
});
});
});