From a80458f2c30974c8c7999263a01a1a6bd71cea9f Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 10 Oct 2017 18:57:15 +0200 Subject: [PATCH 1/8] Show dynamic font loading in the ugly vector-labels example --- examples/vector-labels.html | 9 +++++---- examples/vector-labels.js | 8 ++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/examples/vector-labels.html b/examples/vector-labels.html index 872bbb8213..67b00a4020 100644 --- a/examples/vector-labels.html +++ b/examples/vector-labels.html @@ -6,7 +6,8 @@ docs: > This example showcases a number of options that can be set on text styles. When "Text/Wrap" is chosen (for example for the line features), the label is wrapped by inserting the character `\n`, which will create a multi-line - label. + label. The "Open Sans" web font will be loaded on demand, to show dynamic font + loading. tags: "geojson, vector, openstreetmap, label" ---
@@ -70,7 +71,7 @@ tags: "geojson, vector, openstreetmap, label"
@@ -179,7 +180,7 @@ tags: "geojson, vector, openstreetmap, label"
@@ -288,7 +289,7 @@ tags: "geojson, vector, openstreetmap, label"
diff --git a/examples/vector-labels.js b/examples/vector-labels.js index c11bf40825..4e0e28f81b 100644 --- a/examples/vector-labels.js +++ b/examples/vector-labels.js @@ -11,6 +11,7 @@ goog.require('ol.style.Stroke'); goog.require('ol.style.Style'); goog.require('ol.style.Text'); +var openSansAdded = false; var myDom = { points: { @@ -96,6 +97,13 @@ var createTextStyle = function(feature, resolution, dom) { var maxAngle = dom.maxangle ? parseFloat(dom.maxangle.value) : undefined; var exceedLength = dom.exceedlength ? (dom.exceedlength.value == 'true') : undefined; var rotation = parseFloat(dom.rotation.value); + if (dom.font.value == 'Open Sans' && !openSansAdded) { + var openSans = document.createElement('link'); + openSans.href = 'https://fonts.googleapis.com/css?family=Open+Sans'; + openSans.rel = 'stylesheet'; + document.getElementsByTagName('head')[0].appendChild(openSans); + openSansAdded = true; + } var font = weight + ' ' + size + ' ' + dom.font.value; var fillColor = dom.color.value; var outlineColor = dom.outline.value; From 06728ab0fae89b702e7f222c35a0aeec2715dcea Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Sat, 14 Oct 2017 12:30:27 -0600 Subject: [PATCH 2/8] Quote font names with spaces --- examples/vector-labels.html | 12 ++++++------ examples/vector-labels.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/vector-labels.html b/examples/vector-labels.html index 67b00a4020..69c6acf50f 100644 --- a/examples/vector-labels.html +++ b/examples/vector-labels.html @@ -70,8 +70,8 @@ tags: "geojson, vector, openstreetmap, label"
@@ -179,8 +179,8 @@ tags: "geojson, vector, openstreetmap, label"
@@ -288,8 +288,8 @@ tags: "geojson, vector, openstreetmap, label"
diff --git a/examples/vector-labels.js b/examples/vector-labels.js index 4e0e28f81b..75aded09a9 100644 --- a/examples/vector-labels.js +++ b/examples/vector-labels.js @@ -97,7 +97,7 @@ var createTextStyle = function(feature, resolution, dom) { var maxAngle = dom.maxangle ? parseFloat(dom.maxangle.value) : undefined; var exceedLength = dom.exceedlength ? (dom.exceedlength.value == 'true') : undefined; var rotation = parseFloat(dom.rotation.value); - if (dom.font.value == 'Open Sans' && !openSansAdded) { + if (dom.font.value == '\'Open Sans\'' && !openSansAdded) { var openSans = document.createElement('link'); openSans.href = 'https://fonts.googleapis.com/css?family=Open+Sans'; openSans.rel = 'stylesheet'; From dea8a340a66ba069f3baca9054dc23e017c97677 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Sat, 14 Oct 2017 13:17:28 -0600 Subject: [PATCH 3/8] Add utility method for extracting font families from a font spec --- src/ol/css.js | 27 +++++++++++++++++++++++ test/spec/ol/css.test.js | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/spec/ol/css.test.js diff --git a/src/ol/css.js b/src/ol/css.js index 915893ea34..c6582bec30 100644 --- a/src/ol/css.js +++ b/src/ol/css.js @@ -43,3 +43,30 @@ ol.css.CLASS_UNSUPPORTED = 'ol-unsupported'; * @type {string} */ ol.css.CLASS_CONTROL = 'ol-control'; + + +/** + * Get the list of font families from a font spec. Note that this doesn't work + * for font families that have commas in them. + * @param {string} The CSS font property. + * @return {Array.} The font families (or null if the input spec is invalid). + */ +ol.css.getFontFamilies = (function() { + var style; + var cache = {}; + return function(font) { + if (!style) { + style = document.createElement('div').style; + } + if (!(font in cache)) { + style.font = font; + var family = style.fontFamily; + style.font = ''; + if (!family) { + return null; + } + cache[font] = family.split(/,\s?/); + } + return cache[font]; + }; +})(); diff --git a/test/spec/ol/css.test.js b/test/spec/ol/css.test.js new file mode 100644 index 0000000000..cf5e40f18b --- /dev/null +++ b/test/spec/ol/css.test.js @@ -0,0 +1,47 @@ +goog.require('ol.css'); + +describe('ol.css', function() { + + describe('getFontFamilies()', function() { + var cases = [{ + font: '2em "Open Sans"', + families: ['"Open Sans"'] + }, { + font: '2em \'Open Sans\'', + families: ['"Open Sans"'] + }, { + font: '2em "Open Sans", sans-serif', + families: ['"Open Sans"', 'sans-serif'] + }, { + font: 'italic small-caps bolder 16px/3 cursive', + families: ['cursive'] + }, { + font: 'garbage 2px input', + families: null + }, { + font: '100% fantasy', + families: ['fantasy'] + }]; + + cases.forEach(function(c, i) { + it('works for ' + c.font, function() { + var families = ol.css.getFontFamilies(c.font); + if (c.families === null) { + expect(families).to.be(null); + return; + } + families.forEach(function(family, j) { + // Safari uses single quotes for font families, so we have to do extra work + if (family.charAt(0) === '\'') { + // 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 + families[j] = '"' + family.slice(1, -1) + '"'; + } + }); + expect(families).to.eql(c.families); + }); + }); + + }); + +}); From 7f865b85203c27d9858b0f6fadfa1cd69ba5138d Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 20 Oct 2017 00:02:20 +0200 Subject: [PATCH 4/8] 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) { From 8c46f6d1f02220a113c17f1ec0e35785dd53bbfa Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 20 Oct 2017 01:22:45 +0200 Subject: [PATCH 5/8] Workaround for a Safari issue --- src/ol/render/canvas.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 33b39c2db1..724630d83e 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -108,7 +108,7 @@ ol.render.canvas.checkFont = (function() { function isAvailable(fontFamily) { if (!context) { - context = ol.dom.createCanvasContext2D(); + context = ol.dom.createCanvasContext2D(1, 1); context.font = '32px monospace'; referenceWidth = context.measureText(text).width; } @@ -120,6 +120,10 @@ 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 = '32px monospace'; } return available; } From 5483fa318170378040b36933b7ba53330e12722c Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 20 Oct 2017 10:35:16 +0200 Subject: [PATCH 6/8] More efficient font checking loop --- src/ol/render/canvas.js | 42 +++++++++++++++++++++------------- src/ol/render/canvas/replay.js | 2 +- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 724630d83e..6f148597b7 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -91,7 +91,7 @@ ol.render.canvas.labelCache = new ol.structs.LRUCache(); /** - * @type {!Object.} + * @type {!Object.} */ ol.render.canvas.checkedFonts_ = {}; @@ -128,29 +128,39 @@ ol.render.canvas.checkFont = (function() { return available; } + function check() { + var done = true; + for (var font in checked) { + if (checked[font] < 60) { + if (isAvailable(font)) { + checked[font] = 60; + labelCache.clear(); + } else { + ++checked[font]; + done = false; + } + } + } + if (!done) { + window.setTimeout(check, 32); + } + } + return function(fontSpec) { var fontFamilies = ol.css.getFontFamilies(fontSpec); if (!fontFamilies) { return; } - fontFamilies.forEach(function(fontFamily) { - if (!checked[fontFamily]) { - checked[fontFamily] = true; + for (var i = 0, ii = fontFamilies.length; i < ii; ++i) { + var fontFamily = fontFamilies[i]; + if (!(fontFamily in checked)) { + checked[fontFamily] = 60; 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); + checked[fontFamily] = 0; + window.setTimeout(check, 25); } } - }); + } }; })(); diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 6d214409b0..15075c158e 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -596,7 +596,7 @@ ol.render.canvas.Replay.prototype.replay_ = function( chars = /** @type {string} */ (part[4]); label = /** @type {ol.render.canvas.TextReplay} */ (this).getImage(chars, false, true); anchorX = /** @type {number} */ (part[2]) + strokeWidth; - anchorY = baseline * label.height + (0.5 - baseline) * strokeWidth - offsetY; + anchorY = baseline * label.height + (0.5 - baseline) * 2 * strokeWidth - offsetY; this.replayImage_(context, /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, From 72eb8ab5e876eae4ec00d825b049178fb9a1a339 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 3 Nov 2017 13:05:50 +0100 Subject: [PATCH 7/8] Define font as variable --- src/ol/render/canvas.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 6f148597b7..d4b23ca122 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -103,13 +103,14 @@ ol.render.canvas.checkedFonts_ = {}; ol.render.canvas.checkFont = (function() { var checked = ol.render.canvas.checkedFonts_; var labelCache = ol.render.canvas.labelCache; + var font = '32px monospace'; var text = 'wmytzilWMYTZIL@#/&?$%10'; var context, referenceWidth; function isAvailable(fontFamily) { if (!context) { context = ol.dom.createCanvasContext2D(1, 1); - context.font = '32px monospace'; + context.font = font; referenceWidth = context.measureText(text).width; } var available = true; @@ -123,7 +124,7 @@ ol.render.canvas.checkFont = (function() { // 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 = '32px monospace'; + context.font = font; } return available; } From 129350d863905f8551914462850f5e6ea1992790 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 3 Nov 2017 13:54:41 +0100 Subject: [PATCH 8/8] Fix return type --- src/ol/css.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/css.js b/src/ol/css.js index c6582bec30..599bdacc4f 100644 --- a/src/ol/css.js +++ b/src/ol/css.js @@ -49,7 +49,7 @@ ol.css.CLASS_CONTROL = 'ol-control'; * Get the list of font families from a font spec. Note that this doesn't work * for font families that have commas in them. * @param {string} The CSS font property. - * @return {Array.} The font families (or null if the input spec is invalid). + * @return {Object.} The font families (or null if the input spec is invalid). */ ol.css.getFontFamilies = (function() { var style;