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/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/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 45fcf8b1fd..1421d5a93a 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'); @@ -12,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'); @@ -157,12 +155,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(); }; @@ -249,35 +244,18 @@ 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)) { return false; } // 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)) { - 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 { - ol.renderer.vector.renderFeature( - replayGroup, feature, style, squaredTolerance, feature); - } + loading = ol.renderer.vector.renderFeature( + replayGroup, feature, styles[i], squaredTolerance, feature, + this.handleImageChange_, this) || loading; } return loading; }; 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/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 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); }; diff --git a/src/ol/source/imagevectorsource.js b/src/ol/source/imagevectorsource.js index 7b7cd876dc..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'); @@ -194,12 +193,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(); }; @@ -223,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; @@ -231,26 +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)) { - 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 { - ol.renderer.vector.renderFeature( - replayGroup, feature, style, squaredTolerance, feature); - } + loading = ol.renderer.vector.renderFeature( + replayGroup, feature, styles[i], squaredTolerance, feature, + this.handleImageChange_, this) || loading; } return loading; }; 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..6af27dd27a 100644 --- a/src/ol/style/iconstyle.js +++ b/src/ol/style/iconstyle.js @@ -2,12 +2,14 @@ goog.provide('ol.style.Icon'); goog.provide('ol.style.IconAnchorUnits'); +goog.provide('ol.style.IconImageCache'); goog.require('goog.array'); 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 +34,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 +55,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_ = ol.style.IconImage_.get(options.src, crossOrigin); + + /** + * @private + * @type {ol.Size} + */ + this.size_ = goog.isDef(options.size) ? options.size : null; /** * @type {number} @@ -106,11 +83,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 +94,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 +282,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 +351,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 +370,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,8 +391,97 @@ 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; }; + + + +/** + * @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.clear = function() { + this.cache_ = {}; + this.cacheSize_ = 0; +}; + + +/** + * 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_; +}; 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; 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); 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'); 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');