From 3626ff5b165eafdedb3512dd4b896ff500f223b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 13:58:20 +0100 Subject: [PATCH 01/12] Icon images are handled by a separate class --- src/ol/style/circlestyle.js | 51 +++++- src/ol/style/iconstyle.js | 304 +++++++++++++++++++++++++++--------- src/ol/style/imagestyle.js | 97 +++++------- 3 files changed, 315 insertions(+), 137 deletions(-) diff --git a/src/ol/style/circlestyle.js b/src/ol/style/circlestyle.js index 28ed96a17a..eb7d53cc13 100644 --- a/src/ol/style/circlestyle.js +++ b/src/ol/style/circlestyle.js @@ -56,12 +56,21 @@ ol.style.Circle = function(opt_options) { var size = this.render_(); + /** + * @private + * @type {Array.} + */ + this.anchor_ = [size / 2, size / 2]; + + /** + * @private + * @type {ol.Size} + */ + this.size_ = [size, size]; + goog.base(this, { - anchor: [size / 2, size / 2], - imageState: ol.style.ImageState.LOADED, rotation: 0, scale: 1, - size: [size, size], snapToPixel: undefined, subtractViewRotation: false }); @@ -70,6 +79,14 @@ ol.style.Circle = function(opt_options) { goog.inherits(ol.style.Circle, ol.style.Image); +/** + * @inheritDoc + */ +ol.style.Circle.prototype.getAnchor = function() { + return this.anchor_; +}; + + /** * @return {ol.style.Fill} Fill style. */ @@ -94,6 +111,14 @@ ol.style.Circle.prototype.getImage = function(pixelRatio) { }; +/** + * @inheritDoc + */ +ol.style.Circle.prototype.getImageState = function() { + return ol.style.ImageState.LOADED; +}; + + /** * @return {number} Radius. */ @@ -102,6 +127,14 @@ ol.style.Circle.prototype.getRadius = function() { }; +/** + * @inheritDoc + */ +ol.style.Circle.prototype.getSize = function() { + return this.size_; +}; + + /** * @return {ol.style.Stroke} Stroke style. */ @@ -110,12 +143,24 @@ ol.style.Circle.prototype.getStroke = function() { }; +/** + * @inheritDoc + */ +ol.style.Circle.prototype.listenImageChange = goog.nullFunction; + + /** * @inheritDoc */ ol.style.Circle.prototype.load = goog.nullFunction; +/** + * @inheritDoc + */ +ol.style.Circle.prototype.unlistenImageChange = goog.nullFunction; + + /** * @private * @return {number} Size. diff --git a/src/ol/style/iconstyle.js b/src/ol/style/iconstyle.js index 5cb250f9ef..827ed4bd39 100644 --- a/src/ol/style/iconstyle.js +++ b/src/ol/style/iconstyle.js @@ -8,6 +8,7 @@ goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.events'); +goog.require('goog.events.EventTarget'); goog.require('goog.events.EventType'); goog.require('ol.style.Image'); goog.require('ol.style.ImageState'); @@ -32,49 +33,11 @@ ol.style.Icon = function(opt_options) { var options = goog.isDef(opt_options) ? opt_options : {}; - /** - * @private - * @type {Image|HTMLCanvasElement} - */ - this.hitDetectionImage_ = null; - - /** - * @private - * @type {Image} - */ - this.image_ = new Image(); - - /** - * @type {?string} - */ - var crossOrigin = - goog.isDef(options.crossOrigin) ? options.crossOrigin : null; - if (!goog.isNull(crossOrigin)) { - this.image_.crossOrigin = crossOrigin; - } - /** * @private * @type {Array.} */ - this.imageListenerKeys_ = null; - - /** - * @private - * @type {string|undefined} - */ - this.src_ = options.src; - - /** - * @private - * @type {boolean} - */ - this.tainting_ = false; - - /** - * @type {ol.Size} - */ - var size = goog.isDef(options.size) ? options.size : null; + this.anchor_ = goog.isDef(options.anchor) ? options.anchor : [0.5, 0.5]; /** * @private @@ -91,9 +54,22 @@ ol.style.Icon = function(opt_options) { options.anchorYUnits : ol.style.IconAnchorUnits.FRACTION; /** - * @type {Array.} + * @type {?string} */ - var anchor = goog.isDef(options.anchor) ? options.anchor : [0.5, 0.5]; + var crossOrigin = + goog.isDef(options.crossOrigin) ? options.crossOrigin : null; + + /** + * @private + * @type {ol.style.IconImage_} + */ + this.iconImage_ = new ol.style.IconImage_(options.src, crossOrigin); + + /** + * @private + * @type {ol.Size} + */ + this.size_ = goog.isDef(options.size) ? options.size : null; /** * @type {number} @@ -106,11 +82,8 @@ ol.style.Icon = function(opt_options) { var scale = goog.isDef(options.scale) ? options.scale : 1; goog.base(this, { - anchor: anchor, - imageState: ol.style.ImageState.IDLE, rotation: rotation, scale: scale, - size: size, snapToPixel: undefined, subtractViewRotation: false }); @@ -120,9 +93,176 @@ goog.inherits(ol.style.Icon, ol.style.Image); /** + * @inheritDoc + */ +ol.style.Icon.prototype.getAnchor = function() { + var anchor = this.anchor_; + if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION || + this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) { + var size = this.getSize(); + if (goog.isNull(size)) { + return null; + } + anchor = [this.anchor_[0], this.anchor_[1]]; + if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION) { + anchor[0] *= size[0]; + } + if (this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) { + anchor[1] *= size[1]; + } + } + return anchor; +}; + + +/** + * @inheritDoc + */ +ol.style.Icon.prototype.getImage = function(pixelRatio) { + return this.iconImage_.getImage(pixelRatio); +}; + + +/** + * @inheritDoc + */ +ol.style.Icon.prototype.getImageState = function() { + return this.iconImage_.getImageState(); +}; + + +/** + * @inheritDoc + */ +ol.style.Icon.prototype.getHitDetectionImage = function(pixelRatio) { + return this.iconImage_.getHitDetectionImage(pixelRatio); +}; + + +/** + * @return {string|undefined} Image src. + */ +ol.style.Icon.prototype.getSrc = function() { + return this.iconImage_.getSrc(); +}; + + +/** + * @inheritDoc + */ +ol.style.Icon.prototype.getSize = function() { + return goog.isNull(this.size_) ? this.iconImage_.getSize() : this.size_; +}; + + +/** + * @inheritDoc + */ +ol.style.Icon.prototype.listenImageChange = function(listener, thisArg) { + return goog.events.listen(this.iconImage_, goog.events.EventType.CHANGE, + listener, false, thisArg); +}; + + +/** + * Load not yet loaded URI. + */ +ol.style.Icon.prototype.load = function() { + this.iconImage_.load(); +}; + + +/** + * @inheritDoc + */ +ol.style.Icon.prototype.unlistenImageChange = function(listener, thisArg) { + goog.events.unlisten(this.iconImage_, goog.events.EventType.CHANGE, + listener, false, thisArg); +}; + + + +/** + * @constructor + * @param {string} src Src. + * @param {?string} crossOrigin Cross origin. + * @extends {goog.events.EventTarget} * @private */ -ol.style.Icon.prototype.determineTainting_ = function() { +ol.style.IconImage_ = function(src, crossOrigin) { + + goog.base(this); + + /** + * @private + * @type {Image|HTMLCanvasElement} + */ + this.hitDetectionImage_ = null; + + /** + * @private + * @type {Image} + */ + this.image_ = new Image(); + + if (!goog.isNull(crossOrigin)) { + this.image_.crossOrigin = crossOrigin; + } + + /** + * @private + * @type {Array.} + */ + this.imageListenerKeys_ = null; + + /** + * @private + * @type {ol.style.ImageState} + */ + this.imageState_ = ol.style.ImageState.IDLE; + + /** + * @private + * @type {ol.Size} + */ + this.size_ = null; + + /** + * @private + * @type {string} + */ + this.src_ = src; + + /** + * @private + * @type {boolean} + */ + this.tainting_ = false; + +}; +goog.inherits(ol.style.IconImage_, goog.events.EventTarget); + + +/** + * @param {string} src Src. + * @param {?string} crossOrigin Cross origin. + * @return {ol.style.IconImage_} Icon image. + */ +ol.style.IconImage_.get = function(src, crossOrigin) { + var iconImageCache = ol.style.IconImageCache.getInstance(); + var iconImage = iconImageCache.get(src, crossOrigin); + if (goog.isNull(iconImage)) { + iconImage = new ol.style.IconImage_(src, crossOrigin); + iconImageCache.set(src, crossOrigin, iconImage); + } + return iconImage; +}; + + +/** + * @private + */ +ol.style.IconImage_.prototype.determineTainting_ = function() { var canvas = /** @type {HTMLCanvasElement} */ (goog.dom.createElement(goog.dom.TagName.CANVAS)); canvas.width = 1; @@ -141,51 +281,61 @@ ol.style.Icon.prototype.determineTainting_ = function() { /** * @private */ -ol.style.Icon.prototype.handleImageError_ = function() { - this.imageState = ol.style.ImageState.ERROR; - this.unlistenImage_(); - this.dispatchChangeEvent(); +ol.style.IconImage_.prototype.dispatchChangeEvent_ = function() { + this.dispatchEvent(goog.events.EventType.CHANGE); }; /** * @private */ -ol.style.Icon.prototype.handleImageLoad_ = function() { - this.imageState = ol.style.ImageState.LOADED; - if (goog.isNull(this.size)) { - this.size = [this.image_.width, this.image_.height]; - } - if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION) { - this.anchor[0] = this.size[0] * this.anchor[0]; - } - if (this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) { - this.anchor[1] = this.size[1] * this.anchor[1]; - } +ol.style.IconImage_.prototype.handleImageError_ = function() { + this.imageState_ = ol.style.ImageState.ERROR; this.unlistenImage_(); - this.determineTainting_(); - this.dispatchChangeEvent(); + this.dispatchChangeEvent_(); }; /** - * @inheritDoc + * @private */ -ol.style.Icon.prototype.getImage = function(pixelRatio) { +ol.style.IconImage_.prototype.handleImageLoad_ = function() { + this.imageState_ = ol.style.ImageState.LOADED; + this.size_ = [this.image_.width, this.image_.height]; + this.unlistenImage_(); + this.determineTainting_(); + this.dispatchChangeEvent_(); +}; + + +/** + * @param {number} pixelRatio Pixel ratio. + * @return {Image} Image element. + */ +ol.style.IconImage_.prototype.getImage = function(pixelRatio) { return this.image_; }; /** - * @inheritDoc + * @return {ol.style.ImageState} Image state. */ -ol.style.Icon.prototype.getHitDetectionImage = function(pixelRatio) { +ol.style.IconImage_.prototype.getImageState = function() { + return this.imageState_; +}; + + +/** + * @param {number} pixelRatio Pixel ratio. + * @return {Image|HTMLCanvasElement} Image element. + */ +ol.style.IconImage_.prototype.getHitDetectionImage = function(pixelRatio) { if (goog.isNull(this.hitDetectionImage_)) { if (this.tainting_) { var canvas = /** @type {HTMLCanvasElement} */ (goog.dom.createElement(goog.dom.TagName.CANVAS)); - var width = this.size[0]; - var height = this.size[1]; + var width = this.size_[0]; + var height = this.size_[1]; canvas.width = width; canvas.height = height; var context = /** @type {CanvasRenderingContext2D} */ @@ -200,10 +350,18 @@ ol.style.Icon.prototype.getHitDetectionImage = function(pixelRatio) { }; +/** + * @return {ol.Size} Image size. + */ +ol.style.IconImage_.prototype.getSize = function() { + return this.size_; +}; + + /** * @return {string|undefined} Image src. */ -ol.style.Icon.prototype.getSrc = function() { +ol.style.IconImage_.prototype.getSrc = function() { return this.src_; }; @@ -211,11 +369,11 @@ ol.style.Icon.prototype.getSrc = function() { /** * Load not yet loaded URI. */ -ol.style.Icon.prototype.load = function() { - if (this.imageState == ol.style.ImageState.IDLE) { +ol.style.IconImage_.prototype.load = function() { + if (this.imageState_ == ol.style.ImageState.IDLE) { goog.asserts.assert(goog.isDef(this.src_)); goog.asserts.assert(goog.isNull(this.imageListenerKeys_)); - this.imageState = ol.style.ImageState.LOADING; + this.imageState_ = ol.style.ImageState.LOADING; this.imageListenerKeys_ = [ goog.events.listenOnce(this.image_, goog.events.EventType.ERROR, this.handleImageError_, false, this), @@ -232,7 +390,7 @@ ol.style.Icon.prototype.load = function() { * * @private */ -ol.style.Icon.prototype.unlistenImage_ = function() { +ol.style.IconImage_.prototype.unlistenImage_ = function() { goog.asserts.assert(!goog.isNull(this.imageListenerKeys_)); goog.array.forEach(this.imageListenerKeys_, goog.events.unlistenByKey); this.imageListenerKeys_ = null; diff --git a/src/ol/style/imagestyle.js b/src/ol/style/imagestyle.js index 6592f81313..9063d65f99 100644 --- a/src/ol/style/imagestyle.js +++ b/src/ol/style/imagestyle.js @@ -2,9 +2,6 @@ goog.provide('ol.style.Image'); goog.provide('ol.style.ImageState'); goog.require('goog.array'); -goog.require('goog.events'); -goog.require('goog.events.EventTarget'); -goog.require('goog.events.EventType'); /** @@ -19,11 +16,8 @@ ol.style.ImageState = { /** - * @typedef {{anchor: Array., - * imageState: ol.style.ImageState, - * rotation: number, + * @typedef {{rotation: number, * scale: number, - * size: ol.Size, * snapToPixel: (boolean|undefined), * subtractViewRotation: boolean}} */ @@ -34,24 +28,9 @@ ol.style.ImageOptions; /** * @constructor * @param {ol.style.ImageOptions} options Options. - * @extends {goog.events.EventTarget} */ ol.style.Image = function(options) { - goog.base(this); - - /** - * @protected - * @type {Array.} - */ - this.anchor = options.anchor; - - /** - * @protected - * @type {ol.style.ImageState} - */ - this.imageState = options.imageState; - /** * @private * @type {number} @@ -64,12 +43,6 @@ ol.style.Image = function(options) { */ this.scale_ = options.scale; - /** - * @protected - * @type {ol.Size} - */ - this.size = options.size; - /** * @private * @type {boolean|undefined} @@ -83,31 +56,6 @@ ol.style.Image = function(options) { this.subtractViewRotation_ = options.subtractViewRotation; }; -goog.inherits(ol.style.Image, goog.events.EventTarget); - - -/** - * @protected - */ -ol.style.Image.prototype.dispatchChangeEvent = function() { - this.dispatchEvent(goog.events.EventType.CHANGE); -}; - - -/** - * @return {Array.} Anchor. - */ -ol.style.Image.prototype.getAnchor = function() { - return this.anchor; -}; - - -/** - * @return {ol.style.ImageState} Image state. - */ -ol.style.Image.prototype.getImageState = function() { - return this.imageState; -}; /** @@ -126,14 +74,6 @@ ol.style.Image.prototype.getScale = function() { }; -/** - * @return {ol.Size} Size. - */ -ol.style.Image.prototype.getSize = function() { - return this.size; -}; - - /** * @return {boolean|undefined} Snap to pixel? */ @@ -150,6 +90,12 @@ ol.style.Image.prototype.getSubtractViewRotation = function() { }; +/** + * @return {Array.} Anchor. + */ +ol.style.Image.prototype.getAnchor = goog.abstractMethod; + + /** * @param {number} pixelRatio Pixel ratio. * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element. @@ -157,6 +103,12 @@ ol.style.Image.prototype.getSubtractViewRotation = function() { ol.style.Image.prototype.getImage = goog.abstractMethod; +/** + * @return {ol.style.ImageState} Image state. + */ +ol.style.Image.prototype.getImageState = goog.abstractMethod; + + /** * @param {number} pixelRatio Pixel ratio. * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element. @@ -164,7 +116,30 @@ ol.style.Image.prototype.getImage = goog.abstractMethod; ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod; +/** + * @return {ol.Size} Size. + */ +ol.style.Image.prototype.getSize = goog.abstractMethod; + + +/** + * @param {function(this: T, goog.events.Event)} listener Listener function. + * @param {T} thisArg Value to use as `this` when executing `listener`. + * @return {goog.events.Key|undefined} Listener key. + * @template T + */ +ol.style.Image.prototype.listenImageChange = goog.abstractMethod; + + /** * Load not yet loaded URI. */ ol.style.Image.prototype.load = goog.abstractMethod; + + +/** + * @param {function(this: T, goog.events.Event)} listener Listener function. + * @param {T} thisArg Value to use as `this` when executing `listener`. + * @template T + */ +ol.style.Image.prototype.unlistenImageChange = goog.abstractMethod; From ae2b3359f8ec6cc1ab66b94bcdd39b4e3eafbd0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Tue, 4 Feb 2014 14:16:16 +0100 Subject: [PATCH 02/12] Fix the kml format tests --- test/spec/ol/format/kmlformat.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/ol/format/kmlformat.test.js b/test/spec/ol/format/kmlformat.test.js index 3fb57ec856..141f0c0f19 100644 --- a/test/spec/ol/format/kmlformat.test.js +++ b/test/spec/ol/format/kmlformat.test.js @@ -588,7 +588,7 @@ describe('ol.format.KML', function() { var imageStyle = style.getImage(); expect(imageStyle).to.be.an(ol.style.Icon); expect(imageStyle.getSrc()).to.eql('http://foo.png'); - expect(imageStyle.getAnchor()).to.eql([0.5, 0.5]); + expect(imageStyle.getAnchor()).to.be(null); expect(imageStyle.getRotation()).to.eql(0); expect(imageStyle.getSize()).to.be(null); expect(style.getText()).to.be(null); From 78aef2f58c8da08a216397072ffdbc435f14e578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 14:03:07 +0100 Subject: [PATCH 03/12] Vector renderer uses new image style interface --- .../canvas/canvasvectorlayerrenderer.js | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 45fcf8b1fd..962c81320a 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -2,7 +2,6 @@ goog.provide('ol.renderer.canvas.VectorLayer'); goog.require('goog.asserts'); goog.require('goog.events'); -goog.require('goog.events.EventType'); goog.require('goog.functions'); goog.require('ol.ViewHint'); goog.require('ol.extent'); @@ -157,12 +156,9 @@ ol.renderer.canvas.VectorLayer.prototype.getRenderGeometryFunction_ = * @param {goog.events.Event} event Image style change event. * @private */ -ol.renderer.canvas.VectorLayer.prototype.handleImageStyleChange_ = +ol.renderer.canvas.VectorLayer.prototype.handleImageChange_ = function(event) { - var imageStyle = /** @type {ol.style.Image} */ (event.target); - if (imageStyle.getImageState() == ol.style.ImageState.LOADED) { - this.renderIfReadyAndVisible(); - } + this.renderIfReadyAndVisible(); }; @@ -262,21 +258,27 @@ ol.renderer.canvas.VectorLayer.prototype.renderFeature = for (i = 0, ii = styles.length; i < ii; ++i) { style = styles[i]; imageStyle = style.getImage(); - if (!goog.isNull(imageStyle)) { - if (imageStyle.getImageState() == ol.style.ImageState.IDLE) { - goog.events.listenOnce(imageStyle, goog.events.EventType.CHANGE, - this.handleImageStyleChange_, false, this); - imageStyle.load(); - } else if (imageStyle.getImageState() == ol.style.ImageState.LOADED) { - ol.renderer.vector.renderFeature( - replayGroup, feature, style, squaredTolerance, feature); - } - goog.asserts.assert( - imageStyle.getImageState() != ol.style.ImageState.IDLE); - loading = imageStyle.getImageState() == ol.style.ImageState.LOADING; - } else { + if (goog.isNull(imageStyle)) { ol.renderer.vector.renderFeature( replayGroup, feature, style, squaredTolerance, feature); + } else { + imageState = imageStyle.getImageState(); + if (imageState == ol.style.ImageState.LOADED || + imageState == ol.style.ImageState.ERROR) { + imageStyle.unlistenImageChange(this.handleImageChange_, this); + if (imageState == ol.style.ImageState.LOADED) { + ol.renderer.vector.renderFeature( + replayGroup, feature, style, squaredTolerance, feature); + } + } else { + if (imageState == ol.style.ImageState.IDLE) { + imageStyle.load(); + } + imageState = imageStyle.getImageState(); + goog.asserts.assert(imageState == ol.style.ImageState.LOADING); + imageStyle.listenImageChange(this.handleImageChange_, this); + loading = true; + } } } return loading; From bf8520096e784813b2160dbb94526a984f402d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 14:03:35 +0100 Subject: [PATCH 04/12] Vector image source uses new image style interface --- src/ol/source/imagevectorsource.js | 39 ++++++++++++++++-------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/ol/source/imagevectorsource.js b/src/ol/source/imagevectorsource.js index 7b7cd876dc..79759757ae 100644 --- a/src/ol/source/imagevectorsource.js +++ b/src/ol/source/imagevectorsource.js @@ -194,12 +194,9 @@ ol.source.ImageVector.prototype.getTransform_ = * @param {goog.events.Event} event Image style change event. * @private */ -ol.source.ImageVector.prototype.handleImageStyleChange_ = +ol.source.ImageVector.prototype.handleImageChange_ = function(event) { - var imageStyle = /** @type {ol.style.Image} */ (event.target); - if (imageStyle.getImageState() == ol.style.ImageState.LOADED) { - this.dispatchChangeEvent(); - } + this.dispatchChangeEvent(); }; @@ -235,21 +232,27 @@ ol.source.ImageVector.prototype.renderFeature_ = for (i = 0, ii = styles.length; i < ii; ++i) { style = styles[i]; imageStyle = style.getImage(); - if (!goog.isNull(imageStyle)) { - if (imageStyle.getImageState() == ol.style.ImageState.IDLE) { - goog.events.listenOnce(imageStyle, goog.events.EventType.CHANGE, - this.handleImageStyleChange_, false, this); - imageStyle.load(); - } else if (imageStyle.getImageState() == ol.style.ImageState.LOADED) { - ol.renderer.vector.renderFeature( - replayGroup, feature, style, squaredTolerance, feature); - } - goog.asserts.assert( - imageStyle.getImageState() != ol.style.ImageState.IDLE); - loading = imageStyle.getImageState() == ol.style.ImageState.LOADING; - } else { + if (goog.isNull(imageStyle)) { ol.renderer.vector.renderFeature( replayGroup, feature, style, squaredTolerance, feature); + } else { + imageState = imageStyle.getImageState(); + if (imageState == ol.style.ImageState.LOADED || + imageState == ol.style.ImageState.ERROR) { + imageStyle.unlistenImageChange(this.handleImageChange_, this); + if (imageState == ol.style.ImageState.LOADED) { + ol.renderer.vector.renderFeature( + replayGroup, feature, style, squaredTolerance, feature); + } + } else { + if (imageState == ol.style.ImageState.IDLE) { + imageStyle.load(); + } + imageState = imageStyle.getImageState(); + goog.asserts.assert(imageState == ol.style.ImageState.LOADING); + imageStyle.listenImageChange(this.handleImageChange_, this); + loading = true; + } } } return loading; From 065663b2429a2f03f1fbb3311d62b717b6cbfa69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 14:21:59 +0100 Subject: [PATCH 05/12] Introduce an icon image cache --- src/ol/style/iconstyle.js | 81 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/ol/style/iconstyle.js b/src/ol/style/iconstyle.js index 827ed4bd39..c04b9a9bd8 100644 --- a/src/ol/style/iconstyle.js +++ b/src/ol/style/iconstyle.js @@ -2,6 +2,7 @@ goog.provide('ol.style.Icon'); goog.provide('ol.style.IconAnchorUnits'); +goog.provide('ol.style.IconImageCache'); goog.require('goog.array'); goog.require('goog.asserts'); @@ -395,3 +396,83 @@ ol.style.IconImage_.prototype.unlistenImage_ = function() { goog.array.forEach(this.imageListenerKeys_, goog.events.unlistenByKey); this.imageListenerKeys_ = null; }; + + + +/** + * @constructor + */ +ol.style.IconImageCache = function() { + + /** + * @type {Object.} + * @private + */ + this.cache_ = {}; + + /** + * @type {number} + * @private + */ + this.cacheSize_ = 0; + + /** + * @const + * @type {number} + * @private + */ + this.maxCacheSize_ = 32; +}; +goog.addSingletonGetter(ol.style.IconImageCache); + + +/** + * @param {string} src Src. + * @param {?string} crossOrigin Cross origin. + * @return {string} Cache key. + */ +ol.style.IconImageCache.getKey = function(src, crossOrigin) { + goog.asserts.assert(goog.isDef(crossOrigin)); + return crossOrigin + ':' + src; +}; + + +/** + * FIXME empty description for jsdoc + */ +ol.style.IconImageCache.prototype.expire = function() { + if (this.cacheSize_ > this.maxCacheSize_) { + var i = 0; + var key, iconImage; + for (key in this.cache_) { + iconImage = this.cache_[key]; + if ((i++ & 3) === 0 && !goog.events.hasListener(iconImage)) { + delete this.cache_[key]; + --this.cacheSize_; + } + } + } +}; + + +/** + * @param {string} src Src. + * @param {?string} crossOrigin Cross origin. + * @return {ol.style.IconImage_} Icon image. + */ +ol.style.IconImageCache.prototype.get = function(src, crossOrigin) { + var key = ol.style.IconImageCache.getKey(src, crossOrigin); + return key in this.cache_ ? this.cache_[key] : null; +}; + + +/** + * @param {string} src Src. + * @param {?string} crossOrigin Cross origin. + * @param {ol.style.IconImage_} iconImage Icon image. + */ +ol.style.IconImageCache.prototype.set = function(src, crossOrigin, iconImage) { + var key = ol.style.IconImageCache.getKey(src, crossOrigin); + this.cache_[key] = iconImage; + ++this.cacheSize_; +}; From c2fe25ee2608ae36690082d027472fa19b53aca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 14:22:30 +0100 Subject: [PATCH 06/12] Use the icon image cache --- src/ol/style/iconstyle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/style/iconstyle.js b/src/ol/style/iconstyle.js index c04b9a9bd8..041daefb4b 100644 --- a/src/ol/style/iconstyle.js +++ b/src/ol/style/iconstyle.js @@ -64,7 +64,7 @@ ol.style.Icon = function(opt_options) { * @private * @type {ol.style.IconImage_} */ - this.iconImage_ = new ol.style.IconImage_(options.src, crossOrigin); + this.iconImage_ = ol.style.IconImage_.get(options.src, crossOrigin); /** * @private From b43dd6db44739f501bc3d0526c65913fa0e1d5c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 14:23:08 +0100 Subject: [PATCH 07/12] Add ol.renderer.Map#scheduleExpireIconCache --- src/ol/renderer/maprenderer.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index dab1d58fdf..519ac52a65 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -8,6 +8,7 @@ goog.require('goog.vec.Mat4'); goog.require('ol.FrameState'); goog.require('ol.layer.Layer'); goog.require('ol.renderer.Layer'); +goog.require('ol.style.IconImageCache'); goog.require('ol.vec.Mat4'); @@ -195,6 +196,22 @@ ol.renderer.Map.prototype.removeUnusedLayerRenderers_ = }; +/** + * @param {ol.FrameState} frameState Frame state. + * @protected + */ +ol.renderer.Map.prototype.scheduleExpireIconCache = function(frameState) { + frameState.postRenderFunctions.push( + /** + * @param {ol.Map} map Map. + * @param {ol.FrameState} frameState Frame state. + */ + function(map, frameState) { + ol.style.IconImageCache.getInstance().expire(); + }); +}; + + /** * @param {!ol.FrameState} frameState Frame state. * @protected From f9e04ad7d5141c49a8154800628e89c6644df306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 14:23:31 +0100 Subject: [PATCH 08/12] Use ol.renderer.Map#scheduleExpireIconCache --- src/ol/renderer/canvas/canvasmaprenderer.js | 2 +- src/ol/renderer/dom/dommaprenderer.js | 1 + src/ol/renderer/webgl/webglmaprenderer.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index bd9c0fcfda..5e9469a002 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -179,5 +179,5 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { } this.scheduleRemoveUnusedLayerRenderers(frameState); - + this.scheduleExpireIconCache(frameState); }; diff --git a/src/ol/renderer/dom/dommaprenderer.js b/src/ol/renderer/dom/dommaprenderer.js index 42913d4b94..e680a02fd2 100644 --- a/src/ol/renderer/dom/dommaprenderer.js +++ b/src/ol/renderer/dom/dommaprenderer.js @@ -107,5 +107,6 @@ ol.renderer.dom.Map.prototype.renderFrame = function(frameState) { this.calculateMatrices2D(frameState); this.scheduleRemoveUnusedLayerRenderers(frameState); + this.scheduleExpireIconCache(frameState); }; diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 25939e6b1f..9eba422d21 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -517,5 +517,6 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { this.dispatchComposeEvent_(ol.render.EventType.POSTCOMPOSE, frameState); this.scheduleRemoveUnusedLayerRenderers(frameState); + this.scheduleExpireIconCache(frameState); }; From 63aadc5c0fd743266be29adf738e2661c027ff85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 14:46:45 +0100 Subject: [PATCH 09/12] Factor out icon image loading code --- src/ol/render/vector.js | 44 +++++++++++++++++++ .../canvas/canvasvectorlayerrenderer.js | 31 ++----------- src/ol/source/imagevectorsource.js | 31 ++----------- 3 files changed, 52 insertions(+), 54 deletions(-) diff --git a/src/ol/render/vector.js b/src/ol/render/vector.js index 40bc524d94..b78a8cd324 100644 --- a/src/ol/render/vector.js +++ b/src/ol/render/vector.js @@ -10,6 +10,7 @@ goog.require('ol.geom.MultiPolygon'); goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); goog.require('ol.render.IReplayGroup'); +goog.require('ol.style.ImageState'); goog.require('ol.style.Style'); @@ -47,8 +48,51 @@ ol.renderer.vector.renderCircleGeometry_ = * @param {ol.style.Style} style Style. * @param {number} squaredTolerance Squared tolerance. * @param {Object} data Opaque data object. + * @param {function(this: T, goog.events.Event)} listener Listener function. + * @param {T} thisArg Value to use as `this` when executing `listener`. + * @return {boolean} `true` if style is loading. + * @template T */ ol.renderer.vector.renderFeature = function( + replayGroup, feature, style, squaredTolerance, data, listener, thisArg) { + var loading = false; + var imageStyle, imageState; + imageStyle = style.getImage(); + if (goog.isNull(imageStyle)) { + ol.renderer.vector.renderFeature_( + replayGroup, feature, style, squaredTolerance, data); + } else { + imageState = imageStyle.getImageState(); + if (imageState == ol.style.ImageState.LOADED || + imageState == ol.style.ImageState.ERROR) { + imageStyle.unlistenImageChange(listener, thisArg); + if (imageState == ol.style.ImageState.LOADED) { + ol.renderer.vector.renderFeature_( + replayGroup, feature, style, squaredTolerance, data); + } + } else { + if (imageState == ol.style.ImageState.IDLE) { + imageStyle.load(); + } + imageState = imageStyle.getImageState(); + goog.asserts.assert(imageState == ol.style.ImageState.LOADING); + imageStyle.listenImageChange(listener, thisArg); + loading = true; + } + } + return loading; +}; + + +/** + * @param {ol.render.IReplayGroup} replayGroup Replay group. + * @param {ol.Feature} feature Feature. + * @param {ol.style.Style} style Style. + * @param {number} squaredTolerance Squared tolerance. + * @param {Object} data Opaque data object. + * @private + */ +ol.renderer.vector.renderFeature_ = function( replayGroup, feature, style, squaredTolerance, data) { var geometry = feature.getGeometry(); if (goog.isNull(geometry)) { diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 962c81320a..0ad1f0ac4c 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -11,7 +11,6 @@ goog.require('ol.render.canvas.ReplayGroup'); goog.require('ol.renderer.canvas.Layer'); goog.require('ol.renderer.vector'); goog.require('ol.source.Vector'); -goog.require('ol.style.ImageState'); @@ -245,7 +244,6 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = */ ol.renderer.canvas.VectorLayer.prototype.renderFeature = function(feature, resolution, pixelRatio, styleFunction, replayGroup) { - var loading = false; var styles = styleFunction(feature, resolution); // FIXME if styles is null, should we use the default style? if (!goog.isDefAndNotNull(styles)) { @@ -254,32 +252,11 @@ ol.renderer.canvas.VectorLayer.prototype.renderFeature = // simplify to a tolerance of half a device pixel var squaredTolerance = resolution * resolution / (4 * pixelRatio * pixelRatio); - var i, ii, style, imageStyle, imageState; + var i, ii, loading = false; for (i = 0, ii = styles.length; i < ii; ++i) { - style = styles[i]; - imageStyle = style.getImage(); - if (goog.isNull(imageStyle)) { - ol.renderer.vector.renderFeature( - replayGroup, feature, style, squaredTolerance, feature); - } else { - imageState = imageStyle.getImageState(); - if (imageState == ol.style.ImageState.LOADED || - imageState == ol.style.ImageState.ERROR) { - imageStyle.unlistenImageChange(this.handleImageChange_, this); - if (imageState == ol.style.ImageState.LOADED) { - ol.renderer.vector.renderFeature( - replayGroup, feature, style, squaredTolerance, feature); - } - } else { - if (imageState == ol.style.ImageState.IDLE) { - imageStyle.load(); - } - imageState = imageStyle.getImageState(); - goog.asserts.assert(imageState == ol.style.ImageState.LOADING); - imageStyle.listenImageChange(this.handleImageChange_, this); - loading = true; - } - } + loading = ol.renderer.vector.renderFeature( + replayGroup, feature, styles[i], squaredTolerance, feature, + this.handleImageChange_, this) || loading; } return loading; }; diff --git a/src/ol/source/imagevectorsource.js b/src/ol/source/imagevectorsource.js index 79759757ae..97d84e32e6 100644 --- a/src/ol/source/imagevectorsource.js +++ b/src/ol/source/imagevectorsource.js @@ -12,7 +12,6 @@ goog.require('ol.render.canvas.ReplayGroup'); goog.require('ol.renderer.vector'); goog.require('ol.source.ImageCanvas'); goog.require('ol.source.Vector'); -goog.require('ol.style.ImageState'); goog.require('ol.vec.Mat4'); @@ -220,7 +219,6 @@ ol.source.ImageVector.prototype.handleSourceChange_ = function() { */ ol.source.ImageVector.prototype.renderFeature_ = function(feature, resolution, pixelRatio, replayGroup) { - var loading = false; var styles = this.styleFunction_(feature, resolution); if (!goog.isDefAndNotNull(styles)) { return false; @@ -228,32 +226,11 @@ ol.source.ImageVector.prototype.renderFeature_ = // simplify to a tolerance of half a device pixel var squaredTolerance = resolution * resolution / (4 * pixelRatio * pixelRatio); - var i, ii, style, imageStyle, imageState; + var i, ii, loading = false; for (i = 0, ii = styles.length; i < ii; ++i) { - style = styles[i]; - imageStyle = style.getImage(); - if (goog.isNull(imageStyle)) { - ol.renderer.vector.renderFeature( - replayGroup, feature, style, squaredTolerance, feature); - } else { - imageState = imageStyle.getImageState(); - if (imageState == ol.style.ImageState.LOADED || - imageState == ol.style.ImageState.ERROR) { - imageStyle.unlistenImageChange(this.handleImageChange_, this); - if (imageState == ol.style.ImageState.LOADED) { - ol.renderer.vector.renderFeature( - replayGroup, feature, style, squaredTolerance, feature); - } - } else { - if (imageState == ol.style.ImageState.IDLE) { - imageStyle.load(); - } - imageState = imageStyle.getImageState(); - goog.asserts.assert(imageState == ol.style.ImageState.LOADING); - imageStyle.listenImageChange(this.handleImageChange_, this); - loading = true; - } - } + loading = ol.renderer.vector.renderFeature( + replayGroup, feature, styles[i], squaredTolerance, feature, + this.handleImageChange_, this) || loading; } return loading; }; From d13d2fdc6ab0d73c021ef94b3952dde681e0554e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Mon, 3 Feb 2014 17:21:11 +0100 Subject: [PATCH 10/12] Remove a FIXME --- src/ol/renderer/canvas/canvasvectorlayerrenderer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 0ad1f0ac4c..1421d5a93a 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -245,7 +245,6 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = ol.renderer.canvas.VectorLayer.prototype.renderFeature = function(feature, resolution, pixelRatio, styleFunction, replayGroup) { var styles = styleFunction(feature, resolution); - // FIXME if styles is null, should we use the default style? if (!goog.isDefAndNotNull(styles)) { return false; } From 90c41523a2b8dc8207bb6eb8dd3e48ba136f6882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Tue, 4 Feb 2014 15:23:26 +0100 Subject: [PATCH 11/12] Add tests for ol.renderer.vector --- test/spec/ol/render/vector.test.js | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 test/spec/ol/render/vector.test.js diff --git a/test/spec/ol/render/vector.test.js b/test/spec/ol/render/vector.test.js new file mode 100644 index 0000000000..a8715a39d8 --- /dev/null +++ b/test/spec/ol/render/vector.test.js @@ -0,0 +1,65 @@ +goog.provide('ol.test.renderer.vector'); + +describe('ol.renderer.vector', function() { + describe('#renderFeature', function() { + var replayGroup; + + beforeEach(function() { + replayGroup = new ol.render.canvas.ReplayGroup(1); + }); + + describe('call multiple times', function() { + + it('does not set multiple listeners', function() { + var iconStyle = new ol.style.Icon({ + src: 'http://example.com/icon.png' + }); + + var iconImage = iconStyle.iconImage_; + + var iconStyleLoadSpy = sinon.stub(iconStyle, 'load', function() { + iconImage.imageState_ = ol.style.ImageState.LOADING; + }); + + var style = new ol.style.Style({ + image: iconStyle + }); + + var feature = new ol.Feature(); + + var listener = function() {}; + var listenerThis = {}; + var listeners; + + // call #1 + ol.renderer.vector.renderFeature(replayGroup, feature, + style, 1, feature, listener, listenerThis); + + expect(iconStyleLoadSpy.calledOnce).to.be.ok(); + listeners = goog.events.getListeners( + iconStyle.iconImage_, goog.events.EventType.CHANGE, false); + expect(listeners.length).to.eql(1); + + // call #2 + ol.renderer.vector.renderFeature(replayGroup, feature, + style, 1, feature, listener, listenerThis); + + expect(iconStyleLoadSpy.calledOnce).to.be.ok(); + listeners = goog.events.getListeners( + iconStyle.iconImage_, goog.events.EventType.CHANGE, false); + expect(listeners.length).to.eql(1); + }); + + }); + + }); +}); + +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('ol.render.canvas.ReplayGroup'); +goog.require('ol.renderer.vector'); +goog.require('ol.style.Icon'); +goog.require('ol.style.ImageState'); +goog.require('ol.style.Style'); +goog.require('ol.Feature'); From c2d0cab07ad0852c91bc913d9185d8ab538bd38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Lemoine?= Date: Tue, 4 Feb 2014 15:24:26 +0100 Subject: [PATCH 12/12] Add tests for ol.style.IconImageCache --- src/ol/style/iconstyle.js | 9 ++++ test/spec/ol/style/iconstyle.test.js | 71 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 test/spec/ol/style/iconstyle.test.js diff --git a/src/ol/style/iconstyle.js b/src/ol/style/iconstyle.js index 041daefb4b..6af27dd27a 100644 --- a/src/ol/style/iconstyle.js +++ b/src/ol/style/iconstyle.js @@ -437,6 +437,15 @@ ol.style.IconImageCache.getKey = function(src, crossOrigin) { }; +/** + * FIXME empty description for jsdoc + */ +ol.style.IconImageCache.prototype.clear = function() { + this.cache_ = {}; + this.cacheSize_ = 0; +}; + + /** * FIXME empty description for jsdoc */ diff --git a/test/spec/ol/style/iconstyle.test.js b/test/spec/ol/style/iconstyle.test.js new file mode 100644 index 0000000000..f6d7dfdafb --- /dev/null +++ b/test/spec/ol/style/iconstyle.test.js @@ -0,0 +1,71 @@ +goog.provide('ol.test.style.IconImageCache'); + +describe('ol.style.IconImageCache', function() { + var originalMaxCacheSize; + + beforeEach(function() { + var cache = ol.style.IconImageCache.getInstance(); + cache.clear(); + originalMaxCacheSize = cache.maxCacheSize; + cache.maxCacheSize_ = 4; + }); + + afterEach(function() { + var cache = ol.style.IconImageCache.getInstance(); + cache.maxCacheSize_ = originalMaxCacheSize; + cache.clear(); + }); + + describe('#expire', function() { + it('expires images when expected', function() { + var cache = ol.style.IconImageCache.getInstance(); + + var i, src, iconImage, key; + + for (i = 0; i < 4; ++i) { + src = i + ''; + iconImage = new ol.style.IconImage_(src, null); + cache.set(src, null, iconImage); + } + + expect(cache.cacheSize_).to.eql(4); + + cache.expire(); + expect(cache.cacheSize_).to.eql(4); + + src = '4'; + iconImage = new ol.style.IconImage_(src, null); + cache.set(src, null, iconImage); + expect(cache.cacheSize_).to.eql(5); + + cache.expire(); // remove '0' and '4' + expect(cache.cacheSize_).to.eql(3); + + src = '0'; + iconImage = new ol.style.IconImage_(src, null); + goog.events.listen(iconImage, goog.events.EventType.CHANGE, + goog.nullFunction, false); + cache.set(src, null, iconImage); + expect(cache.cacheSize_).to.eql(4); + + src = '4'; + iconImage = new ol.style.IconImage_(src, null); + goog.events.listen(iconImage, goog.events.EventType.CHANGE, + goog.nullFunction, false); + cache.set(src, null, iconImage); + expect(cache.cacheSize_).to.eql(5); + + // check that '0' and '4' are not removed from the cache + cache.expire(); + key = ol.style.IconImageCache.getKey('0', null); + expect(key in cache.cache_).to.be.ok(); + key = ol.style.IconImageCache.getKey('4', null); + expect(key in cache.cache_).to.be.ok(); + + }); + }); +}); + +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('ol.style.IconImageCache');