diff --git a/examples/symbol-atlas-webgl.html b/examples/symbol-atlas-webgl.html new file mode 100644 index 0000000000..36774b6681 --- /dev/null +++ b/examples/symbol-atlas-webgl.html @@ -0,0 +1,52 @@ + + + + + + + + + + + Symbols with WebGL example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Symbols with WebGL example

+

Using symbols in an atlas with WebGL.

+
+

See the symbol-atlas-webgl.js source to see how this is done.

+
+
webgl, symbol, atlas, vector, point
+
+ +
+ +
+ + + + + + + + diff --git a/examples/symbol-atlas-webgl.js b/examples/symbol-atlas-webgl.js new file mode 100644 index 0000000000..3a4c96194e --- /dev/null +++ b/examples/symbol-atlas-webgl.js @@ -0,0 +1,123 @@ +goog.require('ol.Feature'); +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.geom.Point'); +goog.require('ol.layer.Vector'); +goog.require('ol.source.Vector'); +goog.require('ol.style.AtlasManager'); +goog.require('ol.style.Circle'); +goog.require('ol.style.Fill'); +goog.require('ol.style.RegularShape'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + +var atlasManager = new ol.style.AtlasManager({ + // we increase the default size so that all symbols fit into + // a single atlas image + size: 512, + maxSize: ol.has.WEBGL_MAX_TEXTURE_SIZE}); + +var symbolInfo = [{ + opacity: 1.0, + scale: 1.0, + fillColor: 'rgba(255, 153, 0, 0.4)', + strokeColor: 'rgba(255, 204, 0, 0.2)' +}, { + opacity: 0.75, + scale: 1.25, + fillColor: 'rgba(70, 80, 224, 0.4)', + strokeColor: 'rgba(12, 21, 138, 0.2)' +}, { + opacity: 0.5, + scale: 1.5, + fillColor: 'rgba(66, 150, 79, 0.4)', + strokeColor: 'rgba(20, 99, 32, 0.2)' +}, { + opacity: 1.0, + scale: 1.0, + fillColor: 'rgba(176, 61, 35, 0.4)', + strokeColor: 'rgba(145, 43, 20, 0.2)' +}]; + +var radiuses = [3, 6, 9, 15, 19, 25]; +var symbolCount = symbolInfo.length * radiuses.length * 2; +var symbols = []; +var i, j; +for (i = 0; i < symbolInfo.length; ++i) { + var info = symbolInfo[i]; + for (j = 0; j < radiuses.length; ++j) { + // circle symbol + symbols.push(new ol.style.Circle({ + opacity: info.opacity, + scale: info.scale, + radius: radiuses[j], + fill: new ol.style.Fill({ + color: info.fillColor + }), + stroke: new ol.style.Stroke({ + color: info.strokeColor, + width: 1 + }), + // by passing the atlas manager to the symbol, + // the symbol will be added to an atlas + atlasManager: atlasManager + })); + + // star symbol + symbols.push(new ol.style.RegularShape({ + points: 8, + opacity: info.opacity, + scale: info.scale, + radius: radiuses[j], + radius2: radiuses[j] * 0.7, + angle: 1.4, + fill: new ol.style.Fill({ + color: info.fillColor + }), + stroke: new ol.style.Stroke({ + color: info.strokeColor, + width: 1 + }), + atlasManager: atlasManager + })); + } +} + +var featureCount = 30000; +var features = new Array(featureCount); +var feature, geometry; +var e = 25000000; +for (i = 0; i < featureCount; ++i) { + geometry = new ol.geom.Point( + [2 * e * Math.random() - e, 2 * e * Math.random() - e]); + feature = new ol.Feature(geometry); + feature.setStyle( + new ol.style.Style({ + image: symbols[i % (symbolCount - 1)] + }) + ); + features[i] = feature; +} + +var vectorSource = new ol.source.Vector({ + features: features +}); +var vector = new ol.layer.Vector({ + source: vectorSource +}); + +// Use the "webgl" renderer by default. +var renderer = exampleNS.getRendererFromQueryString(); +if (!renderer) { + renderer = 'webgl'; +} + +var map = new ol.Map({ + renderer: renderer, + layers: [vector], + target: document.getElementById('map'), + view: new ol.View({ + center: [0, 0], + zoom: 4 + }) +}); diff --git a/examples/symbol-stars.html b/examples/symbol-stars.html new file mode 100644 index 0000000000..68f0b1fab0 --- /dev/null +++ b/examples/symbol-stars.html @@ -0,0 +1,52 @@ + + + + + + + + + + + Regular shape symbols + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Regular shape symbols

