diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 710519d128..8e5183f120 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -3,6 +3,7 @@ goog.provide('ol.render.canvas'); goog.require('ol.css'); goog.require('ol.dom'); +goog.require('ol.obj'); goog.require('ol.structs.LRUCache'); goog.require('ol.transform'); @@ -98,28 +99,39 @@ ol.render.canvas.labelCache = new ol.structs.LRUCache(); /** - * @type {!Object.} + * @type {!Object.} */ ol.render.canvas.checkedFonts_ = {}; +/** + * @type {CanvasRenderingContext2D} + */ +ol.render.canvas.measureContext_ = null; + + +/** + * @type {!Object.} + */ +ol.render.canvas.textHeights_ = {}; + + /** * Clears the label cache when a font becomes available. * @param {string} fontSpec CSS font spec. */ ol.render.canvas.checkFont = (function() { + var retries = 60; var checked = ol.render.canvas.checkedFonts_; var labelCache = ol.render.canvas.labelCache; var font = '32px monospace'; var text = 'wmytzilWMYTZIL@#/&?$%10'; - var context, interval, referenceWidth; + var interval, referenceWidth; function isAvailable(fontFamily) { - if (!context) { - context = ol.dom.createCanvasContext2D(1, 1); - context.font = font; - referenceWidth = context.measureText(text).width; - } + var context = ol.render.canvas.getMeasureContext(); + context.font = font; + referenceWidth = context.measureText(text).width; var available = true; if (fontFamily != 'monospace') { context.font = '32px ' + fontFamily + ',monospace'; @@ -128,10 +140,6 @@ ol.render.canvas.checkFont = (function() { // fallback was used instead of the font we wanted, so the font is not // available. available = width != referenceWidth; - // Setting the font back to a different one works around an issue in - // Safari where subsequent `context.font` assignments with the same font - // will not re-attempt to use a font that is currently loading. - context.font = font; } return available; } @@ -139,9 +147,12 @@ ol.render.canvas.checkFont = (function() { function check() { var done = true; for (var font in checked) { - if (checked[font] < 60) { + if (checked[font] < retries) { if (isAvailable(font)) { - checked[font] = 60; + checked[font] = retries; + ol.obj.clear(ol.render.canvas.textHeights_); + // Make sure that loaded fonts are picked up by Safari + ol.render.canvas.measureContext_ = null; labelCache.clear(); } else { ++checked[font]; @@ -163,7 +174,7 @@ ol.render.canvas.checkFont = (function() { for (var i = 0, ii = fontFamilies.length; i < ii; ++i) { var fontFamily = fontFamilies[i]; if (!(fontFamily in checked)) { - checked[fontFamily] = 60; + checked[fontFamily] = retries; if (!isAvailable(fontFamily)) { checked[fontFamily] = 0; if (interval === undefined) { @@ -176,6 +187,59 @@ ol.render.canvas.checkFont = (function() { })(); +/** + * @return {CanvasRenderingContext2D} Measure context. + */ +ol.render.canvas.getMeasureContext = function() { + var context = ol.render.canvas.measureContext_; + if (!context) { + context = ol.render.canvas.measureContext_ = ol.dom.createCanvasContext2D(1, 1); + } + return context; +}; + + +/** + * @param {string} font Font to use for measuring. + * @return {ol.Size} Measurement. + */ +ol.render.canvas.measureTextHeight = (function() { + var span; + var heights = ol.render.canvas.textHeights_; + return function(font) { + var height = heights[font]; + if (height == undefined) { + if (!span) { + span = document.createElement('span'); + span.textContent = 'M'; + span.style.margin = span.style.padding = '0 !important'; + span.style.position = 'absolute !important'; + span.style.left = '-99999px !important'; + } + span.style.font = font; + document.body.appendChild(span); + height = heights[font] = span.offsetHeight; + document.body.removeChild(span); + } + return height; + }; +})(); + + +/** + * @param {string} font Font. + * @param {string} text Text. + * @return {number} Width. + */ +ol.render.canvas.measureTextWidth = function(font, text) { + var measureContext = ol.render.canvas.getMeasureContext(); + if (font != measureContext.font) { + measureContext.font = font; + } + return measureContext.measureText(text).width; +}; + + /** * @param {CanvasRenderingContext2D} context Context. * @param {number} rotation Rotation. diff --git a/src/ol/render/canvas/textreplay.js b/src/ol/render/canvas/textreplay.js index 12a258908e..ac8371bfc4 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -118,53 +118,6 @@ ol.render.canvas.TextReplay = function( ol.inherits(ol.render.canvas.TextReplay, ol.render.canvas.Replay); -/** - * @param {string} font Font to use for measuring. - * @return {ol.Size} Measurement. - */ -ol.render.canvas.TextReplay.measureTextHeight = (function() { - var span; - var heights = {}; - return function(font) { - var height = heights[font]; - if (height == undefined) { - if (!span) { - span = document.createElement('span'); - span.textContent = 'M'; - span.style.margin = span.style.padding = '0 !important'; - span.style.position = 'absolute !important'; - span.style.left = '-99999px !important'; - } - span.style.font = font; - document.body.appendChild(span); - height = heights[font] = span.offsetHeight; - document.body.removeChild(span); - } - return height; - }; -})(); - - -/** - * @param {string} font Font. - * @param {string} text Text. - * @return {number} Width. - */ -ol.render.canvas.TextReplay.measureTextWidth = (function() { - var measureContext; - var currentFont; - return function(font, text) { - if (!measureContext) { - measureContext = ol.dom.createCanvasContext2D(1, 1); - } - if (font != currentFont) { - currentFont = measureContext.font = font; - } - return measureContext.measureText(text).width; - }; -})(); - - /** * @param {string} font Font to use for measuring. * @param {Array.} lines Lines to measure. @@ -177,7 +130,7 @@ ol.render.canvas.TextReplay.measureTextWidths = function(font, lines, widths) { var width = 0; var currentWidth, i; for (i = 0; i < numLines; ++i) { - currentWidth = ol.render.canvas.TextReplay.measureTextWidth(font, lines[i]); + currentWidth = ol.render.canvas.measureTextWidth(font, lines[i]); width = Math.max(width, currentWidth); widths.push(currentWidth); } @@ -325,7 +278,7 @@ ol.render.canvas.TextReplay.prototype.getImage = function(text, fill, stroke) { var numLines = lines.length; var widths = []; var width = ol.render.canvas.TextReplay.measureTextWidths(textState.font, lines, widths); - var lineHeight = ol.render.canvas.TextReplay.measureTextHeight(textState.font); + var lineHeight = ol.render.canvas.measureTextHeight(textState.font); var height = lineHeight * numLines; var renderWidth = (width + strokeWidth); var context = ol.dom.createCanvasContext2D( @@ -433,7 +386,7 @@ ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end, declutte function(text) { var width = widths[text]; if (!width) { - width = widths[text] = ol.render.canvas.TextReplay.measureTextWidth(font, text); + width = widths[text] = ol.render.canvas.measureTextWidth(font, text); } return width * textScale * pixelRatio; }, @@ -445,7 +398,7 @@ ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end, declutte function(text) { var width = widths[text]; if (!width) { - width = widths[text] = ol.render.canvas.TextReplay.measureTextWidth(font, text); + width = widths[text] = ol.render.canvas.measureTextWidth(font, text); } return width * textScale; }, diff --git a/test/spec/ol/render/canvas/index.test.js b/test/spec/ol/render/canvas/index.test.js index 3c46d3f546..ebe852b826 100644 --- a/test/spec/ol/render/canvas/index.test.js +++ b/test/spec/ol/render/canvas/index.test.js @@ -12,56 +12,72 @@ describe('ol.render.canvas', function() { describe('ol.render.canvas.checkFont()', function() { + beforeEach(function() { + ol.obj.clear(ol.render.canvas.checkedFonts_); + ol.render.canvas.getMeasureContext(); + ol.render.canvas.measureTextHeight('12px sans-serif'); + }); + var checkFont = ol.render.canvas.checkFont; + var retries = 60; - it('does not clear the label cache for unavailable fonts', function(done) { - ol.obj.clear(ol.render.canvas.checkedFonts_); + it('does not clear label cache and measurements for unavailable fonts', function(done) { + this.timeout(3000); var spy = sinon.spy(); ol.events.listen(ol.render.canvas.labelCache, 'clear', spy); + var interval = setInterval(function() { + if (ol.render.canvas.checkedFonts_['foo'] == retries && ol.render.canvas.checkedFonts_['sans-serif'] == retries) { + clearInterval(interval); + ol.events.unlisten(ol.render.canvas.labelCache, 'clear', spy); + expect(spy.callCount).to.be(0); + expect(ol.render.canvas.measureContext_).to.not.be(null); + expect(ol.render.canvas.textHeights_).to.not.eql({}); + done(); + } + }, 32); checkFont('12px foo,sans-serif'); - setTimeout(function() { - ol.events.unlisten(ol.render.canvas.labelCache, 'clear', spy); - expect(spy.callCount).to.be(0); - done(); - }, 1600); }); - it('does not clear the label cache for available fonts', function(done) { - ol.obj.clear(ol.render.canvas.checkedFonts_); + it('does not clear label cache and measurements for available fonts', function(done) { var spy = sinon.spy(); ol.events.listen(ol.render.canvas.labelCache, 'clear', spy); + var interval = setInterval(function() { + if (ol.render.canvas.checkedFonts_['sans-serif'] == retries) { + clearInterval(interval); + ol.events.unlisten(ol.render.canvas.labelCache, 'clear', spy); + expect(spy.callCount).to.be(0); + expect(ol.render.canvas.measureContext_).to.not.be(null); + expect(ol.render.canvas.textHeights_).to.not.eql({}); + done(); + } + }, 32); checkFont('12px sans-serif'); - setTimeout(function() { - ol.events.unlisten(ol.render.canvas.labelCache, 'clear', spy); - expect(spy.callCount).to.be(0); - done(); - }, 800); }); - it('does not clear the label cache for the \'monospace\' font', function(done) { - ol.obj.clear(ol.render.canvas.checkedFonts_); + it('does not clear label cache and measurements for the \'monospace\' font', function(done) { var spy = sinon.spy(); ol.events.listen(ol.render.canvas.labelCache, 'clear', spy); + var interval = setInterval(function() { + if (ol.render.canvas.checkedFonts_['monospace'] == retries) { + clearInterval(interval); + ol.events.unlisten(ol.render.canvas.labelCache, 'clear', spy); + expect(spy.callCount).to.be(0); + expect(ol.render.canvas.measureContext_).to.not.be(null); + expect(ol.render.canvas.textHeights_).to.not.eql({}); + done(); + } + }, 32); checkFont('12px monospace'); - setTimeout(function() { - ol.events.unlisten(ol.render.canvas.labelCache, 'clear', spy); - expect(spy.callCount).to.be(0); - done(); - }, 800); }); - it('clears the label cache for fonts that become available', function(done) { - ol.obj.clear(ol.render.canvas.checkedFonts_); + it('clears label cache and measurements for fonts that become available', function(done) { head.appendChild(font); - var spy = sinon.spy(); - ol.events.listen(ol.render.canvas.labelCache, 'clear', spy); - checkFont('12px Abel'); - setTimeout(function() { - ol.events.unlisten(ol.render.canvas.labelCache, 'clear', spy); - head.removeChild(font); - expect(spy.callCount).to.be(1); + ol.events.listen(ol.render.canvas.labelCache, 'clear', function() { + expect(ol.render.canvas.measureContext_).to.be(null); + expect(ol.render.canvas.textHeights_).to.eql({}); done(); - }, 1600); + }); + checkFont('12px Abel'); }); });