diff --git a/externs/olx.js b/externs/olx.js index f7bcedf399..5f938f19a7 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -6227,3 +6227,36 @@ olx.ViewState.prototype.resolution; * @api */ olx.ViewState.prototype.rotation; + + +/** + * @typedef {{size: (number|undefined), + * maxSize: (number|undefined), + * space: (number|undefined)}} + * @api + */ +olx.style.AtlasManagerOptions; + + +/** + * The size in pixels of the first atlas image (default: 256). + * @type {number|undefined} + * @api + */ +olx.style.AtlasManagerOptions.prototype.size; + + +/** + * The maximum size in pixels of atlas images (default: 2048). + * @type {number|undefined} + * @api + */ +olx.style.AtlasManagerOptions.prototype.maxSize; + + +/** + * The space in pixels between images (default: 1). + * @type {number|undefined} + * @api + */ +olx.style.AtlasManagerOptions.prototype.space; diff --git a/src/ol/has.js b/src/ol/has.js index 1472c8b53c..63745b48ac 100644 --- a/src/ol/has.js +++ b/src/ol/has.js @@ -113,27 +113,42 @@ ol.has.POINTER = 'PointerEvent' in goog.global; ol.has.MSPOINTER = !!(goog.global.navigator.msPointerEnabled); +/** + * The maximum supported WebGL texture size in pixels. If WebGL is not + * supported, the value is set to `-1`. + * @const + * @type {number} + */ +ol.has.WEBGL_MAX_TEXTURE_SIZE; + + /** * True if browser supports WebGL. * @const * @type {boolean} * @api stable */ -ol.has.WEBGL = ol.ENABLE_WEBGL && ( - /** - * @return {boolean} WebGL supported. - */ - function() { - if (!('WebGLRenderingContext' in goog.global)) { - return false; - } +ol.has.WEBGL; + + +(function() { + if (ol.ENABLE_WEBGL) { + var hasWebGL = false, textureSize = -1; + if ('WebGLRenderingContext' in goog.global) { try { var canvas = /** @type {HTMLCanvasElement} */ (goog.dom.createElement(goog.dom.TagName.CANVAS)); - return !goog.isNull(ol.webgl.getContext(canvas, { + var gl = ol.webgl.getContext(canvas, { failIfMajorPerformanceCaveat: true - })); - } catch (e) { - return false; - } - })(); + }); + if (!goog.isNull(gl)) { + hasWebGL = true; + textureSize = /** @type {number} */ + (gl.getParameter(gl.MAX_TEXTURE_SIZE)); + } + } catch (e) {} + } + ol.has.WEBGL = hasWebGL; + ol.has.WEBGL_MAX_TEXTURE_SIZE = textureSize; + } +})(); diff --git a/src/ol/structs/checksum.js b/src/ol/structs/checksum.js new file mode 100644 index 0000000000..423da745cf --- /dev/null +++ b/src/ol/structs/checksum.js @@ -0,0 +1,22 @@ +goog.provide('ol.structs.IHasChecksum'); + + +/** + * @typedef {string} + */ +ol.structs.Checksum; + + + +/** + * @interface + */ +ol.structs.IHasChecksum = function() { +}; + + +/** + * @return {string} The checksum. + */ +ol.structs.IHasChecksum.prototype.getChecksum = function() { +}; diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js new file mode 100644 index 0000000000..8c03c38d06 --- /dev/null +++ b/src/ol/style/atlasmanager.js @@ -0,0 +1,321 @@ +goog.provide('ol.style.Atlas'); +goog.provide('ol.style.AtlasManager'); + +goog.require('goog.asserts'); +goog.require('goog.dom'); +goog.require('goog.dom.TagName'); +goog.require('goog.object'); + + +/** + * Provides information for an image inside an atlas. + * `offsetX` and `offsetY` are the position of the image inside + * the atlas image `image`. + * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement}} + */ +ol.style.AtlasInfo; + + + +/** + * Manages the creation of image atlases. + * + * Images added to this manager will be inserted into an atlas, which + * will be used for rendering. + * The `size` given in the constructor is the size for the first + * atlas. After that, when new atlases are created, they will have + * twice the size as the latest atlas (until `maxSize` is reached). + * + * When used for WebGL, it is recommended to use `ol.has.WEBGL_MAX_TEXTURE_SIZE` + * as `maxSize` value. Also, if an application uses a lot, or a lot of + * large images, it is recommend to set a higher `size` value to avoid + * the creation of too many atlases. + * + * @constructor + * @struct + * @api + * @param {olx.style.AtlasManagerOptions=} opt_options Options. + */ +ol.style.AtlasManager = function(opt_options) { + + var options = goog.isDef(opt_options) ? opt_options : {}; + + /** + * The size in pixels of the latest atlas image. + * @private + * @type {number} + */ + this.currentSize_ = goog.isDef(options.size) ? options.size : 256; + + /** + * The maximum size in pixels of atlas images. + * @private + * @type {number} + */ + this.maxSize_ = goog.isDef(options.maxSize) ? options.maxSize : 2048; + + /** + * The size in pixels between images. + * @private + * @type {number} + */ + this.space_ = goog.isDef(options.space) ? options.space : 1; + + /** + * @private + * @type {Array.} + */ + this.atlases_ = [new ol.style.Atlas(this.currentSize_, this.space_)]; +}; + + +/** + * @param {string} id The identifier of the entry to check. + * @return {?ol.style.AtlasInfo} The position and atlas image for the entry, + * or `null` if the entry is not part of the atlas manager. + */ +ol.style.AtlasManager.prototype.getInfo = function(id) { + var atlas, info, i, ii; + for (i = 0, ii = this.atlases_.length; i < ii; ++i) { + atlas = this.atlases_[i]; + info = atlas.get(id); + if (info !== null) { + return info; + } + } + return null; +}; + + +/** + * Add an image to the atlas manager. + * + * If an entry for the given id already exists, the entry will + * be overridden (but the space on the atlas graphic will not be freed). + * + * @param {string} id The identifier of the entry to add. + * @param {number} width The width. + * @param {number} height The height. + * @param {function(CanvasRenderingContext2D, number, number)} renderCallback + * Called to render the new image onto an atlas image. + * @param {Object=} opt_this Value to use as `this` when executing + * `renderCallback`. + * @return {?ol.style.AtlasInfo} The position and atlas image for the entry, + * or `null` if the image is too big. + */ +ol.style.AtlasManager.prototype.add = + function(id, width, height, renderCallback, opt_this) { + if (width + this.space_ > this.maxSize_ || + height + this.space_ > this.maxSize_) { + return null; + } + + var atlas, info, i, ii; + for (i = 0, ii = this.atlases_.length; i < ii; ++i) { + atlas = this.atlases_[i]; + info = atlas.add(id, width, height, renderCallback, opt_this); + if (info !== null) { + return info; + } else if (info === null && i === ii - 1) { + // the entry could not be added to one of the existing atlases, + // create a new atlas that is twice as big and try to add to this one. + this.currentSize_ = Math.min(this.currentSize_ * 2, this.maxSize_); + atlas = new ol.style.Atlas(this.currentSize_, this.space_); + this.atlases_.push(atlas); + // run the loop another time + ++ii; + } + } +}; + + + +/** + * This class facilitates the creation of image atlases. + * + * Images added to an atlas will be rendered onto a single + * atlas canvas. The distribution of images on the canvas is + * managed with the bin packing algorithm described in: + * http://www.blackpawn.com/texts/lightmaps/ + * + * @constructor + * @struct + * @param {number} size The size in pixels of the sprite image. + * @param {number} space The space in pixels between images. + */ +ol.style.Atlas = function(size, space) { + + /** + * @private + * @type {number} The space in pixels between images. + * Because texture coordinates are float values, the edges of + * images might not be completely correct (in a way that the + * edges overlap when being rendered). To avoid this we add a + * padding around each image. + */ + this.space_ = space; + + /** + * @private + * @type {Array.} + */ + this.emptyBlocks_ = [{x: 0, y: 0, width: size, height: size}]; + + /** + * @private + * @type {Object.} + */ + this.entries_ = {}; + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + this.canvas_.width = size; + this.canvas_.height = size; + + /** + * @private + * @type {CanvasRenderingContext2D} + */ + this.context_ = /** @type {CanvasRenderingContext2D} */ + (this.canvas_.getContext('2d')); +}; + + +/** + * @param {string} id The identifier of the entry to check. + * @return {?ol.style.AtlasInfo} + */ +ol.style.Atlas.prototype.get = function(id) { + return /** @type {?ol.style.AtlasInfo} */ ( + goog.object.get(this.entries_, id, null)); +}; + + +/** + * @param {string} id The identifier of the entry to add. + * @param {number} width The width. + * @param {number} height The height. + * @param {function(CanvasRenderingContext2D, number, number)} renderCallback + * Called to render the new image onto an atlas image. + * @param {Object=} opt_this Value to use as `this` when executing + * `renderCallback`. + * @return {?ol.style.AtlasInfo} The position and atlas image for the entry. + */ +ol.style.Atlas.prototype.add = + function(id, width, height, renderCallback, opt_this) { + var block, i, ii; + for (i = 0, ii = this.emptyBlocks_.length; i < ii; ++i) { + block = this.emptyBlocks_[i]; + if (block.width >= width + this.space_ && + block.height >= height + this.space_) { + // we found a block that is big enough for our entry + var entry = { + offsetX: block.x + this.space_, + offsetY: block.y + this.space_, + image: this.canvas_ + }; + this.entries_[id] = entry; + + // render the image on the atlas image + renderCallback.call(opt_this, this.context_, + block.x + this.space_, block.y + this.space_); + + // split the block after the insertion, either horizontally or vertically + this.split_(i, block, width + this.space_, height + this.space_); + + return entry; + } + } + + // there is no space for the new entry in this atlas + return null; +}; + + +/** + * @private + * @param {number} index The index of the block. + * @param {ol.style.Atlas.Block} block The block to split. + * @param {number} width The width of the entry to insert. + * @param {number} height The height of the entry to insert. + */ +ol.style.Atlas.prototype.split_ = + function(index, block, width, height) { + var deltaWidth = block.width - width; + var deltaHeight = block.height - height; + + /** @type {ol.style.Atlas.Block} */ + var newBlock1; + /** @type {ol.style.Atlas.Block} */ + var newBlock2; + + if (deltaWidth > deltaHeight) { + // split vertically + // block right of the inserted entry + newBlock1 = { + x: block.x + width, + y: block.y, + width: block.width - width, + height: block.height + }; + + // block below the inserted entry + newBlock2 = { + x: block.x, + y: block.y + height, + width: width, + height: block.height - height + }; + this.updateBlocks_(index, newBlock1, newBlock2); + } else { + // split horizontally + // block right of the inserted entry + newBlock1 = { + x: block.x + width, + y: block.y, + width: block.width - width, + height: height + }; + + // block below the inserted entry + newBlock2 = { + x: block.x, + y: block.y + height, + width: block.width, + height: block.height - height + }; + this.updateBlocks_(index, newBlock1, newBlock2); + } +}; + + +/** + * Remove the old block and insert new blocks at the same array position. + * The new blocks are inserted at the same position, so that splitted + * blocks (that are potentially smaller) are filled first. + * @private + * @param {number} index The index of the block to remove. + * @param {ol.style.Atlas.Block} newBlock1 The 1st block to add. + * @param {ol.style.Atlas.Block} newBlock2 The 2nd block to add. + */ +ol.style.Atlas.prototype.updateBlocks_ = + function(index, newBlock1, newBlock2) { + var args = [index, 1]; + if (newBlock1.width > 0 && newBlock1.height > 0) { + args.push(newBlock1); + } + if (newBlock2.width > 0 && newBlock2.height > 0) { + args.push(newBlock2); + } + this.emptyBlocks_.splice.apply(this.emptyBlocks_, args); +}; + + +/** + * @typedef {{x: number, y: number, width: number, height: number}} + */ +ol.style.Atlas.Block; diff --git a/src/ol/style/circlestyle.js b/src/ol/style/circlestyle.js index 6c3490e17e..7983b0a280 100644 --- a/src/ol/style/circlestyle.js +++ b/src/ol/style/circlestyle.js @@ -4,6 +4,7 @@ goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('ol.color'); goog.require('ol.render.canvas'); +goog.require('ol.structs.IHasChecksum'); goog.require('ol.style.Fill'); goog.require('ol.style.Image'); goog.require('ol.style.ImageState'); @@ -18,6 +19,7 @@ goog.require('ol.style.Stroke'); * @constructor * @param {olx.style.CircleOptions=} opt_options Options. * @extends {ol.style.Image} + * @implements {ol.structs.IHasChecksum} * @api */ ol.style.Circle = function(opt_options) { @@ -75,6 +77,12 @@ ol.style.Circle = function(opt_options) { */ this.size_ = [size, size]; + /** + * @private + * @type {Array.|null} + */ + this.checksums_ = null; + /** * @type {boolean} */ @@ -268,3 +276,27 @@ ol.style.Circle.prototype.render_ = function() { return size; }; + + +/** + * @inheritDoc + */ +ol.style.Circle.prototype.getChecksum = function() { + var strokeChecksum = !goog.isNull(this.stroke_) ? + this.stroke_.getChecksum() : '-'; + var fillChecksum = !goog.isNull(this.fill_) ? + this.fill_.getChecksum() : '-'; + + var recalculate = goog.isNull(this.checksums_) || + (strokeChecksum != this.checksums_[1] || + fillChecksum != this.checksums_[2] || + this.radius_ != this.checksums_[3]); + + if (recalculate) { + var checksum = 'c' + strokeChecksum + fillChecksum + + (goog.isDef(this.radius_) ? this.radius_.toString() : '-'); + this.checksums_ = [checksum, strokeChecksum, fillChecksum, this.radius_]; + } + + return this.checksums_[0]; +}; diff --git a/src/ol/style/fillstyle.js b/src/ol/style/fillstyle.js index 6714445868..a54d8d2498 100644 --- a/src/ol/style/fillstyle.js +++ b/src/ol/style/fillstyle.js @@ -1,5 +1,8 @@ goog.provide('ol.style.Fill'); +goog.require('ol.color'); +goog.require('ol.structs.IHasChecksum'); + /** @@ -8,6 +11,7 @@ goog.provide('ol.style.Fill'); * * @constructor * @param {olx.style.FillOptions=} opt_options Options. + * @implements {ol.structs.IHasChecksum} * @api */ ol.style.Fill = function(opt_options) { @@ -19,6 +23,12 @@ ol.style.Fill = function(opt_options) { * @type {ol.Color|string} */ this.color_ = goog.isDef(options.color) ? options.color : null; + + /** + * @private + * @type {?ol.structs.Checksum} + */ + this.checksum_ = null; }; @@ -39,4 +49,18 @@ ol.style.Fill.prototype.getColor = function() { */ ol.style.Fill.prototype.setColor = function(color) { this.color_ = color; + this.checksum_ = null; +}; + + +/** + * @inheritDoc + */ +ol.style.Fill.prototype.getChecksum = function() { + if (goog.isNull(this.checksum_)) { + this.checksum_ = 'f' + (!goog.isNull(this.color_) ? + ol.color.asString(this.color_) : '-'); + } + + return this.checksum_; }; diff --git a/src/ol/style/strokestyle.js b/src/ol/style/strokestyle.js index 15e99bf260..419e771cd3 100644 --- a/src/ol/style/strokestyle.js +++ b/src/ol/style/strokestyle.js @@ -1,5 +1,10 @@ goog.provide('ol.style.Stroke'); +goog.require('goog.crypt'); +goog.require('goog.crypt.Md5'); +goog.require('ol.color'); +goog.require('ol.structs.IHasChecksum'); + /** @@ -11,6 +16,7 @@ goog.provide('ol.style.Stroke'); * * @constructor * @param {olx.style.StrokeOptions=} opt_options Options. + * @implements {ol.structs.IHasChecksum} * @api */ ol.style.Stroke = function(opt_options) { @@ -52,6 +58,12 @@ ol.style.Stroke = function(opt_options) { * @type {number|undefined} */ this.width_ = options.width; + + /** + * @private + * @type {?ol.structs.Checksum} + */ + this.checksum_ = null; }; @@ -117,6 +129,7 @@ ol.style.Stroke.prototype.getWidth = function() { */ ol.style.Stroke.prototype.setColor = function(color) { this.color_ = color; + this.checksum_ = null; }; @@ -128,6 +141,7 @@ ol.style.Stroke.prototype.setColor = function(color) { */ ol.style.Stroke.prototype.setLineCap = function(lineCap) { this.lineCap_ = lineCap; + this.checksum_ = null; }; @@ -139,6 +153,7 @@ ol.style.Stroke.prototype.setLineCap = function(lineCap) { */ ol.style.Stroke.prototype.setLineDash = function(lineDash) { this.lineDash_ = lineDash; + this.checksum_ = null; }; @@ -150,6 +165,7 @@ ol.style.Stroke.prototype.setLineDash = function(lineDash) { */ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) { this.lineJoin_ = lineJoin; + this.checksum_ = null; }; @@ -161,6 +177,7 @@ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) { */ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { this.miterLimit_ = miterLimit; + this.checksum_ = null; }; @@ -172,4 +189,33 @@ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { */ ol.style.Stroke.prototype.setWidth = function(width) { this.width_ = width; + this.checksum_ = null; +}; + + +/** + * @inheritDoc + */ +ol.style.Stroke.prototype.getChecksum = function() { + if (goog.isNull(this.checksum_)) { + var raw = 's' + + (!goog.isNull(this.color_) ? + ol.color.asString(this.color_) : '-') + ',' + + (goog.isDef(this.lineCap_) ? + this.lineCap_.toString() : '-') + ',' + + (!goog.isNull(this.lineDash_) ? + this.lineDash_.toString() : '-') + ',' + + (goog.isDef(this.lineJoin_) ? + this.lineJoin_ : '-') + ',' + + (goog.isDef(this.miterLimit_) ? + this.miterLimit_.toString() : '-') + ',' + + (goog.isDef(this.width_) ? + this.width_.toString() : '-'); + + var md5 = new goog.crypt.Md5(); + md5.update(raw); + this.checksum_ = goog.crypt.byteArrayToString(md5.digest()); + } + + return this.checksum_; }; diff --git a/test/spec/ol/style/atlasmanager.test.js b/test/spec/ol/style/atlasmanager.test.js new file mode 100644 index 0000000000..b24d832dc0 --- /dev/null +++ b/test/spec/ol/style/atlasmanager.test.js @@ -0,0 +1,245 @@ +goog.provide('ol.test.style.AtlasManager'); + + +describe('ol.style.Atlas', function() { + + var defaultRender = function(context, x, y) { + }; + + describe('#constructor', function() { + + it('inits the atlas', function() { + var atlas = new ol.style.Atlas(256, 1); + expect(atlas.emptyBlocks_).to.eql( + [{x: 0, y: 0, width: 256, height: 256}]); + }); + }); + + describe('#add (squares with same size)', function() { + + it('adds one entry', function() { + var atlas = new ol.style.Atlas(128, 1); + var info = atlas.add('1', 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 1, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.get('1')).to.eql(info); + }); + + it('adds two entries', function() { + var atlas = new ol.style.Atlas(128, 1); + + atlas.add('1', 32, 32, defaultRender); + var info = atlas.add('2', 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 34, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.get('2')).to.eql(info); + }); + + it('adds three entries', function() { + var atlas = new ol.style.Atlas(128, 1); + + atlas.add('1', 32, 32, defaultRender); + atlas.add('2', 32, 32, defaultRender); + var info = atlas.add('3', 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 67, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.get('3')).to.eql(info); + }); + + it('adds four entries (new row)', function() { + var atlas = new ol.style.Atlas(128, 1); + + atlas.add('1', 32, 32, defaultRender); + atlas.add('2', 32, 32, defaultRender); + atlas.add('3', 32, 32, defaultRender); + var info = atlas.add('4', 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 1, offsetY: 34, image: atlas.canvas_}); + + expect(atlas.get('4')).to.eql(info); + }); + + it('returns null when an entry is too big', function() { + var atlas = new ol.style.Atlas(128, 1); + + atlas.add('1', 32, 32, defaultRender); + atlas.add('2', 32, 32, defaultRender); + atlas.add('3', 32, 32, defaultRender); + var info = atlas.add(4, 100, 100, defaultRender); + + expect(info).to.eql(null); + }); + + it('fills up the whole atlas', function() { + var atlas = new ol.style.Atlas(128, 1); + + for (var i = 1; i <= 16; i++) { + expect(atlas.add(i.toString(), 28, 28, defaultRender)).to.be.ok(); + } + + // there is no more space for items of this size, the next one will fail + expect(atlas.add('17', 28, 28, defaultRender)).to.eql(null); + }); + }); + + describe('#add (rectangles with different sizes)', function() { + + it('adds a bunch of rectangles', function() { + var atlas = new ol.style.Atlas(128, 1); + + expect(atlas.add('1', 64, 32, defaultRender)).to.eql( + {offsetX: 1, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.add('2', 64, 32, defaultRender)).to.eql( + {offsetX: 1, offsetY: 34, image: atlas.canvas_}); + + expect(atlas.add('3', 64, 32, defaultRender)).to.eql( + {offsetX: 1, offsetY: 67, image: atlas.canvas_}); + + // this one can not be added anymore + expect(atlas.add('4', 64, 32, defaultRender)).to.eql(null); + + // but there is still room for smaller ones + expect(atlas.add('5', 40, 32, defaultRender)).to.eql( + {offsetX: 66, offsetY: 1, image: atlas.canvas_}); + + expect(atlas.add('6', 40, 32, defaultRender)).to.eql( + {offsetX: 66, offsetY: 34, image: atlas.canvas_}); + }); + + it('fills up the whole atlas (rectangles in portrait format)', function() { + var atlas = new ol.style.Atlas(128, 1); + + for (var i = 1; i <= 32; i++) { + expect(atlas.add(i.toString(), 28, 14, defaultRender)).to.be.ok(); + } + + // there is no more space for items of this size, the next one will fail + expect(atlas.add('33', 28, 14, defaultRender)).to.eql(null); + }); + + it('fills up the whole atlas (rectangles in landscape format)', function() { + var atlas = new ol.style.Atlas(128, 1); + + for (var i = 1; i <= 32; i++) { + expect(atlas.add(i.toString(), 14, 28, defaultRender)).to.be.ok(); + } + + // there is no more space for items of this size, the next one will fail + expect(atlas.add('33', 14, 28, defaultRender)).to.eql(null); + }); + }); + + describe('#add (rendering)', function() { + + it('calls the render callback with the right values', function() { + var atlas = new ol.style.Atlas(128, 1); + var rendererCallback = sinon.spy(); + atlas.add('1', 32, 32, rendererCallback); + + expect(rendererCallback.calledOnce).to.be.ok(); + expect(rendererCallback.calledWith(atlas.context_, 1, 1)).to.be.ok(); + + rendererCallback = sinon.spy(); + atlas.add('2', 32, 32, rendererCallback); + + expect(rendererCallback.calledOnce).to.be.ok(); + expect(rendererCallback.calledWith(atlas.context_, 34, 1)).to.be.ok(); + }); + + it('is possible to actually draw on the canvas', function() { + var atlas = new ol.style.Atlas(128, 1); + + var rendererCallback = function(context, x, y) { + context.fillStyle = '#FFA500'; + context.fillRect(x, y, 32, 32); + }; + + expect(atlas.add('1', 32, 32, rendererCallback)).to.be.ok(); + expect(atlas.add('2', 32, 32, rendererCallback)).to.be.ok(); + // no error, ok + }); + }); +}); + + +describe('ol.style.AtlasManager', function() { + + var defaultRender = function(context, x, y) { + }; + + describe('#constructor', function() { + + it('inits the atlas manager', function() { + var manager = new ol.style.AtlasManager(); + expect(manager.atlases_).to.not.be.empty(); + }); + }); + + describe('#add', function() { + + it('adds one entry', function() { + var manager = new ol.style.AtlasManager({size: 128}); + var info = manager.add('1', 32, 32, defaultRender); + + expect(info).to.eql( + {offsetX: 1, offsetY: 1, image: manager.atlases_[0].canvas_}); + + expect(manager.getInfo('1')).to.eql(info); + }); + + it('creates a new atlas if needed', function() { + var manager = new ol.style.AtlasManager({size: 128}); + expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); + var info = manager.add('2', 100, 100, defaultRender); + expect(info).to.be.ok(); + expect(info.image.width).to.eql(256); + expect(manager.atlases_).to.have.length(2); + }); + + it('creates new atlases until one is large enough', function() { + var manager = new ol.style.AtlasManager({size: 128}); + expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); + expect(manager.atlases_).to.have.length(1); + var info = manager.add('2', 500, 500, defaultRender); + expect(info).to.be.ok(); + expect(info.image.width).to.eql(512); + expect(manager.atlases_).to.have.length(3); + }); + + it('checks all existing atlases and create a new if needed', function() { + var manager = new ol.style.AtlasManager({size: 128}); + expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); + expect(manager.add('2', 100, 100, defaultRender)).to.be.ok(); + expect(manager.atlases_).to.have.length(2); + var info = manager.add(3, 500, 500, defaultRender); + expect(info).to.be.ok(); + expect(info.image.width).to.eql(512); + expect(manager.atlases_).to.have.length(3); + }); + + it('returns null if the size exceeds the maximum size', function() { + var manager = new ol.style.AtlasManager({size: 128}); + expect(manager.add('1', 100, 100, defaultRender)).to.be.ok(); + expect(manager.add('2', 2048, 2048, defaultRender)).to.eql(null); + }); + }); + + describe('#getInfo', function() { + + it('returns null if no entry for the given id', function() { + var manager = new ol.style.AtlasManager({size: 128}); + expect(manager.getInfo('123456')).to.eql(null); + }); + }); +}); + +goog.require('ol.style.Atlas'); +goog.require('ol.style.AtlasManager'); diff --git a/test/spec/ol/style/circlestyle.test.js b/test/spec/ol/style/circlestyle.test.js new file mode 100644 index 0000000000..00eb1765a6 --- /dev/null +++ b/test/spec/ol/style/circlestyle.test.js @@ -0,0 +1,167 @@ +goog.provide('ol.test.style.Circle'); + + +describe('ol.style.Circle', function() { + + describe('#getChecksum', function() { + + it('calculates the same hash code for default options', function() { + var style1 = new ol.style.Circle(); + var style2 = new ol.style.Circle(); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + }); + + it('calculates not the same hash code (radius)', function() { + var style1 = new ol.style.Circle(); + var style2 = new ol.style.Circle({ + radius: 5 + }); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + it('calculates the same hash code (radius)', function() { + var style1 = new ol.style.Circle({ + radius: 5 + }); + var style2 = new ol.style.Circle({ + radius: 5 + }); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + }); + + it('calculates not the same hash code (color)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + it('calculates the same hash code (everything set)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + lineCap: 'round', + lineDash: [5, 15, 25], + lineJoin: 'miter', + miterLimit: 4, + width: 2 + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + lineCap: 'round', + lineDash: [5, 15, 25], + lineJoin: 'miter', + miterLimit: 4, + width: 2 + }) + }); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + }); + + it('calculates not the same hash code (stroke width differs)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + lineCap: 'round', + lineDash: [5, 15, 25], + lineJoin: 'miter', + miterLimit: 4, + width: 3 + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3', + lineCap: 'round', + lineDash: [5, 15, 25], + lineJoin: 'miter', + miterLimit: 4, + width: 2 + }) + }); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + it('invalidates a cached checksum if values change (fill)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + + style1.getFill().setColor('red'); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + it('invalidates a cached checksum if values change (stroke)', function() { + var style1 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.Circle({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + + style1.getStroke().setWidth(4); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + }); +}); + +goog.require('ol.style.Circle'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke');