diff --git a/examples/data/Butterfly.png b/examples/data/Butterfly.png new file mode 100644 index 00000000000..0a32d018ce2 Binary files /dev/null and b/examples/data/Butterfly.png differ diff --git a/examples/draw-features.js b/examples/draw-features.js index 59cc07984e0..dc66e2c1f70 100644 --- a/examples/draw-features.js +++ b/examples/draw-features.js @@ -38,6 +38,7 @@ var vector = new ol.layer.Vector({ var map = new ol.Map({ layers: [raster, vector], + renderer: exampleNS.getRendererFromQueryString(), target: 'map', view: new ol.View({ center: [-11000000, 4600000], diff --git a/examples/dynamic-data.js b/examples/dynamic-data.js index 00d87e840ea..793d4ef7615 100644 --- a/examples/dynamic-data.js +++ b/examples/dynamic-data.js @@ -1,11 +1,14 @@ +goog.require('ol.Feature'); goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.geom.MultiPoint'); +goog.require('ol.geom.Point'); goog.require('ol.layer.Tile'); goog.require('ol.source.MapQuest'); goog.require('ol.style.Circle'); goog.require('ol.style.Fill'); goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); var map = new ol.Map({ @@ -14,6 +17,7 @@ var map = new ol.Map({ source: new ol.source.MapQuest({layer: 'sat'}) }) ], + renderer: exampleNS.getRendererFromQueryString(), target: 'map', view: new ol.View({ center: [0, 0], @@ -28,6 +32,20 @@ var imageStyle = new ol.style.Circle({ stroke: new ol.style.Stroke({color: 'red', width: 1}) }); +var headInnerImageStyle = new ol.style.Style({ + image: new ol.style.Circle({ + radius: 2, + snapToPixel: false, + fill: new ol.style.Fill({color: 'blue'}) + }) +}); + +var headOuterImageStyle = new ol.style.Circle({ + radius: 5, + snapToPixel: false, + fill: new ol.style.Fill({color: 'black'}) +}); + var n = 200; var omegaTheta = 30000; // Rotation period in ms var R = 7e6; @@ -48,6 +66,14 @@ map.on('postcompose', function(event) { vectorContext.setImageStyle(imageStyle); vectorContext.drawMultiPointGeometry( new ol.geom.MultiPoint(coordinates), null); + + var headPoint = new ol.geom.Point(coordinates[coordinates.length - 1]); + var headFeature = new ol.Feature(headPoint); + vectorContext.drawFeature(headFeature, headInnerImageStyle); + + vectorContext.setImageStyle(headOuterImageStyle); + vectorContext.drawMultiPointGeometry(headPoint, null); + map.render(); }); map.render(); diff --git a/examples/icon-sprite-webgl.html b/examples/icon-sprite-webgl.html new file mode 100644 index 00000000000..7f074097463 --- /dev/null +++ b/examples/icon-sprite-webgl.html @@ -0,0 +1,53 @@ + + + + + + + + + + + Icon sprites with WebGL example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Icon sprite with WebGL example

+

Icon sprite with WebGL.

+
+

See the icon-sprite-webgl.js source to see how this is done.

+

In this example a sprite image is used for the icon styles. Using a sprite is required to get good performance with WebGL.

+
+
webgl, icon, sprite, vector, point
+
+ +
+ +
+ + + + + + + + diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js new file mode 100644 index 00000000000..26933f30f13 --- /dev/null +++ b/examples/icon-sprite-webgl.js @@ -0,0 +1,111 @@ +goog.require('ol.Feature'); +goog.require('ol.FeatureOverlay'); +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.Icon'); +goog.require('ol.style.Style'); + + +var iconInfo = [{ + offset: [0, 0], + opacity: 1.0, + rotateWithView: true, + rotation: 0.0, + scale: 1.0, + size: [55, 55] +}, { + offset: [110, 86], + opacity: 0.75, + rotateWithView: false, + rotation: Math.PI / 2.0, + scale: 1.25, + size: [55, 55] +}, { + offset: [55, 0], + opacity: 0.5, + rotateWithView: true, + rotation: Math.PI / 3.0, + scale: 1.5, + size: [55, 86] +}, { + offset: [212, 0], + opacity: 1.0, + rotateWithView: true, + rotation: 0.0, + scale: 1.0, + size: [44, 44] +}]; + +var i; + +var iconCount = iconInfo.length; +var icons = new Array(iconCount); +for (i = 0; i < iconCount; ++i) { + var info = iconInfo[i]; + icons[i] = new ol.style.Icon({ + offset: info.offset, + opacity: info.opacity, + rotateWithView: info.rotateWithView, + rotation: info.rotation, + scale: info.scale, + size: info.size, + src: 'data/Butterfly.png' + }); +} + +var featureCount = 50000; +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: icons[i % (iconCount - 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: 5 + }) +}); + +var overlayFeatures = []; +for (i = 0; i < featureCount; i += 30) { + var clone = features[i].clone(); + clone.setStyle(null); + overlayFeatures.push(clone); +} + +var featureOverlay = new ol.FeatureOverlay({ + map: map, + style: new ol.style.Style({ + image: icons[iconCount - 1] + }), + features: overlayFeatures +}); diff --git a/examples/symbol-atlas-webgl.html b/examples/symbol-atlas-webgl.html new file mode 100644 index 00000000000..666bc43ab5b --- /dev/null +++ b/examples/symbol-atlas-webgl.html @@ -0,0 +1,58 @@ + + + + + + + + + + + Symbols with WebGL example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Symbols with WebGL example

+

Using symbols in an atlas with WebGL.

+
+

When using symbol styles with WebGL, OpenLayers would render the symbol + on a temporary image and would create a WebGL texture for each image. For a + better performance, it is recommended to use atlas images (similar to + image sprites with CSS), so that the number of textures is reduced. OpenLayers + provides an AtlasManager, which when passed to the constructor + of a symbol style, will create atlases for the symbols.

+

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 00000000000..22423c2adf1 --- /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 initial size so that all symbols fit into + // a single atlas image + initialSize: 512 +}); + +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 = 50000; +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] + }) + ); + 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/externs/olx.js b/externs/olx.js index e850627709a..05013fc9b47 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -5438,7 +5438,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; @@ -5482,6 +5483,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 @@ -5661,7 +5672,8 @@ olx.style.IconOptions.prototype.src; * radius2: (number|undefined), * angle: (number|undefined), * snapToPixel: (boolean|undefined), - * stroke: (ol.style.Stroke|undefined)}} + * stroke: (ol.style.Stroke|undefined), + * atlasManager: (ol.style.AtlasManager|undefined)}} * @api */ olx.style.RegularShapeOptions; @@ -5740,6 +5752,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), @@ -6279,3 +6301,41 @@ olx.ViewState.prototype.resolution; * @api */ olx.ViewState.prototype.rotation; + + +/** + * @typedef {{initialSize: (number|undefined), + * maxSize: (number|undefined), + * space: (number|undefined)}} + * @api + */ +olx.style.AtlasManagerOptions; + + +/** + * The size in pixels of the first atlas image. If no value is given the + * `ol.INITIAL_ATLAS_SIZE` compile-time constant will be used. + * @type {number|undefined} + * @api + */ +olx.style.AtlasManagerOptions.prototype.initialSize; + + +/** + * The maximum size in pixels of atlas images. If no value is given then + * the `ol.MAX_ATLAS_SIZE` compile-time constant will be used. And if + * `ol.MAX_ATLAS_SIZE` is set to `-1` (the default) then + * `ol.WEBGL_MAX_TEXTURE_SIZE` will used if WebGL is supported. Otherwise + * 2048 is used. + * @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 1472c8b53c6..f86249f9767 100644 --- a/src/ol/has.js +++ b/src/ol/has.js @@ -119,21 +119,32 @@ ol.has.MSPOINTER = !!(goog.global.navigator.msPointerEnabled); * @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; + var textureSize; + var /** @type {Array.} */ extensions = []; + + 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)); + extensions = gl.getSupportedExtensions(); + } + } catch (e) {} + } + ol.has.WEBGL = hasWebGL; + ol.WEBGL_EXTENSIONS = extensions; + ol.WEBGL_MAX_TEXTURE_SIZE = textureSize; + } +})(); diff --git a/src/ol/ol.js b/src/ol/ol.js index 6e0968f9d36..01ca3649399 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -152,6 +152,13 @@ ol.ENABLE_WEBGL = true; ol.LEGACY_IE_SUPPORT = false; +/** + * @define {number} The size in pixels of the first atlas image. Default is + * `256`. + */ +ol.INITIAL_ATLAS_SIZE = 256; + + /** * The page is loaded using HTTPS. * @const @@ -175,6 +182,14 @@ ol.IS_LEGACY_IE = goog.userAgent.IE && ol.KEYBOARD_PAN_DURATION = 100; +/** + * @define {number} The maximum size in pixels of atlas images. Default is + * `-1`, meaning it is not used (and `ol.ol.WEBGL_MAX_TEXTURE_SIZE` is + * used instead). + */ +ol.MAX_ATLAS_SIZE = -1; + + /** * @define {number} Maximum mouse wheel delta. */ @@ -219,6 +234,24 @@ ol.SIMPLIFY_TOLERANCE = 0.5; ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024; +/** + * The maximum supported WebGL texture size in pixels. If WebGL is not + * supported, the value is set to `undefined`. + * @const + * @type {number|undefined} + * @api + */ +ol.WEBGL_MAX_TEXTURE_SIZE; // value is set in `ol.has` + + +/** + * List of supported WebGL extensions. + * @const + * @type {Array.} + */ +ol.WEBGL_EXTENSIONS; // value is set in `ol.has` + + /** * @define {number} Zoom slider animation duration. */ diff --git a/src/ol/render/canvas/canvasreplay.js b/src/ol/render/canvas/canvasreplay.js index 5d7b35aef6f..e2ab0f38e08 100644 --- a/src/ol/render/canvas/canvasreplay.js +++ b/src/ol/render/canvas/canvasreplay.js @@ -2011,7 +2011,7 @@ ol.render.canvas.ReplayGroup.prototype.forEachGeometryAtPixel = function( /** - * @inheritDoc + * FIXME empty description for jsdoc */ ol.render.canvas.ReplayGroup.prototype.finish = function() { var zKey; diff --git a/src/ol/render/ireplay.js b/src/ol/render/ireplay.js index 3f4d7c122a1..78e2faf67a0 100644 --- a/src/ol/render/ireplay.js +++ b/src/ol/render/ireplay.js @@ -35,13 +35,6 @@ ol.render.IReplayGroup = function() { }; -/** - * FIXME empty description for jsdoc - */ -ol.render.IReplayGroup.prototype.finish = function() { -}; - - /** * @param {number|undefined} zIndex Z index. * @param {ol.render.ReplayType} replayType Replay type. diff --git a/src/ol/render/ivectorcontext.js b/src/ol/render/ivectorcontext.js index 22ba1248318..e331bec0db5 100644 --- a/src/ol/render/ivectorcontext.js +++ b/src/ol/render/ivectorcontext.js @@ -5,8 +5,8 @@ goog.provide('ol.render.IVectorContext'); /** - * VectorContext interface. Currently implemented by - * {@link ol.render.canvas.Immediate} + * VectorContext interface. Implemented by + * {@link ol.render.canvas.Immediate} and {@link ol.render.webgl.Immediate}. * @interface */ ol.render.IVectorContext = function() { @@ -15,7 +15,7 @@ ol.render.IVectorContext = function() { /** * @param {number} zIndex Z index. - * @param {function(ol.render.canvas.Immediate)} callback Callback. + * @param {function(ol.render.IVectorContext)} callback Callback. */ ol.render.IVectorContext.prototype.drawAsync = function(zIndex, callback) { }; diff --git a/src/ol/render/webgl/webglimagecolor.glsl b/src/ol/render/webgl/webglimagecolor.glsl new file mode 100644 index 00000000000..56e045f4025 --- /dev/null +++ b/src/ol/render/webgl/webglimagecolor.glsl @@ -0,0 +1,46 @@ +//! NAMESPACE=ol.render.webgl.imagereplay.shader.Color +//! CLASS=ol.render.webgl.imagereplay.shader.Color + + +//! COMMON +varying vec2 v_texCoord; +varying float v_opacity; + +//! VERTEX +attribute vec2 a_position; +attribute vec2 a_texCoord; +attribute vec2 a_offsets; +attribute float a_opacity; +attribute float a_rotateWithView; + +uniform mat4 u_projectionMatrix; +uniform mat4 u_offsetScaleMatrix; +uniform mat4 u_offsetRotateMatrix; + +void main(void) { + mat4 offsetMatrix = u_offsetScaleMatrix; + if (a_rotateWithView == 1.0) { + offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix; + } + vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.); + gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets; + v_texCoord = a_texCoord; + v_opacity = a_opacity; +} + + +//! FRAGMENT +// @see https://svn.webkit.org/repository/webkit/trunk/Source/WebCore/platform/graphics/filters/skia/SkiaImageFilterBuilder.cpp +uniform mat4 u_colorMatrix; +uniform float u_opacity; +uniform sampler2D u_image; + +void main(void) { + vec4 texColor = texture2D(u_image, v_texCoord); + float alpha = texColor.a * v_opacity * u_opacity; + if (alpha == 0.0) { + discard; + } + gl_FragColor.a = alpha; + gl_FragColor.rgb = (u_colorMatrix * vec4(texColor.rgb, 1.)).rgb; +} diff --git a/src/ol/render/webgl/webglimagecolorshader.js b/src/ol/render/webgl/webglimagecolorshader.js new file mode 100644 index 00000000000..94f1b8a74bc --- /dev/null +++ b/src/ol/render/webgl/webglimagecolorshader.js @@ -0,0 +1,153 @@ +// This file is automatically generated, do not edit +goog.provide('ol.render.webgl.imagereplay.shader.Color'); + +goog.require('ol.webgl.shader'); + + + +/** + * @constructor + * @extends {ol.webgl.shader.Fragment} + * @struct + */ +ol.render.webgl.imagereplay.shader.ColorFragment = function() { + goog.base(this, ol.render.webgl.imagereplay.shader.ColorFragment.SOURCE); +}; +goog.inherits(ol.render.webgl.imagereplay.shader.ColorFragment, ol.webgl.shader.Fragment); +goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.ColorFragment); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.ColorFragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\nvarying float v_opacity;\n\n// @see https://svn.webkit.org/repository/webkit/trunk/Source/WebCore/platform/graphics/filters/skia/SkiaImageFilterBuilder.cpp\nuniform mat4 u_colorMatrix;\nuniform float u_opacity;\nuniform sampler2D u_image;\n\nvoid main(void) {\n vec4 texColor = texture2D(u_image, v_texCoord);\n float alpha = texColor.a * v_opacity * u_opacity;\n if (alpha == 0.0) {\n discard;\n }\n gl_FragColor.a = alpha;\n gl_FragColor.rgb = (u_colorMatrix * vec4(texColor.rgb, 1.)).rgb;\n}\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.ColorFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform mat4 k;uniform float l;uniform sampler2D m;void main(void){vec4 texColor=texture2D(m,a);float alpha=texColor.a*b*l;if(alpha==0.0){discard;}gl_FragColor.a=alpha;gl_FragColor.rgb=(k*vec4(texColor.rgb,1.)).rgb;}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.ColorFragment.SOURCE = goog.DEBUG ? + ol.render.webgl.imagereplay.shader.ColorFragment.DEBUG_SOURCE : + ol.render.webgl.imagereplay.shader.ColorFragment.OPTIMIZED_SOURCE; + + + +/** + * @constructor + * @extends {ol.webgl.shader.Vertex} + * @struct + */ +ol.render.webgl.imagereplay.shader.ColorVertex = function() { + goog.base(this, ol.render.webgl.imagereplay.shader.ColorVertex.SOURCE); +}; +goog.inherits(ol.render.webgl.imagereplay.shader.ColorVertex, ol.webgl.shader.Vertex); +goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.ColorVertex); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.ColorVertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\nattribute float a_rotateWithView;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n mat4 offsetMatrix = u_offsetScaleMatrix;\n if (a_rotateWithView == 1.0) {\n offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n }\n vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.ColorVertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;void main(void){mat4 offsetMatrix=i;if(g==1.0){offsetMatrix=i*j;}vec4 offsets=offsetMatrix*vec4(e,0.,0.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.ColorVertex.SOURCE = goog.DEBUG ? + ol.render.webgl.imagereplay.shader.ColorVertex.DEBUG_SOURCE : + ol.render.webgl.imagereplay.shader.ColorVertex.OPTIMIZED_SOURCE; + + + +/** + * @constructor + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLProgram} program Program. + * @struct + */ +ol.render.webgl.imagereplay.shader.Color.Locations = function(gl, program) { + + /** + * @type {WebGLUniformLocation} + */ + this.u_colorMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_colorMatrix' : 'k'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_image = gl.getUniformLocation( + program, goog.DEBUG ? 'u_image' : 'm'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetRotateMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_offsetRotateMatrix' : 'j'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetScaleMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_offsetScaleMatrix' : 'i'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_opacity = gl.getUniformLocation( + program, goog.DEBUG ? 'u_opacity' : 'l'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_projectionMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_projectionMatrix' : 'h'); + + /** + * @type {number} + */ + this.a_offsets = gl.getAttribLocation( + program, goog.DEBUG ? 'a_offsets' : 'e'); + + /** + * @type {number} + */ + this.a_opacity = gl.getAttribLocation( + program, goog.DEBUG ? 'a_opacity' : 'f'); + + /** + * @type {number} + */ + this.a_position = gl.getAttribLocation( + program, goog.DEBUG ? 'a_position' : 'c'); + + /** + * @type {number} + */ + this.a_rotateWithView = gl.getAttribLocation( + program, goog.DEBUG ? 'a_rotateWithView' : 'g'); + + /** + * @type {number} + */ + this.a_texCoord = gl.getAttribLocation( + program, goog.DEBUG ? 'a_texCoord' : 'd'); +}; diff --git a/src/ol/render/webgl/webglimagedefault.glsl b/src/ol/render/webgl/webglimagedefault.glsl new file mode 100644 index 00000000000..15d2bce128f --- /dev/null +++ b/src/ol/render/webgl/webglimagedefault.glsl @@ -0,0 +1,44 @@ +//! NAMESPACE=ol.render.webgl.imagereplay.shader.Default +//! CLASS=ol.render.webgl.imagereplay.shader.Default + + +//! COMMON +varying vec2 v_texCoord; +varying float v_opacity; + +//! VERTEX +attribute vec2 a_position; +attribute vec2 a_texCoord; +attribute vec2 a_offsets; +attribute float a_opacity; +attribute float a_rotateWithView; + +uniform mat4 u_projectionMatrix; +uniform mat4 u_offsetScaleMatrix; +uniform mat4 u_offsetRotateMatrix; + +void main(void) { + mat4 offsetMatrix = u_offsetScaleMatrix; + if (a_rotateWithView == 1.0) { + offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix; + } + vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.); + gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets; + v_texCoord = a_texCoord; + v_opacity = a_opacity; +} + + +//! FRAGMENT +uniform float u_opacity; +uniform sampler2D u_image; + +void main(void) { + vec4 texColor = texture2D(u_image, v_texCoord); + gl_FragColor.rgb = texColor.rgb; + float alpha = texColor.a * v_opacity * u_opacity; + if (alpha == 0.0) { + discard; + } + gl_FragColor.a = alpha; +} diff --git a/src/ol/render/webgl/webglimagedefaultshader.js b/src/ol/render/webgl/webglimagedefaultshader.js new file mode 100644 index 00000000000..7df0fe26c5e --- /dev/null +++ b/src/ol/render/webgl/webglimagedefaultshader.js @@ -0,0 +1,147 @@ +// This file is automatically generated, do not edit +goog.provide('ol.render.webgl.imagereplay.shader.Default'); + +goog.require('ol.webgl.shader'); + + + +/** + * @constructor + * @extends {ol.webgl.shader.Fragment} + * @struct + */ +ol.render.webgl.imagereplay.shader.DefaultFragment = function() { + goog.base(this, ol.render.webgl.imagereplay.shader.DefaultFragment.SOURCE); +}; +goog.inherits(ol.render.webgl.imagereplay.shader.DefaultFragment, ol.webgl.shader.Fragment); +goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.DefaultFragment); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.DefaultFragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\nvarying float v_opacity;\n\nuniform float u_opacity;\nuniform sampler2D u_image;\n\nvoid main(void) {\n vec4 texColor = texture2D(u_image, v_texCoord);\n gl_FragColor.rgb = texColor.rgb;\n float alpha = texColor.a * v_opacity * u_opacity;\n if (alpha == 0.0) {\n discard;\n }\n gl_FragColor.a = alpha;\n}\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.DefaultFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform float k;uniform sampler2D l;void main(void){vec4 texColor=texture2D(l,a);gl_FragColor.rgb=texColor.rgb;float alpha=texColor.a*b*k;if(alpha==0.0){discard;}gl_FragColor.a=alpha;}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.DefaultFragment.SOURCE = goog.DEBUG ? + ol.render.webgl.imagereplay.shader.DefaultFragment.DEBUG_SOURCE : + ol.render.webgl.imagereplay.shader.DefaultFragment.OPTIMIZED_SOURCE; + + + +/** + * @constructor + * @extends {ol.webgl.shader.Vertex} + * @struct + */ +ol.render.webgl.imagereplay.shader.DefaultVertex = function() { + goog.base(this, ol.render.webgl.imagereplay.shader.DefaultVertex.SOURCE); +}; +goog.inherits(ol.render.webgl.imagereplay.shader.DefaultVertex, ol.webgl.shader.Vertex); +goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.DefaultVertex); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.DefaultVertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\nattribute float a_rotateWithView;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n mat4 offsetMatrix = u_offsetScaleMatrix;\n if (a_rotateWithView == 1.0) {\n offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n }\n vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.DefaultVertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;void main(void){mat4 offsetMatrix=i;if(g==1.0){offsetMatrix=i*j;}vec4 offsets=offsetMatrix*vec4(e,0.,0.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.imagereplay.shader.DefaultVertex.SOURCE = goog.DEBUG ? + ol.render.webgl.imagereplay.shader.DefaultVertex.DEBUG_SOURCE : + ol.render.webgl.imagereplay.shader.DefaultVertex.OPTIMIZED_SOURCE; + + + +/** + * @constructor + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLProgram} program Program. + * @struct + */ +ol.render.webgl.imagereplay.shader.Default.Locations = function(gl, program) { + + /** + * @type {WebGLUniformLocation} + */ + this.u_image = gl.getUniformLocation( + program, goog.DEBUG ? 'u_image' : 'l'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetRotateMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_offsetRotateMatrix' : 'j'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetScaleMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_offsetScaleMatrix' : 'i'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_opacity = gl.getUniformLocation( + program, goog.DEBUG ? 'u_opacity' : 'k'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_projectionMatrix = gl.getUniformLocation( + program, goog.DEBUG ? 'u_projectionMatrix' : 'h'); + + /** + * @type {number} + */ + this.a_offsets = gl.getAttribLocation( + program, goog.DEBUG ? 'a_offsets' : 'e'); + + /** + * @type {number} + */ + this.a_opacity = gl.getAttribLocation( + program, goog.DEBUG ? 'a_opacity' : 'f'); + + /** + * @type {number} + */ + this.a_position = gl.getAttribLocation( + program, goog.DEBUG ? 'a_position' : 'c'); + + /** + * @type {number} + */ + this.a_rotateWithView = gl.getAttribLocation( + program, goog.DEBUG ? 'a_rotateWithView' : 'g'); + + /** + * @type {number} + */ + this.a_texCoord = gl.getAttribLocation( + program, goog.DEBUG ? 'a_texCoord' : 'd'); +}; diff --git a/src/ol/render/webgl/webglimmediate.js b/src/ol/render/webgl/webglimmediate.js index 375b0c96cf5..5095504b7a1 100644 --- a/src/ol/render/webgl/webglimmediate.js +++ b/src/ol/render/webgl/webglimmediate.js @@ -1,4 +1,8 @@ goog.provide('ol.render.webgl.Immediate'); +goog.require('goog.array'); +goog.require('goog.object'); +goog.require('ol.extent'); +goog.require('ol.render.webgl.ReplayGroup'); @@ -6,22 +10,103 @@ goog.provide('ol.render.webgl.Immediate'); * @constructor * @implements {ol.render.IVectorContext} * @param {ol.webgl.Context} context Context. + * @param {ol.Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {ol.Size} size Size. + * @param {ol.Extent} extent Extent. * @param {number} pixelRatio Pixel ratio. * @struct */ -ol.render.webgl.Immediate = function(context, pixelRatio) { +ol.render.webgl.Immediate = function(context, + center, resolution, rotation, size, extent, pixelRatio) { + + /** + * @private + */ + this.context_ = context; + + /** + * @private + */ + this.center_ = center; + + /** + * @private + */ + this.extent_ = extent; + + /** + * @private + */ + this.pixelRatio_ = pixelRatio; + + /** + * @private + */ + this.size_ = size; + + /** + * @private + */ + this.rotation_ = rotation; + + /** + * @private + */ + this.resolution_ = resolution; + + /** + * @private + * @type {ol.style.Image} + */ + this.imageStyle_ = null; + + /** + * @private + * @type {Object.>} + */ + this.callbacksByZIndex_ = {}; }; /** - * @inheritDoc + * FIXME: empty description for jsdoc + */ +ol.render.webgl.Immediate.prototype.flush = function() { + /** @type {Array.} */ + var zs = goog.array.map(goog.object.getKeys(this.callbacksByZIndex_), Number); + goog.array.sort(zs); + var i, ii, callbacks, j, jj; + for (i = 0, ii = zs.length; i < ii; ++i) { + callbacks = this.callbacksByZIndex_[zs[i].toString()]; + for (j = 0, jj = callbacks.length; j < jj; ++j) { + callbacks[j](this); + } + } +}; + + +/** + * @param {number} zIndex Z index. + * @param {function(ol.render.webgl.Immediate)} callback Callback. + * @api */ ol.render.webgl.Immediate.prototype.drawAsync = function(zIndex, callback) { + var zIndexKey = zIndex.toString(); + var callbacks = this.callbacksByZIndex_[zIndexKey]; + if (goog.isDef(callbacks)) { + callbacks.push(callback); + } else { + this.callbacksByZIndex_[zIndexKey] = [callback]; + } }; /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawCircleGeometry = function(circleGeometry, data) { @@ -30,29 +115,83 @@ ol.render.webgl.Immediate.prototype.drawCircleGeometry = /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawFeature = function(feature, style) { + var geometry = feature.getGeometry(); + if (!goog.isDefAndNotNull(geometry) || + !ol.extent.intersects(this.extent_, geometry.getExtent())) { + return; + } + var zIndex = style.getZIndex(); + if (!goog.isDef(zIndex)) { + zIndex = 0; + } + this.drawAsync(zIndex, function(render) { + render.setFillStrokeStyle(style.getFill(), style.getStroke()); + render.setImageStyle(style.getImage()); + render.setTextStyle(style.getText()); + var type = geometry.getType(); + var renderGeometry = ol.render.webgl.Immediate.GEOMETRY_RENDERERS_[type]; + // Do not assert since all kinds of geometries are not handled yet. + // In spite, render what we support. + if (renderGeometry) { + renderGeometry.call(render, geometry, null); + } + }); }; /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawGeometryCollectionGeometry = function(geometryCollectionGeometry, data) { + var geometries = geometryCollectionGeometry.getGeometriesArray(); + var renderers = ol.render.webgl.Immediate.GEOMETRY_RENDERERS_; + var i, ii; + for (i = 0, ii = geometries.length; i < ii; ++i) { + var geometry = geometries[i]; + var geometryRenderer = renderers[geometry.getType()]; + // Do not assert since all kinds of geometries are not handled yet. + // In order to support hierarchies, delegate instead what we can to + // valid renderers. + if (geometryRenderer) { + geometryRenderer.call(this, geometry, data); + } + } }; /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawPointGeometry = function(pointGeometry, data) { + var context = this.context_; + var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); + var replay = replayGroup.getReplay(0, ol.render.ReplayType.IMAGE); + replay.setImageStyle(this.imageStyle_); + replay.drawPointGeometry(pointGeometry, data); + replay.finish(context); + // default colors + var opacity = 1; + var brightness = 0; + var contrast = 1; + var hue = 0; + var saturation = 1; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.extent_, this.pixelRatio_, opacity, brightness, + contrast, hue, saturation, {}); + replay.getDeleteResourcesFunction(context)(); }; /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawLineStringGeometry = function(lineStringGeometry, data) { @@ -61,6 +200,7 @@ ol.render.webgl.Immediate.prototype.drawLineStringGeometry = /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawMultiLineStringGeometry = function(multiLineStringGeometry, data) { @@ -69,14 +209,32 @@ ol.render.webgl.Immediate.prototype.drawMultiLineStringGeometry = /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawMultiPointGeometry = function(multiPointGeometry, data) { + var context = this.context_; + var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); + var replay = replayGroup.getReplay(0, ol.render.ReplayType.IMAGE); + replay.setImageStyle(this.imageStyle_); + replay.drawMultiPointGeometry(multiPointGeometry, data); + replay.finish(context); + // default colors + var opacity = 1; + var brightness = 0; + var contrast = 1; + var hue = 0; + var saturation = 1; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.extent_, this.pixelRatio_, opacity, brightness, + contrast, hue, saturation, {}); + replay.getDeleteResourcesFunction(context)(); }; /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawMultiPolygonGeometry = function(multiPolygonGeometry, data) { @@ -85,6 +243,7 @@ ol.render.webgl.Immediate.prototype.drawMultiPolygonGeometry = /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawPolygonGeometry = function(polygonGeometry, data) { @@ -93,6 +252,7 @@ ol.render.webgl.Immediate.prototype.drawPolygonGeometry = /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.drawText = function(flatCoordinates, offset, end, stride, geometry, data) { @@ -101,6 +261,7 @@ ol.render.webgl.Immediate.prototype.drawText = /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { @@ -109,13 +270,31 @@ ol.render.webgl.Immediate.prototype.setFillStrokeStyle = /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.setImageStyle = function(imageStyle) { + this.imageStyle_ = imageStyle; }; /** * @inheritDoc + * @api */ ol.render.webgl.Immediate.prototype.setTextStyle = function(textStyle) { }; + + +/** + * @const + * @private + * @type {Object.} + */ +ol.render.webgl.Immediate.GEOMETRY_RENDERERS_ = { + 'Point': ol.render.webgl.Immediate.prototype.drawPointGeometry, + 'MultiPoint': ol.render.webgl.Immediate.prototype.drawMultiPointGeometry, + 'GeometryCollection': + ol.render.webgl.Immediate.prototype.drawGeometryCollectionGeometry +}; diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js new file mode 100644 index 00000000000..a2263934cab --- /dev/null +++ b/src/ol/render/webgl/webglreplay.js @@ -0,0 +1,841 @@ +goog.provide('ol.render.webgl.ImageReplay'); +goog.provide('ol.render.webgl.ReplayGroup'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.functions'); +goog.require('goog.object'); +goog.require('goog.vec.Mat4'); +goog.require('ol.color.Matrix'); +goog.require('ol.extent'); +goog.require('ol.render.IReplayGroup'); +goog.require('ol.render.webgl.imagereplay.shader.Color'); +goog.require('ol.render.webgl.imagereplay.shader.Default'); +goog.require('ol.vec.Mat4'); +goog.require('ol.webgl.Buffer'); + + + +/** + * @constructor + * @implements {ol.render.IVectorContext} + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @protected + * @struct + */ +ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { + + /** + * @type {number|undefined} + * @private + */ + this.anchorX_ = undefined; + + /** + * @type {number|undefined} + * @private + */ + this.anchorY_ = undefined; + + /** + * @private + * @type {ol.color.Matrix} + */ + this.colorMatrix_ = new ol.color.Matrix(); + + /** + * The origin of the coordinate system for the point coordinates sent to + * the GPU. To eliminate jitter caused by precision problems in the GPU + * we use the "Rendering Relative to Eye" technique described in the "3D + * Engine Design for Virtual Globes" book. + * @private + * @type {ol.Coordinate} + */ + this.origin_ = ol.extent.getCenter(maxExtent); + + /** + * @type {ol.Extent} + * @private + */ + this.extent_ = ol.extent.createEmpty(); + + /** + * @type {Array.} + * @private + */ + this.groupIndices_ = []; + + /** + * @type {number|undefined} + * @private + */ + this.height_ = undefined; + + /** + * @type {Array.} + * @private + */ + this.images_ = []; + + /** + * @type {number|undefined} + * @private + */ + this.imageHeight_ = undefined; + + /** + * @type {number|undefined} + * @private + */ + this.imageWidth_ = undefined; + + /** + * @type {Array.} + * @private + */ + this.indices_ = []; + + /** + * @type {ol.webgl.Buffer} + * @private + */ + this.indicesBuffer_ = null; + + /** + * @private + * @type {ol.render.webgl.imagereplay.shader.Color.Locations} + */ + this.colorLocations_ = null; + + /** + * @private + * @type {ol.render.webgl.imagereplay.shader.Default.Locations} + */ + this.defaultLocations_ = null; + + /** + * @private + * @type {number|undefined} + */ + this.opacity_ = undefined; + + /** + * @type {!goog.vec.Mat4.Number} + * @private + */ + this.offsetRotateMatrix_ = goog.vec.Mat4.createNumberIdentity(); + + /** + * @type {!goog.vec.Mat4.Number} + * @private + */ + this.offsetScaleMatrix_ = goog.vec.Mat4.createNumberIdentity(); + + /** + * @type {number|undefined} + * @private + */ + this.originX_ = undefined; + + /** + * @type {number|undefined} + * @private + */ + this.originY_ = undefined; + + /** + * @type {!goog.vec.Mat4.Number} + * @private + */ + this.projectionMatrix_ = goog.vec.Mat4.createNumberIdentity(); + + /** + * @private + * @type {boolean|undefined} + */ + this.rotateWithView_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.rotation_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.scale_ = undefined; + + /** + * @type {Array.} + * @private + */ + this.textures_ = []; + + /** + * @type {Array.} + * @private + */ + this.vertices_ = []; + + /** + * @type {ol.webgl.Buffer} + * @private + */ + this.verticesBuffer_ = null; + + /** + * @type {number|undefined} + * @private + */ + this.width_ = undefined; + +}; + + +/** + * @param {ol.webgl.Context} context WebGL context. + * @return {function()} Delete resources function. + */ +ol.render.webgl.ImageReplay.prototype.getDeleteResourcesFunction = + function(context) { + // We only delete our stuff here. The shaders and the program may + // be used by other ImageReplay instances (for other layers). And + // they will be deleted when disposing of the ol.webgl.Context + // object. + goog.asserts.assert(!goog.isNull(this.verticesBuffer_)); + goog.asserts.assert(!goog.isNull(this.indicesBuffer_)); + var verticesBuffer = this.verticesBuffer_; + var indicesBuffer = this.indicesBuffer_; + var textures = this.textures_; + var gl = context.getGL(); + return function() { + if (!gl.isContextLost()) { + var i, ii; + for (i = 0, ii = textures.length; i < ii; ++i) { + gl.deleteTexture(textures[i]); + } + } + context.deleteBuffer(verticesBuffer); + context.deleteBuffer(indicesBuffer); + }; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawAsync = goog.abstractMethod; + + +/** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @return {number} My end. + * @private + */ +ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = + function(flatCoordinates, offset, end, stride) { + goog.asserts.assert(goog.isDef(this.anchorX_)); + goog.asserts.assert(goog.isDef(this.anchorY_)); + goog.asserts.assert(goog.isDef(this.height_)); + goog.asserts.assert(goog.isDef(this.imageHeight_)); + goog.asserts.assert(goog.isDef(this.imageWidth_)); + goog.asserts.assert(goog.isDef(this.opacity_)); + goog.asserts.assert(goog.isDef(this.originX_)); + goog.asserts.assert(goog.isDef(this.originY_)); + goog.asserts.assert(goog.isDef(this.rotateWithView_)); + goog.asserts.assert(goog.isDef(this.rotation_)); + goog.asserts.assert(goog.isDef(this.scale_)); + goog.asserts.assert(goog.isDef(this.width_)); + var anchorX = this.anchorX_; + var anchorY = this.anchorY_; + var height = this.height_; + var imageHeight = this.imageHeight_; + var imageWidth = this.imageWidth_; + var opacity = this.opacity_; + var originX = this.originX_; + var originY = this.originY_; + var rotateWithView = this.rotateWithView_ ? 1.0 : 0.0; + var rotation = this.rotation_; + var scale = this.scale_; + var width = this.width_; + var cos = Math.cos(rotation); + var sin = Math.sin(rotation); + var numIndices = this.indices_.length; + var numVertices = this.vertices_.length; + var i, n, offsetX, offsetY, x, y; + for (i = offset; i < end; i += stride) { + x = flatCoordinates[i] - this.origin_[0]; + y = flatCoordinates[i + 1] - this.origin_[1]; + + // There are 4 vertices per [x, y] point, one for each corner of the + // rectangle we're going to draw. We'd use 1 vertex per [x, y] point if + // WebGL supported Geometry Shaders (which can emit new vertices), but that + // is not currently the case. + // + // And each vertex includes 8 values: the x and y coordinates, the x and + // y offsets used to calculate the position of the corner, the u and + // v texture coordinates for the corner, the opacity, and whether the + // the image should be rotated with the view (rotateWithView). + + n = numVertices / 8; + + // bottom-left corner + offsetX = -scale * anchorX; + offsetY = -scale * (height - anchorY); + this.vertices_[numVertices++] = x; + this.vertices_[numVertices++] = y; + this.vertices_[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices_[numVertices++] = offsetX * sin + offsetY * cos; + this.vertices_[numVertices++] = originX / imageWidth; + this.vertices_[numVertices++] = (originY + height) / imageHeight; + this.vertices_[numVertices++] = opacity; + this.vertices_[numVertices++] = rotateWithView; + + // bottom-right corner + offsetX = scale * (width - anchorX); + offsetY = -scale * (height - anchorY); + this.vertices_[numVertices++] = x; + this.vertices_[numVertices++] = y; + this.vertices_[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices_[numVertices++] = offsetX * sin + offsetY * cos; + this.vertices_[numVertices++] = (originX + width) / imageWidth; + this.vertices_[numVertices++] = (originY + height) / imageHeight; + this.vertices_[numVertices++] = opacity; + this.vertices_[numVertices++] = rotateWithView; + + // top-right corner + offsetX = scale * (width - anchorX); + offsetY = scale * anchorY; + this.vertices_[numVertices++] = x; + this.vertices_[numVertices++] = y; + this.vertices_[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices_[numVertices++] = offsetX * sin + offsetY * cos; + this.vertices_[numVertices++] = (originX + width) / imageWidth; + this.vertices_[numVertices++] = originY / imageHeight; + this.vertices_[numVertices++] = opacity; + this.vertices_[numVertices++] = rotateWithView; + + // top-left corner + offsetX = -scale * anchorX; + offsetY = scale * anchorY; + this.vertices_[numVertices++] = x; + this.vertices_[numVertices++] = y; + this.vertices_[numVertices++] = offsetX * cos - offsetY * sin; + this.vertices_[numVertices++] = offsetX * sin + offsetY * cos; + this.vertices_[numVertices++] = originX / imageWidth; + this.vertices_[numVertices++] = originY / imageHeight; + this.vertices_[numVertices++] = opacity; + this.vertices_[numVertices++] = rotateWithView; + + this.indices_[numIndices++] = n; + this.indices_[numIndices++] = n + 1; + this.indices_[numIndices++] = n + 2; + this.indices_[numIndices++] = n; + this.indices_[numIndices++] = n + 2; + this.indices_[numIndices++] = n + 3; + } + + return numVertices; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawCircleGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawFeature = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawGeometryCollectionGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawLineStringGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawMultiLineStringGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry = + function(multiPointGeometry, data) { + ol.extent.extend(this.extent_, multiPointGeometry.getExtent()); + var flatCoordinates = multiPointGeometry.getFlatCoordinates(); + var stride = multiPointGeometry.getStride(); + this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawMultiPolygonGeometry = + goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawPointGeometry = + function(pointGeometry, data) { + ol.extent.extend(this.extent_, pointGeometry.getExtent()); + var flatCoordinates = pointGeometry.getFlatCoordinates(); + var stride = pointGeometry.getStride(); + this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawPolygonGeometry = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.drawText = goog.abstractMethod; + + +/** + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.ImageReplay.prototype.finish = function(context) { + var gl = context.getGL(); + + this.groupIndices_.push(this.indices_.length); + goog.asserts.assert(this.images_.length == this.groupIndices_.length); + + // create, bind, and populate the vertices buffer + this.verticesBuffer_ = new ol.webgl.Buffer(this.vertices_); + context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); + + var indices = this.indices_; + var bits = context.hasOESElementIndexUint ? 32 : 16; + goog.asserts.assert(indices[indices.length - 1] < Math.pow(2, bits), + 'Too large element index detected [%s] (OES_element_index_uint "%s")', + indices[indices.length - 1], context.hasOESElementIndexUint); + + // create, bind, and populate the indices buffer + this.indicesBuffer_ = new ol.webgl.Buffer(indices); + context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); + + goog.asserts.assert(this.textures_.length === 0); + + // create textures + var texture, image, uid; + /** @type {Object.} */ + var texturePerImage = {}; + var i; + var ii = this.images_.length; + for (i = 0; i < ii; ++i) { + image = this.images_[i]; + + uid = goog.getUid(image).toString(); + if (goog.object.containsKey(texturePerImage, uid)) { + texture = goog.object.get(texturePerImage, uid); + } else { + texture = gl.createTexture(); + gl.bindTexture(goog.webgl.TEXTURE_2D, texture); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_WRAP_S, goog.webgl.CLAMP_TO_EDGE); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_WRAP_T, goog.webgl.CLAMP_TO_EDGE); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_MIN_FILTER, goog.webgl.LINEAR); + gl.texParameteri(goog.webgl.TEXTURE_2D, + goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.LINEAR); + gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA, + goog.webgl.UNSIGNED_BYTE, image); + goog.object.set(texturePerImage, uid, texture); + } + this.textures_[i] = texture; + } + + goog.asserts.assert(this.textures_.length == this.groupIndices_.length); + + this.anchorX_ = undefined; + this.anchorY_ = undefined; + this.height_ = undefined; + this.images_ = null; + this.imageHeight_ = undefined; + this.imageWidth_ = undefined; + this.indices_ = null; + this.opacity_ = undefined; + this.originX_ = undefined; + this.originY_ = undefined; + this.rotateWithView_ = undefined; + this.rotation_ = undefined; + this.scale_ = undefined; + this.vertices_ = null; + this.width_ = undefined; +}; + + +/** + * @return {ol.Extent} Extent. + */ +ol.render.webgl.ImageReplay.prototype.getExtent = function() { + return this.extent_; +}; + + +/** + * @param {ol.webgl.Context} context Context. + * @param {ol.Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {ol.Size} size Size. + * @param {ol.Extent} extent Extent. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {number} brightness Global brightness. + * @param {number} contrast Global contrast. + * @param {number} hue Global hue. + * @param {number} saturation Global saturation. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ImageReplay.prototype.replay = function(context, + center, resolution, rotation, size, extent, pixelRatio, + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash) { + var gl = context.getGL(); + + // bind the vertices buffer + goog.asserts.assert(!goog.isNull(this.verticesBuffer_)); + context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_); + + // bind the indices buffer + goog.asserts.assert(!goog.isNull(this.indicesBuffer_)); + context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); + + var useColor = brightness || contrast != 1 || hue || saturation != 1; + + // get the program + var fragmentShader, vertexShader; + if (useColor) { + fragmentShader = + ol.render.webgl.imagereplay.shader.ColorFragment.getInstance(); + vertexShader = + ol.render.webgl.imagereplay.shader.ColorVertex.getInstance(); + } else { + fragmentShader = + ol.render.webgl.imagereplay.shader.DefaultFragment.getInstance(); + vertexShader = + ol.render.webgl.imagereplay.shader.DefaultVertex.getInstance(); + } + var program = context.getProgram(fragmentShader, vertexShader); + + // get the locations + var locations; + if (useColor) { + if (goog.isNull(this.colorLocations_)) { + locations = + new ol.render.webgl.imagereplay.shader.Color.Locations(gl, program); + this.colorLocations_ = locations; + } else { + locations = this.colorLocations_; + } + } else { + if (goog.isNull(this.defaultLocations_)) { + locations = + new ol.render.webgl.imagereplay.shader.Default.Locations(gl, program); + this.defaultLocations_ = locations; + } else { + locations = this.defaultLocations_; + } + } + + // use the program (FIXME: use the return value) + context.useProgram(program); + + // enable the vertex attrib arrays + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, goog.webgl.FLOAT, + false, 32, 0); + + gl.enableVertexAttribArray(locations.a_offsets); + gl.vertexAttribPointer(locations.a_offsets, 2, goog.webgl.FLOAT, + false, 32, 8); + + gl.enableVertexAttribArray(locations.a_texCoord); + gl.vertexAttribPointer(locations.a_texCoord, 2, goog.webgl.FLOAT, + false, 32, 16); + + gl.enableVertexAttribArray(locations.a_opacity); + gl.vertexAttribPointer(locations.a_opacity, 1, goog.webgl.FLOAT, + false, 32, 24); + + gl.enableVertexAttribArray(locations.a_rotateWithView); + gl.vertexAttribPointer(locations.a_rotateWithView, 1, goog.webgl.FLOAT, + false, 32, 28); + + // set the "uniform" values + var projectionMatrix = this.projectionMatrix_; + ol.vec.Mat4.makeTransform2D(projectionMatrix, + 0.0, 0.0, + 2 / (resolution * size[0]), + 2 / (resolution * size[1]), + -rotation, + -(center[0] - this.origin_[0]), -(center[1] - this.origin_[1])); + + var offsetScaleMatrix = this.offsetScaleMatrix_; + goog.vec.Mat4.makeScale(offsetScaleMatrix, 2 / size[0], 2 / size[1], 1); + + var offsetRotateMatrix = this.offsetRotateMatrix_; + goog.vec.Mat4.makeIdentity(offsetRotateMatrix); + if (rotation !== 0) { + goog.vec.Mat4.rotateZ(offsetRotateMatrix, -rotation); + } + + gl.uniformMatrix4fv(locations.u_projectionMatrix, false, projectionMatrix); + gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, offsetScaleMatrix); + gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false, + offsetRotateMatrix); + gl.uniform1f(locations.u_opacity, opacity); + if (useColor) { + gl.uniformMatrix4fv(locations.u_colorMatrix, false, + this.colorMatrix_.getMatrix(brightness, contrast, hue, saturation)); + } + + // draw! + goog.asserts.assert(this.textures_.length == this.groupIndices_.length); + var i, ii, start; + for (i = 0, ii = this.textures_.length, start = 0; i < ii; ++i) { + gl.bindTexture(goog.webgl.TEXTURE_2D, this.textures_[i]); + var end = this.groupIndices_[i]; + var numItems = end - start; + var offsetInBytes = start * (context.hasOESElementIndexUint ? 4 : 2); + var elementType = context.hasOESElementIndexUint ? + goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT; + gl.drawElements(goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes); + start = end; + } + + // disable the vertex attrib arrays + gl.disableVertexAttribArray(locations.a_position); + gl.disableVertexAttribArray(locations.a_offsets); + gl.disableVertexAttribArray(locations.a_texCoord); + gl.disableVertexAttribArray(locations.a_opacity); + gl.disableVertexAttribArray(locations.a_rotateWithView); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.setFillStrokeStyle = goog.abstractMethod; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { + var anchor = imageStyle.getAnchor(); + goog.asserts.assert(!goog.isNull(anchor)); + var image = imageStyle.getImage(1); + goog.asserts.assert(!goog.isNull(image)); + var imageSize = imageStyle.getImageSize(); + goog.asserts.assert(!goog.isNull(imageSize)); + var opacity = imageStyle.getOpacity(); + goog.asserts.assert(goog.isDef(opacity)); + var origin = imageStyle.getOrigin(); + goog.asserts.assert(!goog.isNull(origin)); + var rotateWithView = imageStyle.getRotateWithView(); + goog.asserts.assert(goog.isDef(rotateWithView)); + var rotation = imageStyle.getRotation(); + goog.asserts.assert(goog.isDef(rotation)); + var size = imageStyle.getSize(); + goog.asserts.assert(!goog.isNull(size)); + var scale = imageStyle.getScale(); + goog.asserts.assert(goog.isDef(scale)); + + if (this.images_.length === 0) { + this.images_.push(image); + } else { + var currentImage = this.images_[this.images_.length - 1]; + if (goog.getUid(currentImage) != goog.getUid(image)) { + this.groupIndices_.push(this.indices_.length); + goog.asserts.assert(this.groupIndices_.length == this.images_.length); + this.images_.push(image); + } + } + + this.anchorX_ = anchor[0]; + this.anchorY_ = anchor[1]; + this.height_ = size[1]; + this.imageHeight_ = imageSize[1]; + this.imageWidth_ = imageSize[0]; + this.opacity_ = opacity; + this.originX_ = origin[0]; + this.originY_ = origin[1]; + this.rotation_ = rotation; + this.rotateWithView_ = rotateWithView; + this.scale_ = scale; + this.width_ = size[0]; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ImageReplay.prototype.setTextStyle = goog.abstractMethod; + + + +/** + * @constructor + * @implements {ol.render.IReplayGroup} + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @struct + */ +ol.render.webgl.ReplayGroup = function(tolerance, maxExtent) { + + /** + * @type {ol.Extent} + * @private + */ + this.maxExtent_ = maxExtent; + + /** + * @type {number} + * @private + */ + this.tolerance_ = tolerance; + + /** + * ImageReplay only is supported at this point. + * @type {Object.} + * @private + */ + this.replays_ = {}; + +}; + + +/** + * @param {ol.webgl.Context} context WebGL context. + * @return {function()} Delete resources function. + */ +ol.render.webgl.ReplayGroup.prototype.getDeleteResourcesFunction = + function(context) { + var functions = []; + var replayKey; + for (replayKey in this.replays_) { + functions.push( + this.replays_[replayKey].getDeleteResourcesFunction(context)); + } + return goog.functions.sequence.apply(null, functions); +}; + + +/** + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.ReplayGroup.prototype.finish = function(context) { + var replayKey; + for (replayKey in this.replays_) { + this.replays_[replayKey].finish(context); + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ReplayGroup.prototype.getReplay = + function(zIndex, replayType) { + var replay = this.replays_[replayType]; + if (!goog.isDef(replay)) { + var constructor = ol.render.webgl.BATCH_CONSTRUCTORS_[replayType]; + goog.asserts.assert(goog.isDef(constructor)); + replay = new constructor(this.tolerance_, this.maxExtent_); + this.replays_[replayType] = replay; + } + return replay; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { + return goog.object.isEmpty(this.replays_); +}; + + +/** + * @param {ol.webgl.Context} context Context. + * @param {ol.Coordinate} center Center. + * @param {number} resolution Resolution. + * @param {number} rotation Rotation. + * @param {ol.Size} size Size. + * @param {ol.Extent} extent Extent. + * @param {number} pixelRatio Pixel ratio. + * @param {number} opacity Global opacity. + * @param {number} brightness Global brightness. + * @param {number} contrast Global contrast. + * @param {number} hue Global hue. + * @param {number} saturation Global saturation. + * @param {Object} skippedFeaturesHash Ids of features to skip. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ReplayGroup.prototype.replay = function(context, + center, resolution, rotation, size, extent, pixelRatio, + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash) { + var i, ii, replay, result; + for (i = 0, ii = ol.render.REPLAY_ORDER.length; i < ii; ++i) { + replay = this.replays_[ol.render.REPLAY_ORDER[i]]; + if (goog.isDef(replay) && + ol.extent.intersects(extent, replay.getExtent())) { + result = replay.replay(context, + center, resolution, rotation, size, extent, pixelRatio, + opacity, brightness, contrast, hue, saturation, skippedFeaturesHash); + if (result) { + return result; + } + } + } + return undefined; +}; + + +/** + * @const + * @private + * @type {Object.} + */ +ol.render.webgl.BATCH_CONSTRUCTORS_ = { + 'Image': ol.render.webgl.ImageReplay +}; diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js index 25538ddf569..74bca84a42d 100644 --- a/src/ol/renderer/canvas/canvaslayerrenderer.js +++ b/src/ol/renderer/canvas/canvaslayerrenderer.js @@ -207,6 +207,14 @@ ol.renderer.canvas.Layer.prototype.getTransform = function(frameState) { }; +/** + * @param {olx.FrameState} frameState Frame state. + * @param {ol.layer.LayerState} layerState Layer state. + * @return {boolean} whether composeFrame should be called. + */ +ol.renderer.canvas.Layer.prototype.prepareFrame = goog.abstractMethod; + + /** * @param {ol.Size} size Size. * @return {boolean} True when the canvas with the current size does not exceed diff --git a/src/ol/renderer/dom/domlayerrenderer.js b/src/ol/renderer/dom/domlayerrenderer.js index df94a74b2e6..072e32435ea 100644 --- a/src/ol/renderer/dom/domlayerrenderer.js +++ b/src/ol/renderer/dom/domlayerrenderer.js @@ -46,3 +46,11 @@ ol.renderer.dom.Layer.prototype.composeFrame = goog.nullFunction; ol.renderer.dom.Layer.prototype.getTarget = function() { return this.target; }; + + +/** + * @param {olx.FrameState} frameState Frame state. + * @param {ol.layer.LayerState} layerState Layer state. + * @return {boolean} whether composeFrame should be called. + */ +ol.renderer.dom.Layer.prototype.prepareFrame = goog.abstractMethod; diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index ef9ee9029d4..356561bb0f1 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -6,7 +6,6 @@ goog.require('ol.ImageState'); goog.require('ol.TileRange'); goog.require('ol.TileState'); goog.require('ol.layer.Layer'); -goog.require('ol.layer.LayerState'); goog.require('ol.source.Source'); goog.require('ol.source.State'); goog.require('ol.source.Tile'); @@ -95,14 +94,6 @@ ol.renderer.Layer.prototype.handleImageChange = function(event) { }; -/** - * @param {olx.FrameState} frameState Frame state. - * @param {ol.layer.LayerState} layerState Layer state. - * @return {boolean} whether composeFrame should be called. - */ -ol.renderer.Layer.prototype.prepareFrame = goog.abstractMethod; - - /** * @protected */ diff --git a/src/ol/renderer/webgl/webglimagelayerrenderer.js b/src/ol/renderer/webgl/webglimagelayerrenderer.js index b8ce5a1fa07..6f31e1cf428 100644 --- a/src/ol/renderer/webgl/webglimagelayerrenderer.js +++ b/src/ol/renderer/webgl/webglimagelayerrenderer.js @@ -101,7 +101,7 @@ ol.renderer.webgl.ImageLayer.prototype.forEachFeatureAtPixel = * @inheritDoc */ ol.renderer.webgl.ImageLayer.prototype.prepareFrame = - function(frameState, layerState) { + function(frameState, layerState, context) { var gl = this.getWebGLMapRenderer().getGL(); diff --git a/src/ol/renderer/webgl/webgllayerrenderer.js b/src/ol/renderer/webgl/webgllayerrenderer.js index 823ffcc1848..4f09f7b881c 100644 --- a/src/ol/renderer/webgl/webgllayerrenderer.js +++ b/src/ol/renderer/webgl/webgllayerrenderer.js @@ -10,7 +10,7 @@ goog.require('ol.render.webgl.Immediate'); goog.require('ol.renderer.Layer'); goog.require('ol.renderer.webgl.map.shader.Color'); goog.require('ol.renderer.webgl.map.shader.Default'); -goog.require('ol.structs.Buffer'); +goog.require('ol.webgl.Buffer'); @@ -26,9 +26,9 @@ ol.renderer.webgl.Layer = function(mapRenderer, layer) { /** * @private - * @type {ol.structs.Buffer} + * @type {ol.webgl.Buffer} */ - this.arrayBuffer_ = new ol.structs.Buffer([ + this.arrayBuffer_ = new ol.webgl.Buffer([ -1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, @@ -237,7 +237,16 @@ ol.renderer.webgl.Layer.prototype.dispatchComposeEvent_ = function(type, context, frameState) { var layer = this.getLayer(); if (layer.hasListener(type)) { - var render = new ol.render.webgl.Immediate(context, frameState.pixelRatio); + var viewState = frameState.viewState; + var resolution = viewState.resolution; + var pixelRatio = frameState.pixelRatio; + var extent = frameState.extent; + var center = viewState.center; + var rotation = viewState.rotation; + var size = frameState.size; + + var render = new ol.render.webgl.Immediate( + context, center, resolution, rotation, size, extent, pixelRatio); var composeEvent = new ol.render.Event( type, layer, render, null, frameState, null, context); layer.dispatchEvent(composeEvent); @@ -286,3 +295,12 @@ ol.renderer.webgl.Layer.prototype.handleWebGLContextLost = function() { this.framebuffer = null; this.framebufferDimension = undefined; }; + + +/** + * @param {olx.FrameState} frameState Frame state. + * @param {ol.layer.LayerState} layerState Layer state. + * @param {ol.webgl.Context} context Context. + * @return {boolean} whether composeFrame should be called. + */ +ol.renderer.webgl.Layer.prototype.prepareFrame = goog.abstractMethod; diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 12d5982bee1..b7d65306700 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -20,13 +20,17 @@ goog.require('ol.dom'); goog.require('ol.layer.Image'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Tile'); +goog.require('ol.layer.Vector'); goog.require('ol.render.Event'); goog.require('ol.render.EventType'); goog.require('ol.render.webgl.Immediate'); +goog.require('ol.render.webgl.ReplayGroup'); goog.require('ol.renderer.Map'); +goog.require('ol.renderer.vector'); goog.require('ol.renderer.webgl.ImageLayer'); goog.require('ol.renderer.webgl.Layer'); goog.require('ol.renderer.webgl.TileLayer'); +goog.require('ol.renderer.webgl.VectorLayer'); goog.require('ol.source.State'); goog.require('ol.structs.LRUCache'); goog.require('ol.structs.PriorityQueue'); @@ -250,6 +254,8 @@ ol.renderer.webgl.Map.prototype.createLayerRenderer = function(layer) { return new ol.renderer.webgl.ImageLayer(this, layer); } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) { return new ol.renderer.webgl.TileLayer(this, layer); + } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) { + return new ol.renderer.webgl.VectorLayer(this, layer); } else { goog.asserts.fail(); return null; @@ -266,11 +272,40 @@ ol.renderer.webgl.Map.prototype.dispatchComposeEvent_ = function(type, frameState) { var map = this.getMap(); if (map.hasListener(type)) { - var context = this.getContext(); - var render = new ol.render.webgl.Immediate(context, frameState.pixelRatio); - var composeEvent = new ol.render.Event( - type, map, render, null, frameState, null, context); + var context = this.context_; + + var extent = frameState.extent; + var size = frameState.size; + var viewState = frameState.viewState; + var pixelRatio = frameState.pixelRatio; + + var resolution = viewState.resolution; + var center = viewState.center; + var rotation = viewState.rotation; + var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio); + + var vectorContext = new ol.render.webgl.Immediate(context, + center, resolution, rotation, size, extent, pixelRatio); + var replayGroup = new ol.render.webgl.ReplayGroup(tolerance, extent); + var composeEvent = new ol.render.Event(type, map, vectorContext, + replayGroup, frameState, null, context); map.dispatchEvent(composeEvent); + + replayGroup.finish(context); + if (!replayGroup.isEmpty()) { + // use default color values + var opacity = 1; + var brightness = 0; + var contrast = 1; + var hue = 0; + var saturation = 1; + replayGroup.replay(context, center, resolution, rotation, size, extent, + pixelRatio, opacity, brightness, contrast, hue, saturation, {}); + } + replayGroup.getDeleteResourcesFunction(context)(); + + vectorContext.flush(); + this.replayGroup = replayGroup; } }; @@ -455,7 +490,7 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { layerState.sourceState == ol.source.State.READY) { layerRenderer = this.getLayerRenderer(layerState.layer); goog.asserts.assertInstanceof(layerRenderer, ol.renderer.webgl.Layer); - if (layerRenderer.prepareFrame(frameState, layerState)) { + if (layerRenderer.prepareFrame(frameState, layerState, context)) { layerStatesToDraw.push(layerState); } } diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index b9cfb26b244..005eebcf382 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -16,8 +16,8 @@ goog.require('ol.layer.Tile'); goog.require('ol.math'); goog.require('ol.renderer.webgl.Layer'); goog.require('ol.renderer.webgl.tilelayer.shader'); -goog.require('ol.structs.Buffer'); goog.require('ol.tilecoord'); +goog.require('ol.webgl.Buffer'); @@ -52,9 +52,9 @@ ol.renderer.webgl.TileLayer = function(mapRenderer, tileLayer) { /** * @private - * @type {ol.structs.Buffer} + * @type {ol.webgl.Buffer} */ - this.renderArrayBuffer_ = new ol.structs.Buffer([ + this.renderArrayBuffer_ = new ol.webgl.Buffer([ 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, @@ -107,11 +107,10 @@ ol.renderer.webgl.TileLayer.prototype.handleWebGLContextLost = function() { * @inheritDoc */ ol.renderer.webgl.TileLayer.prototype.prepareFrame = - function(frameState, layerState) { + function(frameState, layerState, context) { var mapRenderer = this.getWebGLMapRenderer(); - var context = mapRenderer.getContext(); - var gl = mapRenderer.getGL(); + var gl = context.getGL(); var viewState = frameState.viewState; var projection = viewState.projection; diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js new file mode 100644 index 00000000000..b52f6ac512e --- /dev/null +++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js @@ -0,0 +1,240 @@ +goog.provide('ol.renderer.webgl.VectorLayer'); + +goog.require('goog.array'); +goog.require('goog.asserts'); +goog.require('goog.events'); +goog.require('ol.ViewHint'); +goog.require('ol.extent'); +goog.require('ol.layer.Vector'); +goog.require('ol.render.webgl.ReplayGroup'); +goog.require('ol.renderer.vector'); +goog.require('ol.renderer.webgl.Layer'); + + + +/** + * @constructor + * @extends {ol.renderer.webgl.Layer} + * @param {ol.renderer.Map} mapRenderer Map renderer. + * @param {ol.layer.Vector} vectorLayer Vector layer. + */ +ol.renderer.webgl.VectorLayer = function(mapRenderer, vectorLayer) { + + goog.base(this, mapRenderer, vectorLayer); + + /** + * @private + * @type {boolean} + */ + this.dirty_ = false; + + /** + * @private + * @type {number} + */ + this.renderedRevision_ = -1; + + /** + * @private + * @type {number} + */ + this.renderedResolution_ = NaN; + + /** + * @private + * @type {ol.Extent} + */ + this.renderedExtent_ = ol.extent.createEmpty(); + + /** + * @private + * @type {function(ol.Feature, ol.Feature): number|null} + */ + this.renderedRenderOrder_ = null; + + /** + * @private + * @type {ol.render.webgl.ReplayGroup} + */ + this.replayGroup_ = null; + +}; +goog.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer); + + +/** + * @inheritDoc + */ +ol.renderer.webgl.VectorLayer.prototype.composeFrame = + function(frameState, layerState, context) { + var viewState = frameState.viewState; + var replayGroup = this.replayGroup_; + if (!goog.isNull(replayGroup) && !replayGroup.isEmpty()) { + replayGroup.replay(context, + viewState.center, viewState.resolution, viewState.rotation, + frameState.size, frameState.extent, frameState.pixelRatio, + layerState.opacity, layerState.brightness, layerState.contrast, + layerState.hue, layerState.saturation, frameState.skippedFeatureUids); + } + +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.VectorLayer.prototype.disposeInternal = function() { + var replayGroup = this.replayGroup_; + if (!goog.isNull(replayGroup)) { + var mapRenderer = this.getWebGLMapRenderer(); + var context = mapRenderer.getContext(); + replayGroup.getDeleteResourcesFunction(context)(); + this.replayGroup_ = null; + } + goog.base(this, 'disposeInternal'); +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtPixel = + function(coordinate, frameState, callback, thisArg) { +}; + + +/** + * Handle changes in image style state. + * @param {goog.events.Event} event Image style change event. + * @private + */ +ol.renderer.webgl.VectorLayer.prototype.handleImageChange_ = + function(event) { + this.renderIfReadyAndVisible(); +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.VectorLayer.prototype.prepareFrame = + function(frameState, layerState, context) { + + var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer()); + goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector); + var vectorSource = vectorLayer.getSource(); + + this.updateAttributions( + frameState.attributions, vectorSource.getAttributions()); + this.updateLogos(frameState, vectorSource); + + if (!this.dirty_ && (frameState.viewHints[ol.ViewHint.ANIMATING] || + frameState.viewHints[ol.ViewHint.INTERACTING])) { + return true; + } + + var frameStateExtent = frameState.extent; + var viewState = frameState.viewState; + var projection = viewState.projection; + var resolution = viewState.resolution; + var pixelRatio = frameState.pixelRatio; + var vectorLayerRevision = vectorLayer.getRevision(); + var vectorLayerRenderOrder = vectorLayer.getRenderOrder(); + if (!goog.isDef(vectorLayerRenderOrder)) { + vectorLayerRenderOrder = ol.renderer.vector.defaultOrder; + } + + if (!this.dirty_ && + this.renderedResolution_ == resolution && + this.renderedRevision_ == vectorLayerRevision && + this.renderedRenderOrder_ == vectorLayerRenderOrder && + ol.extent.containsExtent(this.renderedExtent_, frameStateExtent)) { + return true; + } + + var extent = this.renderedExtent_; + var xBuffer = ol.extent.getWidth(frameStateExtent) / 4; + var yBuffer = ol.extent.getHeight(frameStateExtent) / 4; + extent[0] = frameStateExtent[0] - xBuffer; + extent[1] = frameStateExtent[1] - yBuffer; + extent[2] = frameStateExtent[2] + xBuffer; + extent[3] = frameStateExtent[3] + yBuffer; + + if (!goog.isNull(this.replayGroup_)) { + frameState.postRenderFunctions.push( + this.replayGroup_.getDeleteResourcesFunction(context)); + } + + this.dirty_ = false; + + var replayGroup = new ol.render.webgl.ReplayGroup( + ol.renderer.vector.getTolerance(resolution, pixelRatio), + extent); + vectorSource.loadFeatures(extent, resolution, projection); + var renderFeature = + /** + * @param {ol.Feature} feature Feature. + * @this {ol.renderer.webgl.VectorLayer} + */ + function(feature) { + var styles; + if (goog.isDef(feature.getStyleFunction())) { + styles = feature.getStyleFunction().call(feature, resolution); + } else if (goog.isDef(vectorLayer.getStyleFunction())) { + styles = vectorLayer.getStyleFunction()(feature, resolution); + } + if (goog.isDefAndNotNull(styles)) { + var dirty = this.renderFeature( + feature, resolution, pixelRatio, styles, replayGroup); + this.dirty_ = this.dirty_ || dirty; + } + }; + if (!goog.isNull(vectorLayerRenderOrder)) { + /** @type {Array.} */ + var features = []; + vectorSource.forEachFeatureInExtentAtResolution(extent, resolution, + /** + * @param {ol.Feature} feature Feature. + */ + function(feature) { + features.push(feature); + }, this); + goog.array.sort(features, vectorLayerRenderOrder); + goog.array.forEach(features, renderFeature, this); + } else { + vectorSource.forEachFeatureInExtentAtResolution( + extent, resolution, renderFeature, this); + } + replayGroup.finish(context); + + this.renderedResolution_ = resolution; + this.renderedRevision_ = vectorLayerRevision; + this.renderedRenderOrder_ = vectorLayerRenderOrder; + this.replayGroup_ = replayGroup; + + return true; +}; + + +/** + * @param {ol.Feature} feature Feature. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {Array.} styles Array of styles + * @param {ol.render.webgl.ReplayGroup} replayGroup Replay group. + * @return {boolean} `true` if an image is loading. + */ +ol.renderer.webgl.VectorLayer.prototype.renderFeature = + function(feature, resolution, pixelRatio, styles, replayGroup) { + if (!goog.isDefAndNotNull(styles)) { + return false; + } + var i, ii, loading = false; + for (i = 0, ii = styles.length; i < ii; ++i) { + loading = ol.renderer.vector.renderFeature( + replayGroup, feature, styles[i], + ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), + feature, this.handleImageChange_, this) || loading; + } + return loading; +}; diff --git a/src/ol/structs/buffer.js b/src/ol/structs/buffer.js deleted file mode 100644 index 29ddb99b560..00000000000 --- a/src/ol/structs/buffer.js +++ /dev/null @@ -1,257 +0,0 @@ -goog.provide('ol.structs.Buffer'); - -goog.require('goog.array'); -goog.require('goog.asserts'); -goog.require('goog.webgl'); -goog.require('ol'); -goog.require('ol.structs.IntegerSet'); - - -/** - * @enum {number} - */ -ol.structs.BufferUsage = { - STATIC_DRAW: goog.webgl.STATIC_DRAW, - STREAM_DRAW: goog.webgl.STREAM_DRAW, - DYNAMIC_DRAW: goog.webgl.DYNAMIC_DRAW -}; - - - -/** - * @constructor - * @param {Array.=} opt_arr Array. - * @param {number=} opt_used Used. - * @param {number=} opt_usage Usage. - * @struct - */ -ol.structs.Buffer = function(opt_arr, opt_used, opt_usage) { - - /** - * @private - * @type {Array.} - */ - this.arr_ = goog.isDef(opt_arr) ? opt_arr : []; - - /** - * @private - * @type {Array.} - */ - this.dirtySets_ = []; - - /** - * @private - * @type {ol.structs.IntegerSet} - */ - this.freeSet_ = new ol.structs.IntegerSet(); - - var used = goog.isDef(opt_used) ? opt_used : this.arr_.length; - if (used < this.arr_.length) { - this.freeSet_.addRange(used, this.arr_.length); - } - if (ol.BUFFER_REPLACE_UNUSED_ENTRIES_WITH_NANS) { - var arr = this.arr_; - var n = arr.length; - var i; - for (i = used; i < n; ++i) { - arr[i] = NaN; - } - } - - /** - * @private - * @type {?Float32Array} - */ - this.split32_ = null; - - /** - * @private - * @type {ol.structs.IntegerSet} - */ - this.split32DirtySet_ = null; - - /** - * @private - * @type {number} - */ - this.usage_ = goog.isDef(opt_usage) ? - opt_usage : ol.structs.BufferUsage.STATIC_DRAW; - -}; - - -/** - * @param {number} size Size. - * @return {number} Offset. - */ -ol.structs.Buffer.prototype.allocate = function(size) { - goog.asserts.assert(size > 0); - var offset = this.freeSet_.findRange(size); - goog.asserts.assert(offset != -1); // FIXME - this.freeSet_.removeRange(offset, offset + size); - return offset; -}; - - -/** - * @param {Array.} values Values. - * @return {number} Offset. - */ -ol.structs.Buffer.prototype.add = function(values) { - var size = values.length; - var offset = this.allocate(size); - var i; - for (i = 0; i < size; ++i) { - this.arr_[offset + i] = values[i]; - } - this.markDirty(size, offset); - return offset; -}; - - -/** - * @param {ol.structs.IntegerSet} dirtySet Dirty set. - */ -ol.structs.Buffer.prototype.addDirtySet = function(dirtySet) { - goog.asserts.assert(!goog.array.contains(this.dirtySets_, dirtySet)); - this.dirtySets_.push(dirtySet); -}; - - -/** - * @param {function(this: T, number, number)} f Callback. - * @param {T=} opt_this The object to use as `this` in `f`. - * @template T - */ -ol.structs.Buffer.prototype.forEachRange = function(f, opt_this) { - if (this.arr_.length !== 0) { - this.freeSet_.forEachRangeInverted(0, this.arr_.length, f, opt_this); - } -}; - - -/** - * @return {Array.} Array. - */ -ol.structs.Buffer.prototype.getArray = function() { - return this.arr_; -}; - - -/** - * @return {number} Count. - */ -ol.structs.Buffer.prototype.getCount = function() { - return this.arr_.length - this.freeSet_.getSize(); -}; - - -/** - * @return {ol.structs.IntegerSet} Free set. - */ -ol.structs.Buffer.prototype.getFreeSet = function() { - return this.freeSet_; -}; - - -/** - * Returns a Float32Array twice the length of the buffer containing each value - * split into two 32-bit floating point values that, when added together, - * approximate the original value. Even indicies contain the high bits, odd - * indicies contain the low bits. - * @see http://blogs.agi.com/insight3d/index.php/2008/09/03/precisions-precisions/ - * @return {Float32Array} Split. - */ -ol.structs.Buffer.prototype.getSplit32 = function() { - var arr = this.arr_; - var n = arr.length; - if (goog.isNull(this.split32DirtySet_)) { - this.split32DirtySet_ = new ol.structs.IntegerSet([0, n]); - this.addDirtySet(this.split32DirtySet_); - } - if (goog.isNull(this.split32_)) { - this.split32_ = new Float32Array(2 * n); - } - var split32 = this.split32_; - this.split32DirtySet_.forEachRange(function(start, stop) { - var doubleHigh, i, j, value; - for (i = start, j = 2 * start; i < stop; ++i, j += 2) { - value = arr[i]; - if (value < 0) { - doubleHigh = 65536 * Math.floor(-value / 65536); - split32[j] = -doubleHigh; - split32[j + 1] = value + doubleHigh; - } else { - doubleHigh = 65536 * Math.floor(value / 65536); - split32[j] = doubleHigh; - split32[j + 1] = value - doubleHigh; - } - } - }); - this.split32DirtySet_.clear(); - return this.split32_; -}; - - -/** - * @return {number} Usage. - */ -ol.structs.Buffer.prototype.getUsage = function() { - return this.usage_; -}; - - -/** - * @param {number} size Size. - * @param {number} offset Offset. - */ -ol.structs.Buffer.prototype.markDirty = function(size, offset) { - var i, ii; - for (i = 0, ii = this.dirtySets_.length; i < ii; ++i) { - this.dirtySets_[i].addRange(offset, offset + size); - } -}; - - -/** - * @param {number} size Size. - * @param {number} offset Offset. - */ -ol.structs.Buffer.prototype.remove = function(size, offset) { - var i, ii; - this.freeSet_.addRange(offset, offset + size); - for (i = 0, ii = this.dirtySets_.length; i < ii; ++i) { - this.dirtySets_[i].removeRange(offset, offset + size); - } - if (ol.BUFFER_REPLACE_UNUSED_ENTRIES_WITH_NANS) { - var arr = this.arr_; - for (i = 0; i < size; ++i) { - arr[offset + i] = NaN; - } - } -}; - - -/** - * @param {ol.structs.IntegerSet} dirtySet Dirty set. - */ -ol.structs.Buffer.prototype.removeDirtySet = function(dirtySet) { - var removed = goog.array.remove(this.dirtySets_, dirtySet); - goog.asserts.assert(removed); -}; - - -/** - * @param {Array.} values Values. - * @param {number} offset Offset. - */ -ol.structs.Buffer.prototype.set = function(values, offset) { - var arr = this.arr_; - var n = values.length; - goog.asserts.assert(0 <= offset && offset + n <= arr.length); - var i; - for (i = 0; i < n; ++i) { - arr[offset + i] = values[i]; - } - this.markDirty(n, offset); -}; diff --git a/src/ol/structs/checksum.js b/src/ol/structs/checksum.js new file mode 100644 index 00000000000..ff72308ad21 --- /dev/null +++ b/src/ol/structs/checksum.js @@ -0,0 +1,16 @@ +goog.provide('ol.structs.IHasChecksum'); + + + +/** + * @interface + */ +ol.structs.IHasChecksum = function() { +}; + + +/** + * @return {string} The checksum. + */ +ol.structs.IHasChecksum.prototype.getChecksum = function() { +}; diff --git a/src/ol/structs/integerset.js b/src/ol/structs/integerset.js deleted file mode 100644 index de716c6842f..00000000000 --- a/src/ol/structs/integerset.js +++ /dev/null @@ -1,330 +0,0 @@ -goog.provide('ol.structs.IntegerSet'); - -goog.require('goog.asserts'); - - - -/** - * A set of integers represented as a set of integer ranges. - * This implementation is designed for the case when the number of distinct - * integer ranges is small. - * @constructor - * @struct - * @param {Array.=} opt_arr Array. - */ -ol.structs.IntegerSet = function(opt_arr) { - - /** - * @private - * @type {Array.} - */ - this.arr_ = goog.isDef(opt_arr) ? opt_arr : []; - - if (goog.DEBUG) { - this.assertValid(); - } - -}; - - -/** - * @param {number} addStart Start. - * @param {number} addStop Stop. - */ -ol.structs.IntegerSet.prototype.addRange = function(addStart, addStop) { - goog.asserts.assert(addStart <= addStop); - if (addStart == addStop) { - return; - } - var arr = this.arr_; - var n = arr.length; - var i; - for (i = 0; i < n; i += 2) { - if (addStart <= arr[i]) { - // FIXME check if splice is really needed - arr.splice(i, 0, addStart, addStop); - this.compactRanges_(); - return; - } - } - arr.push(addStart, addStop); - this.compactRanges_(); -}; - - -/** - * FIXME empty description for jsdoc - */ -ol.structs.IntegerSet.prototype.assertValid = function() { - var arr = this.arr_; - var n = arr.length; - goog.asserts.assert(n % 2 === 0); - var i; - for (i = 1; i < n; ++i) { - goog.asserts.assert(arr[i] > arr[i - 1]); - } -}; - - -/** - * FIXME empty description for jsdoc - */ -ol.structs.IntegerSet.prototype.clear = function() { - this.arr_.length = 0; -}; - - -/** - * @private - */ -ol.structs.IntegerSet.prototype.compactRanges_ = function() { - var arr = this.arr_; - var n = arr.length; - var rangeIndex = 0; - var i; - for (i = 0; i < n; i += 2) { - if (arr[i] == arr[i + 1]) { - // pass - } else if (rangeIndex > 0 && - arr[rangeIndex - 2] <= arr[i] && - arr[i] <= arr[rangeIndex - 1]) { - arr[rangeIndex - 1] = Math.max(arr[rangeIndex - 1], arr[i + 1]); - } else { - arr[rangeIndex++] = arr[i]; - arr[rangeIndex++] = arr[i + 1]; - } - } - arr.length = rangeIndex; -}; - - -/** - * Finds the start of smallest range that is at least of length minSize, or -1 - * if no such range exists. - * @param {number} minSize Minimum size. - * @return {number} Index. - */ -ol.structs.IntegerSet.prototype.findRange = function(minSize) { - goog.asserts.assert(minSize > 0); - var arr = this.arr_; - var n = arr.length; - var bestIndex = -1; - var bestSize, i, size; - for (i = 0; i < n; i += 2) { - size = arr[i + 1] - arr[i]; - if (size == minSize) { - return arr[i]; - } else if (size > minSize && (bestIndex == -1 || size < bestSize)) { - bestIndex = arr[i]; - bestSize = size; - } - } - return bestIndex; -}; - - -/** - * Calls f with each integer range. - * @param {function(this: T, number, number)} f Callback. - * @param {T=} opt_this The object to use as `this` in `f`. - * @template T - */ -ol.structs.IntegerSet.prototype.forEachRange = function(f, opt_this) { - var arr = this.arr_; - var n = arr.length; - var i; - for (i = 0; i < n; i += 2) { - f.call(opt_this, arr[i], arr[i + 1]); - } -}; - - -/** - * Calls f with each integer range not in [start, stop) - 'this'. - * @param {number} start Start. - * @param {number} stop Stop. - * @param {function(this: T, number, number)} f Callback. - * @param {T=} opt_this The object to use as `this` in `f`. - * @template T - */ -ol.structs.IntegerSet.prototype.forEachRangeInverted = - function(start, stop, f, opt_this) { - goog.asserts.assert(start < stop); - var arr = this.arr_; - var n = arr.length; - if (n === 0) { - f.call(opt_this, start, stop); - } else { - if (start < arr[0]) { - f.call(opt_this, start, arr[0]); - } - var i; - for (i = 1; i < n - 1; i += 2) { - f.call(opt_this, arr[i], arr[i + 1]); - } - if (arr[n - 1] < stop) { - f.call(opt_this, arr[n - 1], stop); - } - } -}; - - -/** - * @return {Array.} Array. - */ -ol.structs.IntegerSet.prototype.getArray = function() { - return this.arr_; -}; - - -/** - * Returns the first element in the set, or -1 if the set is empty. - * @return {number} Start. - */ -ol.structs.IntegerSet.prototype.getFirst = function() { - return this.arr_.length === 0 ? -1 : this.arr_[0]; -}; - - -/** - * Returns the first integer after the last element in the set, or -1 if the - * set is empty. - * @return {number} Last. - */ -ol.structs.IntegerSet.prototype.getLast = function() { - var n = this.arr_.length; - return n === 0 ? -1 : this.arr_[n - 1]; -}; - - -/** - * Returns the number of integers in the set. - * @return {number} Size. - */ -ol.structs.IntegerSet.prototype.getSize = function() { - var arr = this.arr_; - var n = arr.length; - var size = 0; - var i; - for (i = 0; i < n; i += 2) { - size += arr[i + 1] - arr[i]; - } - return size; -}; - - -/** - * @param {number} start Start. - * @param {number} stop Stop. - * @return {boolean} Intersects range. - */ -ol.structs.IntegerSet.prototype.intersectsRange = function(start, stop) { - goog.asserts.assert(start <= stop); - if (start == stop) { - return false; - } else { - var arr = this.arr_; - var n = arr.length; - var i = 0; - for (i = 0; i < n; i += 2) { - if (arr[i] <= start && start < arr[i + 1] || - arr[i] < stop && stop - 1 < arr[i + 1] || - start < arr[i] && arr[i + 1] <= stop) { - return true; - } - } - return false; - } -}; - - -/** - * @return {boolean} Is empty. - */ -ol.structs.IntegerSet.prototype.isEmpty = function() { - return this.arr_.length === 0; -}; - - -/** - * @return {Array.} Array. - */ -ol.structs.IntegerSet.prototype.pack = function() { - return this.arr_; -}; - - -/** - * @param {number} removeStart Start. - * @param {number} removeStop Stop. - */ -ol.structs.IntegerSet.prototype.removeRange = - function(removeStart, removeStop) { - // FIXME this could be more efficient - goog.asserts.assert(removeStart <= removeStop); - var arr = this.arr_; - var n = arr.length; - var i; - for (i = 0; i < n; i += 2) { - if (removeStop < arr[i] || arr[i + 1] < removeStart) { - continue; - } else if (arr[i] > removeStop) { - break; - } - if (removeStart < arr[i]) { - if (removeStop == arr[i]) { - break; - } else if (removeStop < arr[i + 1]) { - arr[i] = Math.max(arr[i], removeStop); - break; - } else { - arr.splice(i, 2); - i -= 2; - n -= 2; - } - } else if (removeStart == arr[i]) { - if (removeStop < arr[i + 1]) { - arr[i] = removeStop; - break; - } else if (removeStop == arr[i + 1]) { - arr.splice(i, 2); - break; - } else { - arr.splice(i, 2); - i -= 2; - n -= 2; - } - } else { - if (removeStop < arr[i + 1]) { - arr.splice(i, 2, arr[i], removeStart, removeStop, arr[i + 1]); - break; - } else if (removeStop == arr[i + 1]) { - arr[i + 1] = removeStart; - break; - } else { - arr[i + 1] = removeStart; - } - } - } - this.compactRanges_(); -}; - - -if (goog.DEBUG) { - - /** - * @return {string} String. - */ - ol.structs.IntegerSet.prototype.toString = function() { - var arr = this.arr_; - var n = arr.length; - var result = new Array(n / 2); - var resultIndex = 0; - var i; - for (i = 0; i < n; i += 2) { - result[resultIndex++] = arr[i] + '-' + arr[i + 1]; - } - return result.join(', '); - }; - -} diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js new file mode 100644 index 00000000000..a3cb260ecad --- /dev/null +++ b/src/ol/style/atlasmanager.js @@ -0,0 +1,440 @@ +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'); +goog.require('ol'); + + +/** + * Provides information for an image inside an atlas manager. + * `offsetX` and `offsetY` is the position of the image inside + * the atlas image `image`. + * `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.AtlasManagerInfo; + + + +/** + * 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). + * + * If an application uses many images or very large images, it is recommended + * 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.initialSize) ? + options.initialSize : ol.INITIAL_ATLAS_SIZE; + + /** + * The maximum size in pixels of atlas images. + * @private + * @type {number} + */ + this.maxSize_ = goog.isDef(options.maxSize) ? + options.maxSize : ol.MAX_ATLAS_SIZE != -1 ? + ol.MAX_ATLAS_SIZE : goog.isDef(ol.WEBGL_MAX_TEXTURE_SIZE) ? + ol.WEBGL_MAX_TEXTURE_SIZE : 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_)]; + + /** + * 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.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 (goog.isNull(info)) { + 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 = atlases.length; i < ii; ++i) { + atlas = atlases[i]; + info = atlas.get(id); + if (!goog.isNull(info)) { + return info; + } + } + return null; +}; + + +/** + * @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: goog.isNull(hitInfo) ? undefined : hitInfo.offsetX, + hitOffsetY: goog.isNull(hitInfo) ? undefined : hitInfo.offsetY, + hitImage: goog.isNull(hitInfo) ? 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` 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_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 (goog.isNull(info)) { + return null; + } + + /** @type {?ol.style.AtlasInfo} */ + var hitInfo = null; + if (goog.isDef(opt_renderHitCallback)) { + 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 = atlases.length; i < ii; ++i) { + atlas = atlases[i]; + info = atlas.add(id, width, height, renderCallback, opt_this); + if (!goog.isNull(info)) { + return info; + } else if (goog.isNull(info) && 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. + 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; + } + } + goog.asserts.fail(); +}; + + +/** + * 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. + * + * 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. + * 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} + */ + 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 8901d8362b3..96632d06aaa 100644 --- a/src/ol/style/circlestyle.js +++ b/src/ol/style/circlestyle.js @@ -1,10 +1,12 @@ goog.provide('ol.style.Circle'); +goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('ol.color'); goog.require('ol.has'); 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'); @@ -19,18 +21,24 @@ 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) { var options = goog.isDef(opt_options) ? opt_options : {}; + /** + * @private + * @type {Array.} + */ + this.checksums_ = null; + /** * @private * @type {HTMLCanvasElement} */ - this.canvas_ = /** @type {HTMLCanvasElement} */ - (goog.dom.createElement(goog.dom.TagName.CANVAS)); + this.canvas_ = null; /** * @private @@ -46,9 +54,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 @@ -58,23 +66,41 @@ ol.style.Circle = function(opt_options) { /** * @private - * @type {ol.style.Stroke} + * @type {Array.} */ - this.stroke_ = goog.isDef(options.stroke) ? options.stroke : null; + this.origin_ = [0, 0]; - var size = this.render_(); + /** + * @private + * @type {Array.} + */ + this.hitDetectionOrigin_ = [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 {ol.Size} + */ + this.imageSize_ = null; + + /** + * @private + * @type {ol.Size} + */ + this.hitDetectionImageSize_ = null; + + this.render_(options.atlasManager); /** * @type {boolean} @@ -138,6 +164,22 @@ ol.style.Circle.prototype.getImageState = function() { }; +/** + * @inheritDoc + */ +ol.style.Circle.prototype.getImageSize = function() { + return this.imageSize_; +}; + + +/** + * @inheritDoc + */ +ol.style.Circle.prototype.getHitDetectionImageSize = function() { + return this.hitDetectionImageSize_; +}; + + /** * @inheritDoc * @api @@ -147,6 +189,14 @@ ol.style.Circle.prototype.getOrigin = function() { }; +/** + * @inheritDoc + */ +ol.style.Circle.prototype.getHitDetectionOrigin = function() { + return this.hitDetectionOrigin_; +}; + + /** * @return {number} Radius. * @api @@ -192,80 +242,217 @@ ol.style.Circle.prototype.load = goog.nullFunction; ol.style.Circle.prototype.unlistenImageChange = goog.nullFunction; +/** + * @typedef {{strokeStyle: (string|undefined), strokeWidth: number, + * size: number, lineDash: Array.}} + */ +ol.style.Circle.RenderOptions; + + /** * @private - * @return {number} Size. + * @param {ol.style.AtlasManager|undefined} atlasManager */ -ol.style.Circle.prototype.render_ = function() { - var canvas = this.canvas_; - var strokeStyle, strokeWidth, lineDash; +ol.style.Circle.prototype.render_ = function(atlasManager) { + var imageSize; + var lineDash = null; + var strokeStyle; + var strokeWidth = 0; - if (goog.isNull(this.stroke_)) { - strokeWidth = 0; - } else { + if (!goog.isNull(this.stroke_)) { strokeStyle = ol.color.asString(this.stroke_.getColor()); strokeWidth = this.stroke_.getWidth(); if (!goog.isDef(strokeWidth)) { strokeWidth = ol.render.canvas.defaultLineWidth; } + lineDash = this.stroke_.getLineDash(); + if (!ol.has.CANVAS_LINE_DASH) { + lineDash = null; + } } + 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, + lineDash: lineDash + }; + + 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.height = size; - canvas.width = size; + // canvas.width and height are rounded to the closest integer + size = this.canvas_.width; + imageSize = size; - // canvas.width and height are rounded to the closest integer - size = canvas.width; + // draw the circle on the canvas + var context = /** @type {CanvasRenderingContext2D} */ + (this.canvas_.getContext('2d')); + this.draw_(renderOptions, context, 0, 0); - var context = /** @type {CanvasRenderingContext2D} */ - (canvas.getContext('2d')); - context.arc(size / 2, size / 2, this.radius_, 0, 2 * Math.PI, true); + this.createHitDetectionCanvas_(renderOptions); + } else { + // an atlas manager is used, add the symbol to an atlas + size = Math.round(size); + + var hasCustomHitDetectionImage = goog.isNull(this.fill_); + var renderHitDetectionCallback; + if (hasCustomHitDetectionImage) { + // render the hit-detection image into a separate atlas image + renderHitDetectionCallback = + goog.bind(this.drawHitDetectionCanvas_, this, renderOptions); + } + + var id = this.getChecksum(); + var info = atlasManager.add( + id, size, size, goog.bind(this.draw_, this, renderOptions), + renderHitDetectionCallback); + goog.asserts.assert(info !== null, 'circle radius is too large'); + + this.canvas_ = info.image; + this.origin_ = [info.offsetX, info.offsetY]; + imageSize = info.image.width; + + if (hasCustomHitDetectionImage) { + this.hitDetectionCanvas_ = info.hitImage; + this.hitDetectionOrigin_ = [info.hitOffsetX, info.hitOffsetY]; + this.hitDetectionImageSize_ = + [info.hitImage.width, info.hitImage.height]; + } else { + this.hitDetectionCanvas_ = this.canvas_; + this.hitDetectionOrigin_ = this.origin_; + this.hitDetectionImageSize_ = [imageSize, imageSize]; + } + } + + this.anchor_ = [size / 2, size / 2]; + this.size_ = [size, size]; + this.imageSize_ = [imageSize, imageSize]; +}; + + +/** + * @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; - lineDash = this.stroke_.getLineDash(); - if (ol.has.CANVAS_LINE_DASH && !goog.isNull(lineDash)) { - context.setLineDash(lineDash); + context.strokeStyle = renderOptions.strokeStyle; + context.lineWidth = renderOptions.strokeWidth; + if (!goog.isNull(renderOptions.lineDash)) { + context.setLineDash(renderOptions.lineDash); } - context.lineWidth = strokeWidth; context.stroke(); } + context.closePath(); +}; - // deal with the hit detection canvas +/** + * @private + * @param {ol.style.Circle.RenderOptions} renderOptions + */ +ol.style.Circle.prototype.createHitDetectionCanvas_ = function(renderOptions) { + this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size]; if (!goog.isNull(this.fill_)) { - this.hitDetectionCanvas_ = canvas; - } else { - this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */ - (goog.dom.createElement(goog.dom.TagName.CANVAS)); - canvas = this.hitDetectionCanvas_; + this.hitDetectionCanvas_ = this.canvas_; + return; + } - canvas.height = size; - canvas.width = size; + // if no fill style is set, create an extra hit-detection image with a + // default fill style + this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + var canvas = this.hitDetectionCanvas_; - context = /** @type {CanvasRenderingContext2D} */ - (canvas.getContext('2d')); - context.arc(size / 2, size / 2, this.radius_, 0, 2 * Math.PI, true); + canvas.height = renderOptions.size; + canvas.width = renderOptions.size; - context.fillStyle = ol.render.canvas.defaultFillStyle; - context.fill(); - if (!goog.isNull(this.stroke_)) { - context.strokeStyle = strokeStyle; - lineDash = this.stroke_.getLineDash(); - if (ol.has.CANVAS_LINE_DASH && !goog.isNull(lineDash)) { - context.setLineDash(lineDash); - } - context.lineWidth = strokeWidth; - context.stroke(); + var context = /** @type {CanvasRenderingContext2D} */ + (canvas.getContext('2d')); + this.drawHitDetectionCanvas_(renderOptions, context, 0, 0); +}; + + +/** + * @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.drawHitDetectionCanvas_ = + 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); + + context.fillStyle = ol.render.canvas.defaultFillStyle; + context.fill(); + if (!goog.isNull(this.stroke_)) { + context.strokeStyle = renderOptions.strokeStyle; + context.lineWidth = renderOptions.strokeWidth; + if (!goog.isNull(renderOptions.lineDash)) { + context.setLineDash(renderOptions.lineDash); } + context.stroke(); + } + context.closePath(); +}; + + +/** + * @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 size; + return this.checksums_[0]; }; diff --git a/src/ol/style/fillstyle.js b/src/ol/style/fillstyle.js index 6714445868a..4fd62d77e79 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 {string|undefined} + */ + this.checksum_ = undefined; }; @@ -39,4 +49,18 @@ ol.style.Fill.prototype.getColor = function() { */ ol.style.Fill.prototype.setColor = function(color) { this.color_ = color; + this.checksum_ = undefined; +}; + + +/** + * @inheritDoc + */ +ol.style.Fill.prototype.getChecksum = function() { + if (!goog.isDef(this.checksum_)) { + this.checksum_ = 'f' + (!goog.isNull(this.color_) ? + ol.color.asString(this.color_) : '-'); + } + + return this.checksum_; }; diff --git a/src/ol/style/iconstyle.js b/src/ol/style/iconstyle.js index c82881420a4..9913c6412a1 100644 --- a/src/ol/style/iconstyle.js +++ b/src/ol/style/iconstyle.js @@ -246,6 +246,14 @@ ol.style.Icon.prototype.getImageSize = function() { }; +/** + * @inheritDoc + */ +ol.style.Icon.prototype.getHitDetectionImageSize = function() { + return this.getImageSize(); +}; + + /** * @inheritDoc */ @@ -293,6 +301,14 @@ ol.style.Icon.prototype.getOrigin = function() { }; +/** + * @inheritDoc + */ +ol.style.Icon.prototype.getHitDetectionOrigin = function() { + return this.getOrigin(); +}; + + /** * @return {string|undefined} Image src. * @api diff --git a/src/ol/style/imagestyle.js b/src/ol/style/imagestyle.js index 35c3a3ac16c..be7c2707884 100644 --- a/src/ol/style/imagestyle.js +++ b/src/ol/style/imagestyle.js @@ -126,6 +126,13 @@ ol.style.Image.prototype.getAnchor = goog.abstractMethod; ol.style.Image.prototype.getImage = goog.abstractMethod; +/** + * @param {number} pixelRatio Pixel ratio. + * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element. + */ +ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod; + + /** * @return {ol.style.ImageState} Image state. */ @@ -133,10 +140,15 @@ ol.style.Image.prototype.getImageState = goog.abstractMethod; /** - * @param {number} pixelRatio Pixel ratio. - * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element. + * @return {ol.Size} Image size. */ -ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod; +ol.style.Image.prototype.getImageSize = goog.abstractMethod; + + +/** + * @return {ol.Size} Size of the hit-detection image. + */ +ol.style.Image.prototype.getHitDetectionImageSize = goog.abstractMethod; /** @@ -146,6 +158,13 @@ ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod; ol.style.Image.prototype.getOrigin = goog.abstractMethod; +/** + * @function + * @return {Array.} Origin for the hit-detection image. + */ +ol.style.Image.prototype.getHitDetectionOrigin = goog.abstractMethod; + + /** * @function * @return {ol.Size} Size. diff --git a/src/ol/style/regularshapestyle.js b/src/ol/style/regularshapestyle.js index 579e71f6f7e..f7744cc3ffe 100644 --- a/src/ol/style/regularshapestyle.js +++ b/src/ol/style/regularshapestyle.js @@ -22,18 +22,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.} + */ + this.checksums_ = null; + /** * @private * @type {HTMLCanvasElement} */ - this.canvas_ = /** @type {HTMLCanvasElement} */ - (goog.dom.createElement(goog.dom.TagName.CANVAS)); + this.canvas_ = null; /** * @private @@ -53,6 +59,12 @@ ol.style.RegularShape = function(opt_options) { */ this.origin_ = [0, 0]; + /** + * @private + * @type {Array.} + */ + this.hitDetectionOrigin_ = [0, 0]; + /** * @private * @type {number} @@ -88,19 +100,31 @@ 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; + + /** + * @private + * @type {ol.Size} + */ + this.hitDetectionImageSize_ = null; + + this.render_(options.atlasManager); /** * @type {boolean} @@ -155,6 +179,22 @@ ol.style.RegularShape.prototype.getImage = function(pixelRatio) { }; +/** + * @inheritDoc + */ +ol.style.RegularShape.prototype.getImageSize = function() { + return this.imageSize_; +}; + + +/** + * @inheritDoc + */ +ol.style.RegularShape.prototype.getHitDetectionImageSize = function() { + return this.hitDetectionImageSize_; +}; + + /** * @inheritDoc */ @@ -172,6 +212,14 @@ ol.style.RegularShape.prototype.getOrigin = function() { }; +/** + * @inheritDoc + */ +ol.style.RegularShape.prototype.getHitDetectionOrigin = function() { + return this.hitDetectionOrigin_; +}; + + /** * @return {number} Radius. * @api @@ -226,37 +274,117 @@ ol.style.RegularShape.prototype.load = goog.nullFunction; ol.style.RegularShape.prototype.unlistenImageChange = goog.nullFunction; +/** + * @typedef {{strokeStyle: (string|undefined), strokeWidth: number, + * size: number, lineDash: Array.}} + */ +ol.style.RegularShape.RenderOptions; + + /** * @private - * @return {number} Size. + * @param {ol.style.AtlasManager|undefined} atlasManager */ -ol.style.RegularShape.prototype.render_ = function() { - var canvas = this.canvas_; - var strokeStyle, strokeWidth, lineDash; +ol.style.RegularShape.prototype.render_ = function(atlasManager) { + var imageSize; + var lineDash = null; + var strokeStyle; + var strokeWidth = 0; - if (goog.isNull(this.stroke_)) { - strokeWidth = 0; - } else { + if (!goog.isNull(this.stroke_)) { strokeStyle = ol.color.asString(this.stroke_.getColor()); strokeWidth = this.stroke_.getWidth(); if (!goog.isDef(strokeWidth)) { strokeWidth = ol.render.canvas.defaultLineWidth; } + lineDash = this.stroke_.getLineDash(); + if (!ol.has.CANVAS_LINE_DASH) { + lineDash = null; + } } 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, + lineDash: lineDash + }; + + 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.height = size; - canvas.width = size; + 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')); + var context = /** @type {CanvasRenderingContext2D} */ + (this.canvas_.getContext('2d')); + this.draw_(renderOptions, context, 0, 0); + + this.createHitDetectionCanvas_(renderOptions); + } else { + // an atlas manager is used, add the symbol to an atlas + size = Math.round(size); + + var hasCustomHitDetectionImage = goog.isNull(this.fill_); + var renderHitDetectionCallback; + if (hasCustomHitDetectionImage) { + // render the hit-detection image into a separate atlas image + renderHitDetectionCallback = + goog.bind(this.drawHitDetectionCanvas_, this, renderOptions); + } + + var id = this.getChecksum(); + var info = atlasManager.add( + id, size, size, goog.bind(this.draw_, this, renderOptions), + renderHitDetectionCallback); + goog.asserts.assert(!goog.isNull(info), 'shape size is too large'); + + this.canvas_ = info.image; + this.origin_ = [info.offsetX, info.offsetY]; + imageSize = info.image.width; + + if (hasCustomHitDetectionImage) { + this.hitDetectionCanvas_ = info.hitImage; + this.hitDetectionOrigin_ = [info.hitOffsetX, info.hitOffsetY]; + this.hitDetectionImageSize_ = + [info.hitImage.width, info.hitImage.height]; + } else { + this.hitDetectionCanvas_ = this.canvas_; + this.hitDetectionOrigin_ = this.origin_; + this.hitDetectionImageSize_ = [imageSize, imageSize]; + } + } + + this.anchor_ = [size / 2, size / 2]; + this.size_ = [size, size]; + this.imageSize_ = [imageSize, imageSize]; +}; + + +/** + * @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_; @@ -264,8 +392,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_)) { @@ -273,52 +401,111 @@ ol.style.RegularShape.prototype.render_ = function() { context.fill(); } if (!goog.isNull(this.stroke_)) { - context.strokeStyle = strokeStyle; - lineDash = this.stroke_.getLineDash(); - if (ol.has.CANVAS_LINE_DASH && !goog.isNull(lineDash)) { - context.setLineDash(lineDash); + context.strokeStyle = renderOptions.strokeStyle; + context.lineWidth = renderOptions.strokeWidth; + if (!goog.isNull(renderOptions.lineDash)) { + context.setLineDash(renderOptions.lineDash); } - context.lineWidth = strokeWidth; context.stroke(); } + context.closePath(); +}; - // deal with the hit detection canvas +/** + * @private + * @param {ol.style.RegularShape.RenderOptions} renderOptions + */ +ol.style.RegularShape.prototype.createHitDetectionCanvas_ = + function(renderOptions) { + this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size]; if (!goog.isNull(this.fill_)) { - this.hitDetectionCanvas_ = canvas; - } else { - this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */ - (goog.dom.createElement(goog.dom.TagName.CANVAS)); - canvas = this.hitDetectionCanvas_; + this.hitDetectionCanvas_ = this.canvas_; + return; + } - canvas.height = size; - canvas.width = size; + // if no fill style is set, create an extra hit-detection image with a + // default fill style + this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */ + (goog.dom.createElement(goog.dom.TagName.CANVAS)); + var canvas = this.hitDetectionCanvas_; - 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)); - } + canvas.height = renderOptions.size; + canvas.width = renderOptions.size; - context.fillStyle = ol.render.canvas.defaultFillStyle; - context.fill(); - if (!goog.isNull(this.stroke_)) { - context.strokeStyle = strokeStyle; - lineDash = this.stroke_.getLineDash(); - if (ol.has.CANVAS_LINE_DASH && !goog.isNull(lineDash)) { - context.setLineDash(lineDash); - } - context.lineWidth = strokeWidth; - context.stroke(); + var context = /** @type {CanvasRenderingContext2D} */ + (canvas.getContext('2d')); + this.drawHitDetectionCanvas_(renderOptions, context, 0, 0); +}; + + +/** + * @private + * @param {ol.style.RegularShape.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.drawHitDetectionCanvas_ = + 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(); + 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)); + } + + context.fillStyle = ol.render.canvas.defaultFillStyle; + context.fill(); + if (!goog.isNull(this.stroke_)) { + context.strokeStyle = renderOptions.strokeStyle; + context.lineWidth = renderOptions.strokeWidth; + if (!goog.isNull(renderOptions.lineDash)) { + context.setLineDash(renderOptions.lineDash); } + context.stroke(); + } + context.closePath(); +}; + + +/** + * @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 size; + return this.checksums_[0]; }; diff --git a/src/ol/style/strokestyle.js b/src/ol/style/strokestyle.js index 15e99bf2603..a679df28763 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 {string|undefined} + */ + this.checksum_ = undefined; }; @@ -117,6 +129,7 @@ ol.style.Stroke.prototype.getWidth = function() { */ ol.style.Stroke.prototype.setColor = function(color) { this.color_ = color; + this.checksum_ = undefined; }; @@ -128,6 +141,7 @@ ol.style.Stroke.prototype.setColor = function(color) { */ ol.style.Stroke.prototype.setLineCap = function(lineCap) { this.lineCap_ = lineCap; + this.checksum_ = undefined; }; @@ -139,6 +153,7 @@ ol.style.Stroke.prototype.setLineCap = function(lineCap) { */ ol.style.Stroke.prototype.setLineDash = function(lineDash) { this.lineDash_ = lineDash; + this.checksum_ = undefined; }; @@ -150,6 +165,7 @@ ol.style.Stroke.prototype.setLineDash = function(lineDash) { */ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) { this.lineJoin_ = lineJoin; + this.checksum_ = undefined; }; @@ -161,6 +177,7 @@ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) { */ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { this.miterLimit_ = miterLimit; + this.checksum_ = undefined; }; @@ -172,4 +189,33 @@ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) { */ ol.style.Stroke.prototype.setWidth = function(width) { this.width_ = width; + this.checksum_ = undefined; +}; + + +/** + * @inheritDoc + */ +ol.style.Stroke.prototype.getChecksum = function() { + if (!goog.isDef(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/src/ol/webgl/buffer.js b/src/ol/webgl/buffer.js new file mode 100644 index 00000000000..a39194ccf6b --- /dev/null +++ b/src/ol/webgl/buffer.js @@ -0,0 +1,56 @@ +goog.provide('ol.webgl.Buffer'); + +goog.require('goog.array'); +goog.require('goog.webgl'); +goog.require('ol'); + + +/** + * @enum {number} + */ +ol.webgl.BufferUsage = { + STATIC_DRAW: goog.webgl.STATIC_DRAW, + STREAM_DRAW: goog.webgl.STREAM_DRAW, + DYNAMIC_DRAW: goog.webgl.DYNAMIC_DRAW +}; + + + +/** + * @constructor + * @param {Array.=} opt_arr Array. + * @param {number=} opt_usage Usage. + * @struct + */ +ol.webgl.Buffer = function(opt_arr, opt_usage) { + + /** + * @private + * @type {Array.} + */ + this.arr_ = goog.isDef(opt_arr) ? opt_arr : []; + + /** + * @private + * @type {number} + */ + this.usage_ = goog.isDef(opt_usage) ? + opt_usage : ol.webgl.BufferUsage.STATIC_DRAW; + +}; + + +/** + * @return {Array.} Array. + */ +ol.webgl.Buffer.prototype.getArray = function() { + return this.arr_; +}; + + +/** + * @return {number} Usage. + */ +ol.webgl.Buffer.prototype.getUsage = function() { + return this.usage_; +}; diff --git a/src/ol/webgl/context.js b/src/ol/webgl/context.js index 307837dc669..6b91f6c2a7d 100644 --- a/src/ol/webgl/context.js +++ b/src/ol/webgl/context.js @@ -1,18 +1,18 @@ goog.provide('ol.webgl.Context'); +goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.log'); goog.require('goog.object'); -goog.require('ol.structs.Buffer'); -goog.require('ol.structs.IntegerSet'); +goog.require('ol'); +goog.require('ol.webgl.Buffer'); goog.require('ol.webgl.WebGLContextEventType'); /** - * @typedef {{buf: ol.structs.Buffer, - * buffer: WebGLBuffer, - * dirtySet: ol.structs.IntegerSet}} + * @typedef {{buf: ol.webgl.Buffer, + * buffer: WebGLBuffer}} */ ol.webgl.BufferCacheEntry; @@ -66,6 +66,18 @@ ol.webgl.Context = function(canvas, gl) { */ this.currentProgram_ = null; + /** + * @type {boolean} + */ + this.hasOESElementIndexUint = goog.array.contains( + ol.WEBGL_EXTENSIONS, 'OES_element_index_uint'); + + // use the OES_element_index_uint extension if available + if (this.hasOESElementIndexUint) { + var ext = gl.getExtension('OES_element_index_uint'); + goog.asserts.assert(!goog.isNull(ext)); + } + goog.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.LOST, this.handleWebGLContextLost, false, this); goog.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.RESTORED, @@ -75,8 +87,11 @@ ol.webgl.Context = function(canvas, gl) { /** + * Just bind the buffer if it's in the cache. Otherwise create + * the WebGL buffer, bind it, populate it, and add an entry to + * the cache. * @param {number} target Target. - * @param {ol.structs.Buffer} buf Buffer. + * @param {ol.webgl.Buffer} buf Buffer. */ ol.webgl.Context.prototype.bindBuffer = function(target, buf) { var gl = this.getGL(); @@ -85,45 +100,37 @@ ol.webgl.Context.prototype.bindBuffer = function(target, buf) { if (bufferKey in this.bufferCache_) { var bufferCacheEntry = this.bufferCache_[bufferKey]; gl.bindBuffer(target, bufferCacheEntry.buffer); - bufferCacheEntry.dirtySet.forEachRange(function(start, stop) { - // FIXME check if slice is really efficient here - var slice = arr.slice(start, stop); - gl.bufferSubData( - target, - start, - target == goog.webgl.ARRAY_BUFFER ? - new Float32Array(slice) : - new Uint16Array(slice)); - }); - bufferCacheEntry.dirtySet.clear(); } else { var buffer = gl.createBuffer(); gl.bindBuffer(target, buffer); - gl.bufferData( - target, - target == goog.webgl.ARRAY_BUFFER ? - new Float32Array(arr) : new Uint16Array(arr), - buf.getUsage()); - var dirtySet = new ol.structs.IntegerSet(); - buf.addDirtySet(dirtySet); + goog.asserts.assert(target == goog.webgl.ARRAY_BUFFER || + target == goog.webgl.ELEMENT_ARRAY_BUFFER); + var /** @type {ArrayBufferView} */ arrayBuffer; + if (target == goog.webgl.ARRAY_BUFFER) { + arrayBuffer = new Float32Array(arr); + } else if (target == goog.webgl.ELEMENT_ARRAY_BUFFER) { + arrayBuffer = this.hasOESElementIndexUint ? + new Uint32Array(arr) : new Uint16Array(arr); + } else { + goog.asserts.fail(); + } + gl.bufferData(target, arrayBuffer, buf.getUsage()); this.bufferCache_[bufferKey] = { buf: buf, - buffer: buffer, - dirtySet: dirtySet + buffer: buffer }; } }; /** - * @param {ol.structs.Buffer} buf Buffer. + * @param {ol.webgl.Buffer} buf Buffer. */ ol.webgl.Context.prototype.deleteBuffer = function(buf) { var gl = this.getGL(); var bufferKey = goog.getUid(buf); goog.asserts.assert(bufferKey in this.bufferCache_); var bufferCacheEntry = this.bufferCache_[bufferKey]; - bufferCacheEntry.buf.removeDirtySet(bufferCacheEntry.dirtySet); if (!gl.isContextLost()) { gl.deleteBuffer(bufferCacheEntry.buffer); } @@ -135,9 +142,6 @@ ol.webgl.Context.prototype.deleteBuffer = function(buf) { * @inheritDoc */ ol.webgl.Context.prototype.disposeInternal = function() { - goog.object.forEach(this.bufferCache_, function(bufferCacheEntry) { - bufferCacheEntry.buf.removeDirtySet(bufferCacheEntry.dirtySet); - }); var gl = this.getGL(); if (!gl.isContextLost()) { goog.object.forEach(this.bufferCache_, function(bufferCacheEntry) { @@ -171,6 +175,8 @@ ol.webgl.Context.prototype.getGL = function() { /** + * Get shader from the cache if it's in the cache. Otherwise, create + * the WebGL shader, compile it, and add entry to cache. * @param {ol.webgl.Shader} shaderObject Shader object. * @return {WebGLShader} Shader. */ @@ -199,6 +205,9 @@ ol.webgl.Context.prototype.getShader = function(shaderObject) { /** + * Get the program from the cache if it's in the cache. Otherwise create + * the WebGL program, attach the shaders to it, and add an entry to the + * cache. * @param {ol.webgl.shader.Fragment} fragmentShaderObject Fragment shader. * @param {ol.webgl.shader.Vertex} vertexShaderObject Vertex shader. * @return {WebGLProgram} Program. @@ -249,6 +258,9 @@ ol.webgl.Context.prototype.handleWebGLContextRestored = function() { /** + * Just return false if that program is used already. Other use + * that program (call `gl.useProgram`) and make it the "current + * program". * @param {WebGLProgram} program Program. * @return {boolean} Changed. * @api diff --git a/test/spec/ol/render/webglreplay.test.js b/test/spec/ol/render/webglreplay.test.js new file mode 100644 index 00000000000..8e600186e73 --- /dev/null +++ b/test/spec/ol/render/webglreplay.test.js @@ -0,0 +1,158 @@ +goog.provide('ol.test.render.webgl.Replay'); + +describe('ol.render.webgl.ImageReplay', function() { + var replay; + + var createImageStyle = function(image) { + var imageStyle = new ol.style.Image({ + opacity: 0.1, + rotateWithView: true, + rotation: 1.5, + scale: 2.0 + }); + imageStyle.getAnchor = function() { + return [0.5, 1]; + }; + imageStyle.getImage = function() { + return image; + }; + imageStyle.getImageSize = function() { + return [512, 512]; + }; + imageStyle.getOrigin = function() { + return [200, 200]; + }; + imageStyle.getSize = function() { + return [256, 256]; + }; + return imageStyle; + }; + + beforeEach(function() { + var tolerance = 0.1; + var maxExtent = [-10000, -20000, 10000, 20000]; + replay = new ol.render.webgl.ImageReplay(tolerance, maxExtent); + }); + + describe('#setImageStyle', function() { + + var imageStyle1, imageStyle2; + + beforeEach(function() { + imageStyle1 = createImageStyle(new Image()); + imageStyle2 = createImageStyle(new Image()); + }); + + it('set expected states', function() { + replay.setImageStyle(imageStyle1); + expect(replay.anchorX_).to.be(0.5); + expect(replay.anchorY_).to.be(1); + expect(replay.height_).to.be(256); + expect(replay.imageHeight_).to.be(512); + expect(replay.imageWidth_).to.be(512); + expect(replay.opacity_).to.be(0.1); + expect(replay.originX_).to.be(200); + expect(replay.originY_).to.be(200); + expect(replay.rotation_).to.be(1.5); + expect(replay.rotateWithView_).to.be(true); + expect(replay.scale_).to.be(2.0); + expect(replay.width_).to.be(256); + expect(replay.images_).to.have.length(1); + expect(replay.groupIndices_).to.have.length(0); + + replay.setImageStyle(imageStyle1); + expect(replay.images_).to.have.length(1); + expect(replay.groupIndices_).to.have.length(0); + + replay.setImageStyle(imageStyle2); + expect(replay.images_).to.have.length(2); + expect(replay.groupIndices_).to.have.length(1); + }); + }); + + describe('#drawPointGeometry', function() { + beforeEach(function() { + var imageStyle = createImageStyle(new Image()); + replay.setImageStyle(imageStyle); + }); + + it('sets the buffer data', function() { + var point; + + point = new ol.geom.Point([1000, 2000]); + replay.drawPointGeometry(point, null); + expect(replay.vertices_).to.have.length(32); + expect(replay.indices_).to.have.length(6); + expect(replay.indices_[0]).to.be(0); + expect(replay.indices_[1]).to.be(1); + expect(replay.indices_[2]).to.be(2); + expect(replay.indices_[3]).to.be(0); + expect(replay.indices_[4]).to.be(2); + expect(replay.indices_[5]).to.be(3); + + point = new ol.geom.Point([2000, 3000]); + replay.drawPointGeometry(point, null); + expect(replay.vertices_).to.have.length(64); + expect(replay.indices_).to.have.length(12); + expect(replay.indices_[6]).to.be(4); + expect(replay.indices_[7]).to.be(5); + expect(replay.indices_[8]).to.be(6); + expect(replay.indices_[9]).to.be(4); + expect(replay.indices_[10]).to.be(6); + expect(replay.indices_[11]).to.be(7); + }); + }); + + describe('#drawMultiPointGeometry', function() { + beforeEach(function() { + var imageStyle = createImageStyle(new Image()); + replay.setImageStyle(imageStyle); + }); + + it('sets the buffer data', function() { + var multiPoint; + + multiPoint = new ol.geom.MultiPoint( + [[1000, 2000], [2000, 3000]]); + replay.drawMultiPointGeometry(multiPoint, null); + expect(replay.vertices_).to.have.length(64); + expect(replay.indices_).to.have.length(12); + expect(replay.indices_[0]).to.be(0); + expect(replay.indices_[1]).to.be(1); + expect(replay.indices_[2]).to.be(2); + expect(replay.indices_[3]).to.be(0); + expect(replay.indices_[4]).to.be(2); + expect(replay.indices_[5]).to.be(3); + expect(replay.indices_[6]).to.be(4); + expect(replay.indices_[7]).to.be(5); + expect(replay.indices_[8]).to.be(6); + expect(replay.indices_[9]).to.be(4); + expect(replay.indices_[10]).to.be(6); + expect(replay.indices_[11]).to.be(7); + + multiPoint = new ol.geom.MultiPoint( + [[3000, 4000], [4000, 5000]]); + replay.drawMultiPointGeometry(multiPoint, null); + expect(replay.vertices_).to.have.length(128); + expect(replay.indices_).to.have.length(24); + expect(replay.indices_[12]).to.be(8); + expect(replay.indices_[13]).to.be(9); + expect(replay.indices_[14]).to.be(10); + expect(replay.indices_[15]).to.be(8); + expect(replay.indices_[16]).to.be(10); + expect(replay.indices_[17]).to.be(11); + expect(replay.indices_[18]).to.be(12); + expect(replay.indices_[19]).to.be(13); + expect(replay.indices_[20]).to.be(14); + expect(replay.indices_[21]).to.be(12); + expect(replay.indices_[22]).to.be(14); + expect(replay.indices_[23]).to.be(15); + }); + }); +}); + +goog.require('ol.extent'); +goog.require('ol.geom.MultiPoint'); +goog.require('ol.geom.Point'); +goog.require('ol.render.webgl.ImageReplay'); +goog.require('ol.style.Image'); diff --git a/test/spec/ol/structs/buffer.test.js b/test/spec/ol/structs/buffer.test.js deleted file mode 100644 index 94d24cec860..00000000000 --- a/test/spec/ol/structs/buffer.test.js +++ /dev/null @@ -1,297 +0,0 @@ -goog.provide('ol.test.structs.Buffer'); - - -describe('ol.structs.Buffer', function() { - - describe('constructor', function() { - - describe('without an argument', function() { - - var b; - beforeEach(function() { - b = new ol.structs.Buffer(); - }); - - it('constructs an empty instance', function() { - expect(b.getArray()).to.be.empty(); - expect(b.getCount()).to.be(0); - }); - - }); - - describe('with a single array argument', function() { - - var b; - beforeEach(function() { - b = new ol.structs.Buffer([0, 1, 2, 3]); - }); - - it('constructs a populated instance', function() { - expect(b.getArray()).to.eql([0, 1, 2, 3]); - }); - - }); - - }); - - describe('with an empty instance', function() { - - var b; - beforeEach(function() { - b = new ol.structs.Buffer(); - }); - - describe('forEachRange', function() { - - it('does not call the callback', function() { - var callback = sinon.spy(); - b.forEachRange(callback); - expect(callback).not.to.be.called(); - }); - - }); - - describe('getArray', function() { - - it('returns an empty array', function() { - expect(b.getArray()).to.be.empty(); - }); - - }); - - describe('getCount', function() { - - it('returns 0', function() { - expect(b.getCount()).to.be(0); - }); - - }); - - }); - - describe('with an empty instance with spare capacity', function() { - - var b; - beforeEach(function() { - b = new ol.structs.Buffer(new Array(4), 0); - }); - - describe('add', function() { - - it('allows elements to be added', function() { - expect(b.add([0, 1, 2, 3])).to.be(0); - expect(b.getArray()).to.eql([0, 1, 2, 3]); - }); - - }); - - describe('forEachRange', function() { - - it('does not call the callback', function() { - var callback = sinon.spy(); - b.forEachRange(callback); - expect(callback).not.to.be.called(); - }); - - }); - - describe('getCount', function() { - - it('returns 0', function() { - expect(b.getCount()).to.be(0); - }); - - }); - - }); - - describe('with an instance with no spare capacity', function() { - - var b; - beforeEach(function() { - b = new ol.structs.Buffer([0, 1, 2, 3]); - }); - - describe('add', function() { - - it('throws an exception', function() { - expect(function() { - b.add([4, 5]); - }).to.throwException(); - }); - - }); - - describe('forEachRange', function() { - - it('calls the callback', function() { - var callback = sinon.spy(); - b.forEachRange(callback); - expect(callback.calledOnce).to.be(true); - expect(callback.args[0]).to.eql([0, 4]); - }); - - }); - - describe('getCount', function() { - - it('returns the expected value', function() { - expect(b.getCount()).to.be(4); - }); - - }); - - describe('remove', function() { - - it('allows items to be removes', function() { - expect(function() { - b.remove(4, 2); - }).to.not.throwException(); - }); - - }); - - describe('set', function() { - - it('updates the items', function() { - b.set([5, 6], 2); - expect(b.getArray()).to.eql([0, 1, 5, 6]); - }); - - it('marks the set items as dirty', function() { - var dirtySet = new ol.structs.IntegerSet(); - b.addDirtySet(dirtySet); - expect(dirtySet.isEmpty()).to.be(true); - b.set([5, 6], 2); - expect(dirtySet.isEmpty()).to.be(false); - expect(dirtySet.getArray()).to.eql([2, 4]); - }); - - }); - - }); - - describe('with an instance with spare capacity', function() { - - var b; - beforeEach(function() { - var arr = [0, 1, 2, 3]; - arr.length = 8; - b = new ol.structs.Buffer(arr, 4); - }); - - describe('add', function() { - - it('allows more items to be added', function() { - expect(b.add([4, 5, 6, 7])).to.be(4); - expect(b.getArray()).to.eql([0, 1, 2, 3, 4, 5, 6, 7]); - }); - - }); - - describe('forEachRange', function() { - - it('calls the callback with the expected values', function() { - var callback = sinon.spy(); - b.forEachRange(callback); - expect(callback.calledOnce).to.be(true); - expect(callback.args[0]).to.eql([0, 4]); - }); - - }); - - describe('getCount', function() { - - it('returns the expected value', function() { - expect(b.getCount()).to.be(4); - }); - - }); - - describe('getFreeSet', function() { - - it('returns the expected set', function() { - var freeSet = b.getFreeSet(); - expect(freeSet.isEmpty()).to.be(false); - expect(freeSet.getArray()).to.eql([4, 8]); - }); - - }); - - }); - - describe('with a populated instance', function() { - - var b; - beforeEach(function() { - b = new ol.structs.Buffer([1234567.1234567, -7654321.7654321]); - }); - - describe('getSplit32', function() { - - it('returns the expected value', function() { - var split32 = b.getSplit32(); - expect(split32).to.be.a(Float32Array); - expect(split32).to.have.length(4); - expect(split32[0]).to.roughlyEqual(1179648.0, 1e1); - expect(split32[1]).to.roughlyEqual(54919.12345670001, 1e-2); - expect(split32[2]).to.roughlyEqual(-7602176.0, 1e1); - expect(split32[3]).to.roughlyEqual(-52145.76543209981, 1e-2); - }); - - it('tracks updates', function() { - b.getSplit32(); - b.getArray()[0] = 0; - b.markDirty(1, 0); - var split32 = b.getSplit32(); - expect(split32).to.be.a(Float32Array); - expect(split32).to.have.length(4); - expect(split32[0]).to.be(0); - expect(split32[1]).to.be(0); - expect(split32[2]).to.roughlyEqual(-7602176.0, 1e1); - expect(split32[3]).to.roughlyEqual(-52145.76543209981, 1e-2); - }); - - }); - }); - - describe('usage tests', function() { - - it('allows multiple adds and removes', function() { - var b = new ol.structs.Buffer(new Array(8), 0); - expect(b.add([0, 1])).to.be(0); - expect(b.getArray()).to.arreqlNaN([0, 1, NaN, NaN, NaN, NaN, NaN, NaN]); - expect(b.getCount()).to.be(2); - expect(b.add([2, 3, 4, 5])).to.be(2); - expect(b.getArray()).to.arreqlNaN([0, 1, 2, 3, 4, 5, NaN, NaN]); - expect(b.getCount()).to.be(6); - expect(b.add([6, 7])).to.be(6); - expect(b.getArray()).to.eql([0, 1, 2, 3, 4, 5, 6, 7]); - expect(b.getCount()).to.be(8); - b.remove(2, 2); - expect(b.getArray()).to.arreqlNaN([0, 1, NaN, NaN, 4, 5, 6, 7]); - expect(b.getCount()).to.be(6); - expect(b.add([8, 9])).to.be(2); - expect(b.getArray()).to.eql([0, 1, 8, 9, 4, 5, 6, 7]); - expect(b.getCount()).to.be(8); - b.remove(1, 1); - expect(b.getArray()).to.arreqlNaN([0, NaN, 8, 9, 4, 5, 6, 7]); - expect(b.getCount()).to.be(7); - b.remove(4, 4); - expect(b.getArray()).to.arreqlNaN([0, NaN, 8, 9, NaN, NaN, NaN, NaN]); - expect(b.getCount()).to.be(3); - expect(b.add([10, 11, 12])).to.be(4); - expect(b.getArray()).to.arreqlNaN([0, NaN, 8, 9, 10, 11, 12, NaN]); - expect(b.getCount()).to.be(6); - expect(b.add([13])).to.be(1); - expect(b.getArray()).to.arreqlNaN([0, 13, 8, 9, 10, 11, 12, NaN]); - expect(b.getCount()).to.be(7); - }); - - }); - -}); - - -goog.require('ol.structs.Buffer'); -goog.require('ol.structs.IntegerSet'); diff --git a/test/spec/ol/structs/integerset.test.js b/test/spec/ol/structs/integerset.test.js deleted file mode 100644 index 2fb7af18fac..00000000000 --- a/test/spec/ol/structs/integerset.test.js +++ /dev/null @@ -1,622 +0,0 @@ -goog.provide('ol.test.structs.IntegerSet'); - - -describe('ol.structs.IntegerSet', function() { - - describe('constructor', function() { - - describe('without an argument', function() { - - it('constructs an empty instance', function() { - var is = new ol.structs.IntegerSet(); - expect(is).to.be.an(ol.structs.IntegerSet); - expect(is.getArray()).to.be.empty(); - }); - - }); - - describe('with an argument', function() { - - it('constructs with a valid array', function() { - var is = new ol.structs.IntegerSet([0, 2, 4, 6]); - expect(is).to.be.an(ol.structs.IntegerSet); - expect(is.getArray()).to.eql([0, 2, 4, 6]); - }); - - it('throws an exception with an odd number of elements', function() { - expect(function() { - var is = new ol.structs.IntegerSet([0, 2, 4]); - is = is; // suppress gjslint warning about unused variable - }).to.throwException(); - }); - - it('throws an exception with out-of-order elements', function() { - expect(function() { - var is = new ol.structs.IntegerSet([0, 2, 2, 4]); - is = is; // suppress gjslint warning about unused variable - }).to.throwException(); - }); - - }); - - }); - - describe('with an empty instance', function() { - - var is; - beforeEach(function() { - is = new ol.structs.IntegerSet(); - }); - - describe('addRange', function() { - - it('creates a new element', function() { - is.addRange(0, 2); - expect(is.getArray()).to.eql([0, 2]); - }); - - }); - - describe('findRange', function() { - - it('returns -1', function() { - expect(is.findRange(2)).to.be(-1); - }); - - }); - - describe('forEachRange', function() { - - it('does not call the callback', function() { - var callback = sinon.spy(); - is.forEachRange(callback); - expect(callback).to.not.be.called(); - }); - - }); - - describe('forEachRangeInverted', function() { - - it('does call the callback', function() { - var callback = sinon.spy(); - is.forEachRangeInverted(0, 8, callback); - expect(callback.calledOnce).to.be(true); - expect(callback.args[0]).to.eql([0, 8]); - }); - - }); - - describe('getFirst', function() { - - it('returns -1', function() { - expect(is.getFirst()).to.be(-1); - }); - - }); - - describe('getLast', function() { - - it('returns -1', function() { - expect(is.getLast()).to.be(-1); - }); - - }); - - describe('getSize', function() { - - it('returns 0', function() { - expect(is.getSize()).to.be(0); - }); - - }); - - describe('intersectsRange', function() { - - it('returns false', function() { - expect(is.intersectsRange(0, 0)).to.be(false); - }); - - }); - - describe('isEmpty', function() { - - it('returns true', function() { - expect(is.isEmpty()).to.be(true); - }); - - }); - - describe('toString', function() { - - it('returns an empty string', function() { - expect(is.toString()).to.be.empty(); - }); - - }); - - }); - - describe('with a populated instance', function() { - - var is; - beforeEach(function() { - is = new ol.structs.IntegerSet([4, 6, 8, 10, 12, 14]); - }); - - describe('addRange', function() { - - it('inserts before the first element', function() { - is.addRange(0, 2); - expect(is.getArray()).to.eql([0, 2, 4, 6, 8, 10, 12, 14]); - }); - - it('extends the first element to the left', function() { - is.addRange(0, 4); - expect(is.getArray()).to.eql([0, 6, 8, 10, 12, 14]); - }); - - it('extends the first element to the right', function() { - is.addRange(6, 7); - expect(is.getArray()).to.eql([4, 7, 8, 10, 12, 14]); - }); - - it('merges the first two elements', function() { - is.addRange(6, 8); - expect(is.getArray()).to.eql([4, 10, 12, 14]); - }); - - it('extends middle elements to the left', function() { - is.addRange(7, 8); - expect(is.getArray()).to.eql([4, 6, 7, 10, 12, 14]); - }); - - it('extends middle elements to the right', function() { - is.addRange(10, 11); - expect(is.getArray()).to.eql([4, 6, 8, 11, 12, 14]); - }); - - it('merges the last two elements', function() { - is.addRange(10, 12); - expect(is.getArray()).to.eql([4, 6, 8, 14]); - }); - - it('extends the last element to the left', function() { - is.addRange(11, 12); - expect(is.getArray()).to.eql([4, 6, 8, 10, 11, 14]); - }); - - it('extends the last element to the right', function() { - is.addRange(14, 15); - expect(is.getArray()).to.eql([4, 6, 8, 10, 12, 15]); - }); - - it('inserts after the last element', function() { - is.addRange(16, 18); - expect(is.getArray()).to.eql([4, 6, 8, 10, 12, 14, 16, 18]); - }); - - }); - - describe('clear', function() { - - it('clears the instance', function() { - is.clear(); - expect(is.getArray()).to.be.empty(); - }); - - }); - - describe('findRange', function() { - - it('throws an exception when passed a negative size', function() { - expect(function() { - is.findRange(-1); - }).to.throwException(); - }); - - it('throws an exception when passed a zero size', function() { - expect(function() { - is.findRange(0); - }).to.throwException(); - }); - - it('finds the first range of size 1', function() { - expect(is.findRange(1)).to.be(4); - }); - - it('finds the first range of size 2', function() { - expect(is.findRange(2)).to.be(4); - }); - - it('returns -1 when no range can be found', function() { - expect(is.findRange(3)).to.be(-1); - }); - - }); - - describe('forEachRange', function() { - - it('calls the callback', function() { - var callback = sinon.spy(); - is.forEachRange(callback); - expect(callback).to.be.called(); - expect(callback.calledThrice).to.be(true); - expect(callback.args[0]).to.eql([4, 6]); - expect(callback.args[1]).to.eql([8, 10]); - expect(callback.args[2]).to.eql([12, 14]); - }); - - }); - - describe('forEachRangeInverted', function() { - - it('does call the callback', function() { - var callback = sinon.spy(); - is.forEachRangeInverted(0, 16, callback); - expect(callback.callCount).to.be(4); - expect(callback.args[0]).to.eql([0, 4]); - expect(callback.args[1]).to.eql([6, 8]); - expect(callback.args[2]).to.eql([10, 12]); - expect(callback.args[3]).to.eql([14, 16]); - }); - - }); - - - describe('getFirst', function() { - - it('returns the expected value', function() { - expect(is.getFirst()).to.be(4); - }); - - }); - - describe('getLast', function() { - - it('returns the expected value', function() { - expect(is.getLast()).to.be(14); - }); - - }); - - describe('getSize', function() { - - it('returns the expected value', function() { - expect(is.getSize()).to.be(6); - }); - - }); - - describe('intersectsRange', function() { - - it('returns the expected value for small ranges', function() { - expect(is.intersectsRange(1, 3)).to.be(false); - expect(is.intersectsRange(2, 4)).to.be(false); - expect(is.intersectsRange(3, 5)).to.be(true); - expect(is.intersectsRange(4, 6)).to.be(true); - expect(is.intersectsRange(5, 7)).to.be(true); - expect(is.intersectsRange(6, 8)).to.be(false); - expect(is.intersectsRange(7, 9)).to.be(true); - expect(is.intersectsRange(8, 10)).to.be(true); - expect(is.intersectsRange(9, 11)).to.be(true); - expect(is.intersectsRange(10, 12)).to.be(false); - expect(is.intersectsRange(11, 13)).to.be(true); - expect(is.intersectsRange(12, 14)).to.be(true); - expect(is.intersectsRange(13, 15)).to.be(true); - expect(is.intersectsRange(14, 16)).to.be(false); - expect(is.intersectsRange(15, 17)).to.be(false); - }); - - it('returns the expected value for large ranges', function() { - expect(is.intersectsRange(-3, 1)).to.be(false); - expect(is.intersectsRange(1, 5)).to.be(true); - expect(is.intersectsRange(1, 9)).to.be(true); - expect(is.intersectsRange(1, 13)).to.be(true); - expect(is.intersectsRange(1, 17)).to.be(true); - expect(is.intersectsRange(5, 9)).to.be(true); - expect(is.intersectsRange(5, 13)).to.be(true); - expect(is.intersectsRange(5, 17)).to.be(true); - expect(is.intersectsRange(9, 13)).to.be(true); - expect(is.intersectsRange(9, 17)).to.be(true); - expect(is.intersectsRange(13, 17)).to.be(true); - expect(is.intersectsRange(17, 21)).to.be(false); - }); - - }); - - describe('isEmpty', function() { - - it('returns false', function() { - expect(is.isEmpty()).to.be(false); - }); - - }); - - describe('removeRange', function() { - - it('removes the first part of the first element', function() { - is.removeRange(4, 5); - expect(is.getArray()).to.eql([5, 6, 8, 10, 12, 14]); - }); - - it('removes the last part of the first element', function() { - is.removeRange(5, 6); - expect(is.getArray()).to.eql([4, 5, 8, 10, 12, 14]); - }); - - it('removes the first element', function() { - is.removeRange(4, 6); - expect(is.getArray()).to.eql([8, 10, 12, 14]); - }); - - it('removes the first part of a middle element', function() { - is.removeRange(8, 9); - expect(is.getArray()).to.eql([4, 6, 9, 10, 12, 14]); - }); - - it('removes the last part of a middle element', function() { - is.removeRange(9, 10); - expect(is.getArray()).to.eql([4, 6, 8, 9, 12, 14]); - }); - - it('removes a middle element', function() { - is.removeRange(8, 10); - expect(is.getArray()).to.eql([4, 6, 12, 14]); - }); - - it('removes the first part of the last element', function() { - is.removeRange(12, 13); - expect(is.getArray()).to.eql([4, 6, 8, 10, 13, 14]); - }); - - it('removes the last part of the last element', function() { - is.removeRange(13, 14); - expect(is.getArray()).to.eql([4, 6, 8, 10, 12, 13]); - }); - - it('removes the last element', function() { - is.removeRange(12, 14); - expect(is.getArray()).to.eql([4, 6, 8, 10]); - }); - - it('can remove multiple ranges near the start', function() { - is.removeRange(3, 11); - expect(is.getArray()).to.eql([12, 14]); - }); - - it('can remove multiple ranges near the start', function() { - is.removeRange(7, 15); - expect(is.getArray()).to.eql([4, 6]); - }); - - it('throws an exception when passed an invalid range', function() { - expect(function() { - is.removeRange(2, 0); - }).to.throwException(); - }); - - }); - - describe('toString', function() { - - it('returns the expected value', function() { - expect(is.toString()).to.be('4-6, 8-10, 12-14'); - }); - }); - - }); - - describe('with fragmentation', function() { - - var is; - beforeEach(function() { - is = new ol.structs.IntegerSet([0, 1, 2, 4, 5, 8, 9, 12, 13, 15, 16, 17]); - }); - - describe('findRange', function() { - - it('finds the first range of size 1', function() { - expect(is.findRange(1)).to.be(0); - }); - - it('finds the first range of size 2', function() { - expect(is.findRange(2)).to.be(2); - }); - - it('finds the first range of size 3', function() { - expect(is.findRange(3)).to.be(5); - }); - - it('returns -1 when no range can be found', function() { - expect(is.findRange(4)).to.be(-1); - }); - - }); - - describe('getFirst', function() { - - it('returns the expected value', function() { - expect(is.getFirst()).to.be(0); - }); - - }); - - describe('getLast', function() { - - it('returns the expected value', function() { - expect(is.getLast()).to.be(17); - }); - - }); - - describe('getSize', function() { - - it('returns the expected value', function() { - expect(is.getSize()).to.be(12); - }); - - }); - - describe('removeRange', function() { - - it('removing an empty range has no effect', function() { - is.removeRange(0, 0); - expect(is.getArray()).to.eql( - [0, 1, 2, 4, 5, 8, 9, 12, 13, 15, 16, 17]); - }); - - it('can remove elements from the middle of range', function() { - is.removeRange(6, 7); - expect(is.getArray()).to.eql( - [0, 1, 2, 4, 5, 6, 7, 8, 9, 12, 13, 15, 16, 17]); - }); - - it('can remove multiple ranges', function() { - is.removeRange(2, 12); - expect(is.getArray()).to.eql([0, 1, 13, 15, 16, 17]); - }); - - it('can remove multiple ranges and reduce others', function() { - is.removeRange(0, 10); - expect(is.getArray()).to.eql([10, 12, 13, 15, 16, 17]); - }); - - it('can remove all ranges', function() { - is.removeRange(0, 18); - expect(is.getArray()).to.eql([]); - }); - - }); - - describe('toString', function() { - - it('returns the expected value', function() { - expect(is.toString()).to.be('0-1, 2-4, 5-8, 9-12, 13-15, 16-17'); - }); - - }); - - }); - - describe('compared to a slow reference implementation', function() { - - var SimpleIntegerSet = function() { - this.integers_ = {}; - }; - - SimpleIntegerSet.prototype.addRange = function(addStart, addStop) { - var i; - for (i = addStart; i < addStop; ++i) { - this.integers_[i.toString()] = true; - } - }; - - SimpleIntegerSet.prototype.clear = function() { - this.integers_ = {}; - }; - - SimpleIntegerSet.prototype.getArray = function() { - var integers = goog.array.map( - goog.object.getKeys(this.integers_), Number); - goog.array.sort(integers); - var arr = []; - var start = -1, stop; - var i; - for (i = 0; i < integers.length; ++i) { - if (start == -1) { - start = stop = integers[i]; - } else if (integers[i] == stop + 1) { - ++stop; - } else { - arr.push(start, stop + 1); - start = stop = integers[i]; - } - } - if (start != -1) { - arr.push(start, stop + 1); - } - return arr; - }; - - SimpleIntegerSet.prototype.removeRange = function(removeStart, removeStop) { - var i; - for (i = removeStart; i < removeStop; ++i) { - delete this.integers_[i.toString()]; - } - }; - - var is, sis; - beforeEach(function() { - is = new ol.structs.IntegerSet(); - sis = new SimpleIntegerSet(); - }); - - it('behaves identically with random adds', function() { - var addStart, addStop, i; - for (i = 0; i < 64; ++i) { - addStart = goog.math.randomInt(128); - addStop = addStart + goog.math.randomInt(16); - is.addRange(addStart, addStop); - sis.addRange(addStart, addStop); - expect(is.getArray()).to.eql(sis.getArray()); - } - }); - - it('behaves identically with random removes', function() { - is.addRange(0, 128); - sis.addRange(0, 128); - var i, removeStart, removeStop; - for (i = 0; i < 64; ++i) { - removeStart = goog.math.randomInt(128); - removeStop = removeStart + goog.math.randomInt(16); - is.removeRange(removeStart, removeStop); - sis.removeRange(removeStart, removeStop); - expect(is.getArray()).to.eql(sis.getArray()); - } - }); - - it('behaves identically with random adds and removes', function() { - var i, start, stop; - for (i = 0; i < 64; ++i) { - start = goog.math.randomInt(128); - stop = start + goog.math.randomInt(16); - if (Math.random() < 0.5) { - is.addRange(start, stop); - sis.addRange(start, stop); - } else { - is.removeRange(start, stop); - sis.removeRange(start, stop); - } - expect(is.getArray()).to.eql(sis.getArray()); - } - }); - - it('behaves identically with random adds, removes, and clears', function() { - var i, p, start, stop; - for (i = 0; i < 64; ++i) { - start = goog.math.randomInt(128); - stop = start + goog.math.randomInt(16); - p = Math.random(); - if (p < 0.45) { - is.addRange(start, stop); - sis.addRange(start, stop); - } else if (p < 0.9) { - is.removeRange(start, stop); - sis.removeRange(start, stop); - } else { - is.clear(); - sis.clear(); - } - expect(is.getArray()).to.eql(sis.getArray()); - } - }); - - }); - -}); - - -goog.require('goog.array'); -goog.require('goog.math'); -goog.require('goog.object'); -goog.require('ol.structs.IntegerSet'); diff --git a/test/spec/ol/style/atlasmanager.test.js b/test/spec/ol/style/atlasmanager.test.js new file mode 100644 index 00000000000..66f5707fd13 --- /dev/null +++ b/test/spec/ol/style/atlasmanager.test.js @@ -0,0 +1,273 @@ +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({initialSize: 128}); + var info = manager.add('1', 32, 32, defaultRender); + + 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({initialSize: 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({initialSize: 128}); + 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({initialSize: 128}); + expect(manager.add('1', 100, 100, defaultRender, defaultRender)) + .to.be.ok(); + expect(manager.atlases_).to.have.length(1); + 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({initialSize: 128}); + 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); + 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( + {initialSize: 128, maxSize: 2048}); + expect(manager.add('1', 100, 100, defaultRender, defaultRender)) + .to.be.ok(); + expect(manager.add('2', 2048, 2048, defaultRender, defaultRender)) + .to.eql(null); + }); + }); + + describe('#getInfo', function() { + + it('returns null if no entry for the given id', function() { + var manager = new ol.style.AtlasManager({initialSize: 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 00000000000..4e5d0b985c7 --- /dev/null +++ b/test/spec/ol/style/circlestyle.test.js @@ -0,0 +1,240 @@ +goog.provide('ol.test.style.Circle'); + + +describe('ol.style.Circle', function() { + + describe('#constructor', function() { + + it('creates a canvas if no atlas is used (no fill-style)', 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]); + // hit-detection image is created, because no fill style is set + expect(style.getImage()).to.not.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionOrigin()).to.eql([0, 0]); + }); + + it('creates a canvas if no atlas is used (fill-style)', function() { + var style = new ol.style.Circle({ + radius: 10, + fill: new ol.style.Fill({ + color: '#FFFF00' + }) + }); + 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]); + // no hit-detection image is created, because fill style is set + expect(style.getImage()).to.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionOrigin()).to.eql([0, 0]); + }); + + it('adds itself to an atlas manager (no fill-style)', function() { + var atlasManager = new ol.style.AtlasManager({initialSize: 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]); + // hit-detection image is created, because no fill style is set + expect(style.getImage()).to.not.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + expect(style.getHitDetectionImageSize()).to.eql([512, 512]); + expect(style.getHitDetectionOrigin()).to.eql([1, 1]); + }); + + it('adds itself to an atlas manager (fill-style)', function() { + var atlasManager = new ol.style.AtlasManager({initialSize: 512}); + var style = new ol.style.Circle({ + radius: 10, + atlasManager: atlasManager, + fill: new ol.style.Fill({ + color: '#FFFF00' + }) + }); + 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]); + // no hit-detection image is created, because fill style is set + expect(style.getImage()).to.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + expect(style.getHitDetectionImageSize()).to.eql([512, 512]); + expect(style.getHitDetectionOrigin()).to.eql([1, 1]); + }); + }); + + 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.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 index c5156700567..f972ae7fc6a 100644 --- a/test/spec/ol/style/regularshapestyle.test.js +++ b/test/spec/ol/style/regularshapestyle.test.js @@ -1,36 +1,297 @@ goog.provide('ol.test.style.RegularShape'); describe('ol.style.RegularShape', function() { - it('can use radius', function() { - var style = new ol.style.RegularShape({ - radius: 5, - radius2: 10 + + describe('#constructor', function() { + + it('can use radius', function() { + var style = new ol.style.RegularShape({ + radius: 5, + radius2: 10 + }); + expect(style.getRadius()).to.eql(5); + expect(style.getRadius2()).to.eql(10); }); - expect(style.getRadius()).to.eql(5); - expect(style.getRadius2()).to.eql(10); - }); - it('can use radius1 as an alias for radius', function() { - var style = new ol.style.RegularShape({ - radius1: 5, - radius2: 10 + + it('can use radius1 as an alias for radius', function() { + var style = new ol.style.RegularShape({ + radius1: 5, + radius2: 10 + }); + expect(style.getRadius()).to.eql(5); + expect(style.getRadius2()).to.eql(10); }); - expect(style.getRadius()).to.eql(5); - expect(style.getRadius2()).to.eql(10); - }); - it('will use radius for radius2 if radius2 not defined', function() { - var style = new ol.style.RegularShape({ - radius: 5 + + it('will use radius for radius2 if radius2 not defined', function() { + var style = new ol.style.RegularShape({ + radius: 5 + }); + expect(style.getRadius()).to.eql(5); + expect(style.getRadius2()).to.eql(5); + }); + + it('will use radius1 for radius2 if radius2 not defined', function() { + var style = new ol.style.RegularShape({ + radius1: 5 + }); + expect(style.getRadius()).to.eql(5); + expect(style.getRadius2()).to.eql(5); + }); + + it('creates a canvas if no atlas is used (no fill-style)', 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]); + // hit-detection image is created, because no fill style is set + expect(style.getImage()).to.not.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionOrigin()).to.eql([0, 0]); + }); + + it('creates a canvas if no atlas is used (fill-style)', function() { + var style = new ol.style.RegularShape({ + radius: 10, + fill: new ol.style.Fill({ + color: '#FFFF00' + }) + }); + 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]); + // no hit-detection image is created, because fill style is set + expect(style.getImage()).to.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionOrigin()).to.eql([0, 0]); + }); + + it('adds itself to an atlas manager (no fill-style)', function() { + var atlasManager = new ol.style.AtlasManager({initialSize: 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]); + // hit-detection image is created, because no fill style is set + expect(style.getImage()).to.not.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + expect(style.getHitDetectionImageSize()).to.eql([512, 512]); + expect(style.getHitDetectionOrigin()).to.eql([1, 1]); + }); + + it('adds itself to an atlas manager (fill-style)', function() { + var atlasManager = new ol.style.AtlasManager({initialSize: 512}); + var style = new ol.style.RegularShape({ + radius: 10, + atlasManager: atlasManager, + fill: new ol.style.Fill({ + color: '#FFFF00' + }) + }); + 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]); + // no hit-detection image is created, because fill style is set + expect(style.getImage()).to.be(style.getHitDetectionImage()); + expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); + expect(style.getHitDetectionImageSize()).to.eql([512, 512]); + expect(style.getHitDetectionOrigin()).to.eql([1, 1]); }); - expect(style.getRadius()).to.eql(5); - expect(style.getRadius2()).to.eql(5); }); - it('will use radius1 for radius2 if radius2 not defined', function() { - var style = new ol.style.RegularShape({ - radius1: 5 + + + describe('#getChecksum', function() { + + it('calculates not the same hash code (radius)', function() { + var style1 = new ol.style.RegularShape({ + radius: 4, + radius2: 5 + }); + var style2 = new ol.style.RegularShape({ + radius: 3, + radius2: 5 + }); + expect(style1.getChecksum()).to.not.eql(style2.getChecksum()); + }); + + it('calculates not the same hash code (radius2)', function() { + var style1 = new ol.style.RegularShape({ + radius: 4, + radius2: 5 + }); + var style2 = new ol.style.RegularShape({ + radius: 4, + radius2: 6 + }); + 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()); }); - expect(style.getRadius()).to.eql(5); - expect(style.getRadius2()).to.eql(5); + + 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'); diff --git a/test/spec/ol/webgl/buffer.test.js b/test/spec/ol/webgl/buffer.test.js new file mode 100644 index 00000000000..7d7153e32eb --- /dev/null +++ b/test/spec/ol/webgl/buffer.test.js @@ -0,0 +1,55 @@ +goog.provide('ol.test.webgl.Buffer'); + + +describe('ol.webgl.Buffer', function() { + + describe('constructor', function() { + + describe('without an argument', function() { + + var b; + beforeEach(function() { + b = new ol.webgl.Buffer(); + }); + + it('constructs an empty instance', function() { + expect(b.getArray()).to.be.empty(); + }); + + }); + + describe('with a single array argument', function() { + + var b; + beforeEach(function() { + b = new ol.webgl.Buffer([0, 1, 2, 3]); + }); + + it('constructs a populated instance', function() { + expect(b.getArray()).to.eql([0, 1, 2, 3]); + }); + + }); + + }); + + describe('with an empty instance', function() { + + var b; + beforeEach(function() { + b = new ol.webgl.Buffer(); + }); + + describe('getArray', function() { + + it('returns an empty array', function() { + expect(b.getArray()).to.be.empty(); + }); + + }); + + }); + +}); + +goog.require('ol.webgl.Buffer');