From 7f865b85203c27d9858b0f6fadfa1cd69ba5138d Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 20 Oct 2017 00:02:20 +0200 Subject: [PATCH] Check if fonts are available and redraw when label cache was cleared --- src/ol/events/eventtype.js | 1 + src/ol/render/canvas.js | 70 +++++++++++ src/ol/render/canvas/textreplay.js | 20 +-- src/ol/renderer/canvas/vectorlayer.js | 26 +++- src/ol/renderer/canvas/vectortilelayer.js | 27 +++- src/ol/renderer/vector.js | 1 + src/ol/structs/lrucache.js | 22 ++++ test/spec/ol/render/canvas/index.test.js | 66 +++++++++- .../ol/renderer/canvas/vectorlayer.test.js | 115 ++++++++++++++++++ .../renderer/canvas/vectortilelayer.test.js | 49 +++++++- 10 files changed, 376 insertions(+), 21 deletions(-) diff --git a/src/ol/events/eventtype.js b/src/ol/events/eventtype.js index daec939d69..a7d11a0541 100644 --- a/src/ol/events/eventtype.js +++ b/src/ol/events/eventtype.js @@ -12,6 +12,7 @@ ol.events.EventType = { */ CHANGE: 'change', + CLEAR: 'clear', CLICK: 'click', DBLCLICK: 'dblclick', DRAGENTER: 'dragenter', diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index d2ca4d8764..33b39c2db1 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -1,6 +1,9 @@ goog.provide('ol.render.canvas'); +goog.require('ol.css'); +goog.require('ol.dom'); +goog.require('ol.structs.LRUCache'); goog.require('ol.transform'); @@ -81,6 +84,73 @@ ol.render.canvas.defaultTextBaseline = 'middle'; ol.render.canvas.defaultLineWidth = 1; +/** + * @type {ol.structs.LRUCache.} + */ +ol.render.canvas.labelCache = new ol.structs.LRUCache(); + + +/** + * @type {!Object.} + */ +ol.render.canvas.checkedFonts_ = {}; + + +/** + * Clears the label cache when a font becomes available. + * @param {string} fontSpec CSS font spec. + */ +ol.render.canvas.checkFont = (function() { + var checked = ol.render.canvas.checkedFonts_; + var labelCache = ol.render.canvas.labelCache; + var text = 'wmytzilWMYTZIL@#/&?$%10'; + var context, referenceWidth; + + function isAvailable(fontFamily) { + if (!context) { + context = ol.dom.createCanvasContext2D(); + context.font = '32px monospace'; + referenceWidth = context.measureText(text).width; + } + var available = true; + if (fontFamily != 'monospace') { + context.font = '32px ' + fontFamily + ',monospace'; + var width = context.measureText(text).width; + // If width and referenceWidth are the same, then the 'monospace' + // fallback was used instead of the font we wanted, so the font is not + // available. + available = width != referenceWidth; + } + return available; + } + + return function(fontSpec) { + var fontFamilies = ol.css.getFontFamilies(fontSpec); + if (!fontFamilies) { + return; + } + fontFamilies.forEach(function(fontFamily) { + if (!checked[fontFamily]) { + checked[fontFamily] = true; + if (!isAvailable(fontFamily)) { + var callCount = 0; + var interval = window.setInterval(function() { + ++callCount; + var available = isAvailable(fontFamily); + if (available || callCount >= 60) { + window.clearInterval(interval); + if (available) { + labelCache.clear(); + } + } + }, 25); + } + } + }); + }; +})(); + + /** * @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 349e01152c..c4a158d241 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -11,7 +11,6 @@ goog.require('ol.render.canvas'); goog.require('ol.render.canvas.Instruction'); goog.require('ol.render.canvas.Replay'); goog.require('ol.render.replay'); -goog.require('ol.structs.LRUCache'); goog.require('ol.style.TextPlacement'); @@ -121,21 +120,10 @@ ol.render.canvas.TextReplay = function( */ this.widths_ = {}; - while (ol.render.canvas.TextReplay.labelCache_.canExpireCache()) { - ol.render.canvas.TextReplay.labelCache_.pop(); - } - }; ol.inherits(ol.render.canvas.TextReplay, ol.render.canvas.Replay); -/** - * @private - * @type {ol.structs.LRUCache.} - */ -ol.render.canvas.TextReplay.labelCache_ = new ol.structs.LRUCache(); - - /** * @param {string} font Font to use for measuring. * @return {ol.Size} Measurement. @@ -324,7 +312,8 @@ ol.render.canvas.TextReplay.prototype.getImage = function(text, fill, stroke) { var label; var key = (stroke ? this.strokeKey_ : '') + this.textKey_ + text + (fill ? this.fillKey_ : ''); - if (!ol.render.canvas.TextReplay.labelCache_.containsKey(key)) { + var labelCache = ol.render.canvas.labelCache; + if (!labelCache.containsKey(key)) { var strokeState = this.textStrokeState_; var fillState = this.textFillState_; var textState = this.textState_; @@ -344,7 +333,7 @@ ol.render.canvas.TextReplay.prototype.getImage = function(text, fill, stroke) { Math.ceil(renderWidth * scale), Math.ceil((height + strokeWidth) * scale)); label = context.canvas; - ol.render.canvas.TextReplay.labelCache_.set(key, label); + labelCache.pruneAndSet(key, label); if (scale != 1) { context.scale(scale, scale); } @@ -379,7 +368,7 @@ ol.render.canvas.TextReplay.prototype.getImage = function(text, fill, stroke) { } } } - return ol.render.canvas.TextReplay.labelCache_.get(key); + return labelCache.get(key); }; @@ -537,6 +526,7 @@ ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle, declutt if (!textState) { textState = this.textState_ = /** @type {ol.CanvasTextState} */ ({}); } + ol.render.canvas.checkFont(font); textState.exceedLength = textStyle.getExceedLength(); textState.font = font; textState.maxAngle = textStyle.getMaxAngle(); diff --git a/src/ol/renderer/canvas/vectorlayer.js b/src/ol/renderer/canvas/vectorlayer.js index dc25ba9dac..ca702e15cd 100644 --- a/src/ol/renderer/canvas/vectorlayer.js +++ b/src/ol/renderer/canvas/vectorlayer.js @@ -4,6 +4,8 @@ goog.require('ol'); goog.require('ol.LayerType'); goog.require('ol.ViewHint'); goog.require('ol.dom'); +goog.require('ol.events'); +goog.require('ol.events.EventType'); goog.require('ol.ext.rbush'); goog.require('ol.extent'); goog.require('ol.render.EventType'); @@ -73,6 +75,8 @@ ol.renderer.canvas.VectorLayer = function(vectorLayer) { */ this.context_ = ol.dom.createCanvasContext2D(); + ol.events.listen(ol.render.canvas.labelCache, ol.events.EventType.CLEAR, this.handleFontsChanged_, this); + }; ol.inherits(ol.renderer.canvas.VectorLayer, ol.renderer.canvas.Layer); @@ -99,6 +103,15 @@ ol.renderer.canvas.VectorLayer['create'] = function(mapRenderer, layer) { }; +/** + * @inheritDoc + */ +ol.renderer.canvas.VectorLayer.prototype.disposeInternal = function() { + ol.events.unlisten(ol.render.canvas.labelCache, ol.events.EventType.CLEAR, this.handleFontsChanged_, this); + ol.renderer.canvas.Layer.prototype.disposeInternal.call(this); +}; + + /** * @inheritDoc */ @@ -259,6 +272,17 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c }; +/** + * @param {ol.events.Event} event Event. + */ +ol.renderer.canvas.VectorLayer.prototype.handleFontsChanged_ = function(event) { + var layer = this.getLayer(); + if (layer.getVisible() && this.replayGroup_) { + layer.changed(); + } +}; + + /** * Handle changes in image style state. * @param {ol.events.Event} event Image style change event. @@ -410,7 +434,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFeature = function(feature, resol loading = ol.renderer.vector.renderFeature( replayGroup, feature, styles, ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), - this.handleStyleImageChange_, this) || loading; + this.handleStyleImageChange_, this); } return loading; }; diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index cdf2186d18..3f5e6fc1ff 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -4,6 +4,8 @@ goog.require('ol'); goog.require('ol.LayerType'); goog.require('ol.TileState'); goog.require('ol.dom'); +goog.require('ol.events'); +goog.require('ol.events.EventType'); goog.require('ol.ext.rbush'); goog.require('ol.extent'); goog.require('ol.layer.VectorTileRenderType'); @@ -61,6 +63,9 @@ ol.renderer.canvas.VectorTileLayer = function(layer) { // Use lower resolution for pure vector rendering. Closest resolution otherwise. this.zDirection = layer.getRenderMode() == ol.layer.VectorTileRenderType.VECTOR ? 1 : 0; + + ol.events.listen(ol.render.canvas.labelCache, ol.events.EventType.CLEAR, this.handleFontsChanged_, this); + }; ol.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.TileLayer); @@ -109,6 +114,15 @@ ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS = { }; +/** + * @inheritDoc + */ +ol.renderer.canvas.VectorTileLayer.prototype.disposeInternal = function() { + ol.events.unlisten(ol.render.canvas.labelCache, ol.events.EventType.CLEAR, this.handleFontsChanged_, this); + ol.renderer.canvas.TileLayer.prototype.disposeInternal.call(this); +}; + + /** * @inheritDoc */ @@ -334,6 +348,17 @@ ol.renderer.canvas.VectorTileLayer.prototype.getReplayTransform_ = function(tile }; +/** + * @param {ol.events.Event} event Event. + */ +ol.renderer.canvas.VectorTileLayer.prototype.handleFontsChanged_ = function(event) { + var layer = this.getLayer(); + if (layer.getVisible() && this.renderedLayerRevision_ !== undefined) { + layer.changed(); + } +}; + + /** * Handle changes in image style state. * @param {ol.events.Event} event Image style change event. @@ -443,7 +468,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.renderFeature = function(feature, s } else { loading = ol.renderer.vector.renderFeature( replayGroup, feature, styles, squaredTolerance, - this.handleStyleImageChange_, this) || loading; + this.handleStyleImageChange_, this); } return loading; }; diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 13cd33f827..8fdf23a455 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -94,6 +94,7 @@ ol.renderer.vector.renderFeature = function( } ol.renderer.vector.renderFeature_(replayGroup, feature, style, squaredTolerance); + return loading; }; diff --git a/src/ol/structs/lrucache.js b/src/ol/structs/lrucache.js index 046e8f5adf..2743854de6 100644 --- a/src/ol/structs/lrucache.js +++ b/src/ol/structs/lrucache.js @@ -1,6 +1,9 @@ goog.provide('ol.structs.LRUCache'); +goog.require('ol'); goog.require('ol.asserts'); +goog.require('ol.events.EventTarget'); +goog.require('ol.events.EventType'); /** @@ -8,12 +11,16 @@ goog.require('ol.asserts'); * Object's properties (e.g. 'hasOwnProperty' is not allowed as a key). Expiring * items from the cache is the responsibility of the user. * @constructor + * @extends {ol.events.EventTarget} + * @fires ol.events.Event * @struct * @template T * @param {number=} opt_highWaterMark High water mark. */ ol.structs.LRUCache = function(opt_highWaterMark) { + ol.events.EventTarget.call(this); + /** * @type {number} */ @@ -45,6 +52,8 @@ ol.structs.LRUCache = function(opt_highWaterMark) { }; +ol.inherits(ol.structs.LRUCache, ol.events.EventTarget); + /** * @return {boolean} Can expire cache. @@ -62,6 +71,7 @@ ol.structs.LRUCache.prototype.clear = function() { this.entries_ = {}; this.oldest_ = null; this.newest_ = null; + this.dispatchEvent(ol.events.EventType.CLEAR); }; @@ -255,3 +265,15 @@ ol.structs.LRUCache.prototype.set = function(key, value) { this.entries_[key] = entry; ++this.count_; }; + + +/** + * @param {string} key Key. + * @param {T} value Value. + */ +ol.structs.LRUCache.prototype.pruneAndSet = function(key, value) { + while (this.canExpireCache()) { + this.pop(); + } + this.set(key, value); +}; diff --git a/test/spec/ol/render/canvas/index.test.js b/test/spec/ol/render/canvas/index.test.js index 1321dcb1d3..2e04220d57 100644 --- a/test/spec/ol/render/canvas/index.test.js +++ b/test/spec/ol/render/canvas/index.test.js @@ -1,10 +1,72 @@ - - +goog.require('ol.events'); +goog.require('ol.obj'); goog.require('ol.render.canvas'); describe('ol.render.canvas', function() { + var font = document.createElement('link'); + font.href = 'https://fonts.googleapis.com/css?family=Inconsolata'; + font.rel = 'stylesheet'; + var head = document.getElementsByTagName('head')[0]; + + describe('ol.render.canvas.checkFont()', function() { + + var checkFont = ol.render.canvas.checkFont; + + it('does not clear the label cache for unavailable fonts', function(done) { + ol.obj.clear(ol.render.canvas.checkedFonts_); + var spy = sinon.spy(); + ol.events.listen(ol.render.canvas.labelCache, 'clear', spy); + 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_); + var spy = sinon.spy(); + ol.events.listen(ol.render.canvas.labelCache, 'clear', spy); + 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_); + var spy = sinon.spy(); + ol.events.listen(ol.render.canvas.labelCache, 'clear', spy); + 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_); + head.appendChild(font); + var spy = sinon.spy(); + ol.events.listen(ol.render.canvas.labelCache, 'clear', spy); + checkFont('12px Inconsolata'); + setTimeout(function() { + ol.events.unlisten(ol.render.canvas.labelCache, 'clear', spy); + head.removeChild(font); + expect(spy.callCount).to.be(1); + done(); + }, 1600); + }); + + }); + + describe('rotateAtOffset', function() { it('rotates a canvas at an offset point', function() { var context = { diff --git a/test/spec/ol/renderer/canvas/vectorlayer.test.js b/test/spec/ol/renderer/canvas/vectorlayer.test.js index 1f66feaa77..1a2c88a162 100644 --- a/test/spec/ol/renderer/canvas/vectorlayer.test.js +++ b/test/spec/ol/renderer/canvas/vectorlayer.test.js @@ -7,7 +7,9 @@ goog.require('ol.View'); goog.require('ol.extent'); goog.require('ol.geom.Point'); goog.require('ol.layer.Vector'); +goog.require('ol.obj'); goog.require('ol.proj'); +goog.require('ol.render.canvas'); goog.require('ol.renderer.canvas.VectorLayer'); goog.require('ol.source.Vector'); goog.require('ol.style.Style'); @@ -18,6 +20,24 @@ describe('ol.renderer.canvas.VectorLayer', function() { describe('constructor', function() { + var head = document.getElementsByTagName('head')[0]; + var font = document.createElement('link'); + font.href = 'https://fonts.googleapis.com/css?family=Droid+Sans'; + font.rel = 'stylesheet'; + + var target; + + beforeEach(function() { + target = document.createElement('div'); + target.style.width = '256px'; + target.style.height = '256px'; + document.body.appendChild(target); + }); + + afterEach(function() { + document.body.removeChild(target); + }); + it('creates a new instance', function() { var layer = new ol.layer.Vector({ source: new ol.source.Vector() @@ -66,6 +86,101 @@ describe('ol.renderer.canvas.VectorLayer', function() { document.body.removeChild(target); }); + it('does not re-render for unavailable fonts', function(done) { + ol.obj.clear(ol.render.canvas.checkedFonts_); + var map = new ol.Map({ + view: new ol.View({ + center: [0, 0], + zoom: 0 + }), + target: target + }); + var layerStyle = new ol.style.Style({ + text: new ol.style.Text({ + text: 'layer', + font: '12px "Unavailable Font",sans-serif' + }) + }); + + var feature = new ol.Feature(new ol.geom.Point([0, 0])); + var layer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: [feature] + }), + style: layerStyle + }); + map.addLayer(layer); + var revision = layer.getRevision(); + setTimeout(function() { + expect(layer.getRevision()).to.be(revision); + done(); + }, 800); + }); + + it('does not re-render for available fonts', function(done) { + ol.obj.clear(ol.render.canvas.checkedFonts_); + var map = new ol.Map({ + view: new ol.View({ + center: [0, 0], + zoom: 0 + }), + target: target + }); + var layerStyle = new ol.style.Style({ + text: new ol.style.Text({ + text: 'layer', + font: '12px sans-serif' + }) + }); + + var feature = new ol.Feature(new ol.geom.Point([0, 0])); + var layer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: [feature] + }), + style: layerStyle + }); + map.addLayer(layer); + var revision = layer.getRevision(); + setTimeout(function() { + expect(layer.getRevision()).to.be(revision); + done(); + }, 800); + }); + + it('re-renders for fonts that become available', function(done) { + ol.obj.clear(ol.render.canvas.checkedFonts_); + head.appendChild(font); + var map = new ol.Map({ + view: new ol.View({ + center: [0, 0], + zoom: 0 + }), + target: target + }); + var layerStyle = new ol.style.Style({ + text: new ol.style.Text({ + text: 'layer', + font: '12px "Droid Sans",sans-serif' + }) + }); + + var feature = new ol.Feature(new ol.geom.Point([0, 0])); + var layer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: [feature] + }), + style: layerStyle + }); + map.addLayer(layer); + var revision = layer.getRevision(); + setTimeout(function() { + expect(layer.getRevision()).to.be(revision + 1); + head.removeChild(font); + done(); + }, 1600); + }); + }); describe('#forEachFeatureAtCoordinate', function() { diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js index a1e24ca89c..fcb87f6a9e 100644 --- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js +++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js @@ -1,6 +1,7 @@ goog.require('ol'); +goog.require('ol.obj'); goog.require('ol.Feature'); goog.require('ol.Map'); goog.require('ol.TileState'); @@ -13,6 +14,7 @@ goog.require('ol.geom.Point'); goog.require('ol.layer.VectorTile'); goog.require('ol.proj'); goog.require('ol.proj.Projection'); +goog.require('ol.render.canvas'); goog.require('ol.render.Feature'); goog.require('ol.renderer.canvas.VectorTileLayer'); goog.require('ol.source.VectorTile'); @@ -25,7 +27,12 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { describe('constructor', function() { - var map, layer, source, feature1, feature2, feature3, target, tileCallback; + var head = document.getElementsByTagName('head')[0]; + var font = document.createElement('link'); + font.href = 'https://fonts.googleapis.com/css?family=Dancing+Script'; + font.rel = 'stylesheet'; + + var map, layer, layerStyle, source, feature1, feature2, feature3, target, tileCallback; beforeEach(function() { tileCallback = function() {}; @@ -40,7 +47,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { }), target: target }); - var layerStyle = [new ol.style.Style({ + layerStyle = [new ol.style.Style({ text: new ol.style.Text({ text: 'layer' }) @@ -147,6 +154,44 @@ describe('ol.renderer.canvas.VectorTileLayer', function() { spy.restore(); }); + it('does not re-render for unavailable fonts', function(done) { + map.renderSync(); + ol.obj.clear(ol.render.canvas.checkedFonts_); + layerStyle[0].getText().setFont('12px "Unavailable font",sans-serif'); + layer.changed(); + var revision = layer.getRevision(); + setTimeout(function() { + expect(layer.getRevision()).to.be(revision); + done(); + }, 800); + }); + + it('does not re-render for available fonts', function(done) { + map.renderSync(); + ol.obj.clear(ol.render.canvas.checkedFonts_); + layerStyle[0].getText().setFont('12px sans-serif'); + layer.changed(); + var revision = layer.getRevision(); + setTimeout(function() { + expect(layer.getRevision()).to.be(revision); + done(); + }, 800); + }); + + it('re-renders for fonts that become available', function(done) { + map.renderSync(); + ol.obj.clear(ol.render.canvas.checkedFonts_); + head.appendChild(font); + layerStyle[0].getText().setFont('12px "Dancing Script",sans-serif'); + layer.changed(); + var revision = layer.getRevision(); + setTimeout(function() { + head.removeChild(font); + expect(layer.getRevision()).to.be(revision + 1); + done(); + }, 1600); + }); + it('transforms geometries when tile and view projection are different', function() { var tile; tileCallback = function(t) {