+

Drawing stars with regular shape symbols.

+
+

See the symbol-stars.js source to see how this is done.

+
+
symbol, regular, star, vector, point
+
+ +
+ +
+ + + + + + + + diff --git a/examples/symbol-stars.js b/examples/symbol-stars.js new file mode 100644 index 0000000000..8a84b56455 --- /dev/null +++ b/examples/symbol-stars.js @@ -0,0 +1,89 @@ +goog.require('ol.Feature'); +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.geom.Point'); +goog.require('ol.layer.Vector'); +goog.require('ol.source.Vector'); +goog.require('ol.style.Fill'); +goog.require('ol.style.RegularShape'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + +var symbolInfo = [{ + opacity: 1.0, + scale: 1.0, + fillColor: 'rgba(255, 153, 0, 0.4)', + strokeColor: 'rgba(255, 204, 0, 0.2)' +}, { + opacity: 0.75, + scale: 1.25, + fillColor: 'rgba(70, 80, 224, 0.4)', + strokeColor: 'rgba(12, 21, 138, 0.2)' +}, { + opacity: 0.5, + scale: 1.5, + fillColor: 'rgba(66, 150, 79, 0.4)', + strokeColor: 'rgba(20, 99, 32, 0.2)' +}, { + opacity: 1.0, + scale: 1.0, + fillColor: 'rgba(176, 61, 35, 0.4)', + strokeColor: 'rgba(145, 43, 20, 0.2)' +}]; + +var radiuses = [3, 6, 9, 15, 19, 25]; +var symbolCount = symbolInfo.length * radiuses.length; +var symbols = []; +var i, j; +for (i = 0; i < symbolInfo.length; ++i) { + var info = symbolInfo[i]; + for (j = 0; j < radiuses.length; ++j) { + symbols.push(new ol.style.RegularShape({ + points: 8, + opacity: info.opacity, + scale: info.scale, + radius: radiuses[j], + radius2: radiuses[j] * 0.7, + angle: 1.4, + fill: new ol.style.Fill({ + color: info.fillColor + }), + stroke: new ol.style.Stroke({ + color: info.strokeColor, + width: 1 + }) + })); + } +} + +var featureCount = 5000; +var features = new Array(featureCount); +var feature, geometry; +var e = 25000000; +for (i = 0; i < featureCount; ++i) { + geometry = new ol.geom.Point( + [2 * e * Math.random() - e, 2 * e * Math.random() - e]); + feature = new ol.Feature(geometry); + feature.setStyle( + new ol.style.Style({ + image: symbols[i % (symbolCount - 1)] + }) + ); + features[i] = feature; +} + +var vectorSource = new ol.source.Vector({ + features: features +}); +var vector = new ol.layer.Vector({ + source: vectorSource +}); + +var map = new ol.Map({ + layers: [vector], + target: document.getElementById('map'), + view: new ol.View({ + center: [0, 0], + zoom: 3 + }) +}); diff --git a/externs/olx.js b/externs/olx.js index 3e731883b0..9697bb9332 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -5399,7 +5399,8 @@ olx.style; * @typedef {{fill: (ol.style.Fill|undefined), * radius: number, * snapToPixel: (boolean|undefined), - * stroke: (ol.style.Stroke|undefined)}} + * stroke: (ol.style.Stroke|undefined), + * atlasManager: (ol.style.AtlasManager|undefined)}} * @api */ olx.style.CircleOptions; @@ -5443,6 +5444,16 @@ olx.style.CircleOptions.prototype.snapToPixel; olx.style.CircleOptions.prototype.stroke; +/** + * The atlas manager to use for this circle. When using WebGL it is + * recommended to use an atlas manager to avoid texture switching. + * If an atlas manager is given, the circle is added to an atlas. + * By default no atlas manager is used. + * @type {ol.style.AtlasManager|undefined} + */ +olx.style.CircleOptions.prototype.atlasManager; + + /** * @typedef {{color: (ol.Color|string|undefined)}} * @api @@ -5616,7 +5627,8 @@ olx.style.IconOptions.prototype.src; * radius2: number, * angle: number, * snapToPixel: (boolean|undefined), - * stroke: (ol.style.Stroke|undefined)}} + * stroke: (ol.style.Stroke|undefined), + * atlasManager: (ol.style.AtlasManager|undefined)}} * @api */ olx.style.RegularShapeOptions; @@ -5689,6 +5701,16 @@ olx.style.RegularShapeOptions.prototype.snapToPixel; olx.style.RegularShapeOptions.prototype.stroke; +/** + * The atlas manager to use for this symbol. When using WebGL it is + * recommended to use an atlas manager to avoid texture switching. + * If an atlas manager is given, the symbol is added to an atlas. + * By default no atlas manager is used. + * @type {ol.style.AtlasManager|undefined} + */ +olx.style.RegularShapeOptions.prototype.atlasManager; + + /** * @typedef {{color: (ol.Color|string|undefined), * lineCap: (string|undefined), diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js index eb1683d68d..45bbb1a55f 100644 --- a/src/ol/style/atlasmanager.js +++ b/src/ol/style/atlasmanager.js @@ -143,16 +143,16 @@ ol.style.AtlasManager.prototype.add = * @struct * @param {number} size The size in pixels of the sprite image. * @param {number} space 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. */ 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. + * @type {number} */ this.space_ = space; diff --git a/src/ol/style/circlestyle.js b/src/ol/style/circlestyle.js index 7983b0a280..dd5f63c8f2 100644 --- a/src/ol/style/circlestyle.js +++ b/src/ol/style/circlestyle.js @@ -1,5 +1,6 @@ goog.provide('ol.style.Circle'); +goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('ol.color'); @@ -26,12 +27,17 @@ ol.style.Circle = function(opt_options) { var options = goog.isDef(opt_options) ? opt_options : {}; + /** + * @private + * @type {Array.|null} + */ + this.checksums_ = null; + /** * @private * @type {HTMLCanvasElement} */ - this.canvas_ = /** @type {HTMLCanvasElement} */ - (goog.dom.createElement(goog.dom.TagName.CANVAS)); + this.canvas_ = null; /** * @private @@ -47,9 +53,9 @@ ol.style.Circle = function(opt_options) { /** * @private - * @type {Array.} + * @type {ol.style.Stroke} */ - this.origin_ = [0, 0]; + this.stroke_ = goog.isDef(options.stroke) ? options.stroke : null; /** * @private @@ -59,29 +65,29 @@ ol.style.Circle = function(opt_options) { /** * @private - * @type {ol.style.Stroke} + * @type {Array.} */ - this.stroke_ = goog.isDef(options.stroke) ? options.stroke : null; - - var size = this.render_(); + this.origin_ = [0, 0]; /** * @private * @type {Array.} */ - this.anchor_ = [size / 2, size / 2]; + this.anchor_ = null; /** * @private * @type {ol.Size} */ - this.size_ = [size, size]; + this.size_ = null; /** * @private - * @type {Array.|null} + * @type {ol.Size} */ - this.checksums_ = null; + this.imageSize_ = null; + + this.render_(options.atlasManager); /** * @type {boolean} @@ -149,7 +155,7 @@ ol.style.Circle.prototype.getImageState = function() { * @inheritDoc */ ol.style.Circle.prototype.getImageSize = function() { - return this.size_; + return this.imageSize_; }; @@ -208,16 +214,20 @@ ol.style.Circle.prototype.unlistenImageChange = goog.nullFunction; /** - * @private - * @return {number} Size. + * @typedef {{strokeStyle: (string|undefined), strokeWidth: number, + * size: number}} */ -ol.style.Circle.prototype.render_ = function() { - var canvas = this.canvas_; - var strokeStyle, strokeWidth; +ol.style.Circle.RenderOptions; - if (goog.isNull(this.stroke_)) { - strokeWidth = 0; - } else { + +/** + * @private + * @param {ol.style.AtlasManager|undefined} atlasManager + */ +ol.style.Circle.prototype.render_ = function(atlasManager) { + var strokeStyle, strokeWidth = 0, imageSize; + + if (!goog.isNull(this.stroke_)) { strokeStyle = ol.color.asString(this.stroke_.getColor()); strokeWidth = this.stroke_.getWidth(); if (!goog.isDef(strokeWidth)) { @@ -227,54 +237,112 @@ ol.style.Circle.prototype.render_ = function() { var size = 2 * (this.radius_ + strokeWidth) + 1; - // draw the circle on the canvas + /** @type {ol.style.Circle.RenderOptions} */ + var renderOptions = { + strokeStyle: strokeStyle, + strokeWidth: strokeWidth, + size: size + }; - canvas.height = size; - canvas.width = size; + if (!goog.isDef(atlasManager)) { + // no atlas manager is used, create a new canvas + this.canvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + this.canvas_.height = size; + this.canvas_.width = size; - // canvas.width and height are rounded to the closest integer - size = canvas.width; + // canvas.width and height are rounded to the closest integer + size = this.canvas_.width; + imageSize = size; - var context = /** @type {CanvasRenderingContext2D} */ - (canvas.getContext('2d')); - context.arc(size / 2, size / 2, this.radius_, 0, 2 * Math.PI, true); + // draw the circle on the canvas + var context = /** @type {CanvasRenderingContext2D} */ + (this.canvas_.getContext('2d')); + this.draw_(renderOptions, context, 0, 0); + } else { + // an atlas manager is used, add the symbol to an atlas + size = Math.round(size); + + var id = this.getChecksum(); + var info = atlasManager.add( + id, size, size, goog.bind(this.draw_, this, renderOptions)); + goog.asserts.assert(info !== null, 'circle radius is too large'); + + this.canvas_ = info.image; + this.origin_ = [info.offsetX, info.offsetY]; + imageSize = info.image.width; + } + + this.anchor_ = [size / 2, size / 2]; + this.size_ = [size, size]; + this.imageSize_ = [imageSize, imageSize]; + + // deal with the hit detection canvas + if (!goog.isNull(this.fill_)) { + this.hitDetectionCanvas_ = this.canvas_; + } else { + this.createHitDetectionCanvas_(renderOptions); + } +}; + + +/** + * @private + * @param {ol.style.Circle.RenderOptions} renderOptions + * @param {CanvasRenderingContext2D} context + * @param {number} x The origin for the symbol (x). + * @param {number} y The origin for the symbol (y). + */ +ol.style.Circle.prototype.draw_ = function(renderOptions, context, x, y) { + // reset transform + context.setTransform(1, 0, 0, 1, 0, 0); + + // then move to (x, y) + context.translate(x, y); + + context.beginPath(); + context.arc( + renderOptions.size / 2, renderOptions.size / 2, + this.radius_, 0, 2 * Math.PI, true); if (!goog.isNull(this.fill_)) { context.fillStyle = ol.color.asString(this.fill_.getColor()); context.fill(); } if (!goog.isNull(this.stroke_)) { - context.strokeStyle = strokeStyle; - context.lineWidth = strokeWidth; + context.strokeStyle = renderOptions.strokeStyle; + context.lineWidth = renderOptions.strokeWidth; context.stroke(); } + context.closePath(); +}; - // deal with the hit detection canvas - if (!goog.isNull(this.fill_)) { - this.hitDetectionCanvas_ = canvas; - } else { - this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */ - (goog.dom.createElement(goog.dom.TagName.CANVAS)); - canvas = this.hitDetectionCanvas_; +/** + * @private + * @param {ol.style.Circle.RenderOptions} renderOptions + */ +ol.style.Circle.prototype.createHitDetectionCanvas_ = function(renderOptions) { + this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + var canvas = this.hitDetectionCanvas_; - canvas.height = size; - canvas.width = size; + canvas.height = renderOptions.size; + canvas.width = renderOptions.size; - context = /** @type {CanvasRenderingContext2D} */ - (canvas.getContext('2d')); - context.arc(size / 2, size / 2, this.radius_, 0, 2 * Math.PI, true); + var context = /** @type {CanvasRenderingContext2D} */ + (canvas.getContext('2d')); + context.arc( + renderOptions.size / 2, renderOptions.size / 2, + this.radius_, 0, 2 * Math.PI, true); - context.fillStyle = ol.render.canvas.defaultFillStyle; - context.fill(); - if (!goog.isNull(this.stroke_)) { - context.strokeStyle = strokeStyle; - context.lineWidth = strokeWidth; - context.stroke(); - } + context.fillStyle = ol.render.canvas.defaultFillStyle; + context.fill(); + if (!goog.isNull(this.stroke_)) { + context.strokeStyle = renderOptions.strokeStyle; + context.lineWidth = renderOptions.strokeWidth; + context.stroke(); } - - return size; }; diff --git a/src/ol/style/regularshapestyle.js b/src/ol/style/regularshapestyle.js index 526724ee2e..381a674d5d 100644 --- a/src/ol/style/regularshapestyle.js +++ b/src/ol/style/regularshapestyle.js @@ -1,5 +1,6 @@ goog.provide('ol.style.RegularShape'); +goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('ol.color'); @@ -18,18 +19,24 @@ goog.require('ol.style.Stroke'); * @constructor * @param {olx.style.RegularShapeOptions=} opt_options Options. * @extends {ol.style.Image} + * @implements {ol.structs.IHasChecksum} * @api */ ol.style.RegularShape = function(opt_options) { var options = goog.isDef(opt_options) ? opt_options : {}; + /** + * @private + * @type {Array.|null} + */ + this.checksums_ = null; + /** * @private * @type {HTMLCanvasElement} */ - this.canvas_ = /** @type {HTMLCanvasElement} */ - (goog.dom.createElement(goog.dom.TagName.CANVAS)); + this.canvas_ = null; /** * @private @@ -80,19 +87,25 @@ ol.style.RegularShape = function(opt_options) { */ this.stroke_ = goog.isDef(options.stroke) ? options.stroke : null; - var size = this.render_(); - /** * @private * @type {Array.} */ - this.anchor_ = [size / 2, size / 2]; + this.anchor_ = null; /** * @private * @type {ol.Size} */ - this.size_ = [size, size]; + this.size_ = null; + + /** + * @private + * @type {ol.Size} + */ + this.imageSize_ = null; + + this.render_(options.atlasManager); /** * @type {boolean} @@ -151,7 +164,7 @@ ol.style.RegularShape.prototype.getImage = function(pixelRatio) { * @inheritDoc */ ol.style.RegularShape.prototype.getImageSize = function() { - return this.size_; + return this.imageSize_; }; @@ -218,16 +231,20 @@ ol.style.RegularShape.prototype.unlistenImageChange = goog.nullFunction; /** - * @private - * @return {number} Size. + * @typedef {{strokeStyle: (string|undefined), strokeWidth: number, + * size: number}} */ -ol.style.RegularShape.prototype.render_ = function() { - var canvas = this.canvas_; - var strokeStyle, strokeWidth; +ol.style.RegularShape.RenderOptions; - if (goog.isNull(this.stroke_)) { - strokeWidth = 0; - } else { + +/** + * @private + * @param {ol.style.AtlasManager|undefined} atlasManager + */ +ol.style.RegularShape.prototype.render_ = function(atlasManager) { + var strokeStyle, strokeWidth = 0, imageSize; + + if (!goog.isNull(this.stroke_)) { strokeStyle = ol.color.asString(this.stroke_.getColor()); strokeWidth = this.stroke_.getWidth(); if (!goog.isDef(strokeWidth)) { @@ -237,17 +254,70 @@ ol.style.RegularShape.prototype.render_ = function() { var size = 2 * (this.radius_ + strokeWidth) + 1; - // draw the regular shape on the canvas + /** @type {ol.style.RegularShape.RenderOptions} */ + var renderOptions = { + strokeStyle: strokeStyle, + strokeWidth: strokeWidth, + size: size + }; - canvas.height = size; - canvas.width = size; + if (!goog.isDef(atlasManager)) { + // no atlas manager is used, create a new canvas + this.canvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); - // canvas.width and height are rounded to the closest integer - size = canvas.width; + this.canvas_.height = size; + this.canvas_.width = size; - var context = /** @type {CanvasRenderingContext2D} */ - (canvas.getContext('2d')); + // canvas.width and height are rounded to the closest integer + size = this.canvas_.width; + imageSize = size; + + var context = /** @type {CanvasRenderingContext2D} */ + (this.canvas_.getContext('2d')); + this.draw_(renderOptions, context, 0, 0); + } else { + // an atlas manager is used, add the symbol to an atlas + size = Math.round(size); + + var id = this.getChecksum(); + var info = atlasManager.add( + id, size, size, goog.bind(this.draw_, this, renderOptions)); + goog.asserts.assert(info !== null, 'shape size is too large'); + + this.canvas_ = info.image; + this.origin_ = [info.offsetX, info.offsetY]; + imageSize = info.image.width; + } + + this.anchor_ = [size / 2, size / 2]; + this.size_ = [size, size]; + this.imageSize_ = [imageSize, imageSize]; + + // deal with the hit detection canvas + if (!goog.isNull(this.fill_)) { + this.hitDetectionCanvas_ = this.canvas_; + } else { + this.createHitDetectionCanvas_(renderOptions); + } +}; + + +/** + * @private + * @param {ol.style.Circle.RenderOptions} renderOptions + * @param {CanvasRenderingContext2D} context + * @param {number} x The origin for the symbol (x). + * @param {number} y The origin for the symbol (y). + */ +ol.style.RegularShape.prototype.draw_ = function(renderOptions, context, x, y) { var i, angle0, radiusC; + // reset transform + context.setTransform(1, 0, 0, 1, 0, 0); + + // then move to (x, y) + context.translate(x, y); + context.beginPath(); if (this.radius2_ !== this.radius_) { this.points_ = 2 * this.points_; @@ -255,8 +325,8 @@ ol.style.RegularShape.prototype.render_ = function() { for (i = 0; i <= this.points_; i++) { angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_; radiusC = i % 2 === 0 ? this.radius_ : this.radius2_; - context.lineTo(size / 2 + radiusC * Math.cos(angle0), - size / 2 + radiusC * Math.sin(angle0)); + context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0), + renderOptions.size / 2 + radiusC * Math.sin(angle0)); } if (!goog.isNull(this.fill_)) { @@ -264,44 +334,77 @@ ol.style.RegularShape.prototype.render_ = function() { context.fill(); } if (!goog.isNull(this.stroke_)) { - context.strokeStyle = strokeStyle; - context.lineWidth = strokeWidth; + context.strokeStyle = renderOptions.strokeStyle; + context.lineWidth = renderOptions.strokeWidth; context.stroke(); } + context.closePath(); +}; - // deal with the hit detection canvas - if (!goog.isNull(this.fill_)) { - this.hitDetectionCanvas_ = canvas; - } else { - this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */ - (goog.dom.createElement(goog.dom.TagName.CANVAS)); - canvas = this.hitDetectionCanvas_; +/** + * @private + * @param {ol.style.RegularShape.RenderOptions} renderOptions + */ +ol.style.RegularShape.prototype.createHitDetectionCanvas_ = + function(renderOptions) { + this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + var canvas = this.hitDetectionCanvas_; - canvas.height = size; - canvas.width = size; + canvas.height = renderOptions.size; + canvas.width = renderOptions.size; - context = /** @type {CanvasRenderingContext2D} */ - (canvas.getContext('2d')); - context.beginPath(); - if (this.radius2_ !== this.radius_) { - this.points_ = 2 * this.points_; - } - for (i = 0; i <= this.points_; i++) { - angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_; - radiusC = i % 2 === 0 ? this.radius_ : this.radius2_; - context.lineTo(size / 2 + radiusC * Math.cos(angle0), - size / 2 + radiusC * Math.sin(angle0)); - } - - context.fillStyle = ol.render.canvas.defaultFillStyle; - context.fill(); - if (!goog.isNull(this.stroke_)) { - context.strokeStyle = strokeStyle; - context.lineWidth = strokeWidth; - context.stroke(); - } + var context = /** @type {CanvasRenderingContext2D} */ + (canvas.getContext('2d')); + context.beginPath(); + if (this.radius2_ !== this.radius_) { + this.points_ = 2 * this.points_; + } + var i, radiusC, angle0; + for (i = 0; i <= this.points_; i++) { + angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_; + radiusC = i % 2 === 0 ? this.radius_ : this.radius2_; + context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0), + renderOptions.size / 2 + radiusC * Math.sin(angle0)); } - return size; + context.fillStyle = ol.render.canvas.defaultFillStyle; + context.fill(); + if (!goog.isNull(this.stroke_)) { + context.strokeStyle = renderOptions.strokeStyle; + context.lineWidth = renderOptions.strokeWidth; + context.stroke(); + } +}; + + +/** + * @inheritDoc + */ +ol.style.RegularShape.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] || + this.radius2_ != this.checksums_[4] || + this.angle_ != this.checksums_[5] || + this.points_ != this.checksums_[6]); + + if (recalculate) { + var checksum = 'r' + strokeChecksum + fillChecksum + + (goog.isDef(this.radius_) ? this.radius_.toString() : '-') + + (goog.isDef(this.radius2_) ? this.radius2_.toString() : '-') + + (goog.isDef(this.angle_) ? this.angle_.toString() : '-') + + (goog.isDef(this.points_) ? this.points_.toString() : '-'); + this.checksums_ = [checksum, strokeChecksum, fillChecksum, + this.radius_, this.radius2_, this.angle_, this.points_]; + } + + return this.checksums_[0]; }; diff --git a/test/spec/ol/style/circlestyle.test.js b/test/spec/ol/style/circlestyle.test.js index 00eb1765a6..40c9f66b5a 100644 --- a/test/spec/ol/style/circlestyle.test.js +++ b/test/spec/ol/style/circlestyle.test.js @@ -3,6 +3,32 @@ goog.provide('ol.test.style.Circle'); describe('ol.style.Circle', function() { + describe('#constructor', function() { + + it('creates a canvas if no atlas is used', function() { + var style = new ol.style.Circle({radius: 10}); + expect(style.getImage()).to.be.an(HTMLCanvasElement); + expect(style.getSize()).to.eql([21, 21]); + expect(style.getImageSize()).to.eql([21, 21]); + expect(style.getOrigin()).to.eql([0, 0]); + expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getImage()).to.not.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + }); + + it('adds itself to an atlas manager', function() { + var atlasManager = new ol.style.AtlasManager({size: 512}); + var style = new ol.style.Circle({radius: 10, atlasManager: atlasManager}); + expect(style.getImage()).to.be.an(HTMLCanvasElement); + expect(style.getSize()).to.eql([21, 21]); + expect(style.getImageSize()).to.eql([512, 512]); + expect(style.getOrigin()).to.eql([1, 1]); + expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getImage()).to.not.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + }); + }); + describe('#getChecksum', function() { it('calculates the same hash code for default options', function() { @@ -162,6 +188,7 @@ describe('ol.style.Circle', function() { }); }); +goog.require('ol.style.AtlasManager'); goog.require('ol.style.Circle'); goog.require('ol.style.Fill'); goog.require('ol.style.Stroke'); diff --git a/test/spec/ol/style/regularshapestyle.test.js b/test/spec/ol/style/regularshapestyle.test.js new file mode 100644 index 0000000000..c382da8a59 --- /dev/null +++ b/test/spec/ol/style/regularshapestyle.test.js @@ -0,0 +1,210 @@ +goog.provide('ol.test.style.RegularShape'); + + +describe('ol.style.RegularShape', function() { + + describe('#constructor', function() { + + it('creates a canvas if no atlas is used', function() { + var style = new ol.style.RegularShape({radius: 10}); + expect(style.getImage()).to.be.an(HTMLCanvasElement); + expect(style.getSize()).to.eql([21, 21]); + expect(style.getImageSize()).to.eql([21, 21]); + expect(style.getOrigin()).to.eql([0, 0]); + expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getImage()).to.not.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + }); + + it('adds itself to an atlas manager', function() { + var atlasManager = new ol.style.AtlasManager({size: 512}); + var style = new ol.style.RegularShape( + {radius: 10, atlasManager: atlasManager}); + expect(style.getImage()).to.be.an(HTMLCanvasElement); + expect(style.getSize()).to.eql([21, 21]); + expect(style.getImageSize()).to.eql([512, 512]); + expect(style.getOrigin()).to.eql([1, 1]); + expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getImage()).to.not.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + }); + }); + + + describe('#getChecksum', function() { + + it('calculates the same hash code for default options', function() { + var style1 = new ol.style.RegularShape(); + var style2 = new ol.style.RegularShape(); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + }); + + it('calculates not the same hash code (radius)', function() { + var style1 = new ol.style.RegularShape({ + radius2: 5 + }); + var style2 = new ol.style.RegularShape({ + radius: 5 + }); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + it('calculates the same hash code (radius)', function() { + var style1 = new ol.style.RegularShape({ + radius: 5 + }); + var style2 = new ol.style.RegularShape({ + radius: 5 + }); + expect(style1.getChecksum()).to.eql(style2.getChecksum()); + }); + + it('calculates not the same hash code (color)', function() { + var style1 = new ol.style.RegularShape({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.RegularShape({ + 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.RegularShape({ + radius: 5, + radius2: 3, + angle: 1.41, + points: 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.RegularShape({ + radius: 5, + radius2: 3, + angle: 1.41, + points: 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.RegularShape({ + radius: 5, + radius2: 3, + angle: 1.41, + points: 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.RegularShape({ + radius: 5, + radius2: 3, + angle: 1.41, + points: 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.RegularShape({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.RegularShape({ + 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.RegularShape({ + radius: 5, + fill: new ol.style.Fill({ + color: '#319FD3' + }), + stroke: new ol.style.Stroke({ + color: '#319FD3' + }) + }); + var style2 = new ol.style.RegularShape({ + 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.AtlasManager'); +goog.require('ol.style.RegularShape'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke');