diff --git a/src/ol/render/webgl.js b/src/ol/render/webgl.js index e016bd64cd..26c10c7401 100644 --- a/src/ol/render/webgl.js +++ b/src/ol/render/webgl.js @@ -5,12 +5,20 @@ goog.require('ol'); if (ol.ENABLE_WEBGL) { + /** + * @const + * @type {string} + */ + ol.render.webgl.defaultFont = '10px sans-serif'; + + /** * @const * @type {ol.Color} */ ol.render.webgl.defaultFillStyle = [0.0, 0.0, 0.0, 1.0]; + /** * @const * @type {string} @@ -51,6 +59,21 @@ if (ol.ENABLE_WEBGL) { */ ol.render.webgl.defaultStrokeStyle = [0.0, 0.0, 0.0, 1.0]; + + /** + * @const + * @type {number} + */ + ol.render.webgl.defaultTextAlign = 0.5; + + + /** + * @const + * @type {number} + */ + ol.render.webgl.defaultTextBaseline = 0.5; + + /** * @const * @type {number} diff --git a/src/ol/render/webgl/imagereplay.js b/src/ol/render/webgl/imagereplay.js index 16fb0be045..47f22045ef 100644 --- a/src/ol/render/webgl/imagereplay.js +++ b/src/ol/render/webgl/imagereplay.js @@ -1,123 +1,34 @@ goog.provide('ol.render.webgl.ImageReplay'); goog.require('ol'); -goog.require('ol.extent'); -goog.require('ol.obj'); -goog.require('ol.render.webgl.imagereplay.defaultshader'); -goog.require('ol.render.webgl.Replay'); -goog.require('ol.webgl'); +goog.require('ol.render.webgl.TextureReplay'); goog.require('ol.webgl.Buffer'); -goog.require('ol.webgl.Context'); if (ol.ENABLE_WEBGL) { /** * @constructor - * @extends {ol.render.webgl.Replay} + * @extends {ol.render.webgl.TextureReplay} * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Max extent. * @struct */ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { - ol.render.webgl.Replay.call(this, tolerance, maxExtent); - - /** - * @type {number|undefined} - * @private - */ - this.anchorX_ = undefined; - - /** - * @type {number|undefined} - * @private - */ - this.anchorY_ = undefined; - - /** - * @type {Array.} - * @private - */ - this.groupIndices_ = []; - - /** - * @type {Array.} - * @private - */ - this.hitDetectionGroupIndices_ = []; - - /** - * @type {number|undefined} - * @private - */ - this.height_ = undefined; + ol.render.webgl.TextureReplay.call(this, tolerance, maxExtent); /** * @type {Array.} - * @private + * @protected */ this.images_ = []; /** * @type {Array.} - * @private + * @protected */ this.hitDetectionImages_ = []; - /** - * @type {number|undefined} - * @private - */ - this.imageHeight_ = undefined; - - /** - * @type {number|undefined} - * @private - */ - this.imageWidth_ = undefined; - - /** - * @private - * @type {ol.render.webgl.imagereplay.defaultshader.Locations} - */ - this.defaultLocations_ = null; - - /** - * @private - * @type {number|undefined} - */ - this.opacity_ = undefined; - - /** - * @type {number|undefined} - * @private - */ - this.originX_ = undefined; - - /** - * @type {number|undefined} - * @private - */ - this.originY_ = undefined; - - /** - * @private - * @type {boolean|undefined} - */ - this.rotateWithView_ = undefined; - - /** - * @private - * @type {number|undefined} - */ - this.rotation_ = undefined; - - /** - * @private - * @type {number|undefined} - */ - this.scale_ = undefined; - /** * @type {Array.} * @private @@ -130,141 +41,8 @@ if (ol.ENABLE_WEBGL) { */ this.hitDetectionTextures_ = []; - /** - * @type {number|undefined} - * @private - */ - this.width_ = undefined; - }; - ol.inherits(ol.render.webgl.ImageReplay, ol.render.webgl.Replay); - - - /** - * @inheritDoc - */ - ol.render.webgl.ImageReplay.prototype.getDeleteResourcesFunction = function(context) { - var verticesBuffer = this.verticesBuffer; - var indicesBuffer = this.indicesBuffer; - var textures = this.textures_; - var hitDetectionTextures = this.hitDetectionTextures_; - 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]); - } - for (i = 0, ii = hitDetectionTextures.length; i < ii; ++i) { - gl.deleteTexture(hitDetectionTextures[i]); - } - } - context.deleteBuffer(verticesBuffer); - context.deleteBuffer(indicesBuffer); - }; - }; - - - /** - * @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) { - var anchorX = /** @type {number} */ (this.anchorX_); - var anchorY = /** @type {number} */ (this.anchorY_); - var height = /** @type {number} */ (this.height_); - var imageHeight = /** @type {number} */ (this.imageHeight_); - var imageWidth = /** @type {number} */ (this.imageWidth_); - var opacity = /** @type {number} */ (this.opacity_); - var originX = /** @type {number} */ (this.originX_); - var originY = /** @type {number} */ (this.originY_); - var rotateWithView = this.rotateWithView_ ? 1.0 : 0.0; - // this.rotation_ is anti-clockwise, but rotation is clockwise - var rotation = /** @type {number} */ (-this.rotation_); - var scale = /** @type {number} */ (this.scale_); - var width = /** @type {number} */ (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; }; + ol.inherits(ol.render.webgl.ImageReplay, ol.render.webgl.TextureReplay); /** @@ -275,7 +53,7 @@ if (ol.ENABLE_WEBGL) { this.startIndicesFeature.push(feature); var flatCoordinates = multiPointGeometry.getFlatCoordinates(); var stride = multiPointGeometry.getStride(); - this.drawCoordinates_( + this.drawCoordinates( flatCoordinates, 0, flatCoordinates.length, stride); }; @@ -288,7 +66,7 @@ if (ol.ENABLE_WEBGL) { this.startIndicesFeature.push(feature); var flatCoordinates = pointGeometry.getFlatCoordinates(); var stride = pointGeometry.getStride(); - this.drawCoordinates_( + this.drawCoordinates( flatCoordinates, 0, flatCoordinates.length, stride); }; @@ -299,8 +77,8 @@ if (ol.ENABLE_WEBGL) { ol.render.webgl.ImageReplay.prototype.finish = function(context) { var gl = context.getGL(); - this.groupIndices_.push(this.indices.length); - this.hitDetectionGroupIndices_.push(this.indices.length); + this.groupIndices.push(this.indices.length); + this.hitDetectionGroupIndices.push(this.indices.length); // create, bind, and populate the vertices buffer this.verticesBuffer = new ol.webgl.Buffer(this.vertices); @@ -314,246 +92,14 @@ if (ol.ENABLE_WEBGL) { /** @type {Object.} */ var texturePerImage = {}; - this.createTextures_(this.textures_, this.images_, texturePerImage, gl); + this.createTextures(this.textures_, this.images_, texturePerImage, gl); - this.createTextures_(this.hitDetectionTextures_, this.hitDetectionImages_, + this.createTextures(this.hitDetectionTextures_, this.hitDetectionImages_, texturePerImage, gl); - this.anchorX_ = undefined; - this.anchorY_ = undefined; - this.height_ = undefined; this.images_ = null; this.hitDetectionImages_ = 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; - }; - - - /** - * @private - * @param {Array.} textures Textures. - * @param {Array.} images - * Images. - * @param {Object.} texturePerImage Texture cache. - * @param {WebGLRenderingContext} gl Gl. - */ - ol.render.webgl.ImageReplay.prototype.createTextures_ = function(textures, images, texturePerImage, gl) { - var texture, image, uid, i; - var ii = images.length; - for (i = 0; i < ii; ++i) { - image = images[i]; - - uid = ol.getUid(image).toString(); - if (uid in texturePerImage) { - texture = texturePerImage[uid]; - } else { - texture = ol.webgl.Context.createTexture( - gl, image, ol.webgl.CLAMP_TO_EDGE, ol.webgl.CLAMP_TO_EDGE); - texturePerImage[uid] = texture; - } - textures[i] = texture; - } - }; - - - /** - * @inheritDoc - */ - ol.render.webgl.ImageReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { - // get the program - var fragmentShader = ol.render.webgl.imagereplay.defaultshader.fragment; - var vertexShader = ol.render.webgl.imagereplay.defaultshader.vertex; - var program = context.getProgram(fragmentShader, vertexShader); - - // get the locations - var locations; - if (!this.defaultLocations_) { - // eslint-disable-next-line openlayers-internal/no-missing-requires - locations = new ol.render.webgl.imagereplay.defaultshader.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, ol.webgl.FLOAT, - false, 32, 0); - - gl.enableVertexAttribArray(locations.a_offsets); - gl.vertexAttribPointer(locations.a_offsets, 2, ol.webgl.FLOAT, - false, 32, 8); - - gl.enableVertexAttribArray(locations.a_texCoord); - gl.vertexAttribPointer(locations.a_texCoord, 2, ol.webgl.FLOAT, - false, 32, 16); - - gl.enableVertexAttribArray(locations.a_opacity); - gl.vertexAttribPointer(locations.a_opacity, 1, ol.webgl.FLOAT, - false, 32, 24); - - gl.enableVertexAttribArray(locations.a_rotateWithView); - gl.vertexAttribPointer(locations.a_rotateWithView, 1, ol.webgl.FLOAT, - false, 32, 28); - - return locations; - }; - - - /** - * @inheritDoc - */ - ol.render.webgl.ImageReplay.prototype.shutDownProgram = function(gl, locations) { - 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.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { - var textures = hitDetection ? this.hitDetectionTextures_ : this.textures_; - var groupIndices = hitDetection ? this.hitDetectionGroupIndices_ : this.groupIndices_; - - if (!ol.obj.isEmpty(skippedFeaturesHash)) { - this.drawReplaySkipping_( - gl, context, skippedFeaturesHash, textures, groupIndices); - } else { - var i, ii, start; - for (i = 0, ii = textures.length, start = 0; i < ii; ++i) { - gl.bindTexture(ol.webgl.TEXTURE_2D, textures[i]); - var end = groupIndices[i]; - this.drawElements(gl, context, start, end); - start = end; - } - } - }; - - - /** - * Draw the replay while paying attention to skipped features. - * - * This functions creates groups of features that can be drawn to together, - * so that the number of `drawElements` calls is minimized. - * - * For example given the following texture groups: - * - * Group 1: A B C - * Group 2: D [E] F G - * - * If feature E should be skipped, the following `drawElements` calls will be - * made: - * - * drawElements with feature A, B and C - * drawElements with feature D - * drawElements with feature F and G - * - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {ol.webgl.Context} context Context. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - * @param {Array.} textures Textures. - * @param {Array.} groupIndices Texture group indices. - */ - ol.render.webgl.ImageReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash, textures, - groupIndices) { - var featureIndex = 0; - - var i, ii; - for (i = 0, ii = textures.length; i < ii; ++i) { - gl.bindTexture(ol.webgl.TEXTURE_2D, textures[i]); - var groupStart = (i > 0) ? groupIndices[i - 1] : 0; - var groupEnd = groupIndices[i]; - - var start = groupStart; - var end = groupStart; - while (featureIndex < this.startIndices.length && - this.startIndices[featureIndex] <= groupEnd) { - var feature = this.startIndicesFeature[featureIndex]; - - var featureUid = ol.getUid(feature).toString(); - if (skippedFeaturesHash[featureUid] !== undefined) { - // feature should be skipped - if (start !== end) { - // draw the features so far - this.drawElements(gl, context, start, end); - } - // continue with the next feature - start = (featureIndex === this.startIndices.length - 1) ? - groupEnd : this.startIndices[featureIndex + 1]; - end = start; - } else { - // the feature is not skipped, augment the end index - end = (featureIndex === this.startIndices.length - 1) ? - groupEnd : this.startIndices[featureIndex + 1]; - } - featureIndex++; - } - - if (start !== end) { - // draw the remaining features (in case there was no skipped feature - // in this texture group, all features of a group are drawn together) - this.drawElements(gl, context, start, end); - } - } - }; - - - /** - * @inheritDoc - */ - ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, - featureCallback, opt_hitExtent) { - var i, groupStart, start, end, feature, featureUid; - var featureIndex = this.startIndices.length - 1; - for (i = this.hitDetectionTextures_.length - 1; i >= 0; --i) { - gl.bindTexture(ol.webgl.TEXTURE_2D, this.hitDetectionTextures_[i]); - groupStart = (i > 0) ? this.hitDetectionGroupIndices_[i - 1] : 0; - end = this.hitDetectionGroupIndices_[i]; - - // draw all features for this texture group - while (featureIndex >= 0 && - this.startIndices[featureIndex] >= groupStart) { - start = this.startIndices[featureIndex]; - feature = this.startIndicesFeature[featureIndex]; - featureUid = ol.getUid(feature).toString(); - - if (skippedFeaturesHash[featureUid] === undefined && - feature.getGeometry() && - (opt_hitExtent === undefined || ol.extent.intersects( - /** @type {Array} */ (opt_hitExtent), - feature.getGeometry().getExtent()))) { - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.drawElements(gl, context, start, end); - - var result = featureCallback(feature); - if (result) { - return result; - } - } - - end = start; - featureIndex--; - } - } - return undefined; + ol.render.webgl.TextureReplay.prototype.finish.call(this, context); }; @@ -578,7 +124,7 @@ if (ol.ENABLE_WEBGL) { } else { currentImage = this.images_[this.images_.length - 1]; if (ol.getUid(currentImage) != ol.getUid(image)) { - this.groupIndices_.push(this.indices.length); + this.groupIndices.push(this.indices.length); this.images_.push(image); } } @@ -589,23 +135,39 @@ if (ol.ENABLE_WEBGL) { currentImage = this.hitDetectionImages_[this.hitDetectionImages_.length - 1]; if (ol.getUid(currentImage) != ol.getUid(hitDetectionImage)) { - this.hitDetectionGroupIndices_.push(this.indices.length); + this.hitDetectionGroupIndices.push(this.indices.length); this.hitDetectionImages_.push(hitDetectionImage); } } - 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]; + 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.getTextures = function(opt_all) { + return opt_all ? this.textures_.concat(this.hitDetectionTextures_) : this.textures_; + }; + + + /** + * @inheritDoc + */ + ol.render.webgl.ImageReplay.prototype.getHitDetectionTextures = function() { + return this.hitDetectionTextures_; }; } diff --git a/src/ol/render/webgl/imagereplay/defaultshader.js b/src/ol/render/webgl/imagereplay/defaultshader.js deleted file mode 100644 index ee5fef2ea4..0000000000 --- a/src/ol/render/webgl/imagereplay/defaultshader.js +++ /dev/null @@ -1,154 +0,0 @@ -// This file is automatically generated, do not edit -/* eslint openlayers-internal/no-missing-requires: 0 */ -goog.provide('ol.render.webgl.imagereplay.defaultshader'); - -goog.require('ol'); -goog.require('ol.webgl.Fragment'); -goog.require('ol.webgl.Vertex'); - -if (ol.ENABLE_WEBGL) { - - /** - * @constructor - * @extends {ol.webgl.Fragment} - * @struct - */ - ol.render.webgl.imagereplay.defaultshader.Fragment = function() { - ol.webgl.Fragment.call(this, ol.render.webgl.imagereplay.defaultshader.Fragment.SOURCE); - }; - ol.inherits(ol.render.webgl.imagereplay.defaultshader.Fragment, ol.webgl.Fragment); - - - /** - * @const - * @type {string} - */ - ol.render.webgl.imagereplay.defaultshader.Fragment.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.defaultshader.Fragment.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.defaultshader.Fragment.SOURCE = ol.DEBUG_WEBGL ? - ol.render.webgl.imagereplay.defaultshader.Fragment.DEBUG_SOURCE : - ol.render.webgl.imagereplay.defaultshader.Fragment.OPTIMIZED_SOURCE; - - - ol.render.webgl.imagereplay.defaultshader.fragment = new ol.render.webgl.imagereplay.defaultshader.Fragment(); - - - /** - * @constructor - * @extends {ol.webgl.Vertex} - * @struct - */ - ol.render.webgl.imagereplay.defaultshader.Vertex = function() { - ol.webgl.Vertex.call(this, ol.render.webgl.imagereplay.defaultshader.Vertex.SOURCE); - }; - ol.inherits(ol.render.webgl.imagereplay.defaultshader.Vertex, ol.webgl.Vertex); - - - /** - * @const - * @type {string} - */ - ol.render.webgl.imagereplay.defaultshader.Vertex.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, 0.0);\n gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n'; - - - /** - * @const - * @type {string} - */ - ol.render.webgl.imagereplay.defaultshader.Vertex.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,0.0);gl_Position=h*vec4(c,0.0,1.0)+offsets;a=d;b=f;}'; - - - /** - * @const - * @type {string} - */ - ol.render.webgl.imagereplay.defaultshader.Vertex.SOURCE = ol.DEBUG_WEBGL ? - ol.render.webgl.imagereplay.defaultshader.Vertex.DEBUG_SOURCE : - ol.render.webgl.imagereplay.defaultshader.Vertex.OPTIMIZED_SOURCE; - - - ol.render.webgl.imagereplay.defaultshader.vertex = new ol.render.webgl.imagereplay.defaultshader.Vertex(); - - - /** - * @constructor - * @param {WebGLRenderingContext} gl GL. - * @param {WebGLProgram} program Program. - * @struct - */ - ol.render.webgl.imagereplay.defaultshader.Locations = function(gl, program) { - - /** - * @type {WebGLUniformLocation} - */ - this.u_image = gl.getUniformLocation( - program, ol.DEBUG_WEBGL ? 'u_image' : 'l'); - - /** - * @type {WebGLUniformLocation} - */ - this.u_offsetRotateMatrix = gl.getUniformLocation( - program, ol.DEBUG_WEBGL ? 'u_offsetRotateMatrix' : 'j'); - - /** - * @type {WebGLUniformLocation} - */ - this.u_offsetScaleMatrix = gl.getUniformLocation( - program, ol.DEBUG_WEBGL ? 'u_offsetScaleMatrix' : 'i'); - - /** - * @type {WebGLUniformLocation} - */ - this.u_opacity = gl.getUniformLocation( - program, ol.DEBUG_WEBGL ? 'u_opacity' : 'k'); - - /** - * @type {WebGLUniformLocation} - */ - this.u_projectionMatrix = gl.getUniformLocation( - program, ol.DEBUG_WEBGL ? 'u_projectionMatrix' : 'h'); - - /** - * @type {number} - */ - this.a_offsets = gl.getAttribLocation( - program, ol.DEBUG_WEBGL ? 'a_offsets' : 'e'); - - /** - * @type {number} - */ - this.a_opacity = gl.getAttribLocation( - program, ol.DEBUG_WEBGL ? 'a_opacity' : 'f'); - - /** - * @type {number} - */ - this.a_position = gl.getAttribLocation( - program, ol.DEBUG_WEBGL ? 'a_position' : 'c'); - - /** - * @type {number} - */ - this.a_rotateWithView = gl.getAttribLocation( - program, ol.DEBUG_WEBGL ? 'a_rotateWithView' : 'g'); - - /** - * @type {number} - */ - this.a_texCoord = gl.getAttribLocation( - program, ol.DEBUG_WEBGL ? 'a_texCoord' : 'd'); - }; - -} diff --git a/src/ol/render/webgl/replay.js b/src/ol/render/webgl/replay.js index 543d1048cc..a6d7ef24db 100644 --- a/src/ol/render/webgl/replay.js +++ b/src/ol/render/webgl/replay.js @@ -140,9 +140,9 @@ if (ol.ENABLE_WEBGL) { * @param {ol.Size} size Size. * @param {number} pixelRatio Pixel ratio. * @return {ol.render.webgl.circlereplay.defaultshader.Locations| - ol.render.webgl.imagereplay.defaultshader.Locations| ol.render.webgl.linestringreplay.defaultshader.Locations| - ol.render.webgl.polygonreplay.defaultshader.Locations} Locations. + ol.render.webgl.polygonreplay.defaultshader.Locations| + ol.render.webgl.texturereplay.defaultshader.Locations} Locations. */ ol.render.webgl.Replay.prototype.setUpProgram = function(gl, context, size, pixelRatio) {}; @@ -152,9 +152,9 @@ if (ol.ENABLE_WEBGL) { * @protected * @param {WebGLRenderingContext} gl gl. * @param {ol.render.webgl.circlereplay.defaultshader.Locations| - ol.render.webgl.imagereplay.defaultshader.Locations| ol.render.webgl.linestringreplay.defaultshader.Locations| - ol.render.webgl.polygonreplay.defaultshader.Locations} locations Locations. + ol.render.webgl.polygonreplay.defaultshader.Locations| + ol.render.webgl.texturereplay.defaultshader.Locations} locations Locations. */ ol.render.webgl.Replay.prototype.shutDownProgram = function(gl, locations) {}; diff --git a/src/ol/render/webgl/textreplay.js b/src/ol/render/webgl/textreplay.js index 429b03a923..56f02502db 100644 --- a/src/ol/render/webgl/textreplay.js +++ b/src/ol/render/webgl/textreplay.js @@ -1,71 +1,455 @@ goog.provide('ol.render.webgl.TextReplay'); goog.require('ol'); +goog.require('ol.colorlike'); +goog.require('ol.dom'); +goog.require('ol.has'); +goog.require('ol.render.webgl'); +goog.require('ol.render.webgl.TextureReplay'); +goog.require('ol.style.AtlasManager'); +goog.require('ol.webgl.Buffer'); if (ol.ENABLE_WEBGL) { /** * @constructor - * @abstract + * @extends {ol.render.webgl.TextureReplay} * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Max extent. * @struct */ - ol.render.webgl.TextReplay = function(tolerance, maxExtent) {}; + ol.render.webgl.TextReplay = function(tolerance, maxExtent) { + ol.render.webgl.TextureReplay.call(this, tolerance, maxExtent); + + /** + * @private + * @type {Array.} + */ + this.images_ = []; + + /** + * @private + * @type {Array.} + */ + this.textures_ = []; + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.measureCanvas_ = ol.dom.createCanvasContext2D(0, 0).canvas; + + /** + * @private + * @type {{strokeColor: (ol.ColorLike|null), + * lineCap: (string|undefined), + * lineDash: Array., + * lineDashOffset: (number|undefined), + * lineJoin: (string|undefined), + * lineWidth: number, + * miterLimit: (number|undefined), + * fillColor: (ol.ColorLike|null), + * font: (string|undefined), + * scale: (number|undefined)}} + */ + this.state_ = { + strokeColor: null, + lineCap: undefined, + lineDash: null, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 0, + miterLimit: undefined, + fillColor: null, + font: undefined, + scale: undefined + }; + + /** + * @private + * @type {string} + */ + this.text_ = ''; + + /** + * @private + * @type {number|undefined} + */ + this.textAlign_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.textBaseline_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.offsetX_ = undefined; + + /** + * @private + * @type {number|undefined} + */ + this.offsetY_ = undefined; + + /** + * @private + * @type {Object.} + */ + this.atlases_ = {}; + + /** + * @private + * @type {ol.WebglGlyphAtlas|undefined} + */ + this.currAtlas_ = undefined; + + this.scale = 1; + + this.opacity = 1; + + }; + ol.inherits(ol.render.webgl.TextReplay, ol.render.webgl.TextureReplay); + /** - * @param {ol.style.Text} textStyle Text style. + * @inheritDoc */ - ol.render.webgl.TextReplay.prototype.setTextStyle = function(textStyle) {}; + ol.render.webgl.TextReplay.prototype.drawText = function(flatCoordinates, offset, + end, stride, geometry, feature) { + if (this.text_) { + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); - /** - * @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 {number} pixelRatio Pixel ratio. - * @param {number} opacity Global opacity. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback. - * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion. - * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting - * this extent are checked. - * @return {T|undefined} Callback result. - * @template T - */ - ol.render.webgl.TextReplay.prototype.replay = function(context, - center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash, - featureCallback, oneByOne, opt_hitExtent) { - return undefined; + var glyphAtlas = this.currAtlas_; + var lines = this.text_.split('\n'); + var textSize = this.getTextSize_(lines); + var i, ii, j, jj, currX, currY, charArr, charInfo; + var anchorX = Math.round(textSize[0] * this.textAlign_ + this.offsetX_); + var anchorY = Math.round(textSize[1] * this.textBaseline_ + this.offsetY_); + var lineWidth = (this.state_.lineWidth / 2) * this.state_.scale; + + for (i = 0, ii = lines.length; i < ii; ++i) { + currX = 0; + currY = glyphAtlas.height * i; + charArr = lines[i].split(''); + + for (j = 0, jj = charArr.length; j < jj; ++j) { + charInfo = glyphAtlas.atlas.getInfo(charArr[j]); + + if (charInfo) { + var image = charInfo.image; + + this.anchorX = anchorX - currX; + this.anchorY = anchorY - currY; + this.originX = j === 0 ? charInfo.offsetX - lineWidth : charInfo.offsetX; + this.originY = charInfo.offsetY; + this.height = glyphAtlas.height; + this.width = j === 0 || j === charArr.length - 1 ? + glyphAtlas.width[charArr[j]] + lineWidth : glyphAtlas.width[charArr[j]]; + this.imageHeight = image.height; + this.imageWidth = image.width; + + var currentImage; + if (this.images_.length === 0) { + this.images_.push(image); + } else { + currentImage = this.images_[this.images_.length - 1]; + if (ol.getUid(currentImage) != ol.getUid(image)) { + this.groupIndices.push(this.indices.length); + this.images_.push(image); + } + } + + this.drawText_(flatCoordinates, offset, end, stride); + } + currX += this.width; + } + } + } }; + /** + * @private + * @param {Array.} lines Label to draw split to lines. + * @return {Array.} Size of the label in pixels. + */ + ol.render.webgl.TextReplay.prototype.getTextSize_ = function(lines) { + var self = this; + var glyphAtlas = this.currAtlas_; + var textHeight = lines.length * glyphAtlas.height; + //Split every line to an array of chars, sum up their width, and select the longest. + var textWidth = lines.map(function(str) { + var sum = 0; + var i, ii; + for (i = 0, ii = str.length; i < ii; ++i) { + var curr = str[i]; + if (!glyphAtlas.width[curr]) { + self.addCharToAtlas_(curr); + } + sum += glyphAtlas.width[curr] ? glyphAtlas.width[curr] : 0; + } + return sum; + }).reduce(function(max, curr) { + return Math.max(max, curr); + }); + + return [textWidth, textHeight]; + }; + + + /** + * @private * @param {Array.} flatCoordinates Flat coordinates. * @param {number} offset Offset. * @param {number} end End. * @param {number} stride Stride. - * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry. - * @param {ol.Feature|ol.render.Feature} feature Feature. */ - ol.render.webgl.TextReplay.prototype.drawText = function(flatCoordinates, offset, - end, stride, geometry, feature) {}; + ol.render.webgl.TextReplay.prototype.drawText_ = function(flatCoordinates, offset, + end, stride) { + var i, ii; + for (i = offset, ii = end; i < ii; i += stride) { + this.drawCoordinates(flatCoordinates, offset, end, stride); + } + }; + /** - * @abstract - * @param {ol.webgl.Context} context Context. + * @private + * @param {string} char Character. */ - ol.render.webgl.TextReplay.prototype.finish = function(context) {}; + ol.render.webgl.TextReplay.prototype.addCharToAtlas_ = function(char) { + if (char.length === 1) { + var glyphAtlas = this.currAtlas_; + var state = this.state_; + var mCtx = this.measureCanvas_.getContext('2d'); + mCtx.font = state.font; + var width = Math.ceil(mCtx.measureText(char).width * state.scale); + + var info = glyphAtlas.atlas.add(char, width, glyphAtlas.height, + function(ctx, x, y) { + //Parameterize the canvas + ctx.font = /** @type {string} */ (state.font); + ctx.fillStyle = state.fillColor; + ctx.strokeStyle = state.strokeColor; + ctx.lineWidth = state.lineWidth; + ctx.lineCap = /*** @type {string} */ (state.lineCap); + ctx.lineJoin = /** @type {string} */ (state.lineJoin); + ctx.miterLimit = /** @type {number} */ (state.miterLimit); + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + if (ol.has.CANVAS_LINE_DASH && state.lineDash) { + //FIXME: use pixelRatio + ctx.setLineDash(state.lineDash); + ctx.lineDashOffset = /** @type {number} */ (state.lineDashOffset); + } + if (state.scale !== 1) { + //FIXME: use pixelRatio + ctx.setTransform(/** @type {number} */ (state.scale), 0, 0, + /** @type {number} */ (state.scale), 0, 0); + } + + //Draw the character on the canvas + if (state.strokeColor) { + ctx.strokeText(char, x, y); + } + if (state.fillColor) { + ctx.fillText(char, x, y); + } + }); + + if (info) { + glyphAtlas.width[char] = width; + } + } + }; + /** - * @param {ol.webgl.Context} context WebGL context. - * @return {function()} Delete resources function. + * @inheritDoc */ - ol.render.webgl.TextReplay.prototype.getDeleteResourcesFunction = function(context) { - return ol.nullFunction; + ol.render.webgl.TextReplay.prototype.finish = function(context) { + var gl = context.getGL(); + + this.groupIndices.push(this.indices.length); + this.hitDetectionGroupIndices = this.groupIndices; + + // create, bind, and populate the vertices buffer + this.verticesBuffer = new ol.webgl.Buffer(this.vertices); + + // create, bind, and populate the indices buffer + this.indicesBuffer = new ol.webgl.Buffer(this.indices); + + // create textures + /** @type {Object.} */ + var texturePerImage = {}; + + this.createTextures(this.textures_, this.images_, texturePerImage, gl); + + this.state_ = { + strokeColor: null, + lineCap: undefined, + lineDash: null, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 0, + miterLimit: undefined, + fillColor: null, + font: undefined, + scale: undefined + }; + this.text_ = ''; + this.textAlign_ = undefined; + this.textBaseline_ = undefined; + this.offsetX_ = undefined; + this.offsetY_ = undefined; + this.images_ = null; + this.atlases_ = {}; + this.currAtlas_ = undefined; + ol.render.webgl.TextureReplay.prototype.finish.call(this, context); + }; + + + /** + * @inheritDoc + */ + ol.render.webgl.TextReplay.prototype.setTextStyle = function(textStyle) { + var state = this.state_; + var textFillStyle = textStyle.getFill(); + var textStrokeStyle = textStyle.getStroke(); + if (!textStyle || !textStyle.getText() || (!textFillStyle && !textStrokeStyle)) { + this.text_ = ''; + } else { + if (!textFillStyle) { + state.fillColor = null; + } else { + var textFillStyleColor = textFillStyle.getColor(); + state.fillColor = ol.colorlike.asColorLike(textFillStyleColor ? + textFillStyleColor : ol.render.webgl.defaultFillStyle); + } + if (!textStrokeStyle) { + state.strokeColor = null; + state.lineWidth = 0; + } else { + var textStrokeStyleColor = textStrokeStyle.getColor(); + state.strokeColor = ol.colorlike.asColorLike(textStrokeStyleColor ? + textStrokeStyleColor : ol.render.webgl.defaultStrokeStyle); + state.lineWidth = textStrokeStyle.getWidth() || ol.render.webgl.defaultLineWidth; + state.lineCap = textStrokeStyle.getLineCap() || ol.render.webgl.defaultLineCap; + state.lineDashOffset = textStrokeStyle.getLineDashOffset() || ol.render.webgl.defaultLineDashOffset; + state.lineJoin = textStrokeStyle.getLineJoin() || ol.render.webgl.defaultLineJoin; + state.miterLimit = textStrokeStyle.getMiterLimit() || ol.render.webgl.defaultMiterLimit; + var lineDash = textStrokeStyle.getLineDash(); + state.lineDash = lineDash ? lineDash.slice() : ol.render.webgl.defaultLineDash; + } + state.font = textStyle.getFont() || ol.render.webgl.defaultFont; + state.scale = textStyle.getScale() || 1; + this.text_ = /** @type {string} */ (textStyle.getText()); + var textAlign = ol.render.webgl.TextReplay.Align_[textStyle.getTextAlign()]; + var textBaseline = ol.render.webgl.TextReplay.Align_[textStyle.getTextBaseline()]; + this.textAlign_ = textAlign === undefined ? + ol.render.webgl.defaultTextAlign : textAlign; + this.textBaseline_ = textBaseline === undefined ? + ol.render.webgl.defaultTextBaseline : textBaseline; + this.offsetX_ = textStyle.getOffsetX() || 0; + this.offsetY_ = textStyle.getOffsetY() || 0; + this.rotateWithView = !!textStyle.getRotateWithView(); + this.rotation = textStyle.getRotation() || 0; + + this.currAtlas_ = this.getAtlas_(state); + } + }; + + + /** + * @private + * @param {Object} state Font attributes. + * @return {ol.WebglGlyphAtlas} Glyph atlas. + */ + ol.render.webgl.TextReplay.prototype.getAtlas_ = function(state) { + var params = []; + var i; + for (i in state) { + if (state[i] || state[i] === 0) { + if (Array.isArray(state[i])) { + params = params.concat(state[i]); + } else { + params.push(state[i]); + } + } + } + var hash = this.calculateHash_(params); + if (!this.atlases_[hash]) { + var mCtx = this.measureCanvas_.getContext('2d'); + mCtx.font = state.font; + var height = Math.ceil((mCtx.measureText('M').width * 1.5 + + state.lineWidth / 2) * state.scale); + + this.atlases_[hash] = { + atlas: new ol.style.AtlasManager({ + space: state.lineWidth + 1 + }), + width: {}, + height: height + }; + } + return this.atlases_[hash]; + }; + + + /** + * @private + * @param {Array.} params Array of parameters. + * @return {string} Hash string. + */ + ol.render.webgl.TextReplay.prototype.calculateHash_ = function(params) { + //TODO: Create a more performant, reliable, general hash function. + var i, ii; + var hash = ''; + for (i = 0, ii = params.length; i < ii; ++i) { + hash += params[i]; + } + return hash; + }; + + + /** + * @inheritDoc + */ + ol.render.webgl.TextReplay.prototype.getTextures = function(opt_all) { + return this.textures_; + }; + + + /** + * @inheritDoc + */ + ol.render.webgl.TextReplay.prototype.getHitDetectionTextures = function() { + return this.textures_; + }; + + + /** + * @enum {number} + * @private + */ + ol.render.webgl.TextReplay.Align_ = { + left: 0, + end: 0, + center: 0.5, + right: 1, + start: 1, + top: 0, + middle: 0.5, + hanging: 0.2, + alphabetic: 0.8, + ideographic: 0.8, + bottom: 1 }; } diff --git a/src/ol/render/webgl/texturereplay.js b/src/ol/render/webgl/texturereplay.js new file mode 100644 index 0000000000..3b2fb7b13b --- /dev/null +++ b/src/ol/render/webgl/texturereplay.js @@ -0,0 +1,497 @@ +goog.provide('ol.render.webgl.TextureReplay'); + +goog.require('ol'); +goog.require('ol.extent'); +goog.require('ol.obj'); +goog.require('ol.render.webgl.texturereplay.defaultshader'); +goog.require('ol.render.webgl.Replay'); +goog.require('ol.webgl'); +goog.require('ol.webgl.Context'); + +if (ol.ENABLE_WEBGL) { + + /** + * @constructor + * @abstract + * @extends {ol.render.webgl.Replay} + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @struct + */ + ol.render.webgl.TextureReplay = function(tolerance, maxExtent) { + ol.render.webgl.Replay.call(this, tolerance, maxExtent); + + /** + * @type {number|undefined} + * @protected + */ + this.anchorX = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.anchorY = undefined; + + /** + * @type {Array.} + * @protected + */ + this.groupIndices = []; + + /** + * @type {Array.} + * @protected + */ + this.hitDetectionGroupIndices = []; + + /** + * @type {number|undefined} + * @protected + */ + this.height = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.imageHeight = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.imageWidth = undefined; + + /** + * @protected + * @type {ol.render.webgl.texturereplay.defaultshader.Locations} + */ + this.defaultLocations = null; + + /** + * @protected + * @type {number|undefined} + */ + this.opacity = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.originX = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.originY = undefined; + + /** + * @protected + * @type {boolean|undefined} + */ + this.rotateWithView = undefined; + + /** + * @protected + * @type {number|undefined} + */ + this.rotation = undefined; + + /** + * @protected + * @type {number|undefined} + */ + this.scale = undefined; + + /** + * @type {number|undefined} + * @protected + */ + this.width = undefined; + }; + ol.inherits(ol.render.webgl.TextureReplay, ol.render.webgl.Replay); + + + /** + * @inheritDoc + */ + ol.render.webgl.TextureReplay.prototype.getDeleteResourcesFunction = function(context) { + var verticesBuffer = this.verticesBuffer; + var indicesBuffer = this.indicesBuffer; + var textures = this.getTextures(true); + 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); + }; + }; + + + /** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @return {number} My end. + * @protected + */ + ol.render.webgl.TextureReplay.prototype.drawCoordinates = function(flatCoordinates, offset, end, stride) { + var anchorX = /** @type {number} */ (this.anchorX); + var anchorY = /** @type {number} */ (this.anchorY); + var height = /** @type {number} */ (this.height); + var imageHeight = /** @type {number} */ (this.imageHeight); + var imageWidth = /** @type {number} */ (this.imageWidth); + var opacity = /** @type {number} */ (this.opacity); + var originX = /** @type {number} */ (this.originX); + var originY = /** @type {number} */ (this.originY); + var rotateWithView = this.rotateWithView ? 1.0 : 0.0; + // this.rotation_ is anti-clockwise, but rotation is clockwise + var rotation = /** @type {number} */ (-this.rotation); + var scale = /** @type {number} */ (this.scale); + var width = /** @type {number} */ (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; + }; + + + /** + * @protected + * @param {Array.} textures Textures. + * @param {Array.} images + * Images. + * @param {Object.} texturePerImage Texture cache. + * @param {WebGLRenderingContext} gl Gl. + */ + ol.render.webgl.TextureReplay.prototype.createTextures = function(textures, images, texturePerImage, gl) { + var texture, image, uid, i; + var ii = images.length; + for (i = 0; i < ii; ++i) { + image = images[i]; + + uid = ol.getUid(image).toString(); + if (uid in texturePerImage) { + texture = texturePerImage[uid]; + } else { + texture = ol.webgl.Context.createTexture( + gl, image, ol.webgl.CLAMP_TO_EDGE, ol.webgl.CLAMP_TO_EDGE); + texturePerImage[uid] = texture; + } + textures[i] = texture; + } + }; + + + /** + * @inheritDoc + */ + ol.render.webgl.TextureReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { + // get the program + var fragmentShader = ol.render.webgl.texturereplay.defaultshader.fragment; + var vertexShader = ol.render.webgl.texturereplay.defaultshader.vertex; + var program = context.getProgram(fragmentShader, vertexShader); + + // get the locations + var locations; + if (!this.defaultLocations) { + // eslint-disable-next-line openlayers-internal/no-missing-requires + locations = new ol.render.webgl.texturereplay.defaultshader.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, ol.webgl.FLOAT, + false, 32, 0); + + gl.enableVertexAttribArray(locations.a_offsets); + gl.vertexAttribPointer(locations.a_offsets, 2, ol.webgl.FLOAT, + false, 32, 8); + + gl.enableVertexAttribArray(locations.a_texCoord); + gl.vertexAttribPointer(locations.a_texCoord, 2, ol.webgl.FLOAT, + false, 32, 16); + + gl.enableVertexAttribArray(locations.a_opacity); + gl.vertexAttribPointer(locations.a_opacity, 1, ol.webgl.FLOAT, + false, 32, 24); + + gl.enableVertexAttribArray(locations.a_rotateWithView); + gl.vertexAttribPointer(locations.a_rotateWithView, 1, ol.webgl.FLOAT, + false, 32, 28); + + return locations; + }; + + + /** + * @inheritDoc + */ + ol.render.webgl.TextureReplay.prototype.shutDownProgram = function(gl, locations) { + 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.TextureReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { + var textures = hitDetection ? this.getHitDetectionTextures() : this.getTextures(); + var groupIndices = hitDetection ? this.hitDetectionGroupIndices : this.groupIndices; + + if (!ol.obj.isEmpty(skippedFeaturesHash)) { + this.drawReplaySkipping( + gl, context, skippedFeaturesHash, textures, groupIndices); + } else { + var i, ii, start; + for (i = 0, ii = textures.length, start = 0; i < ii; ++i) { + gl.bindTexture(ol.webgl.TEXTURE_2D, textures[i]); + var end = groupIndices[i]; + this.drawElements(gl, context, start, end); + start = end; + } + } + }; + + + /** + * Draw the replay while paying attention to skipped features. + * + * This functions creates groups of features that can be drawn to together, + * so that the number of `drawElements` calls is minimized. + * + * For example given the following texture groups: + * + * Group 1: A B C + * Group 2: D [E] F G + * + * If feature E should be skipped, the following `drawElements` calls will be + * made: + * + * drawElements with feature A, B and C + * drawElements with feature D + * drawElements with feature F and G + * + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features + * to skip. + * @param {Array.} textures Textures. + * @param {Array.} groupIndices Texture group indices. + */ + ol.render.webgl.TextureReplay.prototype.drawReplaySkipping = function(gl, context, skippedFeaturesHash, textures, + groupIndices) { + var featureIndex = 0; + + var i, ii; + for (i = 0, ii = textures.length; i < ii; ++i) { + gl.bindTexture(ol.webgl.TEXTURE_2D, textures[i]); + var groupStart = (i > 0) ? groupIndices[i - 1] : 0; + var groupEnd = groupIndices[i]; + + var start = groupStart; + var end = groupStart; + while (featureIndex < this.startIndices.length && + this.startIndices[featureIndex] <= groupEnd) { + var feature = this.startIndicesFeature[featureIndex]; + + var featureUid = ol.getUid(feature).toString(); + if (skippedFeaturesHash[featureUid] !== undefined) { + // feature should be skipped + if (start !== end) { + // draw the features so far + this.drawElements(gl, context, start, end); + } + // continue with the next feature + start = (featureIndex === this.startIndices.length - 1) ? + groupEnd : this.startIndices[featureIndex + 1]; + end = start; + } else { + // the feature is not skipped, augment the end index + end = (featureIndex === this.startIndices.length - 1) ? + groupEnd : this.startIndices[featureIndex + 1]; + } + featureIndex++; + } + + if (start !== end) { + // draw the remaining features (in case there was no skipped feature + // in this texture group, all features of a group are drawn together) + this.drawElements(gl, context, start, end); + } + } + }; + + + /** + * @inheritDoc + */ + ol.render.webgl.TextureReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, + featureCallback, opt_hitExtent) { + var i, groupStart, start, end, feature, featureUid; + var featureIndex = this.startIndices.length - 1; + var hitDetectionTextures = this.getHitDetectionTextures(); + for (i = hitDetectionTextures.length - 1; i >= 0; --i) { + gl.bindTexture(ol.webgl.TEXTURE_2D, hitDetectionTextures[i]); + groupStart = (i > 0) ? this.hitDetectionGroupIndices[i - 1] : 0; + end = this.hitDetectionGroupIndices[i]; + + // draw all features for this texture group + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + start = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = ol.getUid(feature).toString(); + + if (skippedFeaturesHash[featureUid] === undefined && + feature.getGeometry() && + (opt_hitExtent === undefined || ol.extent.intersects( + /** @type {Array} */ (opt_hitExtent), + feature.getGeometry().getExtent()))) { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + this.drawElements(gl, context, start, end); + + var result = featureCallback(feature); + if (result) { + return result; + } + } + + end = start; + featureIndex--; + } + } + return undefined; + }; + + + /** + * @inheritDoc + */ + ol.render.webgl.TextureReplay.prototype.finish = function(context) { + this.anchorX = undefined; + this.anchorY = undefined; + this.height = undefined; + 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; + }; + + + /** + * @abstract + * @protected + * @param {boolean=} opt_all Return hit detection textures with regular ones. + * @returns {Array.} Textures. + */ + ol.render.webgl.TextureReplay.prototype.getTextures = function(opt_all) {}; + + + /** + * @abstract + * @protected + * @returns {Array.} Textures. + */ + ol.render.webgl.TextureReplay.prototype.getHitDetectionTextures = function() {}; +} diff --git a/src/ol/render/webgl/imagereplay/defaultshader.glsl b/src/ol/render/webgl/texturereplay/defaultshader.glsl similarity index 89% rename from src/ol/render/webgl/imagereplay/defaultshader.glsl rename to src/ol/render/webgl/texturereplay/defaultshader.glsl index b1b6168fff..3bf3e86da5 100644 --- a/src/ol/render/webgl/imagereplay/defaultshader.glsl +++ b/src/ol/render/webgl/texturereplay/defaultshader.glsl @@ -1,5 +1,5 @@ -//! NAMESPACE=ol.render.webgl.imagereplay.defaultshader -//! CLASS=ol.render.webgl.imagereplay.defaultshader +//! NAMESPACE=ol.render.webgl.texturereplay.defaultshader +//! CLASS=ol.render.webgl.texturereplay.defaultshader //! COMMON diff --git a/src/ol/render/webgl/texturereplay/defaultshader.js b/src/ol/render/webgl/texturereplay/defaultshader.js new file mode 100644 index 0000000000..a2459f0339 --- /dev/null +++ b/src/ol/render/webgl/texturereplay/defaultshader.js @@ -0,0 +1,154 @@ +// This file is automatically generated, do not edit +/* eslint openlayers-internal/no-missing-requires: 0 */ +goog.provide('ol.render.webgl.texturereplay.defaultshader'); + +goog.require('ol'); +goog.require('ol.webgl.Fragment'); +goog.require('ol.webgl.Vertex'); + +if (ol.ENABLE_WEBGL) { + + /** + * @constructor + * @extends {ol.webgl.Fragment} + * @struct + */ + ol.render.webgl.texturereplay.defaultshader.Fragment = function() { + ol.webgl.Fragment.call(this, ol.render.webgl.texturereplay.defaultshader.Fragment.SOURCE); + }; + ol.inherits(ol.render.webgl.texturereplay.defaultshader.Fragment, ol.webgl.Fragment); + + + /** + * @const + * @type {string} + */ + ol.render.webgl.texturereplay.defaultshader.Fragment.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.texturereplay.defaultshader.Fragment.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.texturereplay.defaultshader.Fragment.SOURCE = ol.DEBUG_WEBGL ? + ol.render.webgl.texturereplay.defaultshader.Fragment.DEBUG_SOURCE : + ol.render.webgl.texturereplay.defaultshader.Fragment.OPTIMIZED_SOURCE; + + + ol.render.webgl.texturereplay.defaultshader.fragment = new ol.render.webgl.texturereplay.defaultshader.Fragment(); + + + /** + * @constructor + * @extends {ol.webgl.Vertex} + * @struct + */ + ol.render.webgl.texturereplay.defaultshader.Vertex = function() { + ol.webgl.Vertex.call(this, ol.render.webgl.texturereplay.defaultshader.Vertex.SOURCE); + }; + ol.inherits(ol.render.webgl.texturereplay.defaultshader.Vertex, ol.webgl.Vertex); + + + /** + * @const + * @type {string} + */ + ol.render.webgl.texturereplay.defaultshader.Vertex.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, 0.0);\n gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n'; + + + /** + * @const + * @type {string} + */ + ol.render.webgl.texturereplay.defaultshader.Vertex.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,0.0);gl_Position=h*vec4(c,0.0,1.0)+offsets;a=d;b=f;}'; + + + /** + * @const + * @type {string} + */ + ol.render.webgl.texturereplay.defaultshader.Vertex.SOURCE = ol.DEBUG_WEBGL ? + ol.render.webgl.texturereplay.defaultshader.Vertex.DEBUG_SOURCE : + ol.render.webgl.texturereplay.defaultshader.Vertex.OPTIMIZED_SOURCE; + + + ol.render.webgl.texturereplay.defaultshader.vertex = new ol.render.webgl.texturereplay.defaultshader.Vertex(); + + + /** + * @constructor + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLProgram} program Program. + * @struct + */ + ol.render.webgl.texturereplay.defaultshader.Locations = function(gl, program) { + + /** + * @type {WebGLUniformLocation} + */ + this.u_image = gl.getUniformLocation( + program, ol.DEBUG_WEBGL ? 'u_image' : 'l'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetRotateMatrix = gl.getUniformLocation( + program, ol.DEBUG_WEBGL ? 'u_offsetRotateMatrix' : 'j'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetScaleMatrix = gl.getUniformLocation( + program, ol.DEBUG_WEBGL ? 'u_offsetScaleMatrix' : 'i'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_opacity = gl.getUniformLocation( + program, ol.DEBUG_WEBGL ? 'u_opacity' : 'k'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_projectionMatrix = gl.getUniformLocation( + program, ol.DEBUG_WEBGL ? 'u_projectionMatrix' : 'h'); + + /** + * @type {number} + */ + this.a_offsets = gl.getAttribLocation( + program, ol.DEBUG_WEBGL ? 'a_offsets' : 'e'); + + /** + * @type {number} + */ + this.a_opacity = gl.getAttribLocation( + program, ol.DEBUG_WEBGL ? 'a_opacity' : 'f'); + + /** + * @type {number} + */ + this.a_position = gl.getAttribLocation( + program, ol.DEBUG_WEBGL ? 'a_position' : 'c'); + + /** + * @type {number} + */ + this.a_rotateWithView = gl.getAttribLocation( + program, ol.DEBUG_WEBGL ? 'a_rotateWithView' : 'g'); + + /** + * @type {number} + */ + this.a_texCoord = gl.getAttribLocation( + program, ol.DEBUG_WEBGL ? 'a_texCoord' : 'd'); + }; + +} diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js index 65de15bce1..8581394c33 100644 --- a/src/ol/typedefs.js +++ b/src/ol/typedefs.js @@ -723,6 +723,14 @@ ol.ViewAnimation; ol.WebglBufferCacheEntry; +/** + * @typedef {{atlas: ol.style.AtlasManager, + * width: Object., + * height: number}} + */ +ol.WebglGlyphAtlas; + + /** * @typedef {{p0: ol.WebglPolygonVertex, * p1: ol.WebglPolygonVertex}} diff --git a/test/spec/ol/render/webgl/imagereplay.test.js b/test/spec/ol/render/webgl/imagereplay.test.js index 7e5b1ce621..05e1571d78 100644 --- a/test/spec/ol/render/webgl/imagereplay.test.js +++ b/test/spec/ol/render/webgl/imagereplay.test.js @@ -2,7 +2,6 @@ goog.provide('ol.test.render.webgl.ImageReplay'); goog.require('ol.geom.MultiPoint'); goog.require('ol.geom.Point'); -goog.require('ol.render.webgl.imagereplay.defaultshader'); goog.require('ol.render.webgl.ImageReplay'); goog.require('ol.style.Image'); @@ -57,34 +56,34 @@ describe('ol.render.webgl.ImageReplay', function() { 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.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); + expect(replay.groupIndices).to.have.length(0); expect(replay.hitDetectionImages_).to.have.length(1); - expect(replay.hitDetectionGroupIndices_).to.have.length(0); + expect(replay.hitDetectionGroupIndices).to.have.length(0); replay.setImageStyle(imageStyle1); expect(replay.images_).to.have.length(1); - expect(replay.groupIndices_).to.have.length(0); + expect(replay.groupIndices).to.have.length(0); expect(replay.hitDetectionImages_).to.have.length(1); - expect(replay.hitDetectionGroupIndices_).to.have.length(0); + expect(replay.hitDetectionGroupIndices).to.have.length(0); replay.setImageStyle(imageStyle2); expect(replay.images_).to.have.length(2); - expect(replay.groupIndices_).to.have.length(1); + expect(replay.groupIndices).to.have.length(1); expect(replay.hitDetectionImages_).to.have.length(2); - expect(replay.hitDetectionGroupIndices_).to.have.length(1); + expect(replay.hitDetectionGroupIndices).to.have.length(1); }); }); @@ -168,78 +167,43 @@ describe('ol.render.webgl.ImageReplay', function() { }); }); - describe('#setUpProgram', function() { - var context, gl; + describe('#getTextures', function() { beforeEach(function() { - context = { - getProgram: function() {}, - useProgram: function() {} - }; - gl = { - enableVertexAttribArray: function() {}, - vertexAttribPointer: function() {}, - uniform1f: function() {}, - uniform2fv: function() {}, - getUniformLocation: function() {}, - getAttribLocation: function() {} - }; + replay.textures_ = [1, 2]; + replay.hitDetectionTextures_ = [3, 4]; }); - it('returns the locations used by the shaders', function() { - var locations = replay.setUpProgram(gl, context, [2, 2], 1); - expect(locations).to.be.a( - ol.render.webgl.imagereplay.defaultshader.Locations); + it('returns the textures', function() { + var textures = replay.getTextures(); + + expect(textures).to.have.length(2); + expect(textures[0]).to.be(1); + expect(textures[1]).to.be(2); }); - it('gets and compiles the shaders', function() { - sinon.spy(context, 'getProgram'); - sinon.spy(context, 'useProgram'); + it('can additionally return the hit detection textures', function() { + var textures = replay.getTextures(true); - replay.setUpProgram(gl, context, [2, 2], 1); - expect(context.getProgram.calledWithExactly( - ol.render.webgl.imagereplay.defaultshader.fragment, - ol.render.webgl.imagereplay.defaultshader.vertex)).to.be(true); - expect(context.useProgram.calledOnce).to.be(true); - }); - - it('initializes the attrib pointers', function() { - sinon.spy(gl, 'getAttribLocation'); - sinon.spy(gl, 'vertexAttribPointer'); - sinon.spy(gl, 'enableVertexAttribArray'); - - replay.setUpProgram(gl, context, [2, 2], 1); - expect(gl.vertexAttribPointer.callCount).to.be(gl.getAttribLocation.callCount); - expect(gl.enableVertexAttribArray.callCount).to.be( - gl.getAttribLocation.callCount); + expect(textures).to.have.length(4); + expect(textures[0]).to.be(1); + expect(textures[1]).to.be(2); + expect(textures[2]).to.be(3); + expect(textures[3]).to.be(4); }); }); - describe('#shutDownProgram', function() { - var context, gl; + describe('#getHitDetectionTextures', function() { beforeEach(function() { - context = { - getProgram: function() {}, - useProgram: function() {} - }; - gl = { - enableVertexAttribArray: function() {}, - disableVertexAttribArray: function() {}, - vertexAttribPointer: function() {}, - uniform1f: function() {}, - uniform2fv: function() {}, - getUniformLocation: function() {}, - getAttribLocation: function() {} - }; + replay.textures_ = [1, 2]; + replay.hitDetectionTextures_ = [3, 4]; }); - it('disables the attrib pointers', function() { - sinon.spy(gl, 'getAttribLocation'); - sinon.spy(gl, 'disableVertexAttribArray'); + it('returns the hit detection textures', function() { + var textures = replay.getHitDetectionTextures(); - var locations = replay.setUpProgram(gl, context, [2, 2], 1); - replay.shutDownProgram(gl, locations); - expect(gl.disableVertexAttribArray.callCount).to.be( - gl.getAttribLocation.callCount); + expect(textures).to.have.length(2); + expect(textures[0]).to.be(3); + expect(textures[1]).to.be(4); }); }); }); diff --git a/test/spec/ol/render/webgl/textreplay.test.js b/test/spec/ol/render/webgl/textreplay.test.js new file mode 100644 index 0000000000..69dcaffbf9 --- /dev/null +++ b/test/spec/ol/render/webgl/textreplay.test.js @@ -0,0 +1,320 @@ +goog.provide('ol.test.render.webgl.TextReplay'); + +goog.require('ol.dom'); +goog.require('ol.render.webgl.TextReplay'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Text'); + +describe('ol.render.webgl.TextReplay', function() { + var replay; + + var createTextStyle = function(fillStyle, strokeStyle, text) { + var textStyle = new ol.style.Text({ + rotateWithView: true, + rotation: 1.5, + scale: 2, + textAlign: 'left', + textBaseline: 'top', + font: '12px Arial', + offsetX: 10, + offsetY: 10, + text: text, + fill: fillStyle, + stroke: strokeStyle + }); + return textStyle; + }; + + beforeEach(function() { + var tolerance = 0.1; + var maxExtent = [-10000, -20000, 10000, 20000]; + replay = new ol.render.webgl.TextReplay(tolerance, maxExtent); + }); + + describe('#setTextStyle', function() { + + var textStyle1, textStyle2, textStyle3, textStyle4; + + beforeEach(function() { + textStyle1 = createTextStyle( + new ol.style.Fill({ + color: [0, 0, 0, 1] + }), + new ol.style.Stroke({ + width: 1, + color: [0, 0, 0, 1], + lineCap: 'butt', + lineJoin: 'bevel', + lineDash: [5, 5], + lineDashOffset: 15, + miterLimit: 2 + }), + 'someText'); + textStyle2 = createTextStyle( + new ol.style.Fill({ + color: [255, 255, 255, 1] + }), + new ol.style.Stroke({ + width: 1, + color: [255, 255, 255, 1] + }), + 'someText' + ); + textStyle3 = createTextStyle(null, null, 'someText'); + textStyle4 = createTextStyle( + new ol.style.Fill({ + color: [0, 0, 0, 1] + }), + new ol.style.Stroke({ + width: 1, + color: [0, 0, 0, 1] + }), + '' + ); + }); + + it('set expected states', function() { + replay.setTextStyle(textStyle1); + expect(replay.opacity).to.be(1); + expect(replay.rotation).to.be(1.5); + expect(replay.rotateWithView).to.be(true); + expect(replay.scale).to.be(1); + expect(replay.offsetX_).to.be(10); + expect(replay.offsetY_).to.be(10); + expect(replay.text_).to.be('someText'); + expect(Object.keys(replay.atlases_)).to.have.length(1); + + expect(replay.state_.fillColor).to.be('rgba(0,0,0,1)'); + expect(replay.state_.strokeColor).to.be('rgba(0,0,0,1)'); + expect(replay.state_.scale).to.be(2); + expect(replay.state_.lineWidth).to.be(1); + expect(replay.state_.lineJoin).to.be('bevel'); + expect(replay.state_.lineCap).to.be('butt'); + expect(replay.state_.lineDash).to.eql([5, 5]); + expect(replay.state_.lineDashOffset).to.be(15); + expect(replay.state_.miterLimit).to.be(2); + expect(replay.state_.font).to.be('12px Arial'); + + replay.setTextStyle(textStyle2); + expect(Object.keys(replay.atlases_)).to.have.length(2); + }); + + it('does not create an atlas, if an empty text is supplied', function() { + replay.setTextStyle(textStyle4); + expect(replay.text_).to.be(''); + expect(Object.keys(replay.atlases_)).to.have.length(0); + }); + + it('does not create an atlas, if both fill and stroke styles are missing', function() { + replay.setTextStyle(textStyle3); + expect(replay.text_).to.be(''); + expect(Object.keys(replay.atlases_)).to.have.length(0); + }); + }); + + describe('#drawText', function() { + beforeEach(function() { + var textStyle = createTextStyle( + new ol.style.Fill({ + color: [0, 0, 0, 1] + }), + null, 'someText'); + replay.setTextStyle(textStyle); + }); + + it('sets the buffer data', function() { + var point; + + point = [1000, 2000]; + replay.drawText(point, 0, 2, 2, null, null); + expect(replay.vertices).to.have.length(256); + expect(replay.indices).to.have.length(48); + + point = [2000, 3000]; + replay.drawText(point, 0, 2, 2, null, null); + expect(replay.vertices).to.have.length(512); + expect(replay.indices).to.have.length(96); + }); + + it('sets part of its state during drawing', function() { + var point = [1000, 2000]; + replay.drawText(point, 0, 2, 2, null, null); + + var height = replay.currAtlas_.height; + var widths = replay.currAtlas_.width; + var width = widths.t; + var widthX = widths.s + widths.o + widths.m + widths.e + widths.T + + widths.e + widths.x; + var charInfo = replay.currAtlas_.atlas.getInfo('t'); + + expect(replay.height).to.be(height); + expect(replay.width).to.be(width); + expect(replay.originX).to.be(charInfo.offsetX); + expect(replay.originY).to.be(charInfo.offsetY); + expect(replay.imageHeight).to.be(charInfo.image.height); + expect(replay.imageWidth).to.be(charInfo.image.width); + expect(replay.anchorX).to.be(-widthX + 10); + expect(replay.anchorY).to.be(10); + }); + + it('does not draw if text is empty', function() { + replay.text_ = ''; + var point; + + point = [1000, 2000]; + replay.drawText(point, 0, 2, 2, null, null); + expect(replay.vertices).to.have.length(0); + expect(replay.indices).to.have.length(0); + }); + }); + + describe('#addCharToAtlas_', function() { + beforeEach(function() { + var textStyle = createTextStyle( + new ol.style.Fill({ + color: [0, 0, 0, 1] + }), + null, 'someText'); + replay.setTextStyle(textStyle); + }); + + it('adds a single character to the current atlas', function() { + var glyphAtlas = replay.currAtlas_.atlas; + var info; + + replay.addCharToAtlas_('someText'); + info = glyphAtlas.getInfo('someText'); + expect(info).to.be(null); + + replay.addCharToAtlas_('e'); + replay.addCharToAtlas_('x'); + info = glyphAtlas.getInfo('e'); + expect(info).not.to.be(null); + info = glyphAtlas.getInfo('x'); + expect(info).not.to.be(null); + }); + + it('keeps the atlas and the width dictionary synced', function() { + var glyphAtlas = replay.currAtlas_; + + replay.addCharToAtlas_('e'); + replay.addCharToAtlas_('x'); + expect(Object.keys(glyphAtlas.width)).to.have.length(2); + + replay.addCharToAtlas_('someText'); + expect(Object.keys(glyphAtlas.width)).to.have.length(2); + }); + }); + + describe('#getTextSize_', function() { + beforeEach(function() { + var textStyle = createTextStyle( + new ol.style.Fill({ + color: [0, 0, 0, 1] + }), + null, 'someText'); + textStyle.setScale(1); + replay.setTextStyle(textStyle); + }); + + it('adds missing characters to the current atlas', function() { + var glyphAtlas = replay.currAtlas_; + var info; + + expect(Object.keys(glyphAtlas.width)).to.have.length(0); + replay.getTextSize_(['someText']); + expect(Object.keys(glyphAtlas.width)).to.have.length(7); + info = glyphAtlas.atlas.getInfo('s'); + expect(info).not.to.be(null); + info = glyphAtlas.atlas.getInfo('o'); + expect(info).not.to.be(null); + info = glyphAtlas.atlas.getInfo('m'); + expect(info).not.to.be(null); + info = glyphAtlas.atlas.getInfo('e'); + expect(info).not.to.be(null); + info = glyphAtlas.atlas.getInfo('T'); + expect(info).not.to.be(null); + info = glyphAtlas.atlas.getInfo('x'); + expect(info).not.to.be(null); + info = glyphAtlas.atlas.getInfo('t'); + expect(info).not.to.be(null); + }); + + it('returns the size of the label\'s bounding box in pixels', function() { + var size; + var mCtx = ol.dom.createCanvasContext2D(0, 0); + mCtx.font = '12px Arial'; + var width = mCtx.measureText('someText').width; + var width2 = mCtx.measureText('anEvenLongerLine').width; + var height = Math.ceil(mCtx.measureText('M').width * 1.5); + + size = replay.getTextSize_(['someText']); + expect(size[0]).to.be.within(width, width + 8); + expect(size[1]).to.be(height); + + size = replay.getTextSize_(['someText', 'anEvenLongerLine']); + expect(size[0]).to.be.within(width2, width2 + 16); + expect(size[1]).to.be(height * 2); + }); + }); + + describe('#getAtlas_', function() { + beforeEach(function() { + var textStyle = createTextStyle( + new ol.style.Fill({ + color: [0, 0, 0, 1] + }), + null, 'someText'); + replay.setTextStyle(textStyle); + }); + + it('returns the appropriate atlas for the current state', function() { + var atlas = replay.currAtlas_; + var state = replay.state_; + + expect(Object.keys(replay.atlases_)).to.have.length(1); + expect(replay.getAtlas_(state)).to.be(atlas); + expect(Object.keys(replay.atlases_)).to.have.length(1); + }); + + it('creates a new atlas if it cannot find the one for the current state', function() { + var atlas = replay.currAtlas_; + var state = replay.state_; + state.lineWidth = 50; + + expect(Object.keys(replay.atlases_)).to.have.length(1); + expect(replay.getAtlas_(state)).not.to.be(atlas); + expect(Object.keys(replay.atlases_)).to.have.length(2); + }); + }); + + describe('#getTextures', function() { + beforeEach(function() { + replay.textures_ = [1, 2]; + }); + + it('returns the textures', function() { + var textures = replay.getTextures(); + + expect(textures).to.have.length(2); + expect(textures[0]).to.be(1); + expect(textures[1]).to.be(2); + expect(textures).to.eql(replay.getTextures(true)); + }); + }); + + describe('#getHitDetectionTextures', function() { + beforeEach(function() { + replay.textures_ = [1, 2]; + }); + + it('returns the textures', function() { + var textures = replay.getHitDetectionTextures(); + + expect(textures).to.have.length(2); + expect(textures[0]).to.be(1); + expect(textures[1]).to.be(2); + }); + }); +}); diff --git a/test/spec/ol/render/webgl/texturereplay.test.js b/test/spec/ol/render/webgl/texturereplay.test.js new file mode 100644 index 0000000000..5e85440a6b --- /dev/null +++ b/test/spec/ol/render/webgl/texturereplay.test.js @@ -0,0 +1,89 @@ +goog.provide('ol.test.render.webgl.TextureReplay'); + +goog.require('ol.render.webgl.TextureReplay'); +goog.require('ol.render.webgl.texturereplay.defaultshader'); + +describe('ol.render.webgl.TextureReplay', function() { + var replay; + + beforeEach(function() { + var tolerance = 0.1; + var maxExtent = [-10000, -20000, 10000, 20000]; + replay = new ol.render.webgl.TextureReplay(tolerance, maxExtent); + }); + + describe('#setUpProgram', function() { + var context, gl; + beforeEach(function() { + context = { + getProgram: function() {}, + useProgram: function() {} + }; + gl = { + enableVertexAttribArray: function() {}, + vertexAttribPointer: function() {}, + uniform1f: function() {}, + uniform2fv: function() {}, + getUniformLocation: function() {}, + getAttribLocation: function() {} + }; + }); + + it('returns the locations used by the shaders', function() { + var locations = replay.setUpProgram(gl, context, [2, 2], 1); + expect(locations).to.be.a( + ol.render.webgl.texturereplay.defaultshader.Locations); + }); + + it('gets and compiles the shaders', function() { + sinon.spy(context, 'getProgram'); + sinon.spy(context, 'useProgram'); + + replay.setUpProgram(gl, context, [2, 2], 1); + expect(context.getProgram.calledWithExactly( + ol.render.webgl.texturereplay.defaultshader.fragment, + ol.render.webgl.texturereplay.defaultshader.vertex)).to.be(true); + expect(context.useProgram.calledOnce).to.be(true); + }); + + it('initializes the attrib pointers', function() { + sinon.spy(gl, 'getAttribLocation'); + sinon.spy(gl, 'vertexAttribPointer'); + sinon.spy(gl, 'enableVertexAttribArray'); + + replay.setUpProgram(gl, context, [2, 2], 1); + expect(gl.vertexAttribPointer.callCount).to.be(gl.getAttribLocation.callCount); + expect(gl.enableVertexAttribArray.callCount).to.be( + gl.getAttribLocation.callCount); + }); + }); + + describe('#shutDownProgram', function() { + var context, gl; + beforeEach(function() { + context = { + getProgram: function() {}, + useProgram: function() {} + }; + gl = { + enableVertexAttribArray: function() {}, + disableVertexAttribArray: function() {}, + vertexAttribPointer: function() {}, + uniform1f: function() {}, + uniform2fv: function() {}, + getUniformLocation: function() {}, + getAttribLocation: function() {} + }; + }); + + it('disables the attrib pointers', function() { + sinon.spy(gl, 'getAttribLocation'); + sinon.spy(gl, 'disableVertexAttribArray'); + + var locations = replay.setUpProgram(gl, context, [2, 2], 1); + replay.shutDownProgram(gl, locations); + expect(gl.disableVertexAttribArray.callCount).to.be( + gl.getAttribLocation.callCount); + }); + }); +}); diff --git a/test_rendering/spec/ol/style/expected/text-rotated-webgl.png b/test_rendering/spec/ol/style/expected/text-rotated-webgl.png new file mode 100644 index 0000000000..33f5bfbc43 Binary files /dev/null and b/test_rendering/spec/ol/style/expected/text-rotated-webgl.png differ diff --git a/test_rendering/spec/ol/style/expected/text-webgl.png b/test_rendering/spec/ol/style/expected/text-webgl.png new file mode 100644 index 0000000000..7adae3ea31 Binary files /dev/null and b/test_rendering/spec/ol/style/expected/text-webgl.png differ diff --git a/test_rendering/spec/ol/style/text.test.js b/test_rendering/spec/ol/style/text.test.js index ec162b955c..0dbf7dbf5e 100644 --- a/test_rendering/spec/ol/style/text.test.js +++ b/test_rendering/spec/ol/style/text.test.js @@ -102,5 +102,18 @@ describe('ol.rendering.style.Text', function() { expectResemble(map, 'spec/ol/style/expected/text-rotated-canvas.png', IMAGE_TOLERANCE, done); }); + it('tests the webgl renderer without rotation', function(done) { + map = createMap('webgl'); + createFeatures(); + expectResemble(map, 'spec/ol/style/expected/text-webgl.png', 1.8, done); + }); + + it('tests the webgl renderer with rotation', function(done) { + map = createMap('webgl'); + createFeatures(); + map.getView().setRotation(Math.PI / 7); + expectResemble(map, 'spec/ol/style/expected/text-rotated-webgl.png', 1.8, done); + }); + }); });