Merge pull request #9755 from ahocevar/font-loading
Font loading improvements
This commit is contained in:
+1
-1
@@ -80,7 +80,7 @@
|
|||||||
"loglevelnext": "^3.0.1",
|
"loglevelnext": "^3.0.1",
|
||||||
"marked": "0.7.0",
|
"marked": "0.7.0",
|
||||||
"mocha": "6.1.4",
|
"mocha": "6.1.4",
|
||||||
"ol-mapbox-style": "^5.0.0-beta.2",
|
"ol-mapbox-style": "^5.0.0-beta.3",
|
||||||
"pixelmatch": "^5.0.0",
|
"pixelmatch": "^5.0.0",
|
||||||
"pngjs": "^3.4.0",
|
"pngjs": "^3.4.0",
|
||||||
"proj4": "2.5.0",
|
"proj4": "2.5.0",
|
||||||
|
|||||||
+20
-3
@@ -2,6 +2,13 @@
|
|||||||
* @module ol/css
|
* @module ol/css
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} FontParameters
|
||||||
|
* @property {Array<string>} families
|
||||||
|
* @property {string} style
|
||||||
|
* @property {string} weight
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CSS class for hidden feature.
|
* The CSS class for hidden feature.
|
||||||
@@ -62,10 +69,13 @@ export const CLASS_COLLAPSED = 'ol-collapsed';
|
|||||||
* Get the list of font families from a font spec. Note that this doesn't work
|
* Get the list of font families from a font spec. Note that this doesn't work
|
||||||
* for font families that have commas in them.
|
* for font families that have commas in them.
|
||||||
* @param {string} The CSS font property.
|
* @param {string} The CSS font property.
|
||||||
* @return {Object<string>} The font families (or null if the input spec is invalid).
|
* @return {FontParameters} The font families (or null if the input spec is invalid).
|
||||||
*/
|
*/
|
||||||
export const getFontFamilies = (function() {
|
export const getFontParameters = (function() {
|
||||||
let style;
|
let style;
|
||||||
|
/**
|
||||||
|
* @type {Object<string, FontParameters>}
|
||||||
|
*/
|
||||||
const cache = {};
|
const cache = {};
|
||||||
return function(font) {
|
return function(font) {
|
||||||
if (!style) {
|
if (!style) {
|
||||||
@@ -74,11 +84,18 @@ export const getFontFamilies = (function() {
|
|||||||
if (!(font in cache)) {
|
if (!(font in cache)) {
|
||||||
style.font = font;
|
style.font = font;
|
||||||
const family = style.fontFamily;
|
const family = style.fontFamily;
|
||||||
|
const fontWeight = style.fontWeight;
|
||||||
|
const fontStyle = style.fontStyle;
|
||||||
style.font = '';
|
style.font = '';
|
||||||
if (!family) {
|
if (!family) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
cache[font] = family.split(/,\s?/);
|
const families = family.split(/,\s?/);
|
||||||
|
cache[font] = {
|
||||||
|
families: families,
|
||||||
|
weight: fontWeight,
|
||||||
|
style: fontStyle
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return cache[font];
|
return cache[font];
|
||||||
};
|
};
|
||||||
|
|||||||
+44
-37
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @module ol/render/canvas
|
* @module ol/render/canvas
|
||||||
*/
|
*/
|
||||||
import {getFontFamilies} from '../css.js';
|
import {getFontParameters} from '../css.js';
|
||||||
import {createCanvasContext2D} from '../dom.js';
|
import {createCanvasContext2D} from '../dom.js';
|
||||||
import {clear} from '../obj.js';
|
import {clear} from '../obj.js';
|
||||||
import {create as createTransform} from '../transform.js';
|
import {create as createTransform} from '../transform.js';
|
||||||
@@ -180,6 +180,10 @@ export const checkedFonts = {};
|
|||||||
*/
|
*/
|
||||||
let measureContext = null;
|
let measureContext = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
let measureFont;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {!Object<string, number>}
|
* @type {!Object<string, number>}
|
||||||
@@ -192,7 +196,7 @@ export const textHeights = {};
|
|||||||
* @param {string} fontSpec CSS font spec.
|
* @param {string} fontSpec CSS font spec.
|
||||||
*/
|
*/
|
||||||
export const checkFont = (function() {
|
export const checkFont = (function() {
|
||||||
const retries = 60;
|
const retries = 100;
|
||||||
const checked = checkedFonts;
|
const checked = checkedFonts;
|
||||||
const size = '32px ';
|
const size = '32px ';
|
||||||
const referenceFonts = ['monospace', 'serif'];
|
const referenceFonts = ['monospace', 'serif'];
|
||||||
@@ -200,32 +204,30 @@ export const checkFont = (function() {
|
|||||||
const text = 'wmytzilWMYTZIL@#/&?$%10\uF013';
|
const text = 'wmytzilWMYTZIL@#/&?$%10\uF013';
|
||||||
let interval, referenceWidth;
|
let interval, referenceWidth;
|
||||||
|
|
||||||
function isAvailable(font) {
|
/**
|
||||||
|
* @param {string} fontStyle Css font-style
|
||||||
|
* @param {string} fontWeight Css font-weight
|
||||||
|
* @param {*} fontFamily Css font-family
|
||||||
|
* @return {boolean} Font with style and weight is available
|
||||||
|
*/
|
||||||
|
function isAvailable(fontStyle, fontWeight, fontFamily) {
|
||||||
const context = getMeasureContext();
|
const context = getMeasureContext();
|
||||||
// Check weight ranges according to
|
let available = true;
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Fallback_weights
|
for (let i = 0; i < len; ++i) {
|
||||||
for (let weight = 100; weight <= 700; weight += 300) {
|
const referenceFont = referenceFonts[i];
|
||||||
const fontWeight = weight + ' ';
|
context.font = fontStyle + ' ' + fontWeight + ' ' + size + referenceFont;
|
||||||
let available = true;
|
referenceWidth = context.measureText(text).width;
|
||||||
for (let i = 0; i < len; ++i) {
|
if (fontFamily != referenceFont) {
|
||||||
const referenceFont = referenceFonts[i];
|
context.font = fontStyle + ' ' + fontWeight + ' ' + size + fontFamily + ',' + referenceFont;
|
||||||
context.font = fontWeight + size + referenceFont;
|
const width = context.measureText(text).width;
|
||||||
referenceWidth = context.measureText(text).width;
|
// If width and referenceWidth are the same, then the fallback was used
|
||||||
if (font != referenceFont) {
|
// instead of the font we wanted, so the font is not available.
|
||||||
context.font = fontWeight + size + font + ',' + referenceFont;
|
available = available && width != referenceWidth;
|
||||||
const width = context.measureText(text).width;
|
|
||||||
// If width and referenceWidth are the same, then the fallback was used
|
|
||||||
// instead of the font we wanted, so the font is not available.
|
|
||||||
available = available && width != referenceWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (available) {
|
|
||||||
// Consider font available when it is available in one weight range.
|
|
||||||
//FIXME With this we miss rare corner cases, so we should consider
|
|
||||||
//FIXME checking availability for each requested weight range.
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (available) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +235,15 @@ export const checkFont = (function() {
|
|||||||
let done = true;
|
let done = true;
|
||||||
for (const font in checked) {
|
for (const font in checked) {
|
||||||
if (checked[font] < retries) {
|
if (checked[font] < retries) {
|
||||||
if (isAvailable(font)) {
|
if (isAvailable.apply(this, font.split('\n'))) {
|
||||||
checked[font] = retries;
|
checked[font] = retries;
|
||||||
clear(textHeights);
|
clear(textHeights);
|
||||||
// Make sure that loaded fonts are picked up by Safari
|
// Make sure that loaded fonts are picked up by Safari
|
||||||
measureContext = null;
|
measureContext = null;
|
||||||
labelCache.clear();
|
measureFont = undefined;
|
||||||
|
if (labelCache.getCount()) {
|
||||||
|
labelCache.clear();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
++checked[font];
|
++checked[font];
|
||||||
done = false;
|
done = false;
|
||||||
@@ -252,16 +257,18 @@ export const checkFont = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return function(fontSpec) {
|
return function(fontSpec) {
|
||||||
const fontFamilies = getFontFamilies(fontSpec);
|
const font = getFontParameters(fontSpec);
|
||||||
if (!fontFamilies) {
|
if (!font) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (let i = 0, ii = fontFamilies.length; i < ii; ++i) {
|
const families = font.families;
|
||||||
const fontFamily = fontFamilies[i];
|
for (let i = 0, ii = families.length; i < ii; ++i) {
|
||||||
if (!(fontFamily in checked)) {
|
const family = families[i];
|
||||||
checked[fontFamily] = retries;
|
const key = font.style + '\n' + font.weight + '\n' + family;
|
||||||
if (!isAvailable(fontFamily)) {
|
if (!(key in checked)) {
|
||||||
checked[fontFamily] = 0;
|
checked[key] = retries;
|
||||||
|
if (!isAvailable(font.style, font.weight, family)) {
|
||||||
|
checked[key] = 0;
|
||||||
if (interval === undefined) {
|
if (interval === undefined) {
|
||||||
interval = setInterval(check, 32);
|
interval = setInterval(check, 32);
|
||||||
}
|
}
|
||||||
@@ -317,8 +324,8 @@ export const measureTextHeight = (function() {
|
|||||||
*/
|
*/
|
||||||
export function measureTextWidth(font, text) {
|
export function measureTextWidth(font, text) {
|
||||||
const measureContext = getMeasureContext();
|
const measureContext = getMeasureContext();
|
||||||
if (font != measureContext.font) {
|
if (font != measureFont) {
|
||||||
measureContext.font = font;
|
measureContext.font = measureFont = font;
|
||||||
}
|
}
|
||||||
return measureContext.measureText(text).width;
|
return measureContext.measureText(text).width;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class LabelCache extends LRUCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
super.clear();
|
|
||||||
this.consumers = {};
|
this.consumers = {};
|
||||||
|
super.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
makeInverse
|
makeInverse
|
||||||
} from '../../transform.js';
|
} from '../../transform.js';
|
||||||
import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
|
import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
|
||||||
|
import {clear} from '../../obj.js';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -378,6 +379,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
|
|||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
handleFontsChanged() {
|
handleFontsChanged() {
|
||||||
|
clear(this.renderTileImageQueue_);
|
||||||
const layer = this.getLayer();
|
const layer = this.getLayer();
|
||||||
if (layer.getVisible() && this.renderedLayerRevision_ !== undefined) {
|
if (layer.getVisible() && this.renderedLayerRevision_ !== undefined) {
|
||||||
layer.changed();
|
layer.changed();
|
||||||
|
|||||||
@@ -1,44 +1,56 @@
|
|||||||
import {getFontFamilies} from '../../../src/ol/css.js';
|
import {getFontParameters} from '../../../src/ol/css.js';
|
||||||
|
|
||||||
describe('ol.css', function() {
|
describe('ol.css', function() {
|
||||||
|
|
||||||
describe('getFontFamilies()', function() {
|
describe('getFontParameters()', function() {
|
||||||
const cases = [{
|
const cases = [{
|
||||||
font: '2em "Open Sans"',
|
font: '2em "Open Sans"',
|
||||||
|
style: 'normal',
|
||||||
|
weight: 'normal',
|
||||||
families: ['"Open Sans"']
|
families: ['"Open Sans"']
|
||||||
}, {
|
}, {
|
||||||
font: '2em \'Open Sans\'',
|
font: '2em \'Open Sans\'',
|
||||||
|
style: 'normal',
|
||||||
|
weight: 'normal',
|
||||||
families: ['"Open Sans"']
|
families: ['"Open Sans"']
|
||||||
}, {
|
}, {
|
||||||
font: '2em "Open Sans", sans-serif',
|
font: '2em "Open Sans", sans-serif',
|
||||||
|
style: 'normal',
|
||||||
|
weight: 'normal',
|
||||||
families: ['"Open Sans"', 'sans-serif']
|
families: ['"Open Sans"', 'sans-serif']
|
||||||
}, {
|
}, {
|
||||||
font: 'italic small-caps bolder 16px/3 cursive',
|
font: 'italic small-caps bolder 16px/3 cursive',
|
||||||
|
style: 'italic',
|
||||||
|
weight: 'bolder',
|
||||||
families: ['cursive']
|
families: ['cursive']
|
||||||
}, {
|
}, {
|
||||||
font: 'garbage 2px input',
|
font: 'garbage 2px input',
|
||||||
families: null
|
families: null
|
||||||
}, {
|
}, {
|
||||||
font: '100% fantasy',
|
font: '100% fantasy',
|
||||||
|
style: 'normal',
|
||||||
|
weight: 'normal',
|
||||||
families: ['fantasy']
|
families: ['fantasy']
|
||||||
}];
|
}];
|
||||||
|
|
||||||
cases.forEach(function(c, i) {
|
cases.forEach(function(c, i) {
|
||||||
it('works for ' + c.font, function() {
|
it('works for ' + c.font, function() {
|
||||||
const families = getFontFamilies(c.font);
|
const font = getFontParameters(c.font);
|
||||||
if (c.families === null) {
|
if (c.families === null) {
|
||||||
expect(families).to.be(null);
|
expect(font).to.be(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
families.forEach(function(family, j) {
|
font.families.forEach(function(family, j) {
|
||||||
// Safari uses single quotes for font families, so we have to do extra work
|
// Safari uses single quotes for font families, so we have to do extra work
|
||||||
if (family.charAt(0) === '\'') {
|
if (family.charAt(0) === '\'') {
|
||||||
// we wouldn't want to do this in the lib since it doesn't properly escape quotes
|
// we wouldn't want to do this in the lib since it doesn't properly escape quotes
|
||||||
// but we know that our test cases don't include quotes in font names
|
// but we know that our test cases don't include quotes in font names
|
||||||
families[j] = '"' + family.slice(1, -1) + '"';
|
font.families[j] = '"' + family.slice(1, -1) + '"';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(families).to.eql(c.families);
|
expect(font.style).to.eql(c.style);
|
||||||
|
expect(font.weight).to.eql(c.weight);
|
||||||
|
expect(font.families).to.eql(c.families);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ describe('ol.render.canvas', function() {
|
|||||||
render.measureTextHeight('12px sans-serif');
|
render.measureTextHeight('12px sans-serif');
|
||||||
});
|
});
|
||||||
|
|
||||||
const retries = 60;
|
const retries = 100;
|
||||||
|
|
||||||
it('does not clear label cache and measurements for unavailable fonts', function(done) {
|
it('does not clear label cache and measurements for unavailable fonts', function(done) {
|
||||||
this.timeout(3000);
|
this.timeout(4000);
|
||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
listen(render.labelCache, 'clear', spy);
|
listen(render.labelCache, 'clear', spy);
|
||||||
const interval = setInterval(function() {
|
const interval = setInterval(function() {
|
||||||
if (render.checkedFonts['foo'] == retries && render.checkedFonts['sans-serif'] == retries) {
|
if (render.checkedFonts['normal\nnormal\nfoo'] == retries && render.checkedFonts['normal\nnormal\nsans-serif'] == retries) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
unlisten(render.labelCache, 'clear', spy);
|
unlisten(render.labelCache, 'clear', spy);
|
||||||
expect(spy.callCount).to.be(0);
|
expect(spy.callCount).to.be(0);
|
||||||
@@ -39,7 +39,7 @@ describe('ol.render.canvas', function() {
|
|||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
listen(render.labelCache, 'clear', spy);
|
listen(render.labelCache, 'clear', spy);
|
||||||
const interval = setInterval(function() {
|
const interval = setInterval(function() {
|
||||||
if (render.checkedFonts['sans-serif'] == retries) {
|
if (render.checkedFonts['normal\nnormal\nsans-serif'] == retries) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
unlisten(render.labelCache, 'clear', spy);
|
unlisten(render.labelCache, 'clear', spy);
|
||||||
expect(spy.callCount).to.be(0);
|
expect(spy.callCount).to.be(0);
|
||||||
@@ -54,7 +54,7 @@ describe('ol.render.canvas', function() {
|
|||||||
const spy = sinon.spy();
|
const spy = sinon.spy();
|
||||||
listen(render.labelCache, 'clear', spy);
|
listen(render.labelCache, 'clear', spy);
|
||||||
const interval = setInterval(function() {
|
const interval = setInterval(function() {
|
||||||
if (render.checkedFonts['monospace'] == retries) {
|
if (render.checkedFonts['normal\nnormal\nmonospace'] == retries) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
unlisten(render.labelCache, 'clear', spy);
|
unlisten(render.labelCache, 'clear', spy);
|
||||||
expect(spy.callCount).to.be(0);
|
expect(spy.callCount).to.be(0);
|
||||||
@@ -67,6 +67,7 @@ describe('ol.render.canvas', function() {
|
|||||||
|
|
||||||
it('clears label cache and measurements for fonts that become available', function(done) {
|
it('clears label cache and measurements for fonts that become available', function(done) {
|
||||||
head.appendChild(font);
|
head.appendChild(font);
|
||||||
|
render.labelCache.set('dummy', {});
|
||||||
listen(render.labelCache, 'clear', function() {
|
listen(render.labelCache, 'clear', function() {
|
||||||
expect(render.textHeights).to.eql({});
|
expect(render.textHeights).to.eql({});
|
||||||
done();
|
done();
|
||||||
|
|||||||
Reference in New Issue
Block a user