diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js index 45bbb1a55f..76a7ba8481 100644 --- a/src/ol/style/atlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -8,12 +8,16 @@ goog.require('goog.object'); /** - * Provides information for an image inside an atlas. - * `offsetX` and `offsetY` are the position of the image inside + * Provides information for an image inside an atlas manager. + * `offsetX` and `offsetY` is the position of the image inside * the atlas image `image`. - * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement}} + * `hitOffsetX` and `hitOffsetY` ist the position of the hit-detection image + * inside the hit-detection atlas image `hitImage` (only when a hit-detection + * image was created for this image). + * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement, + * hitOffsetX: number, hitOffsetY: number, hitImage: HTMLCanvasElement}} */ -ol.style.AtlasInfo; +ol.style.AtlasManagerInfo; @@ -66,18 +70,52 @@ ol.style.AtlasManager = function(opt_options) { * @type {Array.} */ this.atlases_ = [new ol.style.Atlas(this.currentSize_, this.space_)]; + + /** + * The size in pixels of the latest atlas image for hit-detection images. + * @private + * @type {number} + */ + this.currentHitSize_ = this.currentSize_; + + /** + * @private + * @type {Array.} + */ + this.hitAtlases_ = [new ol.style.Atlas(this.currentHitSize_, 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. + * @return {?ol.style.AtlasManagerInfo} 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) { + /** @type {?ol.style.AtlasInfo} */ + var info = this.getInfo_(this.atlases_, id); + + if (info === null) { + return null; + } + /** @type {?ol.style.AtlasInfo} */ + var hitInfo = this.getInfo_(this.hitAtlases_, id); + + return this.mergeInfos_(info, hitInfo); +}; + + +/** + * @private + * @param {Array.} atlases The atlases to search. + * @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 atlases. + */ +ol.style.AtlasManager.prototype.getInfo_ = function(atlases, id) { var atlas, info, i, ii; - for (i = 0, ii = this.atlases_.length; i < ii; ++i) { - atlas = this.atlases_[i]; + for (i = 0, ii = atlases.length; i < ii; ++i) { + atlas = atlases[i]; info = atlas.get(id); if (info !== null) { return info; @@ -87,41 +125,109 @@ ol.style.AtlasManager.prototype.getInfo = function(id) { }; +/** + * @private + * @param {ol.style.AtlasInfo} info The info for the real image. + * @param {?ol.style.AtlasInfo} hitInfo The info for the hit-detection + * image. + * @return {?ol.style.AtlasManagerInfo} The position and atlas image for the + * entry, or `null` if the entry is not part of the atlases. + */ +ol.style.AtlasManager.prototype.mergeInfos_ = function(info, hitInfo) { + return /** @type {ol.style.AtlasManagerInfo} */ ({ + offsetX: info.offsetX, + offsetY: info.offsetY, + image: info.image, + hitOffsetX: (hitInfo === null) ? undefined : hitInfo.offsetX, + hitOffsetY: (hitInfo === null) ? undefined : hitInfo.offsetY, + hitImage: (hitInfo === null) ? undefined : hitInfo.image + }); +}; + + /** * 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). * + * If `renderHitCallback` is provided, the image (or the hit-detection version + * of the image) will be rendered into a separate hit-detection atlas image. + * * @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 {function(CanvasRenderingContext2D, number, number)=} + * opt_renderHitCallback Called to render a hit-detection image onto a hit + * detection 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. + * `renderCallback` and `renderHitCallback`. + * @return {?ol.style.AtlasManagerInfo} 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) { + function(id, width, height, + renderCallback, opt_renderHitCallback, opt_this) { if (width + this.space_ > this.maxSize_ || height + this.space_ > this.maxSize_) { return null; } + /** @type {?ol.style.AtlasInfo} */ + var info = this.add_(false, + id, width, height, renderCallback, opt_this); + if (info === null) { + return null; + } + + /** @type {?ol.style.AtlasInfo} */ + var hitInfo = null; + if (opt_renderHitCallback !== undefined) { + hitInfo = this.add_(true, + id, width, height, opt_renderHitCallback, opt_this); + } + return this.mergeInfos_(info, hitInfo); +}; + + +/** + * @private + * @param {boolean} isHitAtlas If the hit-detection atlases are used. + * @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` and `renderHitCallback`. + * @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(isHitAtlas, id, width, height, + renderCallback, opt_this) { + var atlases = (isHitAtlas) ? this.hitAtlases_ : this.atlases_; var atlas, info, i, ii; - for (i = 0, ii = this.atlases_.length; i < ii; ++i) { - atlas = this.atlases_[i]; + for (i = 0, ii = atlases.length; i < ii; ++i) { + atlas = 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); + var size; + if (isHitAtlas) { + size = Math.min(this.currentHitSize_ * 2, this.maxSize_); + this.currentHitSize_ = size; + } else { + size = Math.min(this.currentSize_ * 2, this.maxSize_); + this.currentSize_ = size; + } + atlas = new ol.style.Atlas(size, this.space_); + atlases.push(atlas); // run the loop another time ++ii; } @@ -130,6 +236,15 @@ ol.style.AtlasManager.prototype.add = }; +/** + * 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; + + /** * This class facilitates the creation of image atlases. diff --git a/test/spec/ol/style/atlasmanager.test.js b/test/spec/ol/style/atlasmanager.test.js index b24d832dc0..69c414ba07 100644 --- a/test/spec/ol/style/atlasmanager.test.js +++ b/test/spec/ol/style/atlasmanager.test.js @@ -189,46 +189,73 @@ describe('ol.style.AtlasManager', 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(info).to.eql({ + offsetX: 1, offsetY: 1, image: manager.atlases_[0].canvas_, + hitOffsetX: undefined, hitOffsetY: undefined, hitImage: undefined}); + + expect(manager.getInfo('1')).to.eql(info); + }); + + it('adds one entry (also to the hit detection atlas)', function() { + var manager = new ol.style.AtlasManager({size: 128}); + var info = manager.add('1', 32, 32, defaultRender, defaultRender); + + expect(info).to.eql({ + offsetX: 1, offsetY: 1, image: manager.atlases_[0].canvas_, + hitOffsetX: 1, hitOffsetY: 1, + hitImage: manager.hitAtlases_[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(manager.add('1', 100, 100, defaultRender, defaultRender)) + .to.be.ok(); + var info = manager.add('2', 100, 100, defaultRender, defaultRender); expect(info).to.be.ok(); expect(info.image.width).to.eql(256); expect(manager.atlases_).to.have.length(2); + expect(info.hitImage.width).to.eql(256); + expect(manager.hitAtlases_).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.add('1', 100, 100, defaultRender, defaultRender)) + .to.be.ok(); expect(manager.atlases_).to.have.length(1); - var info = manager.add('2', 500, 500, defaultRender); + expect(manager.hitAtlases_).to.have.length(1); + var info = manager.add('2', 500, 500, defaultRender, defaultRender); expect(info).to.be.ok(); expect(info.image.width).to.eql(512); expect(manager.atlases_).to.have.length(3); + expect(info.hitImage.width).to.eql(512); + expect(manager.hitAtlases_).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.add('1', 100, 100, defaultRender, defaultRender)) + .to.be.ok(); + expect(manager.add('2', 100, 100, defaultRender, defaultRender)) + .to.be.ok(); expect(manager.atlases_).to.have.length(2); - var info = manager.add(3, 500, 500, defaultRender); + expect(manager.hitAtlases_).to.have.length(2); + var info = manager.add(3, 500, 500, defaultRender, defaultRender); expect(info).to.be.ok(); expect(info.image.width).to.eql(512); expect(manager.atlases_).to.have.length(3); + expect(info.hitImage.width).to.eql(512); + expect(manager.hitAtlases_).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); + expect(manager.add('1', 100, 100, defaultRender, defaultRender)) + .to.be.ok(); + expect(manager.add('2', 2048, 2048, defaultRender, defaultRender)) + .to.eql(null); }); });