diff --git a/src/ol/geom/flat/topology.js b/src/ol/geom/flat/topology.js new file mode 100644 index 0000000000..a1b9f053f5 --- /dev/null +++ b/src/ol/geom/flat/topology.js @@ -0,0 +1,20 @@ +goog.provide('ol.geom.flat.topology'); + +goog.require('ol.geom.flat.area'); + +/** + * Check if the linestring is a boundary. + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @return {boolean} The linestring is a boundary. + */ +ol.geom.flat.topology.lineStringIsClosed = function(flatCoordinates, offset, end, stride) { + var lastCoord = end - stride; + if (flatCoordinates[offset] === flatCoordinates[lastCoord] && + flatCoordinates[offset + 1] === flatCoordinates[lastCoord + 1] && (end - offset) / stride > 3) { + return !!ol.geom.flat.area.linearRing(flatCoordinates, offset, end, stride); + } + return false; +}; diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 51a9646099..40a7722be2 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -282,6 +282,7 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function( * number, boolean)>} */ ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = { + 'Circle': ol.render.canvas.PolygonReplay, 'Image': ol.render.canvas.ImageReplay, 'LineString': ol.render.canvas.LineStringReplay, 'Polygon': ol.render.canvas.PolygonReplay, diff --git a/src/ol/render/replay.js b/src/ol/render/replay.js index 3803f79f0c..a0a8e0dc7e 100644 --- a/src/ol/render/replay.js +++ b/src/ol/render/replay.js @@ -9,6 +9,7 @@ goog.require('ol.render.ReplayType'); */ ol.render.replay.ORDER = [ ol.render.ReplayType.POLYGON, + ol.render.ReplayType.CIRCLE, ol.render.ReplayType.LINE_STRING, ol.render.ReplayType.IMAGE, ol.render.ReplayType.TEXT diff --git a/src/ol/render/replaytype.js b/src/ol/render/replaytype.js index 28d37e2ae1..5423cee151 100644 --- a/src/ol/render/replaytype.js +++ b/src/ol/render/replaytype.js @@ -5,6 +5,7 @@ goog.provide('ol.render.ReplayType'); * @enum {string} */ ol.render.ReplayType = { + CIRCLE: 'Circle', IMAGE: 'Image', LINE_STRING: 'LineString', POLYGON: 'Polygon', diff --git a/src/ol/render/vectorcontext.js b/src/ol/render/vectorcontext.js index d601ade6d3..26b8fad6ce 100644 --- a/src/ol/render/vectorcontext.js +++ b/src/ol/render/vectorcontext.js @@ -85,7 +85,7 @@ ol.render.VectorContext.prototype.drawMultiPoint = function(multiPointGeometry, /** * @abstract * @param {ol.geom.MultiPolygon} multiPolygonGeometry MultiPolygon geometry. - * @param {ol.Feature} feature Feature. + * @param {ol.Feature|ol.render.Feature} feature Feature. */ ol.render.VectorContext.prototype.drawMultiPolygon = function(multiPolygonGeometry, feature) {}; diff --git a/src/ol/render/webgl/circlereplay/defaultshader.glsl b/src/ol/render/webgl/circlereplay/defaultshader.glsl new file mode 100644 index 0000000000..e877879fc3 --- /dev/null +++ b/src/ol/render/webgl/circlereplay/defaultshader.glsl @@ -0,0 +1,101 @@ +//! NAMESPACE=ol.render.webgl.circlereplay.defaultshader +//! CLASS=ol.render.webgl.circlereplay.defaultshader + + +//! COMMON +varying vec2 v_center; +varying vec2 v_offset; +varying float v_halfWidth; +varying float v_pixelRatio; + + +//! VERTEX +attribute vec2 a_position; +attribute float a_instruction; +attribute float a_radius; + +uniform mat4 u_projectionMatrix; +uniform mat4 u_offsetScaleMatrix; +uniform mat4 u_offsetRotateMatrix; +uniform float u_lineWidth; +uniform float u_pixelRatio; + +void main(void) { + mat4 offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix; + v_center = vec4(u_projectionMatrix * vec4(a_position, 0.0, 1.0)).xy; + v_pixelRatio = u_pixelRatio; + float lineWidth = u_lineWidth * u_pixelRatio; + v_halfWidth = lineWidth / 2.0; + if (lineWidth == 0.0) { + lineWidth = 2.0 * u_pixelRatio; + } + vec2 offset; + // Radius with anitaliasing (roughly). + float radius = a_radius + 3.0 * u_pixelRatio; + // Until we get gl_VertexID in WebGL, we store an instruction. + if (a_instruction == 0.0) { + // Offsetting the edges of the triangle by lineWidth / 2 is necessary, however + // we should also leave some space for the antialiasing, thus we offset by lineWidth. + offset = vec2(-1.0, 1.0); + } else if (a_instruction == 1.0) { + offset = vec2(-1.0, -1.0); + } else if (a_instruction == 2.0) { + offset = vec2(1.0, -1.0); + } else { + offset = vec2(1.0, 1.0); + } + + gl_Position = u_projectionMatrix * vec4(a_position + offset * radius, 0.0, 1.0) + + offsetMatrix * vec4(offset * lineWidth, 0.0, 0.0); + v_offset = vec4(u_projectionMatrix * vec4(a_position.x + a_radius, a_position.y, + 0.0, 1.0)).xy; + + if (distance(v_center, v_offset) > 20000.0) { + gl_Position = vec4(v_center, 0.0, 1.0); + } +} + + +//! FRAGMENT + +uniform float u_opacity; +uniform vec4 u_fillColor; +uniform vec4 u_strokeColor; +uniform vec2 u_size; + +void main(void) { + vec2 windowCenter = vec2((v_center.x + 1.0) / 2.0 * u_size.x * v_pixelRatio, + (v_center.y + 1.0) / 2.0 * u_size.y * v_pixelRatio); + vec2 windowOffset = vec2((v_offset.x + 1.0) / 2.0 * u_size.x * v_pixelRatio, + (v_offset.y + 1.0) / 2.0 * u_size.y * v_pixelRatio); + float radius = length(windowCenter - windowOffset); + float dist = length(windowCenter - gl_FragCoord.xy); + if (dist > radius + v_halfWidth) { + if (u_strokeColor.a == 0.0) { + gl_FragColor = u_fillColor; + } else { + gl_FragColor = u_strokeColor; + } + gl_FragColor.a = gl_FragColor.a - (dist - (radius + v_halfWidth)); + } else if (u_fillColor.a == 0.0) { + // Hooray, no fill, just stroke. We can use real antialiasing. + gl_FragColor = u_strokeColor; + if (dist < radius - v_halfWidth) { + gl_FragColor.a = gl_FragColor.a - (radius - v_halfWidth - dist); + } + } else { + gl_FragColor = u_fillColor; + float strokeDist = radius - v_halfWidth; + float antialias = 2.0 * v_pixelRatio; + if (dist > strokeDist) { + gl_FragColor = u_strokeColor; + } else if (dist >= strokeDist - antialias) { + float step = smoothstep(strokeDist - antialias, strokeDist, dist); + gl_FragColor = mix(u_fillColor, u_strokeColor, step); + } + } + gl_FragColor.a = gl_FragColor.a * u_opacity; + if (gl_FragColor.a <= 0.0) { + discard; + } +} diff --git a/src/ol/render/webgl/circlereplay/defaultshader.js b/src/ol/render/webgl/circlereplay/defaultshader.js new file mode 100644 index 0000000000..18ae36140a --- /dev/null +++ b/src/ol/render/webgl/circlereplay/defaultshader.js @@ -0,0 +1,162 @@ +// This file is automatically generated, do not edit +goog.provide('ol.render.webgl.circlereplay.defaultshader'); + +goog.require('ol'); +goog.require('ol.webgl.Fragment'); +goog.require('ol.webgl.Vertex'); + + +/** + * @constructor + * @extends {ol.webgl.Fragment} + * @struct + */ +ol.render.webgl.circlereplay.defaultshader.Fragment = function() { + ol.webgl.Fragment.call(this, ol.render.webgl.circlereplay.defaultshader.Fragment.SOURCE); +}; +ol.inherits(ol.render.webgl.circlereplay.defaultshader.Fragment, ol.webgl.Fragment); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.circlereplay.defaultshader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_center;\nvarying vec2 v_offset;\nvarying float v_halfWidth;\nvarying float v_pixelRatio;\n\n\n\nuniform float u_opacity;\nuniform vec4 u_fillColor;\nuniform vec4 u_strokeColor;\nuniform vec2 u_size;\n\nvoid main(void) {\n vec2 windowCenter = vec2((v_center.x + 1.0) / 2.0 * u_size.x * v_pixelRatio,\n (v_center.y + 1.0) / 2.0 * u_size.y * v_pixelRatio);\n vec2 windowOffset = vec2((v_offset.x + 1.0) / 2.0 * u_size.x * v_pixelRatio,\n (v_offset.y + 1.0) / 2.0 * u_size.y * v_pixelRatio);\n float radius = length(windowCenter - windowOffset);\n float dist = length(windowCenter - gl_FragCoord.xy);\n if (dist > radius + v_halfWidth) {\n if (u_strokeColor.a == 0.0) {\n gl_FragColor = u_fillColor;\n } else {\n gl_FragColor = u_strokeColor;\n }\n gl_FragColor.a = gl_FragColor.a - (dist - (radius + v_halfWidth));\n } else if (u_fillColor.a == 0.0) {\n // Hooray, no fill, just stroke. We can use real antialiasing.\n gl_FragColor = u_strokeColor;\n if (dist < radius - v_halfWidth) {\n gl_FragColor.a = gl_FragColor.a - (radius - v_halfWidth - dist);\n }\n } else {\n gl_FragColor = u_fillColor;\n float strokeDist = radius - v_halfWidth;\n float antialias = 2.0 * v_pixelRatio;\n if (dist > strokeDist) {\n gl_FragColor = u_strokeColor;\n } else if (dist >= strokeDist - antialias) {\n float step = smoothstep(strokeDist - antialias, strokeDist, dist);\n gl_FragColor = mix(u_fillColor, u_strokeColor, step);\n }\n }\n gl_FragColor.a = gl_FragColor.a * u_opacity;\n if (gl_FragColor.a <= 0.0) {\n discard;\n }\n}\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.circlereplay.defaultshader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying vec2 b;varying float c;varying float d;uniform float m;uniform vec4 n;uniform vec4 o;uniform vec2 p;void main(void){vec2 windowCenter=vec2((a.x+1.0)/2.0*p.x*d,(a.y+1.0)/2.0*p.y*d);vec2 windowOffset=vec2((b.x+1.0)/2.0*p.x*d,(b.y+1.0)/2.0*p.y*d);float radius=length(windowCenter-windowOffset);float dist=length(windowCenter-gl_FragCoord.xy);if(dist>radius+c){if(o.a==0.0){gl_FragColor=n;}else{gl_FragColor=o;}gl_FragColor.a=gl_FragColor.a-(dist-(radius+c));}else if(n.a==0.0){gl_FragColor=o;if(diststrokeDist){gl_FragColor=o;}else if(dist>=strokeDist-antialias){float step=smoothstep(strokeDist-antialias,strokeDist,dist);gl_FragColor=mix(n,o,step);}} gl_FragColor.a=gl_FragColor.a*m;if(gl_FragColor.a<=0.0){discard;}}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.circlereplay.defaultshader.Fragment.SOURCE = ol.DEBUG ? + ol.render.webgl.circlereplay.defaultshader.Fragment.DEBUG_SOURCE : + ol.render.webgl.circlereplay.defaultshader.Fragment.OPTIMIZED_SOURCE; + + +ol.render.webgl.circlereplay.defaultshader.fragment = new ol.render.webgl.circlereplay.defaultshader.Fragment(); + + +/** + * @constructor + * @extends {ol.webgl.Vertex} + * @struct + */ +ol.render.webgl.circlereplay.defaultshader.Vertex = function() { + ol.webgl.Vertex.call(this, ol.render.webgl.circlereplay.defaultshader.Vertex.SOURCE); +}; +ol.inherits(ol.render.webgl.circlereplay.defaultshader.Vertex, ol.webgl.Vertex); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.circlereplay.defaultshader.Vertex.DEBUG_SOURCE = 'varying vec2 v_center;\nvarying vec2 v_offset;\nvarying float v_halfWidth;\nvarying float v_pixelRatio;\n\n\nattribute vec2 a_position;\nattribute float a_instruction;\nattribute float a_radius;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\nuniform float u_lineWidth;\nuniform float u_pixelRatio;\n\nvoid main(void) {\n mat4 offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n v_center = vec4(u_projectionMatrix * vec4(a_position, 0.0, 1.0)).xy;\n v_pixelRatio = u_pixelRatio;\n float lineWidth = u_lineWidth * u_pixelRatio;\n v_halfWidth = lineWidth / 2.0;\n if (lineWidth == 0.0) {\n lineWidth = 2.0 * u_pixelRatio;\n }\n vec2 offset;\n // Radius with anitaliasing (roughly).\n float radius = a_radius + 3.0 * u_pixelRatio;\n // Until we get gl_VertexID in WebGL, we store an instruction.\n if (a_instruction == 0.0) {\n // Offsetting the edges of the triangle by lineWidth / 2 is necessary, however\n // we should also leave some space for the antialiasing, thus we offset by lineWidth.\n offset = vec2(-1.0, 1.0);\n } else if (a_instruction == 1.0) {\n offset = vec2(-1.0, -1.0);\n } else if (a_instruction == 2.0) {\n offset = vec2(1.0, -1.0);\n } else {\n offset = vec2(1.0, 1.0);\n }\n\n gl_Position = u_projectionMatrix * vec4(a_position + offset * radius, 0.0, 1.0) +\n offsetMatrix * vec4(offset * lineWidth, 0.0, 0.0);\n v_offset = vec4(u_projectionMatrix * vec4(a_position.x + a_radius, a_position.y,\n 0.0, 1.0)).xy;\n\n if (distance(v_center, v_offset) > 20000.0) {\n gl_Position = vec4(v_center, 0.0, 1.0);\n }\n}\n\n\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.circlereplay.defaultshader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying vec2 b;varying float c;varying float d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;uniform float k;uniform float l;void main(void){mat4 offsetMatrix=i*j;a=vec4(h*vec4(e,0.0,1.0)).xy;d=l;float lineWidth=k*l;c=lineWidth/2.0;if(lineWidth==0.0){lineWidth=2.0*l;}vec2 offset;float radius=g+3.0*l;if(f==0.0){offset=vec2(-1.0,1.0);}else if(f==1.0){offset=vec2(-1.0,-1.0);}else if(f==2.0){offset=vec2(1.0,-1.0);}else{offset=vec2(1.0,1.0);}gl_Position=h*vec4(e+offset*radius,0.0,1.0)+offsetMatrix*vec4(offset*lineWidth,0.0,0.0);b=vec4(h*vec4(e.x+g,e.y,0.0,1.0)).xy;if(distance(a,b)>20000.0){gl_Position=vec4(a,0.0,1.0);}}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.circlereplay.defaultshader.Vertex.SOURCE = ol.DEBUG ? + ol.render.webgl.circlereplay.defaultshader.Vertex.DEBUG_SOURCE : + ol.render.webgl.circlereplay.defaultshader.Vertex.OPTIMIZED_SOURCE; + + +ol.render.webgl.circlereplay.defaultshader.vertex = new ol.render.webgl.circlereplay.defaultshader.Vertex(); + + +/** + * @constructor + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLProgram} program Program. + * @struct + */ +ol.render.webgl.circlereplay.defaultshader.Locations = function(gl, program) { + + /** + * @type {WebGLUniformLocation} + */ + this.u_fillColor = gl.getUniformLocation( + program, ol.DEBUG ? 'u_fillColor' : 'n'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_lineWidth = gl.getUniformLocation( + program, ol.DEBUG ? 'u_lineWidth' : 'k'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetRotateMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_offsetRotateMatrix' : 'j'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetScaleMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_offsetScaleMatrix' : 'i'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_opacity = gl.getUniformLocation( + program, ol.DEBUG ? 'u_opacity' : 'm'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_pixelRatio = gl.getUniformLocation( + program, ol.DEBUG ? 'u_pixelRatio' : 'l'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_projectionMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_projectionMatrix' : 'h'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_size = gl.getUniformLocation( + program, ol.DEBUG ? 'u_size' : 'p'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_strokeColor = gl.getUniformLocation( + program, ol.DEBUG ? 'u_strokeColor' : 'o'); + + /** + * @type {number} + */ + this.a_instruction = gl.getAttribLocation( + program, ol.DEBUG ? 'a_instruction' : 'f'); + + /** + * @type {number} + */ + this.a_position = gl.getAttribLocation( + program, ol.DEBUG ? 'a_position' : 'e'); + + /** + * @type {number} + */ + this.a_radius = gl.getAttribLocation( + program, ol.DEBUG ? 'a_radius' : 'g'); +}; diff --git a/src/ol/render/webgl/circlereplay/index.js b/src/ol/render/webgl/circlereplay/index.js new file mode 100644 index 0000000000..c4821560bc --- /dev/null +++ b/src/ol/render/webgl/circlereplay/index.js @@ -0,0 +1,431 @@ +goog.provide('ol.render.webgl.CircleReplay'); + +goog.require('ol'); +goog.require('ol.array'); +goog.require('ol.color'); +goog.require('ol.extent'); +goog.require('ol.obj'); +goog.require('ol.geom.flat.transform'); +goog.require('ol.render.webgl.circlereplay.defaultshader'); +goog.require('ol.render.webgl.Replay'); +goog.require('ol.render.webgl'); +goog.require('ol.webgl'); +goog.require('ol.webgl.Buffer'); + + +/** + * @constructor + * @extends {ol.render.webgl.Replay} + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @struct + */ +ol.render.webgl.CircleReplay = function(tolerance, maxExtent) { + ol.render.webgl.Replay.call(this, tolerance, maxExtent); + + /** + * @private + * @type {ol.render.webgl.circlereplay.defaultshader.Locations} + */ + this.defaultLocations_ = null; + + /** + * @private + * @type {Array.|number>>} + */ + this.styles_ = []; + + /** + * @private + * @type {Array.} + */ + this.styleIndices_ = []; + + /** + * @private + * @type {number} + */ + this.radius_ = 0; + + /** + * @private + * @type {{fillColor: (Array.|null), + * strokeColor: (Array.|null), + * lineDash: Array., + * lineWidth: (number|undefined), + * changed: boolean}|null} + */ + this.state_ = { + fillColor: null, + strokeColor: null, + lineDash: null, + lineWidth: undefined, + changed: false + }; + +}; +ol.inherits(ol.render.webgl.CircleReplay, ol.render.webgl.Replay); + + +/** + * @private + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + */ +ol.render.webgl.CircleReplay.prototype.drawCoordinates_ = function( + flatCoordinates, offset, end, stride) { + var numVertices = this.vertices.length; + var numIndices = this.indices.length; + var n = numVertices / 4; + var i, ii; + for (i = offset, ii = end; i < ii; i += stride) { + this.vertices[numVertices++] = flatCoordinates[i]; + this.vertices[numVertices++] = flatCoordinates[i + 1]; + this.vertices[numVertices++] = 0; + this.vertices[numVertices++] = this.radius_; + + this.vertices[numVertices++] = flatCoordinates[i]; + this.vertices[numVertices++] = flatCoordinates[i + 1]; + this.vertices[numVertices++] = 1; + this.vertices[numVertices++] = this.radius_; + + this.vertices[numVertices++] = flatCoordinates[i]; + this.vertices[numVertices++] = flatCoordinates[i + 1]; + this.vertices[numVertices++] = 2; + this.vertices[numVertices++] = this.radius_; + + this.vertices[numVertices++] = flatCoordinates[i]; + this.vertices[numVertices++] = flatCoordinates[i + 1]; + this.vertices[numVertices++] = 3; + this.vertices[numVertices++] = this.radius_; + + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 2; + + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n + 3; + this.indices[numIndices++] = n; + + n += 4; + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.CircleReplay.prototype.drawCircle = function(circleGeometry, feature) { + var radius = circleGeometry.getRadius(); + var stride = circleGeometry.getStride(); + if (radius) { + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(this.indices.length); + this.state_.changed = false; + } + + this.radius_ = radius; + var flatCoordinates = circleGeometry.getFlatCoordinates(); + flatCoordinates = ol.geom.flat.transform.translate(flatCoordinates, 0, 2, + stride, -this.origin[0], -this.origin[1]); + this.drawCoordinates_(flatCoordinates, 0, 2, stride); + } else { + if (this.state_.changed) { + this.styles_.pop(); + if (this.styles_.length) { + var lastState = this.styles_[this.styles_.length - 1]; + this.state_.fillColor = /** @type {Array.} */ (lastState[0]); + this.state_.strokeColor = /** @type {Array.} */ (lastState[1]); + this.state_.lineWidth = /** @type {number} */ (lastState[2]); + this.state_.changed = false; + } + } + } +}; + + +/** + * @inheritDoc + **/ +ol.render.webgl.CircleReplay.prototype.finish = function(context) { + // 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); + + this.startIndices.push(this.indices.length); + + //Clean up, if there is nothing to draw + if (this.styleIndices_.length === 0 && this.styles_.length > 0) { + this.styles_ = []; + } + + this.vertices = null; + this.indices = null; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.CircleReplay.prototype.getDeleteResourcesFunction = function(context) { + // We only delete our stuff here. The shaders and the program may + // be used by other CircleReplay instances (for other layers). And + // they will be deleted when disposing of the ol.webgl.Context + // object. + ol.DEBUG && console.assert(this.verticesBuffer, + 'verticesBuffer must not be null'); + ol.DEBUG && console.assert(this.indicesBuffer, + 'indicesBuffer must not be null'); + var verticesBuffer = this.verticesBuffer; + var indicesBuffer = this.indicesBuffer; + return function() { + context.deleteBuffer(verticesBuffer); + context.deleteBuffer(indicesBuffer); + }; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.CircleReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { + // get the program + var fragmentShader, vertexShader; + fragmentShader = ol.render.webgl.circlereplay.defaultshader.fragment; + vertexShader = ol.render.webgl.circlereplay.defaultshader.vertex; + var program = context.getProgram(fragmentShader, vertexShader); + + // get the locations + var locations; + if (!this.defaultLocations_) { + locations = + new ol.render.webgl.circlereplay.defaultshader.Locations(gl, program); + this.defaultLocations_ = locations; + } else { + locations = this.defaultLocations_; + } + + context.useProgram(program); + + // enable the vertex attrib arrays + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, ol.webgl.FLOAT, + false, 16, 0); + + gl.enableVertexAttribArray(locations.a_instruction); + gl.vertexAttribPointer(locations.a_instruction, 1, ol.webgl.FLOAT, + false, 16, 8); + + gl.enableVertexAttribArray(locations.a_radius); + gl.vertexAttribPointer(locations.a_radius, 1, ol.webgl.FLOAT, + false, 16, 12); + + // Enable renderer specific uniforms. + gl.uniform2fv(locations.u_size, size); + gl.uniform1f(locations.u_pixelRatio, pixelRatio); + + return locations; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.CircleReplay.prototype.shutDownProgram = function(gl, locations) { + gl.disableVertexAttribArray(locations.a_position); + gl.disableVertexAttribArray(locations.a_instruction); + gl.disableVertexAttribArray(locations.a_radius); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.CircleReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { + if (!ol.obj.isEmpty(skippedFeaturesHash)) { + this.drawReplaySkipping_(gl, context, skippedFeaturesHash); + } else { + ol.DEBUG && console.assert(this.styles_.length === this.styleIndices_.length, + 'number of styles and styleIndices match'); + + //Draw by style groups to minimize drawElements() calls. + var i, start, end, nextStyle; + end = this.startIndices[this.startIndices.length - 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + start = this.styleIndices_[i]; + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, /** @type {Array.} */ (nextStyle[0])); + this.setStrokeStyle_(gl, /** @type {Array.} */ (nextStyle[1]), + /** @type {number} */ (nextStyle[2])); + this.drawElements(gl, context, start, end); + end = start; + } + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.CircleReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, + featureCallback, opt_hitExtent) { + ol.DEBUG && console.assert(this.styles_.length === this.styleIndices_.length, + 'number of styles and styleIndices match'); + ol.DEBUG && console.assert(this.startIndices.length - 1 === this.startIndicesFeature.length, + 'number of startIndices and startIndicesFeature match'); + + var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; + featureIndex = this.startIndices.length - 2; + end = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, /** @type {Array.} */ (nextStyle[0])); + this.setStrokeStyle_(gl, /** @type {Array.} */ (nextStyle[1]), + /** @type {number} */ (nextStyle[2])); + groupStart = this.styleIndices_[i]; + + 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; + } + + } + featureIndex--; + end = start; + } + } + return undefined; +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {Object} skippedFeaturesHash Ids of features to skip. + */ +ol.render.webgl.CircleReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) { + ol.DEBUG && console.assert(this.startIndices.length - 1 === this.startIndicesFeature.length, + 'number of startIndices and startIndicesFeature match'); + + var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; + featureIndex = this.startIndices.length - 2; + end = start = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, /** @type {Array.} */ (nextStyle[0])); + this.setStrokeStyle_(gl, /** @type {Array.} */ (nextStyle[1]), + /** @type {number} */ (nextStyle[2])); + groupStart = this.styleIndices_[i]; + + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + featureStart = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = ol.getUid(feature).toString(); + + if (skippedFeaturesHash[featureUid]) { + if (start !== end) { + this.drawElements(gl, context, start, end); + } + end = featureStart; + } + featureIndex--; + start = featureStart; + } + if (start !== end) { + this.drawElements(gl, context, start, end); + } + start = end = groupStart; + } +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {Array.} color Color. + */ +ol.render.webgl.CircleReplay.prototype.setFillStyle_ = function(gl, color) { + gl.uniform4fv(this.defaultLocations_.u_fillColor, color); +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {Array.} color Color. + * @param {number} lineWidth Line width. + */ +ol.render.webgl.CircleReplay.prototype.setStrokeStyle_ = function(gl, color, lineWidth) { + gl.uniform4fv(this.defaultLocations_.u_strokeColor, color); + gl.uniform1f(this.defaultLocations_.u_lineWidth, lineWidth); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.CircleReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { + ol.DEBUG && console.assert(this.state_, 'this.state_ should not be null'); + var strokeStyleColor, strokeStyleWidth; + if (strokeStyle) { + var strokeStyleLineDash = strokeStyle.getLineDash(); + this.state_.lineDash = strokeStyleLineDash ? + strokeStyleLineDash : ol.render.webgl.defaultLineDash; + strokeStyleColor = strokeStyle.getColor(); + if (!(strokeStyleColor instanceof CanvasGradient) && + !(strokeStyleColor instanceof CanvasPattern)) { + strokeStyleColor = ol.color.asArray(strokeStyleColor).map(function(c, i) { + return i != 3 ? c / 255 : c; + }) || ol.render.webgl.defaultStrokeStyle; + } else { + strokeStyleColor = ol.render.webgl.defaultStrokeStyle; + } + strokeStyleWidth = strokeStyle.getWidth(); + strokeStyleWidth = strokeStyleWidth !== undefined ? + strokeStyleWidth : ol.render.webgl.defaultLineWidth; + } else { + strokeStyleColor = [0, 0, 0, 0]; + strokeStyleWidth = 0; + } + var fillStyleColor = fillStyle ? fillStyle.getColor() : [0, 0, 0, 0]; + if (!(fillStyleColor instanceof CanvasGradient) && + !(fillStyleColor instanceof CanvasPattern)) { + fillStyleColor = ol.color.asArray(fillStyleColor).map(function(c, i) { + return i != 3 ? c / 255 : c; + }) || ol.render.webgl.defaultFillStyle; + } else { + fillStyleColor = ol.render.webgl.defaultFillStyle; + } + if (!this.state_.strokeColor || !ol.array.equals(this.state_.strokeColor, strokeStyleColor) || + !this.state_.fillColor || !ol.array.equals(this.state_.fillColor, fillStyleColor) || + this.state_.lineWidth !== strokeStyleWidth) { + this.state_.changed = true; + this.state_.fillColor = fillStyleColor; + this.state_.strokeColor = strokeStyleColor; + this.state_.lineWidth = strokeStyleWidth; + this.styles_.push([fillStyleColor, strokeStyleColor, strokeStyleWidth]); + } +}; diff --git a/src/ol/render/webgl/imagereplay/defaultshader.glsl b/src/ol/render/webgl/imagereplay/defaultshader.glsl index 1d37462aea..b1b6168fff 100644 --- a/src/ol/render/webgl/imagereplay/defaultshader.glsl +++ b/src/ol/render/webgl/imagereplay/defaultshader.glsl @@ -22,8 +22,8 @@ void main(void) { 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; + vec4 offsets = offsetMatrix * vec4(a_offsets, 0.0, 0.0); + gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets; v_texCoord = a_texCoord; v_opacity = a_opacity; } diff --git a/src/ol/render/webgl/imagereplay/defaultshader.js b/src/ol/render/webgl/imagereplay/defaultshader.js index 15630006e2..374b8a4365 100644 --- a/src/ol/render/webgl/imagereplay/defaultshader.js +++ b/src/ol/render/webgl/imagereplay/defaultshader.js @@ -58,14 +58,14 @@ 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.);\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'; +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.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}'; +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;}'; /** diff --git a/src/ol/render/webgl/imagereplay/index.js b/src/ol/render/webgl/imagereplay/index.js index ff0a89e4ed..22deaa3616 100644 --- a/src/ol/render/webgl/imagereplay/index.js +++ b/src/ol/render/webgl/imagereplay/index.js @@ -1,15 +1,11 @@ goog.provide('ol.render.webgl.ImageReplay'); -goog.provide('ol.render.webgl.ReplayGroup'); goog.require('ol'); goog.require('ol.extent'); goog.require('ol.obj'); -goog.require('ol.render.ReplayGroup'); -goog.require('ol.render.VectorContext'); -goog.require('ol.render.replay'); goog.require('ol.render.webgl.imagereplay.defaultshader'); -goog.require('ol.transform'); -goog.require('ol.vec.Mat4'); +goog.require('ol.render.webgl.Replay'); +goog.require('ol.render.webgl'); goog.require('ol.webgl'); goog.require('ol.webgl.Buffer'); goog.require('ol.webgl.Context'); @@ -17,14 +13,13 @@ goog.require('ol.webgl.Context'); /** * @constructor - * @extends {ol.render.VectorContext} + * @extends {ol.render.webgl.Replay} * @param {number} tolerance Tolerance. * @param {ol.Extent} maxExtent Max extent. - * @protected * @struct */ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { - ol.render.VectorContext.call(this); + ol.render.webgl.Replay.call(this, tolerance, maxExtent); /** * @type {number|undefined} @@ -38,16 +33,6 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.anchorY_ = undefined; - /** - * 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 {Array.} * @private @@ -90,18 +75,6 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.imageWidth_ = undefined; - /** - * @type {Array.} - * @private - */ - this.indices_ = []; - - /** - * @type {ol.webgl.Buffer} - * @private - */ - this.indicesBuffer_ = null; - /** * @private * @type {ol.render.webgl.imagereplay.defaultshader.Locations} @@ -114,18 +87,6 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.opacity_ = undefined; - /** - * @type {ol.Transform} - * @private - */ - this.offsetRotateMatrix_ = ol.transform.create(); - - /** - * @type {ol.Transform} - * @private - */ - this.offsetScaleMatrix_ = ol.transform.create(); - /** * @type {number|undefined} * @private @@ -138,18 +99,6 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.originY_ = undefined; - /** - * @type {ol.Transform} - * @private - */ - this.projectionMatrix_ = ol.transform.create(); - - /** - * @type {Array.} - * @private - */ - this.tmpMat4_ = ol.vec.Mat4.create(); - /** * @private * @type {boolean|undefined} @@ -180,56 +129,29 @@ ol.render.webgl.ImageReplay = function(tolerance, maxExtent) { */ this.hitDetectionTextures_ = []; - /** - * @type {Array.} - * @private - */ - this.vertices_ = []; - - /** - * @type {ol.webgl.Buffer} - * @private - */ - this.verticesBuffer_ = null; - - /** - * Start index per feature (the index). - * @type {Array.} - * @private - */ - this.startIndices_ = []; - - /** - * Start index per feature (the feature). - * @type {Array.} - * @private - */ - this.startIndicesFeature_ = []; - /** * @type {number|undefined} * @private */ this.width_ = undefined; }; -ol.inherits(ol.render.webgl.ImageReplay, ol.render.VectorContext); +ol.inherits(ol.render.webgl.ImageReplay, ol.render.webgl.Replay); /** - * @param {ol.webgl.Context} context WebGL context. - * @return {function()} Delete resources function. + * @inheritDoc */ 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. - ol.DEBUG && console.assert(this.verticesBuffer_, + ol.DEBUG && console.assert(this.verticesBuffer, 'verticesBuffer must not be null'); - ol.DEBUG && console.assert(this.indicesBuffer_, + ol.DEBUG && console.assert(this.indicesBuffer, 'indicesBuffer must not be null'); - var verticesBuffer = this.verticesBuffer_; - var indicesBuffer = this.indicesBuffer_; + var verticesBuffer = this.verticesBuffer; + var indicesBuffer = this.indicesBuffer; var textures = this.textures_; var hitDetectionTextures = this.hitDetectionTextures_; var gl = context.getGL(); @@ -286,12 +208,12 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinate 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 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]; + 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 @@ -308,57 +230,57 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinate // 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; + 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; + 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; + 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.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; + 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; @@ -369,8 +291,8 @@ ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinate * @inheritDoc */ ol.render.webgl.ImageReplay.prototype.drawMultiPoint = function(multiPointGeometry, feature) { - this.startIndices_.push(this.indices_.length); - this.startIndicesFeature_.push(feature); + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); var flatCoordinates = multiPointGeometry.getFlatCoordinates(); var stride = multiPointGeometry.getStride(); this.drawCoordinates_( @@ -382,8 +304,8 @@ ol.render.webgl.ImageReplay.prototype.drawMultiPoint = function(multiPointGeomet * @inheritDoc */ ol.render.webgl.ImageReplay.prototype.drawPoint = function(pointGeometry, feature) { - this.startIndices_.push(this.indices_.length); - this.startIndicesFeature_.push(feature); + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); var flatCoordinates = pointGeometry.getFlatCoordinates(); var stride = pointGeometry.getStride(); this.drawCoordinates_( @@ -392,32 +314,30 @@ ol.render.webgl.ImageReplay.prototype.drawPoint = function(pointGeometry, featur /** - * @param {ol.webgl.Context} context Context. + * @inheritDoc */ ol.render.webgl.ImageReplay.prototype.finish = function(context) { var gl = context.getGL(); - this.groupIndices_.push(this.indices_.length); + this.groupIndices_.push(this.indices.length); ol.DEBUG && console.assert(this.images_.length === this.groupIndices_.length, 'number of images and groupIndices match'); - this.hitDetectionGroupIndices_.push(this.indices_.length); + this.hitDetectionGroupIndices_.push(this.indices.length); ol.DEBUG && console.assert(this.hitDetectionImages_.length === this.hitDetectionGroupIndices_.length, 'number of hitDetectionImages and hitDetectionGroupIndices match'); // create, bind, and populate the vertices buffer - this.verticesBuffer_ = new ol.webgl.Buffer(this.vertices_); - context.bindBuffer(ol.webgl.ARRAY_BUFFER, this.verticesBuffer_); + this.verticesBuffer = new ol.webgl.Buffer(this.vertices); - var indices = this.indices_; + var indices = this.indices; var bits = context.hasOESElementIndexUint ? 32 : 16; ol.DEBUG && console.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(ol.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); + this.indicesBuffer = new ol.webgl.Buffer(indices); // create textures /** @type {Object.} */ @@ -440,14 +360,14 @@ ol.render.webgl.ImageReplay.prototype.finish = function(context) { this.hitDetectionImages_ = null; this.imageHeight_ = undefined; this.imageWidth_ = undefined; - this.indices_ = null; + 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.vertices = null; this.width_ = undefined; }; @@ -483,38 +403,9 @@ ol.render.webgl.ImageReplay.prototype.createTextures_ = function(textures, image /** - * @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 + * @inheritDoc */ -ol.render.webgl.ImageReplay.prototype.replay = function(context, - center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash, - featureCallback, oneByOne, opt_hitExtent) { - var gl = context.getGL(); - - // bind the vertices buffer - ol.DEBUG && console.assert(this.verticesBuffer_, - 'verticesBuffer must not be null'); - context.bindBuffer(ol.webgl.ARRAY_BUFFER, this.verticesBuffer_); - - // bind the indices buffer - ol.DEBUG && console.assert(this.indicesBuffer_, - 'indecesBuffer must not be null'); - context.bindBuffer(ol.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_); - +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; @@ -554,76 +445,40 @@ ol.render.webgl.ImageReplay.prototype.replay = function(context, gl.vertexAttribPointer(locations.a_rotateWithView, 1, ol.webgl.FLOAT, false, 32, 28); - // set the "uniform" values - var projectionMatrix = ol.transform.reset(this.projectionMatrix_); - ol.transform.scale(projectionMatrix, 2 / (resolution * size[0]), 2 / (resolution * size[1])); - ol.transform.rotate(projectionMatrix, -rotation); - ol.transform.translate(projectionMatrix, -(center[0] - this.origin_[0]), -(center[1] - this.origin_[1])); + return locations; +}; - var offsetScaleMatrix = ol.transform.reset(this.offsetScaleMatrix_); - ol.transform.scale(offsetScaleMatrix, 2 / size[0], 2 / size[1]); - var offsetRotateMatrix = ol.transform.reset(this.offsetRotateMatrix_); - if (rotation !== 0) { - ol.transform.rotate(offsetRotateMatrix, -rotation); - } - - gl.uniformMatrix4fv(locations.u_projectionMatrix, false, - ol.vec.Mat4.fromTransform(this.tmpMat4_, projectionMatrix)); - gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, - ol.vec.Mat4.fromTransform(this.tmpMat4_, offsetScaleMatrix)); - gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false, - ol.vec.Mat4.fromTransform(this.tmpMat4_, offsetRotateMatrix)); - gl.uniform1f(locations.u_opacity, opacity); - - // draw! - var result; - if (featureCallback === undefined) { - this.drawReplay_(gl, context, skippedFeaturesHash, - this.textures_, this.groupIndices_); - } else { - // draw feature by feature for the hit-detection - result = this.drawHitDetectionReplay_(gl, context, skippedFeaturesHash, - featureCallback, oneByOne, opt_hitExtent); - } - - // disable the vertex attrib arrays +/** + * @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); - - return result; }; /** - * @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. + * @inheritDoc */ -ol.render.webgl.ImageReplay.prototype.drawReplay_ = function(gl, context, skippedFeaturesHash, textures, groupIndices) { +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_; ol.DEBUG && console.assert(textures.length === groupIndices.length, 'number of textures and groupIndeces match'); - var elementType = context.hasOESElementIndexUint ? - ol.webgl.UNSIGNED_INT : ol.webgl.UNSIGNED_SHORT; - var elementSize = context.hasOESElementIndexUint ? 4 : 2; if (!ol.obj.isEmpty(skippedFeaturesHash)) { this.drawReplaySkipping_( - gl, skippedFeaturesHash, textures, groupIndices, - elementType, elementSize); + 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, start, end, elementType, elementSize); + this.drawElements(gl, context, start, end); start = end; } } @@ -650,15 +505,14 @@ ol.render.webgl.ImageReplay.prototype.drawReplay_ = function(gl, context, skippe * * @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. - * @param {number} elementType Element type. - * @param {number} elementSize Element Size. */ -ol.render.webgl.ImageReplay.prototype.drawReplaySkipping_ = function(gl, skippedFeaturesHash, textures, groupIndices, - elementType, elementSize) { +ol.render.webgl.ImageReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash, textures, + groupIndices) { var featureIndex = 0; var i, ii; @@ -669,25 +523,25 @@ ol.render.webgl.ImageReplay.prototype.drawReplaySkipping_ = function(gl, skipped var start = groupStart; var end = groupStart; - while (featureIndex < this.startIndices_.length && - this.startIndices_[featureIndex] <= groupEnd) { - var feature = this.startIndicesFeature_[featureIndex]; + 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, start, end, elementType, elementSize); + this.drawElements(gl, context, start, end); } // continue with the next feature - start = (featureIndex === this.startIndices_.length - 1) ? - groupEnd : this.startIndices_[featureIndex + 1]; + 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]; + end = (featureIndex === this.startIndices.length - 1) ? + groupEnd : this.startIndices[featureIndex + 1]; } featureIndex++; } @@ -695,102 +549,23 @@ ol.render.webgl.ImageReplay.prototype.drawReplaySkipping_ = function(gl, skipped 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, start, end, elementType, elementSize); + this.drawElements(gl, context, start, end); } } }; /** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {number} start Start index. - * @param {number} end End index. - * @param {number} elementType Element type. - * @param {number} elementSize Element Size. + * @inheritDoc */ -ol.render.webgl.ImageReplay.prototype.drawElements_ = function( - gl, start, end, elementType, elementSize) { - var numItems = end - start; - var offsetInBytes = start * elementSize; - gl.drawElements(ol.webgl.TRIANGLES, numItems, elementType, offsetInBytes); -}; - - -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {ol.webgl.Context} context Context. - * @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.ImageReplay.prototype.drawHitDetectionReplay_ = function(gl, context, skippedFeaturesHash, featureCallback, oneByOne, - opt_hitExtent) { - if (!oneByOne) { - // draw all hit-detection features in "once" (by texture group) - return this.drawHitDetectionReplayAll_(gl, context, - skippedFeaturesHash, featureCallback); - } else { - // draw hit-detection features one by one - return this.drawHitDetectionReplayOneByOne_(gl, context, - skippedFeaturesHash, featureCallback, opt_hitExtent); - } -}; - - -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {ol.webgl.Context} context Context. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback. - * @return {T|undefined} Callback result. - * @template T - */ -ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayAll_ = function(gl, context, skippedFeaturesHash, featureCallback) { - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.drawReplay_(gl, context, skippedFeaturesHash, - this.hitDetectionTextures_, this.hitDetectionGroupIndices_); - - var result = featureCallback(null); - if (result) { - return result; - } else { - return undefined; - } -}; - - -/** - * @private - * @param {WebGLRenderingContext} gl gl. - * @param {ol.webgl.Context} context Context. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback. - * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting - * this extent are checked. - * @return {T|undefined} Callback result. - * @template T - */ -ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne_ = function(gl, context, skippedFeaturesHash, featureCallback, - opt_hitExtent) { +ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, + featureCallback, opt_hitExtent) { ol.DEBUG && console.assert(this.hitDetectionTextures_.length === this.hitDetectionGroupIndices_.length, 'number of hitDetectionTextures and hitDetectionGroupIndices match'); - var elementType = context.hasOESElementIndexUint ? - ol.webgl.UNSIGNED_INT : ol.webgl.UNSIGNED_SHORT; - var elementSize = context.hasOESElementIndexUint ? 4 : 2; var i, groupStart, start, end, feature, featureUid; - var featureIndex = this.startIndices_.length - 1; + 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; @@ -798,9 +573,9 @@ ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne_ = function // draw all features for this texture group while (featureIndex >= 0 && - this.startIndices_[featureIndex] >= groupStart) { - start = this.startIndices_[featureIndex]; - feature = this.startIndicesFeature_[featureIndex]; + this.startIndices[featureIndex] >= groupStart) { + start = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; featureUid = ol.getUid(feature).toString(); if (skippedFeaturesHash[featureUid] === undefined && @@ -809,7 +584,7 @@ ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne_ = function /** @type {Array} */ (opt_hitExtent), feature.getGeometry().getExtent()))) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - this.drawElements_(gl, start, end, elementType, elementSize); + this.drawElements(gl, context, start, end); var result = featureCallback(feature); if (result) { @@ -825,13 +600,6 @@ ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne_ = function }; -/** - * @inheritDoc - * @abstract - */ -ol.render.webgl.ImageReplay.prototype.setFillStrokeStyle = function() {}; - - /** * @inheritDoc */ @@ -869,7 +637,7 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { } 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); ol.DEBUG && console.assert(this.groupIndices_.length === this.images_.length, 'number of groupIndices and images match'); this.images_.push(image); @@ -882,7 +650,7 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { 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); ol.DEBUG && console.assert(this.hitDetectionGroupIndices_.length === this.hitDetectionImages_.length, 'number of hitDetectionGroupIndices and hitDetectionImages match'); @@ -903,276 +671,3 @@ ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) { this.scale_ = scale; this.width_ = size[0]; }; - - -/** - * @constructor - * @extends {ol.render.ReplayGroup} - * @param {number} tolerance Tolerance. - * @param {ol.Extent} maxExtent Max extent. - * @param {number=} opt_renderBuffer Render buffer. - * @struct - */ -ol.render.webgl.ReplayGroup = function(tolerance, maxExtent, opt_renderBuffer) { - ol.render.ReplayGroup.call(this); - - /** - * @type {ol.Extent} - * @private - */ - this.maxExtent_ = maxExtent; - - /** - * @type {number} - * @private - */ - this.tolerance_ = tolerance; - - /** - * @type {number|undefined} - * @private - */ - this.renderBuffer_ = opt_renderBuffer; - - /** - * ImageReplay only is supported at this point. - * @type {Object.} - * @private - */ - this.replays_ = {}; - -}; -ol.inherits(ol.render.webgl.ReplayGroup, ol.render.ReplayGroup); - - -/** - * @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 function() { - var length = functions.length; - var result; - for (var i = 0; i < length; i++) { - result = functions[i].apply(this, arguments); - } - return result; - }; -}; - - -/** - * @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 (replay === undefined) { - var constructor = ol.render.webgl.BATCH_CONSTRUCTORS_[replayType]; - replay = new constructor(this.tolerance_, this.maxExtent_); - this.replays_[replayType] = replay; - } - return replay; -}; - - -/** - * @inheritDoc - */ -ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { - return ol.obj.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 {number} pixelRatio Pixel ratio. - * @param {number} opacity Global opacity. - * @param {Object.} skippedFeaturesHash Ids of features - * to skip. - */ -ol.render.webgl.ReplayGroup.prototype.replay = function(context, - center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash) { - var i, ii, replay; - for (i = 0, ii = ol.render.replay.ORDER.length; i < ii; ++i) { - replay = this.replays_[ol.render.replay.ORDER[i]]; - if (replay !== undefined) { - replay.replay(context, - center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash, - undefined, false); - } - } -}; - - -/** - * @private - * @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.ReplayGroup.prototype.replayHitDetection_ = function(context, - center, resolution, rotation, size, pixelRatio, opacity, - skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent) { - var i, replay, result; - for (i = ol.render.replay.ORDER.length - 1; i >= 0; --i) { - replay = this.replays_[ol.render.replay.ORDER[i]]; - if (replay !== undefined) { - result = replay.replay(context, - center, resolution, rotation, size, pixelRatio, opacity, - skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent); - if (result) { - return result; - } - } - } - return undefined; -}; - - -/** - * @param {ol.Coordinate} coordinate Coordinate. - * @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} callback Feature callback. - * @return {T|undefined} Callback result. - * @template T - */ -ol.render.webgl.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( - coordinate, context, center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash, - callback) { - var gl = context.getGL(); - gl.bindFramebuffer( - gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); - - - /** - * @type {ol.Extent} - */ - var hitExtent; - if (this.renderBuffer_ !== undefined) { - // build an extent around the coordinate, so that only features that - // intersect this extent are checked - hitExtent = ol.extent.buffer( - ol.extent.createOrUpdateFromCoordinate(coordinate), - resolution * this.renderBuffer_); - } - - return this.replayHitDetection_(context, - coordinate, resolution, rotation, ol.render.webgl.HIT_DETECTION_SIZE_, - pixelRatio, opacity, skippedFeaturesHash, - /** - * @param {ol.Feature|ol.render.Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - var imageData = new Uint8Array(4); - gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); - - if (imageData[3] > 0) { - var result = callback(feature); - if (result) { - return result; - } - } - }, true, hitExtent); -}; - - -/** - * @param {ol.Coordinate} coordinate Coordinate. - * @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. - * @return {boolean} Is there a feature at the given coordinate? - */ -ol.render.webgl.ReplayGroup.prototype.hasFeatureAtCoordinate = function( - coordinate, context, center, resolution, rotation, size, pixelRatio, - opacity, skippedFeaturesHash) { - var gl = context.getGL(); - gl.bindFramebuffer( - gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); - - var hasFeature = this.replayHitDetection_(context, - coordinate, resolution, rotation, ol.render.webgl.HIT_DETECTION_SIZE_, - pixelRatio, opacity, skippedFeaturesHash, - /** - * @param {ol.Feature|ol.render.Feature} feature Feature. - * @return {boolean} Is there a feature? - */ - function(feature) { - var imageData = new Uint8Array(4); - gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); - return imageData[3] > 0; - }, false); - - return hasFeature !== undefined; -}; - - -/** - * @const - * @private - * @type {Object.} - */ -ol.render.webgl.BATCH_CONSTRUCTORS_ = { - 'Image': ol.render.webgl.ImageReplay -}; - - -/** - * @const - * @private - * @type {Array.} - */ -ol.render.webgl.HIT_DETECTION_SIZE_ = [1, 1]; diff --git a/src/ol/render/webgl/immediate.js b/src/ol/render/webgl/immediate.js index 2d52703d85..f3e637cbeb 100644 --- a/src/ol/render/webgl/immediate.js +++ b/src/ol/render/webgl/immediate.js @@ -6,6 +6,7 @@ goog.require('ol.geom.GeometryType'); goog.require('ol.render.ReplayType'); goog.require('ol.render.VectorContext'); goog.require('ol.render.webgl.ReplayGroup'); +goog.require('ol.render.webgl'); /** @@ -64,6 +65,18 @@ ol.render.webgl.Immediate = function(context, center, resolution, rotation, size */ this.imageStyle_ = null; + /** + * @private + * @type {ol.style.Fill} + */ + this.fillStyle_ = null; + + /** + * @private + * @type {ol.style.Stroke} + */ + this.strokeStyle_ = null; + }; ol.inherits(ol.render.webgl.Immediate, ol.render.VectorContext); @@ -76,6 +89,7 @@ ol.inherits(ol.render.webgl.Immediate, ol.render.VectorContext); * @api */ ol.render.webgl.Immediate.prototype.setStyle = function(style) { + this.setFillStrokeStyle(style.getFill(), style.getStroke()); this.setImageStyle(style.getImage()); }; @@ -93,12 +107,27 @@ ol.render.webgl.Immediate.prototype.drawGeometry = function(geometry) { case ol.geom.GeometryType.POINT: this.drawPoint(/** @type {ol.geom.Point} */ (geometry), null); break; + case ol.geom.GeometryType.LINE_STRING: + this.drawLineString(/** @type {ol.geom.LineString} */ (geometry), null); + break; + case ol.geom.GeometryType.POLYGON: + this.drawPolygon(/** @type {ol.geom.Polygon} */ (geometry), null); + break; case ol.geom.GeometryType.MULTI_POINT: this.drawMultiPoint(/** @type {ol.geom.MultiPoint} */ (geometry), null); break; + case ol.geom.GeometryType.MULTI_LINE_STRING: + this.drawMultiLineString(/** @type {ol.geom.MultiLineString} */ (geometry), null); + break; + case ol.geom.GeometryType.MULTI_POLYGON: + this.drawMultiPolygon(/** @type {ol.geom.MultiPolygon} */ (geometry), null); + break; case ol.geom.GeometryType.GEOMETRY_COLLECTION: this.drawGeometryCollection(/** @type {ol.geom.GeometryCollection} */ (geometry), null); break; + case ol.geom.GeometryType.CIRCLE: + this.drawCircle(/** @type {ol.geom.Circle} */ (geometry), null); + break; default: ol.DEBUG && console.assert(false, 'Unsupported geometry type: ' + type); } @@ -178,9 +207,128 @@ ol.render.webgl.Immediate.prototype.drawMultiPoint = function(geometry, data) { }; +/** + * @inheritDoc + */ +ol.render.webgl.Immediate.prototype.drawLineString = function(geometry, data) { + var context = this.context_; + var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); + var replay = /** @type {ol.render.webgl.LineStringReplay} */ ( + replayGroup.getReplay(0, ol.render.ReplayType.LINE_STRING)); + replay.setFillStrokeStyle(null, this.strokeStyle_); + replay.drawLineString(geometry, data); + replay.finish(context); + var opacity = 1; + var skippedFeatures = {}; + var featureCallback; + var oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.Immediate.prototype.drawMultiLineString = function(geometry, data) { + var context = this.context_; + var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); + var replay = /** @type {ol.render.webgl.LineStringReplay} */ ( + replayGroup.getReplay(0, ol.render.ReplayType.LINE_STRING)); + replay.setFillStrokeStyle(null, this.strokeStyle_); + replay.drawMultiLineString(geometry, data); + replay.finish(context); + var opacity = 1; + var skippedFeatures = {}; + var featureCallback; + var oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.Immediate.prototype.drawPolygon = function(geometry, data) { + var context = this.context_; + var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); + var replay = /** @type {ol.render.webgl.PolygonReplay} */ ( + replayGroup.getReplay(0, ol.render.ReplayType.POLYGON)); + replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); + replay.drawPolygon(geometry, data); + replay.finish(context); + var opacity = 1; + var skippedFeatures = {}; + var featureCallback; + var oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.Immediate.prototype.drawMultiPolygon = function(geometry, data) { + var context = this.context_; + var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); + var replay = /** @type {ol.render.webgl.PolygonReplay} */ ( + replayGroup.getReplay(0, ol.render.ReplayType.POLYGON)); + replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); + replay.drawMultiPolygon(geometry, data); + replay.finish(context); + var opacity = 1; + var skippedFeatures = {}; + var featureCallback; + var oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.Immediate.prototype.drawCircle = function(geometry, data) { + var context = this.context_; + var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_); + var replay = /** @type {ol.render.webgl.CircleReplay} */ ( + replayGroup.getReplay(0, ol.render.ReplayType.CIRCLE)); + replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_); + replay.drawCircle(geometry, data); + replay.finish(context); + var opacity = 1; + var skippedFeatures = {}; + var featureCallback; + var oneByOne = false; + replay.replay(this.context_, this.center_, this.resolution_, this.rotation_, + this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback, + oneByOne); + replay.getDeleteResourcesFunction(context)(); +}; + + /** * @inheritDoc */ ol.render.webgl.Immediate.prototype.setImageStyle = function(imageStyle) { this.imageStyle_ = imageStyle; }; + + +/** + * @inheritDoc + */ +ol.render.webgl.Immediate.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { + this.fillStyle_ = fillStyle; + this.strokeStyle_ = strokeStyle; +}; diff --git a/src/ol/render/webgl/index.js b/src/ol/render/webgl/index.js new file mode 100644 index 0000000000..8dc0eb2973 --- /dev/null +++ b/src/ol/render/webgl/index.js @@ -0,0 +1,83 @@ +goog.provide('ol.render.webgl'); + +/** + * @const + * @type {ol.Color} + */ +ol.render.webgl.defaultFillStyle = [0.0, 0.0, 0.0, 1.0]; + +/** + * @const + * @type {string} + */ +ol.render.webgl.defaultLineCap = 'round'; + + +/** + * @const + * @type {Array.} + */ +ol.render.webgl.defaultLineDash = []; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.defaultLineJoin = 'round'; + + +/** + * @const + * @type {number} + */ +ol.render.webgl.defaultMiterLimit = 10; + +/** + * @const + * @type {ol.Color} + */ +ol.render.webgl.defaultStrokeStyle = [0.0, 0.0, 0.0, 1.0]; + +/** + * @const + * @type {number} + */ +ol.render.webgl.defaultLineWidth = 1; + +/** + * @enum {number} + */ +ol.render.webgl.lineStringInstruction = { + ROUND: 2, + BEGIN_LINE: 3, + END_LINE: 5, + BEGIN_LINE_CAP: 7, + END_LINE_CAP : 11, + BEVEL_FIRST: 13, + BEVEL_SECOND: 17, + MITER_BOTTOM: 19, + MITER_TOP: 23 +}; + +/** + * Calculates the orientation of a triangle based on the determinant method. + * @param {number} x1 First X coordinate. + * @param {number} y1 First Y coordinate. + * @param {number} x2 Second X coordinate. + * @param {number} y2 Second Y coordinate. + * @param {number} x3 Third X coordinate. + * @param {number} y3 Third Y coordinate. + * @return {boolean|undefined} Triangle is clockwise. + */ +ol.render.webgl.triangleIsCounterClockwise = function(x1, y1, x2, y2, x3, y3) { + var area = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1); + return (area <= ol.render.webgl.EPSILON && area >= -ol.render.webgl.EPSILON) ? + undefined : area > 0; +}; + +/** + * @const + * @type {number} + */ +ol.render.webgl.EPSILON = Number.EPSILON || 2.220446049250313e-16; diff --git a/src/ol/render/webgl/index.jsdoc b/src/ol/render/webgl/index.jsdoc new file mode 100644 index 0000000000..2ac3cda5ae --- /dev/null +++ b/src/ol/render/webgl/index.jsdoc @@ -0,0 +1,3 @@ +/** + * @namespace ol.render.webgl + */ diff --git a/src/ol/render/webgl/linestringreplay/defaultshader.glsl b/src/ol/render/webgl/linestringreplay/defaultshader.glsl new file mode 100644 index 0000000000..ce9586a133 --- /dev/null +++ b/src/ol/render/webgl/linestringreplay/defaultshader.glsl @@ -0,0 +1,161 @@ +//! NAMESPACE=ol.render.webgl.linestringreplay.defaultshader +//! CLASS=ol.render.webgl.linestringreplay.defaultshader + + +//! COMMON +varying float v_round; +varying vec2 v_roundVertex; +varying float v_halfWidth; + + +//! VERTEX +attribute vec2 a_lastPos; +attribute vec2 a_position; +attribute vec2 a_nextPos; +attribute float a_direction; + +uniform mat4 u_projectionMatrix; +uniform mat4 u_offsetScaleMatrix; +uniform mat4 u_offsetRotateMatrix; +uniform float u_lineWidth; +uniform float u_miterLimit; + +bool nearlyEquals(in float value, in float ref) { + float epsilon = 0.000000000001; + return value >= ref - epsilon && value <= ref + epsilon; +} + +void alongNormal(out vec2 offset, in vec2 nextP, in float turnDir, in float direction) { + vec2 dirVect = nextP - a_position; + vec2 normal = normalize(vec2(-turnDir * dirVect.y, turnDir * dirVect.x)); + offset = u_lineWidth / 2.0 * normal * direction; +} + +void miterUp(out vec2 offset, out float round, in bool isRound, in float direction) { + float halfWidth = u_lineWidth / 2.0; + vec2 tangent = normalize(normalize(a_nextPos - a_position) + normalize(a_position - a_lastPos)); + vec2 normal = vec2(-tangent.y, tangent.x); + vec2 dirVect = a_nextPos - a_position; + vec2 tmpNormal = normalize(vec2(-dirVect.y, dirVect.x)); + float miterLength = abs(halfWidth / dot(normal, tmpNormal)); + offset = normal * direction * miterLength; + round = 0.0; + if (isRound) { + round = 1.0; + } else if (miterLength > u_miterLimit + u_lineWidth) { + offset = halfWidth * tmpNormal * direction; + } +} + +bool miterDown(out vec2 offset, in vec4 projPos, in mat4 offsetMatrix, in float direction) { + bool degenerate = false; + vec2 tangent = normalize(normalize(a_nextPos - a_position) + normalize(a_position - a_lastPos)); + vec2 normal = vec2(-tangent.y, tangent.x); + vec2 dirVect = a_lastPos - a_position; + vec2 tmpNormal = normalize(vec2(-dirVect.y, dirVect.x)); + vec2 longOffset, shortOffset, longVertex; + vec4 shortProjVertex; + float halfWidth = u_lineWidth / 2.0; + if (length(a_nextPos - a_position) > length(a_lastPos - a_position)) { + longOffset = tmpNormal * direction * halfWidth; + shortOffset = normalize(vec2(dirVect.y, -dirVect.x)) * direction * halfWidth; + longVertex = a_nextPos; + shortProjVertex = u_projectionMatrix * vec4(a_lastPos, 0.0, 1.0); + } else { + shortOffset = tmpNormal * direction * halfWidth; + longOffset = normalize(vec2(dirVect.y, -dirVect.x)) * direction * halfWidth; + longVertex = a_lastPos; + shortProjVertex = u_projectionMatrix * vec4(a_nextPos, 0.0, 1.0); + } + //Intersection algorithm based on theory by Paul Bourke (http://paulbourke.net/geometry/pointlineplane/). + vec4 p1 = u_projectionMatrix * vec4(longVertex, 0.0, 1.0) + offsetMatrix * vec4(longOffset, 0.0, 0.0); + vec4 p2 = projPos + offsetMatrix * vec4(longOffset, 0.0, 0.0); + vec4 p3 = shortProjVertex + offsetMatrix * vec4(-shortOffset, 0.0, 0.0); + vec4 p4 = shortProjVertex + offsetMatrix * vec4(shortOffset, 0.0, 0.0); + float denom = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y); + float firstU = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denom; + float secondU = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denom; + float epsilon = 0.000000000001; + if (firstU > epsilon && firstU < 1.0 - epsilon && secondU > epsilon && secondU < 1.0 - epsilon) { + shortProjVertex.x = p1.x + firstU * (p2.x - p1.x); + shortProjVertex.y = p1.y + firstU * (p2.y - p1.y); + offset = shortProjVertex.xy; + degenerate = true; + } else { + float miterLength = abs(halfWidth / dot(normal, tmpNormal)); + offset = normal * direction * miterLength; + } + return degenerate; +} + +void squareCap(out vec2 offset, out float round, in bool isRound, in vec2 nextP, + in float turnDir, in float direction) { + round = 0.0; + vec2 dirVect = a_position - nextP; + vec2 firstNormal = normalize(dirVect); + vec2 secondNormal = vec2(turnDir * firstNormal.y * direction, -turnDir * firstNormal.x * direction); + vec2 hypotenuse = normalize(firstNormal - secondNormal); + vec2 normal = vec2(turnDir * hypotenuse.y * direction, -turnDir * hypotenuse.x * direction); + float length = sqrt(v_halfWidth * v_halfWidth * 2.0); + offset = normal * length; + if (isRound) { + round = 1.0; + } +} + +void main(void) { + bool degenerate = false; + float direction = float(sign(a_direction)); + mat4 offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix; + vec2 offset; + vec4 projPos = u_projectionMatrix * vec4(a_position, 0.0, 1.0); + bool round = nearlyEquals(mod(a_direction, 2.0), 0.0); + + v_round = 0.0; + v_halfWidth = u_lineWidth / 2.0; + v_roundVertex = projPos.xy; + + if (nearlyEquals(mod(a_direction, 3.0), 0.0) || nearlyEquals(mod(a_direction, 17.0), 0.0)) { + alongNormal(offset, a_nextPos, 1.0, direction); + } else if (nearlyEquals(mod(a_direction, 5.0), 0.0) || nearlyEquals(mod(a_direction, 13.0), 0.0)) { + alongNormal(offset, a_lastPos, -1.0, direction); + } else if (nearlyEquals(mod(a_direction, 23.0), 0.0)) { + miterUp(offset, v_round, round, direction); + } else if (nearlyEquals(mod(a_direction, 19.0), 0.0)) { + degenerate = miterDown(offset, projPos, offsetMatrix, direction); + } else if (nearlyEquals(mod(a_direction, 7.0), 0.0)) { + squareCap(offset, v_round, round, a_nextPos, 1.0, direction); + } else if (nearlyEquals(mod(a_direction, 11.0), 0.0)) { + squareCap(offset, v_round, round, a_lastPos, -1.0, direction); + } + if (!degenerate) { + vec4 offsets = offsetMatrix * vec4(offset, 0.0, 0.0); + gl_Position = projPos + offsets; + } else { + gl_Position = vec4(offset, 0.0, 1.0); + } +} + + +//! FRAGMENT + +uniform float u_opacity; +uniform vec4 u_color; +uniform vec2 u_size; +uniform float u_pixelRatio; + +void main(void) { + if (v_round > 0.0) { + vec2 windowCoords = vec2((v_roundVertex.x + 1.0) / 2.0 * u_size.x * u_pixelRatio, + (v_roundVertex.y + 1.0) / 2.0 * u_size.y * u_pixelRatio); + if (length(windowCoords - gl_FragCoord.xy) > v_halfWidth * u_pixelRatio) { + discard; + } + } + gl_FragColor = u_color; + float alpha = u_color.a * u_opacity; + if (alpha == 0.0) { + discard; + } + gl_FragColor.a = alpha; +} diff --git a/src/ol/render/webgl/linestringreplay/defaultshader.js b/src/ol/render/webgl/linestringreplay/defaultshader.js new file mode 100644 index 0000000000..195d07a49a --- /dev/null +++ b/src/ol/render/webgl/linestringreplay/defaultshader.js @@ -0,0 +1,168 @@ +// This file is automatically generated, do not edit +goog.provide('ol.render.webgl.linestringreplay.defaultshader'); + +goog.require('ol'); +goog.require('ol.webgl.Fragment'); +goog.require('ol.webgl.Vertex'); + + +/** + * @constructor + * @extends {ol.webgl.Fragment} + * @struct + */ +ol.render.webgl.linestringreplay.defaultshader.Fragment = function() { + ol.webgl.Fragment.call(this, ol.render.webgl.linestringreplay.defaultshader.Fragment.SOURCE); +}; +ol.inherits(ol.render.webgl.linestringreplay.defaultshader.Fragment, ol.webgl.Fragment); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.linestringreplay.defaultshader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying float v_round;\nvarying vec2 v_roundVertex;\nvarying float v_halfWidth;\n\n\n\nuniform float u_opacity;\nuniform vec4 u_color;\nuniform vec2 u_size;\nuniform float u_pixelRatio;\n\nvoid main(void) {\n if (v_round > 0.0) {\n vec2 windowCoords = vec2((v_roundVertex.x + 1.0) / 2.0 * u_size.x * u_pixelRatio,\n (v_roundVertex.y + 1.0) / 2.0 * u_size.y * u_pixelRatio);\n if (length(windowCoords - gl_FragCoord.xy) > v_halfWidth * u_pixelRatio) {\n discard;\n }\n }\n gl_FragColor = u_color;\n float alpha = u_color.a * u_opacity;\n if (alpha == 0.0) {\n discard;\n }\n gl_FragColor.a = alpha;\n}\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.linestringreplay.defaultshader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying float a;varying vec2 b;varying float c;uniform float m;uniform vec4 n;uniform vec2 o;uniform float p;void main(void){if(a>0.0){vec2 windowCoords=vec2((b.x+1.0)/2.0*o.x*p,(b.y+1.0)/2.0*o.y*p);if(length(windowCoords-gl_FragCoord.xy)>c*p){discard;}} gl_FragColor=n;float alpha=n.a*m;if(alpha==0.0){discard;}gl_FragColor.a=alpha;}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.linestringreplay.defaultshader.Fragment.SOURCE = ol.DEBUG ? + ol.render.webgl.linestringreplay.defaultshader.Fragment.DEBUG_SOURCE : + ol.render.webgl.linestringreplay.defaultshader.Fragment.OPTIMIZED_SOURCE; + + +ol.render.webgl.linestringreplay.defaultshader.fragment = new ol.render.webgl.linestringreplay.defaultshader.Fragment(); + + +/** + * @constructor + * @extends {ol.webgl.Vertex} + * @struct + */ +ol.render.webgl.linestringreplay.defaultshader.Vertex = function() { + ol.webgl.Vertex.call(this, ol.render.webgl.linestringreplay.defaultshader.Vertex.SOURCE); +}; +ol.inherits(ol.render.webgl.linestringreplay.defaultshader.Vertex, ol.webgl.Vertex); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.linestringreplay.defaultshader.Vertex.DEBUG_SOURCE = 'varying float v_round;\nvarying vec2 v_roundVertex;\nvarying float v_halfWidth;\n\n\nattribute vec2 a_lastPos;\nattribute vec2 a_position;\nattribute vec2 a_nextPos;\nattribute float a_direction;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\nuniform float u_lineWidth;\nuniform float u_miterLimit;\n\nbool nearlyEquals(in float value, in float ref) {\n float epsilon = 0.000000000001;\n return value >= ref - epsilon && value <= ref + epsilon;\n}\n\nvoid alongNormal(out vec2 offset, in vec2 nextP, in float turnDir, in float direction) {\n vec2 dirVect = nextP - a_position;\n vec2 normal = normalize(vec2(-turnDir * dirVect.y, turnDir * dirVect.x));\n offset = u_lineWidth / 2.0 * normal * direction;\n}\n\nvoid miterUp(out vec2 offset, out float round, in bool isRound, in float direction) {\n float halfWidth = u_lineWidth / 2.0;\n vec2 tangent = normalize(normalize(a_nextPos - a_position) + normalize(a_position - a_lastPos));\n vec2 normal = vec2(-tangent.y, tangent.x);\n vec2 dirVect = a_nextPos - a_position;\n vec2 tmpNormal = normalize(vec2(-dirVect.y, dirVect.x));\n float miterLength = abs(halfWidth / dot(normal, tmpNormal));\n offset = normal * direction * miterLength;\n round = 0.0;\n if (isRound) {\n round = 1.0;\n } else if (miterLength > u_miterLimit + u_lineWidth) {\n offset = halfWidth * tmpNormal * direction;\n }\n}\n\nbool miterDown(out vec2 offset, in vec4 projPos, in mat4 offsetMatrix, in float direction) {\n bool degenerate = false;\n vec2 tangent = normalize(normalize(a_nextPos - a_position) + normalize(a_position - a_lastPos));\n vec2 normal = vec2(-tangent.y, tangent.x);\n vec2 dirVect = a_lastPos - a_position;\n vec2 tmpNormal = normalize(vec2(-dirVect.y, dirVect.x));\n vec2 longOffset, shortOffset, longVertex;\n vec4 shortProjVertex;\n float halfWidth = u_lineWidth / 2.0;\n if (length(a_nextPos - a_position) > length(a_lastPos - a_position)) {\n longOffset = tmpNormal * direction * halfWidth;\n shortOffset = normalize(vec2(dirVect.y, -dirVect.x)) * direction * halfWidth;\n longVertex = a_nextPos;\n shortProjVertex = u_projectionMatrix * vec4(a_lastPos, 0.0, 1.0);\n } else {\n shortOffset = tmpNormal * direction * halfWidth;\n longOffset = normalize(vec2(dirVect.y, -dirVect.x)) * direction * halfWidth;\n longVertex = a_lastPos;\n shortProjVertex = u_projectionMatrix * vec4(a_nextPos, 0.0, 1.0);\n }\n //Intersection algorithm based on theory by Paul Bourke (http://paulbourke.net/geometry/pointlineplane/).\n vec4 p1 = u_projectionMatrix * vec4(longVertex, 0.0, 1.0) + offsetMatrix * vec4(longOffset, 0.0, 0.0);\n vec4 p2 = projPos + offsetMatrix * vec4(longOffset, 0.0, 0.0);\n vec4 p3 = shortProjVertex + offsetMatrix * vec4(-shortOffset, 0.0, 0.0);\n vec4 p4 = shortProjVertex + offsetMatrix * vec4(shortOffset, 0.0, 0.0);\n float denom = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);\n float firstU = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denom;\n float secondU = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denom;\n float epsilon = 0.000000000001;\n if (firstU > epsilon && firstU < 1.0 - epsilon && secondU > epsilon && secondU < 1.0 - epsilon) {\n shortProjVertex.x = p1.x + firstU * (p2.x - p1.x);\n shortProjVertex.y = p1.y + firstU * (p2.y - p1.y);\n offset = shortProjVertex.xy;\n degenerate = true;\n } else {\n float miterLength = abs(halfWidth / dot(normal, tmpNormal));\n offset = normal * direction * miterLength;\n }\n return degenerate;\n}\n\nvoid squareCap(out vec2 offset, out float round, in bool isRound, in vec2 nextP,\n in float turnDir, in float direction) {\n round = 0.0;\n vec2 dirVect = a_position - nextP;\n vec2 firstNormal = normalize(dirVect);\n vec2 secondNormal = vec2(turnDir * firstNormal.y * direction, -turnDir * firstNormal.x * direction);\n vec2 hypotenuse = normalize(firstNormal - secondNormal);\n vec2 normal = vec2(turnDir * hypotenuse.y * direction, -turnDir * hypotenuse.x * direction);\n float length = sqrt(v_halfWidth * v_halfWidth * 2.0);\n offset = normal * length;\n if (isRound) {\n round = 1.0;\n }\n}\n\nvoid main(void) {\n bool degenerate = false;\n float direction = float(sign(a_direction));\n mat4 offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n vec2 offset;\n vec4 projPos = u_projectionMatrix * vec4(a_position, 0.0, 1.0);\n bool round = nearlyEquals(mod(a_direction, 2.0), 0.0);\n\n v_round = 0.0;\n v_halfWidth = u_lineWidth / 2.0;\n v_roundVertex = projPos.xy;\n\n if (nearlyEquals(mod(a_direction, 3.0), 0.0) || nearlyEquals(mod(a_direction, 17.0), 0.0)) {\n alongNormal(offset, a_nextPos, 1.0, direction);\n } else if (nearlyEquals(mod(a_direction, 5.0), 0.0) || nearlyEquals(mod(a_direction, 13.0), 0.0)) {\n alongNormal(offset, a_lastPos, -1.0, direction);\n } else if (nearlyEquals(mod(a_direction, 23.0), 0.0)) {\n miterUp(offset, v_round, round, direction);\n } else if (nearlyEquals(mod(a_direction, 19.0), 0.0)) {\n degenerate = miterDown(offset, projPos, offsetMatrix, direction);\n } else if (nearlyEquals(mod(a_direction, 7.0), 0.0)) {\n squareCap(offset, v_round, round, a_nextPos, 1.0, direction);\n } else if (nearlyEquals(mod(a_direction, 11.0), 0.0)) {\n squareCap(offset, v_round, round, a_lastPos, -1.0, direction);\n }\n if (!degenerate) {\n vec4 offsets = offsetMatrix * vec4(offset, 0.0, 0.0);\n gl_Position = projPos + offsets;\n } else {\n gl_Position = vec4(offset, 0.0, 1.0);\n }\n}\n\n\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.linestringreplay.defaultshader.Vertex.OPTIMIZED_SOURCE = 'varying float a;varying vec2 b;varying float c;attribute vec2 d;attribute vec2 e;attribute vec2 f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;uniform float k;uniform float l;bool nearlyEquals(in float value,in float ref){float epsilon=0.000000000001;return value>=ref-epsilon&&value<=ref+epsilon;}void alongNormal(out vec2 offset,in vec2 nextP,in float turnDir,in float direction){vec2 dirVect=nextP-e;vec2 normal=normalize(vec2(-turnDir*dirVect.y,turnDir*dirVect.x));offset=k/2.0*normal*direction;}void miterUp(out vec2 offset,out float round,in bool isRound,in float direction){float halfWidth=k/2.0;vec2 tangent=normalize(normalize(f-e)+normalize(e-d));vec2 normal=vec2(-tangent.y,tangent.x);vec2 dirVect=f-e;vec2 tmpNormal=normalize(vec2(-dirVect.y,dirVect.x));float miterLength=abs(halfWidth/dot(normal,tmpNormal));offset=normal*direction*miterLength;round=0.0;if(isRound){round=1.0;}else if(miterLength>l+k){offset=halfWidth*tmpNormal*direction;}} bool miterDown(out vec2 offset,in vec4 projPos,in mat4 offsetMatrix,in float direction){bool degenerate=false;vec2 tangent=normalize(normalize(f-e)+normalize(e-d));vec2 normal=vec2(-tangent.y,tangent.x);vec2 dirVect=d-e;vec2 tmpNormal=normalize(vec2(-dirVect.y,dirVect.x));vec2 longOffset,shortOffset,longVertex;vec4 shortProjVertex;float halfWidth=k/2.0;if(length(f-e)>length(d-e)){longOffset=tmpNormal*direction*halfWidth;shortOffset=normalize(vec2(dirVect.y,-dirVect.x))*direction*halfWidth;longVertex=f;shortProjVertex=h*vec4(d,0.0,1.0);}else{shortOffset=tmpNormal*direction*halfWidth;longOffset=normalize(vec2(dirVect.y,-dirVect.x))*direction*halfWidth;longVertex=d;shortProjVertex=h*vec4(f,0.0,1.0);}vec4 p1=h*vec4(longVertex,0.0,1.0)+offsetMatrix*vec4(longOffset,0.0,0.0);vec4 p2=projPos+offsetMatrix*vec4(longOffset,0.0,0.0);vec4 p3=shortProjVertex+offsetMatrix*vec4(-shortOffset,0.0,0.0);vec4 p4=shortProjVertex+offsetMatrix*vec4(shortOffset,0.0,0.0);float denom=(p4.y-p3.y)*(p2.x-p1.x)-(p4.x-p3.x)*(p2.y-p1.y);float firstU=((p4.x-p3.x)*(p1.y-p3.y)-(p4.y-p3.y)*(p1.x-p3.x))/denom;float secondU=((p2.x-p1.x)*(p1.y-p3.y)-(p2.y-p1.y)*(p1.x-p3.x))/denom;float epsilon=0.000000000001;if(firstU>epsilon&&firstU<1.0-epsilon&&secondU>epsilon&&secondU<1.0-epsilon){shortProjVertex.x=p1.x+firstU*(p2.x-p1.x);shortProjVertex.y=p1.y+firstU*(p2.y-p1.y);offset=shortProjVertex.xy;degenerate=true;}else{float miterLength=abs(halfWidth/dot(normal,tmpNormal));offset=normal*direction*miterLength;}return degenerate;}void squareCap(out vec2 offset,out float round,in bool isRound,in vec2 nextP,in float turnDir,in float direction){round=0.0;vec2 dirVect=e-nextP;vec2 firstNormal=normalize(dirVect);vec2 secondNormal=vec2(turnDir*firstNormal.y*direction,-turnDir*firstNormal.x*direction);vec2 hypotenuse=normalize(firstNormal-secondNormal);vec2 normal=vec2(turnDir*hypotenuse.y*direction,-turnDir*hypotenuse.x*direction);float length=sqrt(c*c*2.0);offset=normal*length;if(isRound){round=1.0;}} void main(void){bool degenerate=false;float direction=float(sign(g));mat4 offsetMatrix=i*j;vec2 offset;vec4 projPos=h*vec4(e,0.0,1.0);bool round=nearlyEquals(mod(g,2.0),0.0);a=0.0;c=k/2.0;b=projPos.xy;if(nearlyEquals(mod(g,3.0),0.0)||nearlyEquals(mod(g,17.0),0.0)){alongNormal(offset,f,1.0,direction);}else if(nearlyEquals(mod(g,5.0),0.0)||nearlyEquals(mod(g,13.0),0.0)){alongNormal(offset,d,-1.0,direction);}else if(nearlyEquals(mod(g,23.0),0.0)){miterUp(offset,a,round,direction);}else if(nearlyEquals(mod(g,19.0),0.0)){degenerate=miterDown(offset,projPos,offsetMatrix,direction);}else if(nearlyEquals(mod(g,7.0),0.0)){squareCap(offset,a,round,f,1.0,direction);}else if(nearlyEquals(mod(g,11.0),0.0)){squareCap(offset,a,round,d,-1.0,direction);}if(!degenerate){vec4 offsets=offsetMatrix*vec4(offset,0.0,0.0);gl_Position=projPos+offsets;}else{gl_Position=vec4(offset,0.0,1.0);}}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.linestringreplay.defaultshader.Vertex.SOURCE = ol.DEBUG ? + ol.render.webgl.linestringreplay.defaultshader.Vertex.DEBUG_SOURCE : + ol.render.webgl.linestringreplay.defaultshader.Vertex.OPTIMIZED_SOURCE; + + +ol.render.webgl.linestringreplay.defaultshader.vertex = new ol.render.webgl.linestringreplay.defaultshader.Vertex(); + + +/** + * @constructor + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLProgram} program Program. + * @struct + */ +ol.render.webgl.linestringreplay.defaultshader.Locations = function(gl, program) { + + /** + * @type {WebGLUniformLocation} + */ + this.u_color = gl.getUniformLocation( + program, ol.DEBUG ? 'u_color' : 'n'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_lineWidth = gl.getUniformLocation( + program, ol.DEBUG ? 'u_lineWidth' : 'k'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_miterLimit = gl.getUniformLocation( + program, ol.DEBUG ? 'u_miterLimit' : 'l'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetRotateMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_offsetRotateMatrix' : 'j'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetScaleMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_offsetScaleMatrix' : 'i'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_opacity = gl.getUniformLocation( + program, ol.DEBUG ? 'u_opacity' : 'm'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_pixelRatio = gl.getUniformLocation( + program, ol.DEBUG ? 'u_pixelRatio' : 'p'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_projectionMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_projectionMatrix' : 'h'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_size = gl.getUniformLocation( + program, ol.DEBUG ? 'u_size' : 'o'); + + /** + * @type {number} + */ + this.a_direction = gl.getAttribLocation( + program, ol.DEBUG ? 'a_direction' : 'g'); + + /** + * @type {number} + */ + this.a_lastPos = gl.getAttribLocation( + program, ol.DEBUG ? 'a_lastPos' : 'd'); + + /** + * @type {number} + */ + this.a_nextPos = gl.getAttribLocation( + program, ol.DEBUG ? 'a_nextPos' : 'f'); + + /** + * @type {number} + */ + this.a_position = gl.getAttribLocation( + program, ol.DEBUG ? 'a_position' : 'e'); +}; diff --git a/src/ol/render/webgl/linestringreplay/index.js b/src/ol/render/webgl/linestringreplay/index.js new file mode 100644 index 0000000000..a54280be12 --- /dev/null +++ b/src/ol/render/webgl/linestringreplay/index.js @@ -0,0 +1,683 @@ +goog.provide('ol.render.webgl.LineStringReplay'); + +goog.require('ol'); +goog.require('ol.array'); +goog.require('ol.color'); +goog.require('ol.extent'); +goog.require('ol.geom.flat.orient'); +goog.require('ol.geom.flat.transform'); +goog.require('ol.geom.flat.topology'); +goog.require('ol.obj'); +goog.require('ol.render.webgl'); +goog.require('ol.render.webgl.Replay'); +goog.require('ol.render.webgl.linestringreplay.defaultshader'); +goog.require('ol.webgl'); +goog.require('ol.webgl.Buffer'); + + +/** + * @constructor + * @extends {ol.render.webgl.Replay} + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @struct + */ +ol.render.webgl.LineStringReplay = function(tolerance, maxExtent) { + ol.render.webgl.Replay.call(this, tolerance, maxExtent); + + /** + * @private + * @type {ol.render.webgl.linestringreplay.defaultshader.Locations} + */ + this.defaultLocations_ = null; + + /** + * @private + * @type {Array.>} + */ + this.styles_ = []; + + /** + * @private + * @type {Array.} + */ + this.styleIndices_ = []; + + /** + * @private + * @type {{strokeColor: (Array.|null), + * lineCap: (string|undefined), + * lineDash: Array., + * lineJoin: (string|undefined), + * lineWidth: (number|undefined), + * miterLimit: (number|undefined), + * changed: boolean}|null} + */ + this.state_ = { + strokeColor: null, + lineCap: undefined, + lineDash: null, + lineJoin: undefined, + lineWidth: undefined, + miterLimit: undefined, + changed: false + }; + +}; +ol.inherits(ol.render.webgl.LineStringReplay, ol.render.webgl.Replay); + + +/** + * Draw one segment. + * @private + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + */ +ol.render.webgl.LineStringReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) { + + var i, ii; + var numVertices = this.vertices.length; + var numIndices = this.indices.length; + //To save a vertex, the direction of a point is a product of the sign (1 or -1), a prime from + //ol.render.webgl.lineStringInstruction, and a rounding factor (1 or 2). If the product is even, + //we round it. If it is odd, we don't. + var lineJoin = this.state_.lineJoin === 'bevel' ? 0 : + this.state_.lineJoin === 'miter' ? 1 : 2; + var lineCap = this.state_.lineCap === 'butt' ? 0 : + this.state_.lineCap === 'square' ? 1 : 2; + var closed = ol.geom.flat.topology.lineStringIsClosed(flatCoordinates, offset, end, stride); + var startCoords, sign, n; + var lastIndex = numIndices; + var lastSign = 1; + //We need the adjacent vertices to define normals in joins. p0 = last, p1 = current, p2 = next. + var p0, p1, p2; + + for (i = offset, ii = end; i < ii; i += stride) { + + n = numVertices / 7; + + p0 = p1; + p1 = p2 || [flatCoordinates[i], flatCoordinates[i + 1]]; + //First vertex. + if (i === offset) { + p2 = [flatCoordinates[i + stride], flatCoordinates[i + stride + 1]]; + if (end - offset === stride * 2 && ol.array.equals(p1, p2)) { + break; + } + if (closed) { + //A closed line! Complete the circle. + p0 = [flatCoordinates[end - stride * 2], + flatCoordinates[end - stride * 2 + 1]]; + + startCoords = p2; + } else { + //Add the first two/four vertices. + + if (lineCap) { + numVertices = this.addVertices_([0, 0], p1, p2, + lastSign * ol.render.webgl.lineStringInstruction.BEGIN_LINE_CAP * lineCap, numVertices); + + numVertices = this.addVertices_([0, 0], p1, p2, + -lastSign * ol.render.webgl.lineStringInstruction.BEGIN_LINE_CAP * lineCap, numVertices); + + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 3; + this.indices[numIndices++] = n + 2; + + } + + numVertices = this.addVertices_([0, 0], p1, p2, + lastSign * ol.render.webgl.lineStringInstruction.BEGIN_LINE * (lineCap || 1), numVertices); + + numVertices = this.addVertices_([0, 0], p1, p2, + -lastSign * ol.render.webgl.lineStringInstruction.BEGIN_LINE * (lineCap || 1), numVertices); + + lastIndex = numVertices / 7 - 1; + + continue; + } + } else if (i === end - stride) { + //Last vertex. + if (closed) { + //Same as the first vertex. + p2 = startCoords; + break; + } else { + //For the compiler not to complain. This will never be [0, 0]. + ol.DEBUG && console.assert(p0, 'p0 should be defined'); + p0 = p0 || [0, 0]; + + numVertices = this.addVertices_(p0, p1, [0, 0], + lastSign * ol.render.webgl.lineStringInstruction.END_LINE * (lineCap || 1), numVertices); + + numVertices = this.addVertices_(p0, p1, [0, 0], + -lastSign * ol.render.webgl.lineStringInstruction.END_LINE * (lineCap || 1), numVertices); + + this.indices[numIndices++] = n; + this.indices[numIndices++] = lastIndex - 1; + this.indices[numIndices++] = lastIndex; + + this.indices[numIndices++] = lastIndex; + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n; + + if (lineCap) { + numVertices = this.addVertices_(p0, p1, [0, 0], + lastSign * ol.render.webgl.lineStringInstruction.END_LINE_CAP * lineCap, numVertices); + + numVertices = this.addVertices_(p0, p1, [0, 0], + -lastSign * ol.render.webgl.lineStringInstruction.END_LINE_CAP * lineCap, numVertices); + + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 1; + + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 3; + this.indices[numIndices++] = n + 2; + + } + + break; + } + } else { + p2 = [flatCoordinates[i + stride], flatCoordinates[i + stride + 1]]; + } + + // We group CW and straight lines, thus the not so inituitive CCW checking function. + sign = ol.render.webgl.triangleIsCounterClockwise(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]) + ? -1 : 1; + + numVertices = this.addVertices_(p0, p1, p2, + sign * ol.render.webgl.lineStringInstruction.BEVEL_FIRST * (lineJoin || 1), numVertices); + + numVertices = this.addVertices_(p0, p1, p2, + sign * ol.render.webgl.lineStringInstruction.BEVEL_SECOND * (lineJoin || 1), numVertices); + + numVertices = this.addVertices_(p0, p1, p2, + -sign * ol.render.webgl.lineStringInstruction.MITER_BOTTOM * (lineJoin || 1), numVertices); + + if (i > offset) { + this.indices[numIndices++] = n; + this.indices[numIndices++] = lastIndex - 1; + this.indices[numIndices++] = lastIndex; + + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n; + this.indices[numIndices++] = lastSign * sign > 0 ? lastIndex : lastIndex - 1; + } + + this.indices[numIndices++] = n; + this.indices[numIndices++] = n + 2; + this.indices[numIndices++] = n + 1; + + lastIndex = n + 2; + lastSign = sign; + + //Add miter + if (lineJoin) { + numVertices = this.addVertices_(p0, p1, p2, + sign * ol.render.webgl.lineStringInstruction.MITER_TOP * lineJoin, numVertices); + + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n + 3; + this.indices[numIndices++] = n; + } + } + + if (closed) { + //Link the last triangle/rhombus to the first one. + //n will never be numVertices / 7 here. However, the compiler complains otherwise. + ol.DEBUG && console.assert(n, 'n should be defined'); + n = n || numVertices / 7; + sign = ol.geom.flat.orient.linearRingIsClockwise([p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]], 0, 6, 2) + ? 1 : -1; + + numVertices = this.addVertices_(p0, p1, p2, + sign * ol.render.webgl.lineStringInstruction.BEVEL_FIRST * (lineJoin || 1), numVertices); + + numVertices = this.addVertices_(p0, p1, p2, + -sign * ol.render.webgl.lineStringInstruction.MITER_BOTTOM * (lineJoin || 1), numVertices); + + this.indices[numIndices++] = n; + this.indices[numIndices++] = lastIndex - 1; + this.indices[numIndices++] = lastIndex; + + this.indices[numIndices++] = n + 1; + this.indices[numIndices++] = n; + this.indices[numIndices++] = lastSign * sign > 0 ? lastIndex : lastIndex - 1; + } +}; + +/** + * @param {Array.} p0 Last coordinates. + * @param {Array.} p1 Current coordinates. + * @param {Array.} p2 Next coordinates. + * @param {number} product Sign, instruction, and rounding product. + * @param {number} numVertices Vertex counter. + * @return {number} Vertex counter. + * @private + */ +ol.render.webgl.LineStringReplay.prototype.addVertices_ = function(p0, p1, p2, product, numVertices) { + this.vertices[numVertices++] = p0[0]; + this.vertices[numVertices++] = p0[1]; + this.vertices[numVertices++] = p1[0]; + this.vertices[numVertices++] = p1[1]; + this.vertices[numVertices++] = p2[0]; + this.vertices[numVertices++] = p2[1]; + this.vertices[numVertices++] = product; + + return numVertices; +}; + +/** + * Check if the linestring can be drawn (i. e. valid). + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @return {boolean} The linestring can be drawn. + * @private + */ +ol.render.webgl.LineStringReplay.prototype.isValid_ = function(flatCoordinates, offset, end, stride) { + var range = end - offset; + if (range < stride * 2) { + return false; + } else if (range === stride * 2) { + var firstP = [flatCoordinates[offset], flatCoordinates[offset + 1]]; + var lastP = [flatCoordinates[offset + stride], flatCoordinates[offset + stride + 1]]; + return !ol.array.equals(firstP, lastP); + } + + return true; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.LineStringReplay.prototype.drawLineString = function(lineStringGeometry, feature) { + var flatCoordinates = lineStringGeometry.getFlatCoordinates(); + var stride = lineStringGeometry.getStride(); + if (this.isValid_(flatCoordinates, 0, flatCoordinates.length, stride)) { + flatCoordinates = ol.geom.flat.transform.translate(flatCoordinates, 0, flatCoordinates.length, + stride, -this.origin[0], -this.origin[1]); + if (this.state_.changed) { + this.styleIndices_.push(this.indices.length); + this.state_.changed = false; + } + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.LineStringReplay.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) { + var indexCount = this.indices.length; + var lineStringGeometries = multiLineStringGeometry.getLineStrings(); + var i, ii; + for (i = 0, ii = lineStringGeometries.length; i < ii; ++i) { + var flatCoordinates = lineStringGeometries[i].getFlatCoordinates(); + var stride = lineStringGeometries[i].getStride(); + if (this.isValid_(flatCoordinates, 0, flatCoordinates.length, stride)) { + flatCoordinates = ol.geom.flat.transform.translate(flatCoordinates, 0, flatCoordinates.length, + stride, -this.origin[0], -this.origin[1]); + this.drawCoordinates_( + flatCoordinates, 0, flatCoordinates.length, stride); + } + } + if (this.indices.length > indexCount) { + this.startIndices.push(indexCount); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(indexCount); + this.state_.changed = false; + } + } +}; + + +/** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {Array.>} holeFlatCoordinates Hole flat coordinates. + * @param {number} stride Stride. + */ +ol.render.webgl.LineStringReplay.prototype.drawPolygonCoordinates = function( + flatCoordinates, holeFlatCoordinates, stride) { + if (!ol.geom.flat.topology.lineStringIsClosed(flatCoordinates, 0, + flatCoordinates.length, stride)) { + flatCoordinates.push(flatCoordinates[0]); + flatCoordinates.push(flatCoordinates[1]); + } + this.drawCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride); + if (holeFlatCoordinates.length) { + var i, ii; + for (i = 0, ii = holeFlatCoordinates.length; i < ii; ++i) { + if (!ol.geom.flat.topology.lineStringIsClosed(holeFlatCoordinates[i], 0, + holeFlatCoordinates[i].length, stride)) { + holeFlatCoordinates[i].push(holeFlatCoordinates[i][0]); + holeFlatCoordinates[i].push(holeFlatCoordinates[i][1]); + } + this.drawCoordinates_(holeFlatCoordinates[i], 0, + holeFlatCoordinates[i].length, stride); + } + } +}; + + +/** + * @param {ol.Feature|ol.render.Feature} feature Feature. + * @param {number=} opt_index Index count. + */ +ol.render.webgl.LineStringReplay.prototype.setPolygonStyle = function(feature, opt_index) { + var index = opt_index === undefined ? this.indices.length : opt_index; + this.startIndices.push(index); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(index); + this.state_.changed = false; + } +}; + + +/** + * @return {number} Current index. + */ +ol.render.webgl.LineStringReplay.prototype.getCurrentIndex = function() { + return this.indices.length; +}; + + +/** + * @inheritDoc + **/ +ol.render.webgl.LineStringReplay.prototype.finish = function(context) { + // 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); + + this.startIndices.push(this.indices.length); + + //Clean up, if there is nothing to draw + if (this.styleIndices_.length === 0 && this.styles_.length > 0) { + this.styles_ = []; + } + + this.vertices = null; + this.indices = null; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.LineStringReplay.prototype.getDeleteResourcesFunction = function(context) { + // We only delete our stuff here. The shaders and the program may + // be used by other LineStringReplay instances (for other layers). And + // they will be deleted when disposing of the ol.webgl.Context + // object. + ol.DEBUG && console.assert(this.verticesBuffer, 'verticesBuffer must not be null'); + var verticesBuffer = this.verticesBuffer; + var indicesBuffer = this.indicesBuffer; + return function() { + context.deleteBuffer(verticesBuffer); + context.deleteBuffer(indicesBuffer); + }; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.LineStringReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { + // get the program + var fragmentShader, vertexShader; + fragmentShader = ol.render.webgl.linestringreplay.defaultshader.fragment; + vertexShader = ol.render.webgl.linestringreplay.defaultshader.vertex; + var program = context.getProgram(fragmentShader, vertexShader); + + // get the locations + var locations; + if (!this.defaultLocations_) { + locations = + new ol.render.webgl.linestringreplay.defaultshader.Locations(gl, program); + this.defaultLocations_ = locations; + } else { + locations = this.defaultLocations_; + } + + context.useProgram(program); + + // enable the vertex attrib arrays + gl.enableVertexAttribArray(locations.a_lastPos); + gl.vertexAttribPointer(locations.a_lastPos, 2, ol.webgl.FLOAT, + false, 28, 0); + + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, ol.webgl.FLOAT, + false, 28, 8); + + gl.enableVertexAttribArray(locations.a_nextPos); + gl.vertexAttribPointer(locations.a_nextPos, 2, ol.webgl.FLOAT, + false, 28, 16); + + gl.enableVertexAttribArray(locations.a_direction); + gl.vertexAttribPointer(locations.a_direction, 1, ol.webgl.FLOAT, + false, 28, 24); + + // Enable renderer specific uniforms. + gl.uniform2fv(locations.u_size, size); + gl.uniform1f(locations.u_pixelRatio, pixelRatio); + + return locations; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.LineStringReplay.prototype.shutDownProgram = function(gl, locations) { + gl.disableVertexAttribArray(locations.a_lastPos); + gl.disableVertexAttribArray(locations.a_position); + gl.disableVertexAttribArray(locations.a_nextPos); + gl.disableVertexAttribArray(locations.a_direction); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.LineStringReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { + //Save GL parameters. + var tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC)); + var tmpDepthMask = /** @type {boolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK)); + + if (!hitDetection) { + gl.enable(gl.DEPTH_TEST); + gl.depthMask(true); + gl.depthFunc(gl.NOTEQUAL); + } + + if (!ol.obj.isEmpty(skippedFeaturesHash)) { + this.drawReplaySkipping_(gl, context, skippedFeaturesHash); + } else { + ol.DEBUG && console.assert(this.styles_.length === this.styleIndices_.length, + 'number of styles and styleIndices match'); + + //Draw by style groups to minimize drawElements() calls. + var i, start, end, nextStyle; + end = this.startIndices[this.startIndices.length - 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + start = this.styleIndices_[i]; + nextStyle = this.styles_[i]; + this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]); + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + end = start; + } + } + if (!hitDetection) { + gl.disable(gl.DEPTH_TEST); + gl.clear(gl.DEPTH_BUFFER_BIT); + //Restore GL parameters. + gl.depthMask(tmpDepthMask); + gl.depthFunc(tmpDepthFunc); + } +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {Object} skippedFeaturesHash Ids of features to skip. + */ +ol.render.webgl.LineStringReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) { + ol.DEBUG && console.assert(this.startIndices.length - 1 === this.startIndicesFeature.length, + 'number of startIndices and startIndicesFeature match'); + + var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; + featureIndex = this.startIndices.length - 2; + end = start = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]); + groupStart = this.styleIndices_[i]; + + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + featureStart = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = ol.getUid(feature).toString(); + + if (skippedFeaturesHash[featureUid]) { + if (start !== end) { + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + end = featureStart; + } + featureIndex--; + start = featureStart; + } + if (start !== end) { + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + start = end = groupStart; + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.LineStringReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, + featureCallback, opt_hitExtent) { + ol.DEBUG && console.assert(this.styles_.length === this.styleIndices_.length, + 'number of styles and styleIndices match'); + ol.DEBUG && console.assert(this.startIndices.length - 1 === this.startIndicesFeature.length, + 'number of startIndices and startIndicesFeature match'); + + var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; + featureIndex = this.startIndices.length - 2; + end = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]); + groupStart = this.styleIndices_[i]; + + 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; + } + + } + featureIndex--; + end = start; + } + } + return undefined; +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {Array.} color Color. + * @param {number} lineWidth Line width. + * @param {number} miterLimit Miter limit. + */ +ol.render.webgl.LineStringReplay.prototype.setStrokeStyle_ = function(gl, color, lineWidth, miterLimit) { + gl.uniform4fv(this.defaultLocations_.u_color, color); + gl.uniform1f(this.defaultLocations_.u_lineWidth, lineWidth); + gl.uniform1f(this.defaultLocations_.u_miterLimit, miterLimit); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.LineStringReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { + ol.DEBUG && console.assert(this.state_, 'this.state_ should not be null'); + var strokeStyleLineCap = strokeStyle.getLineCap(); + this.state_.lineCap = strokeStyleLineCap !== undefined ? + strokeStyleLineCap : ol.render.webgl.defaultLineCap; + var strokeStyleLineDash = strokeStyle.getLineDash(); + this.state_.lineDash = strokeStyleLineDash ? + strokeStyleLineDash : ol.render.webgl.defaultLineDash; + var strokeStyleLineJoin = strokeStyle.getLineJoin(); + this.state_.lineJoin = strokeStyleLineJoin !== undefined ? + strokeStyleLineJoin : ol.render.webgl.defaultLineJoin; + var strokeStyleColor = strokeStyle.getColor(); + if (!(strokeStyleColor instanceof CanvasGradient) && + !(strokeStyleColor instanceof CanvasPattern)) { + strokeStyleColor = ol.color.asArray(strokeStyleColor).map(function(c, i) { + return i != 3 ? c / 255 : c; + }) || ol.render.webgl.defaultStrokeStyle; + } else { + strokeStyleColor = ol.render.webgl.defaultStrokeStyle; + } + var strokeStyleWidth = strokeStyle.getWidth(); + strokeStyleWidth = strokeStyleWidth !== undefined ? + strokeStyleWidth : ol.render.webgl.defaultLineWidth; + var strokeStyleMiterLimit = strokeStyle.getMiterLimit(); + strokeStyleMiterLimit = strokeStyleMiterLimit !== undefined ? + strokeStyleMiterLimit : ol.render.webgl.defaultMiterLimit; + if (!this.state_.strokeColor || !ol.array.equals(this.state_.strokeColor, strokeStyleColor) || + this.state_.lineWidth !== strokeStyleWidth || this.state_.miterLimit !== strokeStyleMiterLimit) { + this.state_.changed = true; + this.state_.strokeColor = strokeStyleColor; + this.state_.lineWidth = strokeStyleWidth; + this.state_.miterLimit = strokeStyleMiterLimit; + this.styles_.push([strokeStyleColor, strokeStyleWidth, strokeStyleMiterLimit]); + } +}; diff --git a/src/ol/render/webgl/polygonreplay/defaultshader.glsl b/src/ol/render/webgl/polygonreplay/defaultshader.glsl new file mode 100644 index 0000000000..5810adb17b --- /dev/null +++ b/src/ol/render/webgl/polygonreplay/defaultshader.glsl @@ -0,0 +1,32 @@ +//! NAMESPACE=ol.render.webgl.polygonreplay.defaultshader +//! CLASS=ol.render.webgl.polygonreplay.defaultshader + + +//! COMMON + + +//! VERTEX +attribute vec2 a_position; + +uniform mat4 u_projectionMatrix; +uniform mat4 u_offsetScaleMatrix; +uniform mat4 u_offsetRotateMatrix; + +void main(void) { + gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0); +} + + +//! FRAGMENT + +uniform vec4 u_color; +uniform float u_opacity; + +void main(void) { + gl_FragColor = u_color; + float alpha = u_color.a * u_opacity; + if (alpha == 0.0) { + discard; + } + gl_FragColor.a = alpha; +} diff --git a/src/ol/render/webgl/polygonreplay/defaultshader.js b/src/ol/render/webgl/polygonreplay/defaultshader.js new file mode 100644 index 0000000000..4298d2e83e --- /dev/null +++ b/src/ol/render/webgl/polygonreplay/defaultshader.js @@ -0,0 +1,126 @@ +// This file is automatically generated, do not edit +goog.provide('ol.render.webgl.polygonreplay.defaultshader'); + +goog.require('ol'); +goog.require('ol.webgl.Fragment'); +goog.require('ol.webgl.Vertex'); + + +/** + * @constructor + * @extends {ol.webgl.Fragment} + * @struct + */ +ol.render.webgl.polygonreplay.defaultshader.Fragment = function() { + ol.webgl.Fragment.call(this, ol.render.webgl.polygonreplay.defaultshader.Fragment.SOURCE); +}; +ol.inherits(ol.render.webgl.polygonreplay.defaultshader.Fragment, ol.webgl.Fragment); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.polygonreplay.defaultshader.Fragment.DEBUG_SOURCE = 'precision mediump float;\n\n\n\nuniform vec4 u_color;\nuniform float u_opacity;\n\nvoid main(void) {\n gl_FragColor = u_color;\n float alpha = u_color.a * u_opacity;\n if (alpha == 0.0) {\n discard;\n }\n gl_FragColor.a = alpha;\n}\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.polygonreplay.defaultshader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;uniform vec4 e;uniform float f;void main(void){gl_FragColor=e;float alpha=e.a*f;if(alpha==0.0){discard;}gl_FragColor.a=alpha;}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.polygonreplay.defaultshader.Fragment.SOURCE = ol.DEBUG ? + ol.render.webgl.polygonreplay.defaultshader.Fragment.DEBUG_SOURCE : + ol.render.webgl.polygonreplay.defaultshader.Fragment.OPTIMIZED_SOURCE; + + +ol.render.webgl.polygonreplay.defaultshader.fragment = new ol.render.webgl.polygonreplay.defaultshader.Fragment(); + + +/** + * @constructor + * @extends {ol.webgl.Vertex} + * @struct + */ +ol.render.webgl.polygonreplay.defaultshader.Vertex = function() { + ol.webgl.Vertex.call(this, ol.render.webgl.polygonreplay.defaultshader.Vertex.SOURCE); +}; +ol.inherits(ol.render.webgl.polygonreplay.defaultshader.Vertex, ol.webgl.Vertex); + + +/** + * @const + * @type {string} + */ +ol.render.webgl.polygonreplay.defaultshader.Vertex.DEBUG_SOURCE = '\n\nattribute vec2 a_position;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0);\n}\n\n\n'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.polygonreplay.defaultshader.Vertex.OPTIMIZED_SOURCE = 'attribute vec2 a;uniform mat4 b;uniform mat4 c;uniform mat4 d;void main(void){gl_Position=b*vec4(a,0.0,1.0);}'; + + +/** + * @const + * @type {string} + */ +ol.render.webgl.polygonreplay.defaultshader.Vertex.SOURCE = ol.DEBUG ? + ol.render.webgl.polygonreplay.defaultshader.Vertex.DEBUG_SOURCE : + ol.render.webgl.polygonreplay.defaultshader.Vertex.OPTIMIZED_SOURCE; + + +ol.render.webgl.polygonreplay.defaultshader.vertex = new ol.render.webgl.polygonreplay.defaultshader.Vertex(); + + +/** + * @constructor + * @param {WebGLRenderingContext} gl GL. + * @param {WebGLProgram} program Program. + * @struct + */ +ol.render.webgl.polygonreplay.defaultshader.Locations = function(gl, program) { + + /** + * @type {WebGLUniformLocation} + */ + this.u_color = gl.getUniformLocation( + program, ol.DEBUG ? 'u_color' : 'e'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetRotateMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_offsetRotateMatrix' : 'd'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_offsetScaleMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_offsetScaleMatrix' : 'c'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_opacity = gl.getUniformLocation( + program, ol.DEBUG ? 'u_opacity' : 'f'); + + /** + * @type {WebGLUniformLocation} + */ + this.u_projectionMatrix = gl.getUniformLocation( + program, ol.DEBUG ? 'u_projectionMatrix' : 'b'); + + /** + * @type {number} + */ + this.a_position = gl.getAttribLocation( + program, ol.DEBUG ? 'a_position' : 'a'); +}; diff --git a/src/ol/render/webgl/polygonreplay/index.js b/src/ol/render/webgl/polygonreplay/index.js new file mode 100644 index 0000000000..79d8042b7b --- /dev/null +++ b/src/ol/render/webgl/polygonreplay/index.js @@ -0,0 +1,1040 @@ +goog.provide('ol.render.webgl.PolygonReplay'); + +goog.require('ol'); +goog.require('ol.array'); +goog.require('ol.color'); +goog.require('ol.extent'); +goog.require('ol.obj'); +goog.require('ol.geom.flat.contains'); +goog.require('ol.geom.flat.orient'); +goog.require('ol.geom.flat.transform'); +goog.require('ol.render.webgl.polygonreplay.defaultshader'); +goog.require('ol.render.webgl.LineStringReplay'); +goog.require('ol.render.webgl.Replay'); +goog.require('ol.render.webgl'); +goog.require('ol.style.Stroke'); +goog.require('ol.structs.LinkedList'); +goog.require('ol.structs.RBush'); +goog.require('ol.webgl'); +goog.require('ol.webgl.Buffer'); + + +/** + * @constructor + * @extends {ol.render.webgl.Replay} + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @struct + */ +ol.render.webgl.PolygonReplay = function(tolerance, maxExtent) { + ol.render.webgl.Replay.call(this, tolerance, maxExtent); + + this.lineStringReplay = new ol.render.webgl.LineStringReplay( + tolerance, maxExtent); + + /** + * @private + * @type {ol.render.webgl.polygonreplay.defaultshader.Locations} + */ + this.defaultLocations_ = null; + + /** + * @private + * @type {Array.>} + */ + this.styles_ = []; + + /** + * @private + * @type {Array.} + */ + this.styleIndices_ = []; + + /** + * @private + * @type {{fillColor: (Array.|null), + * changed: boolean}|null} + */ + this.state_ = { + fillColor: null, + changed: false + }; + +}; +ol.inherits(ol.render.webgl.PolygonReplay, ol.render.webgl.Replay); + + +/** + * Draw one polygon. + * @param {Array.} flatCoordinates Flat coordinates. + * @param {Array.>} holeFlatCoordinates Hole flat coordinates. + * @param {number} stride Stride. + * @private + */ +ol.render.webgl.PolygonReplay.prototype.drawCoordinates_ = function( + flatCoordinates, holeFlatCoordinates, stride) { + // Triangulate the polygon + var outerRing = new ol.structs.LinkedList(); + var rtree = new ol.structs.RBush(); + // Initialize the outer ring + var maxX = this.processFlatCoordinates_(flatCoordinates, stride, outerRing, rtree, true); + + // Eliminate holes, if there are any + if (holeFlatCoordinates.length) { + var i, ii; + var holeLists = []; + for (i = 0, ii = holeFlatCoordinates.length; i < ii; ++i) { + var holeList = { + list: new ol.structs.LinkedList(), + maxX: undefined + }; + holeLists.push(holeList); + holeList.maxX = this.processFlatCoordinates_(holeFlatCoordinates[i], + stride, holeList.list, rtree, false); + } + holeLists.sort(function(a, b) { + return b.maxX - a.maxX; + }); + for (i = 0; i < holeLists.length; ++i) { + this.bridgeHole_(holeLists[i].list, holeLists[i].maxX, outerRing, maxX, rtree); + } + } + this.classifyPoints_(outerRing, rtree, false); + this.triangulate_(outerRing, rtree); +}; + + +/** + * Inserts flat coordinates in a linked list and adds them to the vertex buffer. + * @private + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} stride Stride. + * @param {ol.structs.LinkedList} list Linked list. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + * @param {boolean} clockwise Coordinate order should be clockwise. + * @return {number} Maximum X value. + */ +ol.render.webgl.PolygonReplay.prototype.processFlatCoordinates_ = function( + flatCoordinates, stride, list, rtree, clockwise) { + var isClockwise = ol.geom.flat.orient.linearRingIsClockwise(flatCoordinates, + 0, flatCoordinates.length, stride); + var i, ii, maxX; + var n = this.vertices.length / 2; + /** @type {ol.WebglPolygonVertex} */ + var start; + /** @type {ol.WebglPolygonVertex} */ + var p0; + /** @type {ol.WebglPolygonVertex} */ + var p1; + var extents = []; + var segments = []; + if (clockwise === isClockwise) { + start = this.createPoint_(flatCoordinates[0], flatCoordinates[1], n++); + p0 = start; + maxX = flatCoordinates[0]; + for (i = stride, ii = flatCoordinates.length; i < ii; i += stride) { + p1 = this.createPoint_(flatCoordinates[i], flatCoordinates[i + 1], n++); + segments.push(this.insertItem_(p0, p1, list)); + extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), + Math.max(p0.y, p1.y)]); + maxX = flatCoordinates[i] > maxX ? flatCoordinates[i] : maxX; + p0 = p1; + } + segments.push(this.insertItem_(p1, start, list)); + extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), + Math.max(p0.y, p1.y)]); + } else { + var end = flatCoordinates.length - stride; + start = this.createPoint_(flatCoordinates[end], flatCoordinates[end + 1], n++); + p0 = start; + maxX = flatCoordinates[end]; + for (i = end - stride, ii = 0; i >= ii; i -= stride) { + p1 = this.createPoint_(flatCoordinates[i], flatCoordinates[i + 1], n++); + segments.push(this.insertItem_(p0, p1, list)); + extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), + Math.max(p0.y, p1.y)]); + maxX = flatCoordinates[i] > maxX ? flatCoordinates[i] : maxX; + p0 = p1; + } + segments.push(this.insertItem_(p1, start, list)); + extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), + Math.max(p0.y, p1.y)]); + } + rtree.load(extents, segments); + + return maxX; +}; + + +/** + * Classifies the points of a polygon list as convex, reflex. Removes collinear vertices. + * @private + * @param {ol.structs.LinkedList} list Polygon ring. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + * @param {boolean} ccw The orientation of the polygon is counter-clockwise. + * @return {boolean} There were reclassified points. + */ +ol.render.webgl.PolygonReplay.prototype.classifyPoints_ = function(list, rtree, ccw) { + var start = list.firstItem(); + var s0 = start; + var s1 = list.nextItem(); + var pointsReclassified = false; + do { + var reflex = ccw ? ol.render.webgl.triangleIsCounterClockwise(s1.p1.x, + s1.p1.y, s0.p1.x, s0.p1.y, s0.p0.x, s0.p0.y) : + ol.render.webgl.triangleIsCounterClockwise(s0.p0.x, s0.p0.y, s0.p1.x, + s0.p1.y, s1.p1.x, s1.p1.y); + if (reflex === undefined) { + this.removeItem_(s0, s1, list, rtree); + pointsReclassified = true; + if (s1 === start) { + start = list.getNextItem(); + } + s1 = s0; + list.prevItem(); + } else if (s0.p1.reflex !== reflex) { + s0.p1.reflex = reflex; + pointsReclassified = true; + } + s0 = s1; + s1 = list.nextItem(); + } while (s0 !== start); + return pointsReclassified; +}; + + +/** + * @private + * @param {ol.structs.LinkedList} hole Linked list of the hole. + * @param {number} holeMaxX Maximum X value of the hole. + * @param {ol.structs.LinkedList} list Linked list of the polygon. + * @param {number} listMaxX Maximum X value of the polygon. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + */ +ol.render.webgl.PolygonReplay.prototype.bridgeHole_ = function(hole, holeMaxX, + list, listMaxX, rtree) { + this.classifyPoints_(hole, rtree, true); + var seg = hole.firstItem(); + while (seg.p1.x !== holeMaxX) { + seg = hole.nextItem(); + } + + var p1 = seg.p1; + /** @type {ol.WebglPolygonVertex} */ + var p2 = {x: listMaxX, y: p1.y, i: -1}; + var minDist = Infinity; + var i, ii, bestPoint; + /** @type {ol.WebglPolygonVertex} */ + var p5; + + var intersectingSegments = this.getIntersections_({p0: p1, p1: p2}, rtree, true); + for (i = 0, ii = intersectingSegments.length; i < ii; ++i) { + var currSeg = intersectingSegments[i]; + if (currSeg.p0.reflex === undefined) { + var intersection = this.calculateIntersection_(p1, p2, currSeg.p0, + currSeg.p1, true); + var dist = Math.abs(p1.x - intersection[0]); + if (dist < minDist) { + minDist = dist; + p5 = {x: intersection[0], y: intersection[1], i: -1}; + seg = currSeg; + } + } + } + if (minDist === Infinity) { + return; + } + bestPoint = seg.p1; + + if (minDist > 0) { + var pointsInTriangle = this.getPointsInTriangle_(p1, p5, seg.p1, rtree); + if (pointsInTriangle.length) { + var theta = Infinity; + for (i = 0, ii = pointsInTriangle.length; i < ii; ++i) { + var currPoint = pointsInTriangle[i]; + var currTheta = Math.atan2(p1.y - currPoint.y, p2.x - currPoint.x); + if (currTheta < theta || (currTheta === theta && currPoint.x < bestPoint.x)) { + theta = currTheta; + bestPoint = currPoint; + } + } + } + } + + seg = list.firstItem(); + while (seg.p1 !== bestPoint) { + seg = list.nextItem(); + } + + //We clone the bridge points as they can have different convexity. + var p0Bridge = {x: p1.x, y: p1.y, i: p1.i, reflex: undefined}; + var p1Bridge = {x: seg.p1.x, y: seg.p1.y, i: seg.p1.i, reflex: undefined}; + + hole.getNextItem().p0 = p0Bridge; + this.insertItem_(p1, seg.p1, hole, rtree); + this.insertItem_(p1Bridge, p0Bridge, hole, rtree); + seg.p1 = p1Bridge; + hole.setFirstItem(); + list.concat(hole); +}; + + +/** + * @private + * @param {ol.structs.LinkedList} list Linked list of the polygon. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + */ +ol.render.webgl.PolygonReplay.prototype.triangulate_ = function(list, rtree) { + var ccw = false; + var simple = this.isSimple_(list, rtree); + + // Start clipping ears + while (list.getLength() > 3) { + if (simple) { + if (!this.clipEars_(list, rtree, simple, ccw)) { + if (!this.classifyPoints_(list, rtree, ccw)) { + // Due to the behavior of OL's PIP algorithm, the ear clipping cannot + // introduce touching segments. However, the original data may have some. + if (!this.resolveLocalSelfIntersections_(list, rtree, true)) { + // Something went wrong. + ol.DEBUG && console.assert(false, 'Unexpected simple polygon geometry'); + break; + } + } + } + } else { + if (!this.clipEars_(list, rtree, simple, ccw)) { + // We ran out of ears, try to reclassify. + if (!this.classifyPoints_(list, rtree, ccw)) { + // We have a bad polygon, try to resolve local self-intersections. + if (!this.resolveLocalSelfIntersections_(list, rtree)) { + simple = this.isSimple_(list, rtree); + if (!simple) { + // We have a really bad polygon, try more time consuming methods. + this.splitPolygon_(list, rtree); + break; + } else { + ccw = !this.isClockwise_(list); + this.classifyPoints_(list, rtree, ccw); + } + } + } + } + } + } + if (list.getLength() === 3) { + var numIndices = this.indices.length; + this.indices[numIndices++] = list.getPrevItem().p0.i; + this.indices[numIndices++] = list.getCurrItem().p0.i; + this.indices[numIndices++] = list.getNextItem().p0.i; + } +}; + + +/** + * @private + * @param {ol.structs.LinkedList} list Linked list of the polygon. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + * @param {boolean} simple The polygon is simple. + * @param {boolean} ccw Orientation of the polygon is counter-clockwise. + * @return {boolean} There were processed ears. + */ +ol.render.webgl.PolygonReplay.prototype.clipEars_ = function(list, rtree, simple, ccw) { + var numIndices = this.indices.length; + var start = list.firstItem(); + var s0 = list.getPrevItem(); + var s1 = start; + var s2 = list.nextItem(); + var s3 = list.getNextItem(); + var p0, p1, p2; + var processedEars = false; + do { + p0 = s1.p0; + p1 = s1.p1; + p2 = s2.p1; + if (p1.reflex === false) { + // We might have a valid ear + var diagonalIsInside = ccw ? this.diagonalIsInside_(s3.p1, p2, p1, p0, + s0.p0) : this.diagonalIsInside_(s0.p0, p0, p1, p2, s3.p1); + if ((simple || this.getIntersections_({p0: p0, p1: p2}, rtree).length === 0) && + diagonalIsInside && this.getPointsInTriangle_(p0, p1, p2, rtree, true).length === 0) { + //The diagonal is completely inside the polygon + if (simple || p0.reflex === false || p2.reflex === false || + ol.geom.flat.orient.linearRingIsClockwise([s0.p0.x, s0.p0.y, p0.x, + p0.y, p1.x, p1.y, p2.x, p2.y, s3.p1.x, s3.p1.y], 0, 10, 2) === !ccw) { + //The diagonal is persumably valid, we have an ear + this.indices[numIndices++] = p0.i; + this.indices[numIndices++] = p1.i; + this.indices[numIndices++] = p2.i; + this.removeItem_(s1, s2, list, rtree); + if (s2 === start) { + start = s3; + } + processedEars = true; + } + } + } + // Else we have a reflex point. + s0 = list.getPrevItem(); + s1 = list.getCurrItem(); + s2 = list.nextItem(); + s3 = list.getNextItem(); + } while (s1 !== start && list.getLength() > 3); + + return processedEars; +}; + + +/** + * @private + * @param {ol.structs.LinkedList} list Linked list of the polygon. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + * @param {boolean=} opt_touch Resolve touching segments. + * @return {boolean} There were resolved intersections. +*/ +ol.render.webgl.PolygonReplay.prototype.resolveLocalSelfIntersections_ = function( + list, rtree, opt_touch) { + var start = list.firstItem(); + list.nextItem(); + var s0 = start; + var s1 = list.nextItem(); + var resolvedIntersections = false; + + do { + var intersection = this.calculateIntersection_(s0.p0, s0.p1, s1.p0, s1.p1, + opt_touch); + if (intersection) { + var breakCond = false; + var numVertices = this.vertices.length; + var numIndices = this.indices.length; + var n = numVertices / 2; + var seg = list.prevItem(); + list.removeItem(); + rtree.remove(seg); + breakCond = (seg === start); + var p; + if (opt_touch) { + if (intersection[0] === s0.p0.x && intersection[1] === s0.p0.y) { + list.prevItem(); + p = s0.p0; + s1.p0 = p; + rtree.remove(s0); + breakCond = breakCond || (s0 === start); + } else { + p = s1.p1; + s0.p1 = p; + rtree.remove(s1); + breakCond = breakCond || (s1 === start); + } + list.removeItem(); + } else { + p = this.createPoint_(intersection[0], intersection[1], n); + s0.p1 = p; + s1.p0 = p; + rtree.update([Math.min(s0.p0.x, s0.p1.x), Math.min(s0.p0.y, s0.p1.y), + Math.max(s0.p0.x, s0.p1.x), Math.max(s0.p0.y, s0.p1.y)], s0); + rtree.update([Math.min(s1.p0.x, s1.p1.x), Math.min(s1.p0.y, s1.p1.y), + Math.max(s1.p0.x, s1.p1.x), Math.max(s1.p0.y, s1.p1.y)], s1); + } + + this.indices[numIndices++] = seg.p0.i; + this.indices[numIndices++] = seg.p1.i; + this.indices[numIndices++] = p.i; + + resolvedIntersections = true; + if (breakCond) { + break; + } + } + + s0 = list.getPrevItem(); + s1 = list.nextItem(); + } while (s0 !== start); + return resolvedIntersections; +}; + + +/** + * @private + * @param {ol.structs.LinkedList} list Linked list of the polygon. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + * @return {boolean} The polygon is simple. + */ +ol.render.webgl.PolygonReplay.prototype.isSimple_ = function(list, rtree) { + var start = list.firstItem(); + var seg = start; + do { + if (this.getIntersections_(seg, rtree).length) { + return false; + } + seg = list.nextItem(); + } while (seg !== start); + return true; +}; + + +/** + * @private + * @param {ol.structs.LinkedList} list Linked list of the polygon. + * @return {boolean} Orientation is clockwise. + */ +ol.render.webgl.PolygonReplay.prototype.isClockwise_ = function(list) { + var length = list.getLength() * 2; + var flatCoordinates = new Array(length); + var start = list.firstItem(); + var seg = start; + var i = 0; + do { + flatCoordinates[i++] = seg.p0.x; + flatCoordinates[i++] = seg.p0.y; + seg = list.nextItem(); + } while (seg !== start); + return ol.geom.flat.orient.linearRingIsClockwise(flatCoordinates, 0, length, 2); +}; + + +/** + * @private + * @param {ol.structs.LinkedList} list Linked list of the polygon. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + */ +ol.render.webgl.PolygonReplay.prototype.splitPolygon_ = function(list, rtree) { + var start = list.firstItem(); + var s0 = start; + do { + var intersections = this.getIntersections_(s0, rtree); + if (intersections.length) { + var s1 = intersections[0]; + var n = this.vertices.length / 2; + var intersection = this.calculateIntersection_(s0.p0, + s0.p1, s1.p0, s1.p1); + var p = this.createPoint_(intersection[0], intersection[1], n); + var newPolygon = new ol.structs.LinkedList(); + var newRtree = new ol.structs.RBush(); + this.insertItem_(p, s0.p1, newPolygon, newRtree); + s0.p1 = p; + rtree.update([Math.min(s0.p0.x, p.x), Math.min(s0.p0.y, p.y), + Math.max(s0.p0.x, p.x), Math.max(s0.p0.y, p.y)], s0); + var currItem = list.nextItem(); + while (currItem !== s1) { + this.insertItem_(currItem.p0, currItem.p1, newPolygon, newRtree); + rtree.remove(currItem); + list.removeItem(); + currItem = list.getCurrItem(); + } + this.insertItem_(s1.p0, p, newPolygon, newRtree); + s1.p0 = p; + rtree.update([Math.min(s1.p1.x, p.x), Math.min(s1.p1.y, p.y), + Math.max(s1.p1.x, p.x), Math.max(s1.p1.y, p.y)], s1); + this.classifyPoints_(list, rtree, false); + this.triangulate_(list, rtree); + this.classifyPoints_(newPolygon, newRtree, false); + this.triangulate_(newPolygon, newRtree); + break; + } + s0 = list.nextItem(); + } while (s0 !== start); +}; + + +/** + * @private + * @param {number} x X coordinate. + * @param {number} y Y coordinate. + * @param {number} i Index. + * @return {ol.WebglPolygonVertex} List item. + */ +ol.render.webgl.PolygonReplay.prototype.createPoint_ = function(x, y, i) { + var numVertices = this.vertices.length; + this.vertices[numVertices++] = x; + this.vertices[numVertices++] = y; + /** @type {ol.WebglPolygonVertex} */ + var p = { + x: x, + y: y, + i: i, + reflex: undefined + }; + return p; +}; + + +/** + * @private + * @param {ol.WebglPolygonVertex} p0 First point of segment. + * @param {ol.WebglPolygonVertex} p1 Second point of segment. + * @param {ol.structs.LinkedList} list Polygon ring. + * @param {ol.structs.RBush=} opt_rtree Insert the segment into the R-Tree. + * @return {ol.WebglPolygonSegment} segment. + */ +ol.render.webgl.PolygonReplay.prototype.insertItem_ = function(p0, p1, list, opt_rtree) { + var seg = { + p0: p0, + p1: p1 + }; + list.insertItem(seg); + if (opt_rtree) { + opt_rtree.insert([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), + Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)], seg); + } + return seg; +}; + + + /** + * @private + * @param {ol.WebglPolygonSegment} s0 Segment before the remove candidate. + * @param {ol.WebglPolygonSegment} s1 Remove candidate segment. + * @param {ol.structs.LinkedList} list Polygon ring. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + */ +ol.render.webgl.PolygonReplay.prototype.removeItem_ = function(s0, s1, list, rtree) { + if (list.getCurrItem() === s1) { + list.removeItem(); + s0.p1 = s1.p1; + rtree.remove(s1); + rtree.update([Math.min(s0.p0.x, s0.p1.x), Math.min(s0.p0.y, s0.p1.y), + Math.max(s0.p0.x, s0.p1.x), Math.max(s0.p0.y, s0.p1.y)], s0); + } +}; + + +/** + * @private + * @param {ol.WebglPolygonVertex} p0 First point. + * @param {ol.WebglPolygonVertex} p1 Second point. + * @param {ol.WebglPolygonVertex} p2 Third point. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + * @param {boolean=} opt_reflex Only include reflex points. + * @return {Array.} Points in the triangle. + */ +ol.render.webgl.PolygonReplay.prototype.getPointsInTriangle_ = function(p0, p1, + p2, rtree, opt_reflex) { + var i, ii, j, p; + var result = []; + var segmentsInExtent = rtree.getInExtent([Math.min(p0.x, p1.x, p2.x), + Math.min(p0.y, p1.y, p2.y), Math.max(p0.x, p1.x, p2.x), Math.max(p0.y, + p1.y, p2.y)]); + for (i = 0, ii = segmentsInExtent.length; i < ii; ++i) { + for (j in segmentsInExtent[i]) { + p = segmentsInExtent[i][j]; + if (typeof p === 'object' && (!opt_reflex || p.reflex)) { + if ((p.x !== p0.x || p.y !== p0.y) && (p.x !== p1.x || p.y !== p1.y) && + (p.x !== p2.x || p.y !== p2.y) && result.indexOf(p) === -1 && + ol.geom.flat.contains.linearRingContainsXY([p0.x, p0.y, p1.x, p1.y, + p2.x, p2.y], 0, 6, 2, p.x, p.y)) { + result.push(p); + } + } + } + } + return result; +}; + + +/** + * @private + * @param {ol.WebglPolygonSegment} segment Segment. + * @param {ol.structs.RBush} rtree R-Tree of the polygon. + * @param {boolean=} opt_touch Touching segments should be considered an intersection. + * @return {Array.} Intersecting segments. + */ +ol.render.webgl.PolygonReplay.prototype.getIntersections_ = function(segment, rtree, opt_touch) { + var p0 = segment.p0; + var p1 = segment.p1; + var segmentsInExtent = rtree.getInExtent([Math.min(p0.x, p1.x), + Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)]); + var result = []; + var i, ii; + for (i = 0, ii = segmentsInExtent.length; i < ii; ++i) { + var currSeg = segmentsInExtent[i]; + if (segment !== currSeg && (opt_touch || currSeg.p0 !== p1 || currSeg.p1 !== p0) && + this.calculateIntersection_(p0, p1, currSeg.p0, currSeg.p1, opt_touch)) { + result.push(currSeg); + } + } + return result; +}; + + +/** + * Line intersection algorithm by Paul Bourke. + * @see http://paulbourke.net/geometry/pointlineplane/ + * + * @private + * @param {ol.WebglPolygonVertex} p0 First point. + * @param {ol.WebglPolygonVertex} p1 Second point. + * @param {ol.WebglPolygonVertex} p2 Third point. + * @param {ol.WebglPolygonVertex} p3 Fourth point. + * @param {boolean=} opt_touch Touching segments should be considered an intersection. + * @return {Array.|undefined} Intersection coordinates. + */ +ol.render.webgl.PolygonReplay.prototype.calculateIntersection_ = function(p0, + p1, p2, p3, opt_touch) { + var denom = (p3.y - p2.y) * (p1.x - p0.x) - (p3.x - p2.x) * (p1.y - p0.y); + if (denom !== 0) { + var ua = ((p3.x - p2.x) * (p0.y - p2.y) - (p3.y - p2.y) * (p0.x - p2.x)) / denom; + var ub = ((p1.x - p0.x) * (p0.y - p2.y) - (p1.y - p0.y) * (p0.x - p2.x)) / denom; + if ((!opt_touch && ua > ol.render.webgl.EPSILON && ua < 1 - ol.render.webgl.EPSILON && + ub > ol.render.webgl.EPSILON && ub < 1 - ol.render.webgl.EPSILON) || (opt_touch && + ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1)) { + return [p0.x + ua * (p1.x - p0.x), p0.y + ua * (p1.y - p0.y)]; + } + } + return undefined; +}; + + +/** + * @private + * @param {ol.WebglPolygonVertex} p0 Point before the start of the diagonal. + * @param {ol.WebglPolygonVertex} p1 Start point of the diagonal. + * @param {ol.WebglPolygonVertex} p2 Ear candidate. + * @param {ol.WebglPolygonVertex} p3 End point of the diagonal. + * @param {ol.WebglPolygonVertex} p4 Point after the end of the diagonal. + * @return {boolean} Diagonal is inside the polygon. + */ +ol.render.webgl.PolygonReplay.prototype.diagonalIsInside_ = function(p0, p1, p2, p3, p4) { + if (p1.reflex === undefined || p3.reflex === undefined) { + return false; + } + var p1IsLeftOf = (p2.x - p3.x) * (p1.y - p3.y) > (p2.y - p3.y) * (p1.x - p3.x); + var p1IsRightOf = (p4.x - p3.x) * (p1.y - p3.y) < (p4.y - p3.y) * (p1.x - p3.x); + var p3IsLeftOf = (p0.x - p1.x) * (p3.y - p1.y) > (p0.y - p1.y) * (p3.x - p1.x); + var p3IsRightOf = (p2.x - p1.x) * (p3.y - p1.y) < (p2.y - p1.y) * (p3.x - p1.x); + var p1InCone = p3.reflex ? p1IsRightOf || p1IsLeftOf : p1IsRightOf && p1IsLeftOf; + var p3InCone = p1.reflex ? p3IsRightOf || p3IsLeftOf : p3IsRightOf && p3IsLeftOf; + return p1InCone && p3InCone; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.PolygonReplay.prototype.drawMultiPolygon = function(multiPolygonGeometry, feature) { + var polygons = multiPolygonGeometry.getPolygons(); + var stride = multiPolygonGeometry.getStride(); + var currIndex = this.indices.length; + var currLineIndex = this.lineStringReplay.getCurrentIndex(); + var i, ii, j, jj; + for (i = 0, ii = polygons.length; i < ii; ++i) { + var linearRings = polygons[i].getLinearRings(); + if (linearRings.length > 0) { + var flatCoordinates = linearRings[0].getFlatCoordinates(); + flatCoordinates = ol.geom.flat.transform.translate(flatCoordinates, 0, flatCoordinates.length, + stride, -this.origin[0], -this.origin[1]); + var holes = []; + var holeFlatCoords; + for (j = 1, jj = linearRings.length; j < jj; ++j) { + holeFlatCoords = linearRings[j].getFlatCoordinates(); + holeFlatCoords = ol.geom.flat.transform.translate(holeFlatCoords, 0, holeFlatCoords.length, + stride, -this.origin[0], -this.origin[1]); + holes.push(holeFlatCoords); + } + this.lineStringReplay.drawPolygonCoordinates(flatCoordinates, holes, stride); + this.drawCoordinates_(flatCoordinates, holes, stride); + } + } + if (this.indices.length > currIndex) { + this.startIndices.push(currIndex); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(currIndex); + this.state_.changed = false; + } + } + if (this.lineStringReplay.getCurrentIndex() > currLineIndex) { + this.lineStringReplay.setPolygonStyle(feature, currLineIndex); + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.PolygonReplay.prototype.drawPolygon = function(polygonGeometry, feature) { + var linearRings = polygonGeometry.getLinearRings(); + var stride = polygonGeometry.getStride(); + if (linearRings.length > 0) { + this.startIndices.push(this.indices.length); + this.startIndicesFeature.push(feature); + if (this.state_.changed) { + this.styleIndices_.push(this.indices.length); + this.state_.changed = false; + } + this.lineStringReplay.setPolygonStyle(feature); + + var flatCoordinates = linearRings[0].getFlatCoordinates(); + flatCoordinates = ol.geom.flat.transform.translate(flatCoordinates, 0, flatCoordinates.length, + stride, -this.origin[0], -this.origin[1]); + var holes = []; + var i, ii, holeFlatCoords; + for (i = 1, ii = linearRings.length; i < ii; ++i) { + holeFlatCoords = linearRings[i].getFlatCoordinates(); + holeFlatCoords = ol.geom.flat.transform.translate(holeFlatCoords, 0, holeFlatCoords.length, + stride, -this.origin[0], -this.origin[1]); + holes.push(holeFlatCoords); + } + this.lineStringReplay.drawPolygonCoordinates(flatCoordinates, holes, stride); + this.drawCoordinates_(flatCoordinates, holes, stride); + } +}; + + +/** + * @inheritDoc + **/ +ol.render.webgl.PolygonReplay.prototype.finish = function(context) { + // 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); + + this.startIndices.push(this.indices.length); + + this.lineStringReplay.finish(context); + + //Clean up, if there is nothing to draw + if (this.styleIndices_.length === 0 && this.styles_.length > 0) { + this.styles_ = []; + } + + this.vertices = null; + this.indices = null; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.PolygonReplay.prototype.getDeleteResourcesFunction = function(context) { + // We only delete our stuff here. The shaders and the program may + // be used by other PolygonReplay instances (for other layers). And + // they will be deleted when disposing of the ol.webgl.Context + // object. + ol.DEBUG && console.assert(this.verticesBuffer, + 'verticesBuffer must not be null'); + ol.DEBUG && console.assert(this.indicesBuffer, + 'indicesBuffer must not be null'); + var verticesBuffer = this.verticesBuffer; + var indicesBuffer = this.indicesBuffer; + var lineDeleter = this.lineStringReplay.getDeleteResourcesFunction(context); + return function() { + context.deleteBuffer(verticesBuffer); + context.deleteBuffer(indicesBuffer); + lineDeleter(); + }; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.PolygonReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { + // get the program + var fragmentShader, vertexShader; + fragmentShader = ol.render.webgl.polygonreplay.defaultshader.fragment; + vertexShader = ol.render.webgl.polygonreplay.defaultshader.vertex; + var program = context.getProgram(fragmentShader, vertexShader); + + // get the locations + var locations; + if (!this.defaultLocations_) { + locations = + new ol.render.webgl.polygonreplay.defaultshader.Locations(gl, program); + this.defaultLocations_ = locations; + } else { + locations = this.defaultLocations_; + } + + context.useProgram(program); + + // enable the vertex attrib arrays + gl.enableVertexAttribArray(locations.a_position); + gl.vertexAttribPointer(locations.a_position, 2, ol.webgl.FLOAT, + false, 8, 0); + + return locations; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.PolygonReplay.prototype.shutDownProgram = function(gl, locations) { + gl.disableVertexAttribArray(locations.a_position); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.PolygonReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { + //Save GL parameters. + var tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC)); + var tmpDepthMask = /** @type {boolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK)); + + if (!hitDetection) { + gl.enable(gl.DEPTH_TEST); + gl.depthMask(true); + gl.depthFunc(gl.NOTEQUAL); + } + + if (!ol.obj.isEmpty(skippedFeaturesHash)) { + this.drawReplaySkipping_(gl, context, skippedFeaturesHash); + } else { + ol.DEBUG && console.assert(this.styles_.length === this.styleIndices_.length, + 'number of styles and styleIndices match'); + + //Draw by style groups to minimize drawElements() calls. + var i, start, end, nextStyle; + end = this.startIndices[this.startIndices.length - 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + start = this.styleIndices_[i]; + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, nextStyle); + this.drawElements(gl, context, start, end); + end = start; + } + } + if (!hitDetection) { + gl.disable(gl.DEPTH_TEST); + gl.clear(gl.DEPTH_BUFFER_BIT); + //Restore GL parameters. + gl.depthMask(tmpDepthMask); + gl.depthFunc(tmpDepthFunc); + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.PolygonReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, + featureCallback, opt_hitExtent) { + ol.DEBUG && console.assert(this.styles_.length === this.styleIndices_.length, + 'number of styles and styleIndices match'); + ol.DEBUG && console.assert(this.startIndices.length - 1 === this.startIndicesFeature.length, + 'number of startIndices and startIndicesFeature match'); + + var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex; + featureIndex = this.startIndices.length - 2; + end = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, nextStyle); + groupStart = this.styleIndices_[i]; + + 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; + } + + } + featureIndex--; + end = start; + } + } + return undefined; +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {Object} skippedFeaturesHash Ids of features to skip. + */ +ol.render.webgl.PolygonReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) { + ol.DEBUG && console.assert(this.startIndices.length - 1 === this.startIndicesFeature.length, + 'number of startIndices and startIndicesFeature match'); + + var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart; + featureIndex = this.startIndices.length - 2; + end = start = this.startIndices[featureIndex + 1]; + for (i = this.styleIndices_.length - 1; i >= 0; --i) { + nextStyle = this.styles_[i]; + this.setFillStyle_(gl, nextStyle); + groupStart = this.styleIndices_[i]; + + while (featureIndex >= 0 && + this.startIndices[featureIndex] >= groupStart) { + featureStart = this.startIndices[featureIndex]; + feature = this.startIndicesFeature[featureIndex]; + featureUid = ol.getUid(feature).toString(); + + if (skippedFeaturesHash[featureUid]) { + if (start !== end) { + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + end = featureStart; + } + featureIndex--; + start = featureStart; + } + if (start !== end) { + this.drawElements(gl, context, start, end); + gl.clear(gl.DEPTH_BUFFER_BIT); + } + start = end = groupStart; + } +}; + + +/** + * @private + * @param {WebGLRenderingContext} gl gl. + * @param {Array.} color Color. + */ +ol.render.webgl.PolygonReplay.prototype.setFillStyle_ = function(gl, color) { + gl.uniform4fv(this.defaultLocations_.u_color, color); +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { + ol.DEBUG && console.assert(this.state_, 'this.state_ should not be null'); + var fillStyleColor = fillStyle ? fillStyle.getColor() : [0, 0, 0, 0]; + if (!(fillStyleColor instanceof CanvasGradient) && + !(fillStyleColor instanceof CanvasPattern)) { + fillStyleColor = ol.color.asArray(fillStyleColor).map(function(c, i) { + return i != 3 ? c / 255 : c; + }) || ol.render.webgl.defaultFillStyle; + } else { + fillStyleColor = ol.render.webgl.defaultFillStyle; + } + if (!this.state_.fillColor || !ol.array.equals(fillStyleColor, this.state_.fillColor)) { + this.state_.fillColor = fillStyleColor; + this.state_.changed = true; + this.styles_.push(fillStyleColor); + } + //Provide a null stroke style, if no strokeStyle is provided. Required for the draw interaction to work. + if (strokeStyle) { + this.lineStringReplay.setFillStrokeStyle(null, strokeStyle); + } else { + var nullStrokeStyle = new ol.style.Stroke({ + color: [0, 0, 0, 0], + lineWidth: 0 + }); + this.lineStringReplay.setFillStrokeStyle(null, nullStrokeStyle); + } +}; diff --git a/src/ol/render/webgl/replay.js b/src/ol/render/webgl/replay.js new file mode 100644 index 0000000000..d9df4d015a --- /dev/null +++ b/src/ol/render/webgl/replay.js @@ -0,0 +1,365 @@ +goog.provide('ol.render.webgl.Replay'); + +goog.require('ol'); +goog.require('ol.extent'); +goog.require('ol.render.VectorContext'); +goog.require('ol.transform'); +goog.require('ol.vec.Mat4'); +goog.require('ol.webgl'); + +/** + * @constructor + * @extends {ol.render.VectorContext} + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @struct + */ +ol.render.webgl.Replay = function(tolerance, maxExtent) { + ol.render.VectorContext.call(this); + + /** + * @protected + * @type {number} + */ + this.tolerance = tolerance; + + /** + * @protected + * @const + * @type {ol.Extent} + */ + this.maxExtent = maxExtent; + + /** + * 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. + * @protected + * @type {ol.Coordinate} + */ + this.origin = ol.extent.getCenter(maxExtent); + + /** + * @private + * @type {ol.Transform} + */ + this.projectionMatrix_ = ol.transform.create(); + + /** + * @private + * @type {ol.Transform} + */ + this.offsetRotateMatrix_ = ol.transform.create(); + + /** + * @private + * @type {ol.Transform} + */ + this.offsetScaleMatrix_ = ol.transform.create(); + + /** + * @private + * @type {Array.} + */ + this.tmpMat4_ = ol.vec.Mat4.create(); + + /** + * @protected + * @type {Array.} + */ + this.indices = []; + + /** + * @protected + * @type {?ol.webgl.Buffer} + */ + this.indicesBuffer = null; + + /** + * Start index per feature (the index). + * @protected + * @type {Array.} + */ + this.startIndices = []; + + /** + * Start index per feature (the feature). + * @protected + * @type {Array.} + */ + this.startIndicesFeature = []; + + /** + * @protected + * @type {Array.} + */ + this.vertices = []; + + /** + * @protected + * @type {?ol.webgl.Buffer} + */ + this.verticesBuffer = null; + + /** + * Optional parameter for PolygonReplay instances. + * @protected + * @type {ol.render.webgl.LineStringReplay|undefined} + */ + this.lineStringReplay = undefined; + +}; +ol.inherits(ol.render.webgl.Replay, ol.render.VectorContext); + + +/** + * @abstract + * @param {ol.webgl.Context} context WebGL context. + * @return {function()} Delete resources function. + */ +ol.render.webgl.Replay.prototype.getDeleteResourcesFunction = function(context) {}; + + +/** + * @abstract + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.Replay.prototype.finish = function(context) {}; + + +/** + * @abstract + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @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.Replay.prototype.setUpProgram = function(gl, context, size, pixelRatio) {}; + + +/** + * @abstract + * @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.Replay.prototype.shutDownProgram = function(gl, locations) {}; + + +/** + * @abstract + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features + * to skip. + * @param {boolean} hitDetection Hit detection mode. + */ +ol.render.webgl.Replay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) {}; + + +/** + * @abstract + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features + * to skip. + * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback. + * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting + * this extent are checked. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.Replay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) {}; + + +/** + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @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.Replay.prototype.drawHitDetectionReplay = function(gl, context, skippedFeaturesHash, + featureCallback, oneByOne, opt_hitExtent) { + if (!oneByOne) { + // draw all hit-detection features in "once" (by texture group) + return this.drawHitDetectionReplayAll(gl, context, + skippedFeaturesHash, featureCallback); + } else { + // draw hit-detection features one by one + return this.drawHitDetectionReplayOneByOne(gl, context, + skippedFeaturesHash, featureCallback, opt_hitExtent); + } +}; + + +/** + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {Object.} skippedFeaturesHash Ids of features + * to skip. + * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.Replay.prototype.drawHitDetectionReplayAll = function(gl, context, skippedFeaturesHash, + featureCallback) { + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + this.drawReplay(gl, context, skippedFeaturesHash, true); + + var result = featureCallback(null); + if (result) { + return result; + } else { + return undefined; + } +}; + + +/** + * @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.Replay.prototype.replay = function(context, + center, resolution, rotation, size, pixelRatio, + opacity, skippedFeaturesHash, + featureCallback, oneByOne, opt_hitExtent) { + var gl = context.getGL(); + var tmpStencil, tmpStencilFunc, tmpStencilMaskVal, tmpStencilRef, tmpStencilMask, + tmpStencilOpFail, tmpStencilOpPass, tmpStencilOpZFail; + + if (this.lineStringReplay) { + tmpStencil = gl.isEnabled(gl.STENCIL_TEST); + tmpStencilFunc = gl.getParameter(gl.STENCIL_FUNC); + tmpStencilMaskVal = gl.getParameter(gl.STENCIL_VALUE_MASK); + tmpStencilRef = gl.getParameter(gl.STENCIL_REF); + tmpStencilMask = gl.getParameter(gl.STENCIL_WRITEMASK); + tmpStencilOpFail = gl.getParameter(gl.STENCIL_FAIL); + tmpStencilOpPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS); + tmpStencilOpZFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL); + + gl.enable(gl.STENCIL_TEST); + gl.clear(gl.STENCIL_BUFFER_BIT); + gl.stencilMask(255); + gl.stencilFunc(gl.ALWAYS, 1, 255); + gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); + + this.lineStringReplay.replay(context, + center, resolution, rotation, size, pixelRatio, + opacity, skippedFeaturesHash, + featureCallback, oneByOne, opt_hitExtent); + + gl.stencilMask(0); + gl.stencilFunc(gl.NOTEQUAL, 1, 255); + } + + // bind the vertices buffer + ol.DEBUG && console.assert(this.verticesBuffer, + 'verticesBuffer must not be null'); + context.bindBuffer(ol.webgl.ARRAY_BUFFER, this.verticesBuffer); + + // bind the indices buffer + ol.DEBUG && console.assert(this.indicesBuffer, + 'indicesBuffer must not be null'); + context.bindBuffer(ol.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer); + + var locations = this.setUpProgram(gl, context, size, pixelRatio); + + // set the "uniform" values + var projectionMatrix = ol.transform.reset(this.projectionMatrix_); + ol.transform.scale(projectionMatrix, 2 / (resolution * size[0]), 2 / (resolution * size[1])); + ol.transform.rotate(projectionMatrix, -rotation); + ol.transform.translate(projectionMatrix, -(center[0] - this.origin[0]), -(center[1] - this.origin[1])); + + var offsetScaleMatrix = ol.transform.reset(this.offsetScaleMatrix_); + ol.transform.scale(offsetScaleMatrix, 2 / size[0], 2 / size[1]); + + var offsetRotateMatrix = ol.transform.reset(this.offsetRotateMatrix_); + if (rotation !== 0) { + ol.transform.rotate(offsetRotateMatrix, -rotation); + } + + gl.uniformMatrix4fv(locations.u_projectionMatrix, false, + ol.vec.Mat4.fromTransform(this.tmpMat4_, projectionMatrix)); + gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, + ol.vec.Mat4.fromTransform(this.tmpMat4_, offsetScaleMatrix)); + gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false, + ol.vec.Mat4.fromTransform(this.tmpMat4_, offsetRotateMatrix)); + gl.uniform1f(locations.u_opacity, opacity); + + // draw! + var result; + if (featureCallback === undefined) { + this.drawReplay(gl, context, skippedFeaturesHash, false); + } else { + // draw feature by feature for the hit-detection + result = this.drawHitDetectionReplay(gl, context, skippedFeaturesHash, + featureCallback, oneByOne, opt_hitExtent); + } + + // disable the vertex attrib arrays + this.shutDownProgram(gl, locations); + + if (this.lineStringReplay) { + if (!tmpStencil) { + gl.disable(gl.STENCIL_TEST); + } + gl.clear(gl.STENCIL_BUFFER_BIT); + gl.stencilFunc(/** @type {number} */ (tmpStencilFunc), + /** @type {number} */ (tmpStencilRef), /** @type {number} */ (tmpStencilMaskVal)); + gl.stencilMask(/** @type {number} */ (tmpStencilMask)); + gl.stencilOp(/** @type {number} */ (tmpStencilOpFail), + /** @type {number} */ (tmpStencilOpZFail), /** @type {number} */ (tmpStencilOpPass)); + } + + return result; +}; + +/** + * @protected + * @param {WebGLRenderingContext} gl gl. + * @param {ol.webgl.Context} context Context. + * @param {number} start Start index. + * @param {number} end End index. + */ +ol.render.webgl.Replay.prototype.drawElements = function( + gl, context, start, end) { + var elementType = context.hasOESElementIndexUint ? + ol.webgl.UNSIGNED_INT : ol.webgl.UNSIGNED_SHORT; + var elementSize = context.hasOESElementIndexUint ? 4 : 2; + + var numItems = end - start; + var offsetInBytes = start * elementSize; + gl.drawElements(ol.webgl.TRIANGLES, numItems, elementType, offsetInBytes); +}; diff --git a/src/ol/render/webgl/replaygroup.js b/src/ol/render/webgl/replaygroup.js new file mode 100644 index 0000000000..18a97e1e1f --- /dev/null +++ b/src/ol/render/webgl/replaygroup.js @@ -0,0 +1,318 @@ +goog.provide('ol.render.webgl.ReplayGroup'); + +goog.require('ol'); +goog.require('ol.array'); +goog.require('ol.render.ReplayGroup'); +goog.require('ol.render.webgl'); +goog.require('ol.render.webgl.CircleReplay'); +goog.require('ol.render.webgl.ImageReplay'); +goog.require('ol.render.webgl.LineStringReplay'); +goog.require('ol.render.webgl.PolygonReplay'); +goog.require('ol.render.webgl.TextReplay'); + +/** + * @constructor + * @extends {ol.render.ReplayGroup} + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @param {number=} opt_renderBuffer Render buffer. + * @struct + */ +ol.render.webgl.ReplayGroup = function(tolerance, maxExtent, opt_renderBuffer) { + ol.render.ReplayGroup.call(this); + + /** + * @type {ol.Extent} + * @private + */ + this.maxExtent_ = maxExtent; + + /** + * @type {number} + * @private + */ + this.tolerance_ = tolerance; + + /** + * @type {number|undefined} + * @private + */ + this.renderBuffer_ = opt_renderBuffer; + + /** + * @private + * @type {!Object.>} + */ + this.replaysByZIndex_ = {}; + +}; +ol.inherits(ol.render.webgl.ReplayGroup, ol.render.ReplayGroup); + + +/** + * @param {ol.webgl.Context} context WebGL context. + * @return {function()} Delete resources function. + */ +ol.render.webgl.ReplayGroup.prototype.getDeleteResourcesFunction = function(context) { + var functions = []; + var zKey; + for (zKey in this.replaysByZIndex_) { + var replays = this.replaysByZIndex_[zKey]; + var replayKey; + for (replayKey in replays) { + functions.push( + replays[replayKey].getDeleteResourcesFunction(context)); + } + } + return function() { + var length = functions.length; + var result; + for (var i = 0; i < length; i++) { + result = functions[i].apply(this, arguments); + } + return result; + }; +}; + + +/** + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.ReplayGroup.prototype.finish = function(context) { + var zKey; + for (zKey in this.replaysByZIndex_) { + var replays = this.replaysByZIndex_[zKey]; + var replayKey; + for (replayKey in replays) { + replays[replayKey].finish(context); + } + } +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ReplayGroup.prototype.getReplay = function(zIndex, replayType) { + var zIndexKey = zIndex !== undefined ? zIndex.toString() : '0'; + var replays = this.replaysByZIndex_[zIndexKey]; + if (replays === undefined) { + replays = {}; + this.replaysByZIndex_[zIndexKey] = replays; + } + var replay = replays[replayType]; + if (replay === undefined) { + var Constructor = ol.render.webgl.ReplayGroup.BATCH_CONSTRUCTORS_[replayType]; + ol.DEBUG && console.assert(Constructor !== undefined, + replayType + + ' constructor missing from ol.render.webgl.ReplayGroup.BATCH_CONSTRUCTORS_'); + replay = new Constructor(this.tolerance_, this.maxExtent_); + replays[replayType] = replay; + } + return replay; +}; + + +/** + * @inheritDoc + */ +ol.render.webgl.ReplayGroup.prototype.isEmpty = function() { + return ol.obj.isEmpty(this.replaysByZIndex_); +}; + + +/** + * @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. + */ +ol.render.webgl.ReplayGroup.prototype.replay = function(context, + center, resolution, rotation, size, pixelRatio, + opacity, skippedFeaturesHash) { + /** @type {Array.} */ + var zs = Object.keys(this.replaysByZIndex_).map(Number); + zs.sort(ol.array.numberSafeCompareFunction); + + var i, ii, j, jj, replays, replay; + for (i = 0, ii = zs.length; i < ii; ++i) { + replays = this.replaysByZIndex_[zs[i].toString()]; + for (j = 0, jj = ol.render.replay.ORDER.length; j < jj; ++j) { + replay = replays[ol.render.replay.ORDER[j]]; + if (replay !== undefined) { + replay.replay(context, + center, resolution, rotation, size, pixelRatio, + opacity, skippedFeaturesHash, + undefined, false); + } + } + } +}; + + +/** + * @private + * @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.ReplayGroup.prototype.replayHitDetection_ = function(context, + center, resolution, rotation, size, pixelRatio, opacity, + skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent) { + /** @type {Array.} */ + var zs = Object.keys(this.replaysByZIndex_).map(Number); + zs.sort(function(a, b) { + return b - a; + }); + + var i, ii, j, replays, replay, result; + for (i = 0, ii = zs.length; i < ii; ++i) { + replays = this.replaysByZIndex_[zs[i].toString()]; + for (j = ol.render.replay.ORDER.length - 1; j >= 0; --j) { + replay = replays[ol.render.replay.ORDER[j]]; + if (replay !== undefined) { + result = replay.replay(context, + center, resolution, rotation, size, pixelRatio, opacity, + skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent); + if (result) { + return result; + } + } + } + } + return undefined; +}; + + +/** + * @param {ol.Coordinate} coordinate Coordinate. + * @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} callback Feature callback. + * @return {T|undefined} Callback result. + * @template T + */ +ol.render.webgl.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( + coordinate, context, center, resolution, rotation, size, pixelRatio, + opacity, skippedFeaturesHash, + callback) { + var gl = context.getGL(); + gl.bindFramebuffer( + gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); + + + /** + * @type {ol.Extent} + */ + var hitExtent; + if (this.renderBuffer_ !== undefined) { + // build an extent around the coordinate, so that only features that + // intersect this extent are checked + hitExtent = ol.extent.buffer( + ol.extent.createOrUpdateFromCoordinate(coordinate), + resolution * this.renderBuffer_); + } + + return this.replayHitDetection_(context, + coordinate, resolution, rotation, ol.render.webgl.ReplayGroup.HIT_DETECTION_SIZE_, + pixelRatio, opacity, skippedFeaturesHash, + /** + * @param {ol.Feature|ol.render.Feature} feature Feature. + * @return {?} Callback result. + */ + function(feature) { + var imageData = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); + + if (imageData[3] > 0) { + var result = callback(feature); + if (result) { + return result; + } + } + }, true, hitExtent); +}; + + +/** + * @param {ol.Coordinate} coordinate Coordinate. + * @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. + * @return {boolean} Is there a feature at the given coordinate? + */ +ol.render.webgl.ReplayGroup.prototype.hasFeatureAtCoordinate = function( + coordinate, context, center, resolution, rotation, size, pixelRatio, + opacity, skippedFeaturesHash) { + var gl = context.getGL(); + gl.bindFramebuffer( + gl.FRAMEBUFFER, context.getHitDetectionFramebuffer()); + + var hasFeature = this.replayHitDetection_(context, + coordinate, resolution, rotation, ol.render.webgl.ReplayGroup.HIT_DETECTION_SIZE_, + pixelRatio, opacity, skippedFeaturesHash, + /** + * @param {ol.Feature|ol.render.Feature} feature Feature. + * @return {boolean} Is there a feature? + */ + function(feature) { + var imageData = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData); + return imageData[3] > 0; + }, false); + + return hasFeature !== undefined; +}; + +/** + * @const + * @private + * @type {Array.} + */ +ol.render.webgl.ReplayGroup.HIT_DETECTION_SIZE_ = [1, 1]; + +/** + * @const + * @private + * @type {Object.} + */ +ol.render.webgl.ReplayGroup.BATCH_CONSTRUCTORS_ = { + 'Circle': ol.render.webgl.CircleReplay, + 'Image': ol.render.webgl.ImageReplay, + 'LineString': ol.render.webgl.LineStringReplay, + 'Polygon': ol.render.webgl.PolygonReplay, + 'Text': ol.render.webgl.TextReplay +}; diff --git a/src/ol/render/webgl/textreplay/index.js b/src/ol/render/webgl/textreplay/index.js new file mode 100644 index 0000000000..c9b088c1d3 --- /dev/null +++ b/src/ol/render/webgl/textreplay/index.js @@ -0,0 +1,65 @@ +goog.provide('ol.render.webgl.TextReplay'); + +goog.require('ol'); + +/** + * @constructor + * @param {number} tolerance Tolerance. + * @param {ol.Extent} maxExtent Max extent. + * @struct + */ +ol.render.webgl.TextReplay = function(tolerance, maxExtent) {}; + +/** + * @param {ol.style.Text} textStyle Text style. + */ +ol.render.webgl.TextReplay.prototype.setTextStyle = function(textStyle) {}; + +/** + * @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; +}; + +/** + * @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) {}; + +/** + * @abstract + * @param {ol.webgl.Context} context Context. + */ +ol.render.webgl.TextReplay.prototype.finish = function(context) {}; + +/** + * @param {ol.webgl.Context} context WebGL context. + * @return {function()} Delete resources function. + */ +ol.render.webgl.TextReplay.prototype.getDeleteResourcesFunction = function(context) { + return ol.nullFunction; +}; diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index e2832d049d..ffa74f0889 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -47,10 +47,10 @@ ol.renderer.vector.renderCircleGeometry_ = function(replayGroup, geometry, style var fillStyle = style.getFill(); var strokeStyle = style.getStroke(); if (fillStyle || strokeStyle) { - var polygonReplay = replayGroup.getReplay( - style.getZIndex(), ol.render.ReplayType.POLYGON); - polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle); - polygonReplay.drawCircle(geometry, feature); + var circleReplay = replayGroup.getReplay( + style.getZIndex(), ol.render.ReplayType.CIRCLE); + circleReplay.setFillStrokeStyle(fillStyle, strokeStyle); + circleReplay.drawCircle(geometry, feature); } var textStyle = style.getText(); if (textStyle) { diff --git a/src/ol/renderer/webgl/imagelayer.js b/src/ol/renderer/webgl/imagelayer.js index f85bbd4917..8e7507ef9d 100644 --- a/src/ol/renderer/webgl/imagelayer.js +++ b/src/ol/renderer/webgl/imagelayer.js @@ -171,7 +171,7 @@ ol.renderer.webgl.ImageLayer.prototype.prepareFrame = function(frameState, layer this.updateLogos(frameState, imageSource); } - return true; + return !!image; }; diff --git a/src/ol/renderer/webgl/map.js b/src/ol/renderer/webgl/map.js index 607d86600b..e055609ff6 100644 --- a/src/ol/renderer/webgl/map.js +++ b/src/ol/renderer/webgl/map.js @@ -77,7 +77,7 @@ ol.renderer.webgl.Map = function(container, map) { */ this.gl_ = ol.webgl.getContext(this.canvas_, { antialias: true, - depth: false, + depth: true, failIfMajorPerformanceCaveat: true, preserveDrawingBuffer: false, stencil: true diff --git a/src/ol/renderer/webgl/vectorlayer.js b/src/ol/renderer/webgl/vectorlayer.js index c373c721e4..494d5eb149 100644 --- a/src/ol/renderer/webgl/vectorlayer.js +++ b/src/ol/renderer/webgl/vectorlayer.js @@ -73,11 +73,17 @@ ol.renderer.webgl.VectorLayer.prototype.composeFrame = function(frameState, laye this.layerState_ = layerState; var viewState = frameState.viewState; var replayGroup = this.replayGroup_; + var size = frameState.size; + var pixelRatio = frameState.pixelRatio; + var gl = this.mapRenderer.getGL(); if (replayGroup && !replayGroup.isEmpty()) { + gl.enable(gl.SCISSOR_TEST); + gl.scissor(0, 0, size[0] * pixelRatio, size[1] * pixelRatio); replayGroup.replay(context, viewState.center, viewState.resolution, viewState.rotation, - frameState.size, frameState.pixelRatio, layerState.opacity, + size, pixelRatio, layerState.opacity, layerState.managed ? frameState.skippedFeatureUids : {}); + gl.disable(gl.SCISSOR_TEST); } }; @@ -299,7 +305,7 @@ ol.renderer.webgl.VectorLayer.prototype.renderFeature = function(feature, resolu } var loading = false; if (Array.isArray(styles)) { - for (var i = 0, ii = styles.length; i < ii; ++i) { + for (var i = styles.length - 1, ii = 0; i >= ii; --i) { loading = ol.renderer.vector.renderFeature( replayGroup, feature, styles[i], ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio), diff --git a/src/ol/structs/linkedlist.js b/src/ol/structs/linkedlist.js new file mode 100644 index 0000000000..19fe515550 --- /dev/null +++ b/src/ol/structs/linkedlist.js @@ -0,0 +1,248 @@ +goog.provide('ol.structs.LinkedList'); + +/** + * Creates an empty linked list structure. + * + * @constructor + * @struct + * @param {boolean=} opt_circular The last item is connected to the first one, + * and the first item to the last one. Default is true. + */ +ol.structs.LinkedList = function(opt_circular) { + + /** + * @private + * @type {ol.LinkedListItem|undefined} + */ + this.first_ = undefined; + + /** + * @private + * @type {ol.LinkedListItem|undefined} + */ + this.last_ = undefined; + + /** + * @private + * @type {ol.LinkedListItem|undefined} + */ + this.head_ = undefined; + + /** + * @private + * @type {boolean} + */ + this.circular_ = opt_circular === undefined ? true : opt_circular; + + /** + * @private + * @type {number} + */ + this.length_ = 0; +}; + +/** + * Inserts an item into the linked list right after the current one. + * + * @param {?} data Item data. + */ +ol.structs.LinkedList.prototype.insertItem = function(data) { + + /** @type {ol.LinkedListItem} */ + var item = { + prev: undefined, + next: undefined, + data: data + }; + + var head = this.head_; + + //Initialize the list. + if (!head) { + this.first_ = item; + this.last_ = item; + if (this.circular_) { + item.next = item; + item.prev = item; + } + } else { + //Link the new item to the adjacent ones. + var next = head.next; + item.prev = head; + item.next = next; + head.next = item; + if (next) { + next.prev = item; + } + + if (head === this.last_) { + this.last_ = item; + } + } + this.head_ = item; + this.length_++; +}; + +/** + * Removes the current item from the list. Sets the cursor to the next item, + * if possible. + */ +ol.structs.LinkedList.prototype.removeItem = function() { + var head = this.head_; + if (head) { + var next = head.next; + var prev = head.prev; + if (next) { + next.prev = prev; + } + if (prev) { + prev.next = next; + } + this.head_ = next || prev; + + if (this.first_ === this.last_) { + this.head_ = undefined; + this.first_ = undefined; + this.last_ = undefined; + } else if (this.first_ === head) { + this.first_ = this.head_; + } else if (this.last_ === head) { + this.last_ = prev ? this.head_.prev : this.head_; + } + this.length_--; + } +}; + +/** + * Sets the cursor to the first item, and returns the associated data. + * + * @return {?} Item data. + */ +ol.structs.LinkedList.prototype.firstItem = function() { + this.head_ = this.first_; + if (this.head_) { + return this.head_.data; + } + return undefined; +}; + +/** +* Sets the cursor to the last item, and returns the associated data. +* +* @return {?} Item data. +*/ +ol.structs.LinkedList.prototype.lastItem = function() { + this.head_ = this.last_; + if (this.head_) { + return this.head_.data; + } + return undefined; +}; + +/** + * Sets the cursor to the next item, and returns the associated data. + * + * @return {?} Item data. + */ +ol.structs.LinkedList.prototype.nextItem = function() { + if (this.head_ && this.head_.next) { + this.head_ = this.head_.next; + return this.head_.data; + } + return undefined; +}; + +/** + * Returns the next item's data without moving the cursor. + * + * @return {?} Item data. + */ +ol.structs.LinkedList.prototype.getNextItem = function() { + if (this.head_ && this.head_.next) { + return this.head_.next.data; + } + return undefined; +}; + +/** + * Sets the cursor to the previous item, and returns the associated data. + * + * @return {?} Item data. + */ +ol.structs.LinkedList.prototype.prevItem = function() { + if (this.head_ && this.head_.prev) { + this.head_ = this.head_.prev; + return this.head_.data; + } + return undefined; +}; + +/** + * Returns the previous item's data without moving the cursor. + * + * @return {?} Item data. + */ +ol.structs.LinkedList.prototype.getPrevItem = function() { + if (this.head_ && this.head_.prev) { + return this.head_.prev.data; + } + return undefined; +}; + +/** + * Returns the current item's data. + * + * @return {?} Item data. + */ +ol.structs.LinkedList.prototype.getCurrItem = function() { + if (this.head_) { + return this.head_.data; + } + return undefined; +}; + +/** + * Sets the first item of the list. This only works for circular lists, and sets + * the last item accordingly. + */ +ol.structs.LinkedList.prototype.setFirstItem = function() { + if (this.circular_ && this.head_) { + this.first_ = this.head_; + this.last_ = this.head_.prev; + } +}; + +/** + * Concatenates two lists. + * @param {ol.structs.LinkedList} list List to merge into the current list. + */ +ol.structs.LinkedList.prototype.concat = function(list) { + if (list.head_) { + if (this.head_) { + var end = this.head_.next; + this.head_.next = list.first_; + list.first_.prev = this.head_; + end.prev = list.last_; + list.last_.next = end; + this.length_ += list.length_; + } else { + this.head_ = list.head_; + this.first_ = list.first_; + this.last_ = list.last_; + this.length_ = list.length_; + } + list.head_ = undefined; + list.first_ = undefined; + list.last_ = undefined; + list.length_ = 0; + } +}; + +/** + * Returns the current length of the list. + * + * @return {number} Length. + */ +ol.structs.LinkedList.prototype.getLength = function() { + return this.length_; +}; diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js index 90a9684a66..68782fbf3c 100644 --- a/src/ol/typedefs.js +++ b/src/ol/typedefs.js @@ -317,6 +317,14 @@ ol.LayerState; ol.LayoutOptions; +/** + * @typedef {{prev: (ol.LinkedListItem|undefined), + * next: (ol.LinkedListItem|undefined), + * data: ?}} + */ +ol.LinkedListItem; + + /** * A function that takes an {@link ol.Extent} and a resolution as arguments, and * returns an array of {@link ol.Extent} with the extents to load. Usually this @@ -686,6 +694,21 @@ ol.ViewAnimation; ol.WebglBufferCacheEntry; +/** + * @typedef {{p0: ol.WebglPolygonVertex, + * p1: ol.WebglPolygonVertex}} + */ +ol.WebglPolygonSegment; + +/** + * @typedef {{x: number, + * y: number, + * i: number, + * reflex: (boolean|undefined)}} + */ +ol.WebglPolygonVertex; + + /** * @typedef {{magFilter: number, minFilter: number, texture: WebGLTexture}} */ diff --git a/test/spec/ol/geom/flat/topologyflatgeom.test.js b/test/spec/ol/geom/flat/topologyflatgeom.test.js new file mode 100644 index 0000000000..f6ad907c69 --- /dev/null +++ b/test/spec/ol/geom/flat/topologyflatgeom.test.js @@ -0,0 +1,33 @@ +goog.provide('ol.test.geom.flat.topology'); + +goog.require('ol.geom.flat.topology'); + +describe('ol.geom.flat.topology', function() { + + describe('ol.geom.flat.topology.lineStringIsClosed', function() { + + it('identifies closed lines aka boundaries', function() { + var flatCoordinates = [0, 0, 3, 0, 0, 3, 0, 0]; + var isClosed = ol.geom.flat.topology.lineStringIsClosed(flatCoordinates, 0, flatCoordinates.length, 2); + expect(isClosed).to.be(true); + }); + + it('identifies regular linestrings', function() { + var flatCoordinates = [0, 0, 3, 0, 0, 3, 5, 2]; + var isClosed = ol.geom.flat.topology.lineStringIsClosed(flatCoordinates, 0, flatCoordinates.length, 2); + expect(isClosed).to.be(false); + }); + + it('identifies degenerate boundaries', function() { + var flatCoordinates = [0, 0, 3, 0, 0, 0]; + var isClosed = ol.geom.flat.topology.lineStringIsClosed(flatCoordinates, 0, flatCoordinates.length, 2); + expect(isClosed).to.be(false); + + flatCoordinates = [0, 0, 1, 1, 3, 3, 5, 5, 0, 0]; + isClosed = ol.geom.flat.topology.lineStringIsClosed(flatCoordinates, 0, flatCoordinates.length, 2); + expect(isClosed).to.be(false); + }); + + }); + +}); diff --git a/test/spec/ol/render/webgl/circlereplay.test.js b/test/spec/ol/render/webgl/circlereplay.test.js new file mode 100644 index 0000000000..5b8d061e02 --- /dev/null +++ b/test/spec/ol/render/webgl/circlereplay.test.js @@ -0,0 +1,244 @@ +goog.provide('ol.test.render.webgl.CircleReplay'); + +goog.require('ol'); +goog.require('ol.Feature'); +goog.require('ol.geom.Circle'); +goog.require('ol.render.webgl.circlereplay.defaultshader'); +goog.require('ol.render.webgl.CircleReplay'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); + +describe('ol.render.webgl.CircleReplay', function() { + var replay; + + var strokeStyle = new ol.style.Stroke({ + color: [0, 255, 0, 0.4] + }); + + var fillStyle = new ol.style.Fill({ + color: [255, 0, 0, 1] + }); + + beforeEach(function() { + var tolerance = 0.1; + var maxExtent = [-10000, -20000, 10000, 20000]; + replay = new ol.render.webgl.CircleReplay(tolerance, maxExtent); + }); + + describe('#setFillStrokeStyle', function() { + it('set expected states', function() { + replay.setFillStrokeStyle(fillStyle, strokeStyle); + expect(replay.state_).not.be(null); + expect(replay.state_.strokeColor).to.eql([0, 1, 0, 0.4]); + expect(replay.state_.lineWidth).to.be(1); + expect(replay.state_.fillColor).to.eql([1, 0, 0, 1]); + expect(replay.state_.changed).to.be(true); + expect(replay.styles_).to.have.length(1); + }); + + it('sets a transparent stroke, if none provided', function() { + replay.setFillStrokeStyle(fillStyle, null); + expect(replay.state_.strokeColor).to.eql([0, 0, 0, 0]); + }); + + it('sets a transparent fill, if none provided', function() { + replay.setFillStrokeStyle(null, strokeStyle); + expect(replay.state_.fillColor).to.eql([0, 0, 0, 0]); + }); + }); + + describe('#drawCircle', function() { + it('sets the buffer data', function() { + var circle = new ol.geom.Circle([0,0], 5000); + + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawCircle(circle, null); + expect(replay.vertices).to.have.length(16); + expect(replay.indices).to.have.length(6); + expect(replay.state_.changed).to.be(false); + expect(replay.startIndices).to.have.length(1); + expect(replay.startIndicesFeature).to.have.length(1); + expect(replay.radius_).to.be(5000); + }); + + it('does not draw if radius is zero', function() { + var circle = new ol.geom.Circle([0,0], 0); + + replay.drawCircle(circle, null); + expect(replay.vertices).to.have.length(0); + expect(replay.indices).to.have.length(0); + expect(replay.startIndices).to.have.length(0); + expect(replay.startIndicesFeature).to.have.length(0); + }); + + it('resets state and removes style if it belongs to a zero radius circle', function() { + var circle = new ol.geom.Circle([0,0], 0); + + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.setFillStrokeStyle(null, strokeStyle); + replay.drawCircle(circle, null); + expect(replay.styles_).to.have.length(1); + expect(replay.state_).not.be(null); + expect(replay.state_.strokeColor).to.eql([0, 1, 0, 0.4]); + expect(replay.state_.lineWidth).to.be(1); + expect(replay.state_.fillColor).to.eql([1, 0, 0, 1]); + expect(replay.state_.changed).to.be(false); + }); + }); + + describe('#drawCoordinates_', function() { + it('envelopes the circle into a right isosceles triangle', function() { + replay.radius_ = 5000; + replay.drawCoordinates_([0, 0], 0, 2, 2); + + expect(replay.vertices).to.eql([0, 0, 0, 5000, 0, 0, 1, 5000, + 0, 0, 2, 5000, 0, 0, 3, 5000]); + expect(replay.indices).to.eql([0, 1, 2, 2, 3, 0]); + }); + }); + + 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.circlereplay.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.circlereplay.defaultshader.fragment, + ol.render.webgl.circlereplay.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); + }); + }); + + describe('#drawReplay', function() { + var gl, context; + var feature1 = new ol.Feature({ + geometry: new ol.geom.Circle([0, 0], 5000) + }); + var feature2 = new ol.Feature({ + geometry: new ol.geom.Circle([10, 10], 5000) + }); + var feature3 = new ol.Feature({ + geometry: new ol.geom.Circle([20, 20], 5000) + }); + beforeEach(function() { + gl = {}; + context = {}; + replay.setFillStyle_ = function() {}; + replay.setStrokeStyle_ = function() {}; + replay.drawElements = function() {}; + sinon.spy(replay, 'setFillStyle_'); + sinon.spy(replay, 'setStrokeStyle_'); + sinon.spy(replay, 'drawElements'); + }); + + it('draws the elements in a single call if they have the same style', function() { + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawCircle(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawCircle(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawCircle(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + + replay.drawReplay(gl, context, {}, false); + expect(replay.setFillStyle_.calledOnce).to.be(true); + expect(replay.setStrokeStyle_.calledOnce).to.be(true); + expect(replay.drawElements.calledOnce).to.be(true); + }); + + it('draws the elements in batches if there are multiple styles', function() { + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawCircle(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(fillStyle, null); + replay.drawCircle(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(strokeStyle, null); + replay.drawCircle(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + + replay.drawReplay(gl, context, {}, false); + expect(replay.setFillStyle_.calledThrice).to.be(true); + expect(replay.setStrokeStyle_.calledThrice).to.be(true); + expect(replay.drawElements.calledThrice).to.be(true); + }); + + it('can skip elements if needed', function() { + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawCircle(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawCircle(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawCircle(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + var skippedFeatHash = {}; + skippedFeatHash[ol.getUid(feature2).toString()] = true; + + replay.drawReplay(gl, context, skippedFeatHash, false); + expect(replay.setFillStyle_.calledOnce).to.be(true); + expect(replay.setStrokeStyle_.calledOnce).to.be(true); + expect(replay.drawElements.calledTwice).to.be(true); + }); + }); +}); diff --git a/test/spec/ol/render/webgl/imagereplay.test.js b/test/spec/ol/render/webgl/imagereplay.test.js new file mode 100644 index 0000000000..7e5b1ce621 --- /dev/null +++ b/test/spec/ol/render/webgl/imagereplay.test.js @@ -0,0 +1,245 @@ +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'); + +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.getHitDetectionImage = function() { + return image; + }; + imageStyle.getImageSize = function() { + return [512, 512]; + }; + imageStyle.getHitDetectionImageSize = 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); + expect(replay.hitDetectionImages_).to.have.length(1); + 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.hitDetectionImages_).to.have.length(1); + 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.hitDetectionImages_).to.have.length(2); + expect(replay.hitDetectionGroupIndices_).to.have.length(1); + }); + }); + + describe('#drawPoint', 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.drawPoint(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.drawPoint(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('#drawMultiPoint', 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.drawMultiPoint(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.drawMultiPoint(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); + }); + }); + + 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.imagereplay.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.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); + }); + }); + + 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/spec/ol/render/webgl/immediate.test.js b/test/spec/ol/render/webgl/immediate.test.js new file mode 100644 index 0000000000..f3f34b44d7 --- /dev/null +++ b/test/spec/ol/render/webgl/immediate.test.js @@ -0,0 +1,272 @@ +goog.provide('ol.test.render.webgl.Immediate'); + +goog.require('ol.geom.GeometryCollection'); +goog.require('ol.geom.Circle'); +goog.require('ol.geom.Point'); +goog.require('ol.geom.MultiPoint'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.MultiLineString'); +goog.require('ol.geom.Polygon'); +goog.require('ol.geom.MultiPolygon'); +goog.require('ol.render.webgl.ImageReplay'); +goog.require('ol.render.webgl.CircleReplay'); +goog.require('ol.render.webgl.LineStringReplay'); +goog.require('ol.render.webgl.PolygonReplay'); +goog.require('ol.style.Circle'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + +describe('ol.render.webgl.Immediate', function() { + var context, style, circle, line, multiLine, point, multiPoint, polygon, multiPolygon; + beforeEach(function() { + context = new ol.render.webgl.Immediate({}, [0, 0], 0, 0, [0, 0], [-180, -90, 180, 90], 1); + style = new ol.style.Style({ + image: new ol.style.Circle(), + fill: new ol.style.Fill(), + stroke: new ol.style.Stroke() + }); + circle = new ol.geom.Circle([0, 0], 5); + line = new ol.geom.LineString([[0, 0], [5, 5]]); + multiLine = new ol.geom.MultiLineString([[[0, 0], [5, 5]]]); + point = new ol.geom.Point([0, 0]); + multiPoint = new ol.geom.MultiPoint([[0, 0]]); + polygon = new ol.geom.Polygon([[[0, 0], [5, 5], [5, 0], [0, 0]]]); + multiPolygon = new ol.geom.MultiPolygon([[[[0, 0], [5, 5], [5, 0], [0, 0]]]]); + }); + + describe('#setStyle', function() { + it('sets the style of the context', function() { + context.setStyle(style); + expect(context.fillStyle_).to.be(style.getFill()); + expect(context.strokeStyle_).to.be(style.getStroke()); + expect(context.imageStyle_).to.be(style.getImage()); + }); + }); + + describe('#drawFeature', function() { + var feat; + beforeEach(function() { + feat = new ol.Feature({ + geometry: circle + }); + context.setStyle = function() {}; + context.drawGeometry = function() {}; + sinon.spy(context, 'setStyle'); + sinon.spy(context, 'drawGeometry'); + }); + + it('updates the style of the context', function() { + context.drawFeature(feat, style); + expect(context.setStyle.calledOnce).to.be(true); + }); + + it('draws the geometry of the feature', function() { + context.drawFeature(feat, style); + expect(context.drawGeometry.calledOnce).to.be(true); + }); + + it('does nothing if no geometry is provided', function() { + feat = new ol.Feature(); + context.drawFeature(feat, style); + expect(context.setStyle.called).to.be(false); + expect(context.drawGeometry.called).to.be(false); + }); + + it('does nothing if geometry is out of bounds', function() { + feat = new ol.Feature({ + geometry: new ol.geom.Circle([540, 540], 1) + }); + context.drawFeature(feat, style); + expect(context.setStyle.called).to.be(false); + expect(context.drawGeometry.called).to.be(false); + }); + }); + + describe('#drawGeometryCollection', function() { + var geomColl; + beforeEach(function() { + geomColl = new ol.geom.GeometryCollection([circle, point, multiPoint, + line, multiLine, polygon, multiPolygon]); + }); + + it('draws every geometry in the collection', function() { + context.drawGeometry = function() {}; + sinon.spy(context, 'drawGeometry'); + + context.drawGeometryCollection(geomColl); + expect(context.drawGeometry.callCount).to.be(7); + }); + }); + + describe('geometry functions', function() { + function mock(ctor, geomFunc) { + var tmpObj = {}; + tmpObj.replay = ctor.prototype.replay; + ctor.prototype.replay = sinon.spy(); + tmpObj.finish = ctor.prototype.finish; + ctor.prototype.finish = sinon.spy(); + tmpObj.getDeleteResourcesFunction = ctor.prototype.getDeleteResourcesFunction; + ctor.prototype.getDeleteResourcesFunction = sinon.spy(function() { + return function() {}; + }); + sinon.spy(ctor.prototype.getDeleteResourcesFunction); + if (ctor === ol.render.webgl.ImageReplay) { + tmpObj.setImageStyle = ctor.prototype.setImageStyle; + ctor.prototype.setImageStyle = sinon.spy(); + } else { + tmpObj.setFillStrokeStyle = ctor.prototype.setFillStrokeStyle; + ctor.prototype.setFillStrokeStyle = sinon.spy(); + } + tmpObj[geomFunc] = ctor.prototype[geomFunc]; + ctor.prototype[geomFunc] = sinon.spy(); + return tmpObj; + } + + function restore(ctor, tmpObj) { + for (var i in tmpObj) { + ctor.prototype[i] = tmpObj[i]; + } + } + + describe('#drawPoint', function() { + var tmpObj; + beforeEach(function() { + tmpObj = mock(ol.render.webgl.ImageReplay, 'drawPoint'); + }); + + it('draws a point', function() { + context.drawGeometry(point); + expect(ol.render.webgl.ImageReplay.prototype.setImageStyle.calledOnce).to.be(true); + expect(ol.render.webgl.ImageReplay.prototype.drawPoint.calledOnce).to.be(true); + expect(ol.render.webgl.ImageReplay.prototype.finish.calledOnce).to.be(true); + expect(ol.render.webgl.ImageReplay.prototype.replay.calledOnce).to.be(true); + expect(ol.render.webgl.ImageReplay.prototype.getDeleteResourcesFunction.calledOnce).to.be(true); + }); + + after(function() { + restore(ol.render.webgl.ImageReplay, tmpObj); + }); + }); + + describe('#drawMultiPoint', function() { + var tmpObj; + beforeEach(function() { + tmpObj = mock(ol.render.webgl.ImageReplay, 'drawMultiPoint'); + }); + + it('draws a multi point', function() { + context.drawGeometry(multiPoint); + expect(ol.render.webgl.ImageReplay.prototype.setImageStyle.calledOnce).to.be(true); + expect(ol.render.webgl.ImageReplay.prototype.drawMultiPoint.calledOnce).to.be(true); + expect(ol.render.webgl.ImageReplay.prototype.finish.calledOnce).to.be(true); + expect(ol.render.webgl.ImageReplay.prototype.replay.calledOnce).to.be(true); + expect(ol.render.webgl.ImageReplay.prototype.getDeleteResourcesFunction.calledOnce).to.be(true); + }); + + after(function() { + restore(ol.render.webgl.ImageReplay, tmpObj); + }); + }); + + describe('#drawLineString', function() { + var tmpObj; + beforeEach(function() { + tmpObj = mock(ol.render.webgl.LineStringReplay, 'drawLineString'); + }); + + it('draws a line string', function() { + context.drawGeometry(line); + expect(ol.render.webgl.LineStringReplay.prototype.setFillStrokeStyle.calledOnce).to.be(true); + expect(ol.render.webgl.LineStringReplay.prototype.drawLineString.calledOnce).to.be(true); + expect(ol.render.webgl.LineStringReplay.prototype.finish.calledOnce).to.be(true); + expect(ol.render.webgl.LineStringReplay.prototype.replay.calledOnce).to.be(true); + expect(ol.render.webgl.LineStringReplay.prototype.getDeleteResourcesFunction.calledOnce).to.be(true); + }); + + after(function() { + restore(ol.render.webgl.LineStringReplay, tmpObj); + }); + }); + + describe('#drawMultiLineString', function() { + var tmpObj; + beforeEach(function() { + tmpObj = mock(ol.render.webgl.LineStringReplay, 'drawMultiLineString'); + }); + + it('draws a multi line string', function() { + context.drawGeometry(multiLine); + expect(ol.render.webgl.LineStringReplay.prototype.setFillStrokeStyle.calledOnce).to.be(true); + expect(ol.render.webgl.LineStringReplay.prototype.drawMultiLineString.calledOnce).to.be(true); + expect(ol.render.webgl.LineStringReplay.prototype.finish.calledOnce).to.be(true); + expect(ol.render.webgl.LineStringReplay.prototype.replay.calledOnce).to.be(true); + expect(ol.render.webgl.LineStringReplay.prototype.getDeleteResourcesFunction.calledOnce).to.be(true); + }); + + after(function() { + restore(ol.render.webgl.LineStringReplay, tmpObj); + }); + }); + + describe('#drawPolygon', function() { + var tmpObj; + beforeEach(function() { + tmpObj = mock(ol.render.webgl.PolygonReplay, 'drawPolygon'); + }); + + it('draws a polygon', function() { + context.drawGeometry(polygon); + expect(ol.render.webgl.PolygonReplay.prototype.setFillStrokeStyle.calledOnce).to.be(true); + expect(ol.render.webgl.PolygonReplay.prototype.drawPolygon.calledOnce).to.be(true); + expect(ol.render.webgl.PolygonReplay.prototype.finish.calledOnce).to.be(true); + expect(ol.render.webgl.PolygonReplay.prototype.replay.calledOnce).to.be(true); + expect(ol.render.webgl.PolygonReplay.prototype.getDeleteResourcesFunction.calledOnce).to.be(true); + }); + + after(function() { + restore(ol.render.webgl.PolygonReplay, tmpObj); + }); + }); + + describe('#drawMultiPolygon', function() { + var tmpObj; + beforeEach(function() { + tmpObj = mock(ol.render.webgl.PolygonReplay, 'drawMultiPolygon'); + }); + + it('draws a multi polygon', function() { + context.drawGeometry(multiPolygon); + expect(ol.render.webgl.PolygonReplay.prototype.setFillStrokeStyle.calledOnce).to.be(true); + expect(ol.render.webgl.PolygonReplay.prototype.drawMultiPolygon.calledOnce).to.be(true); + expect(ol.render.webgl.PolygonReplay.prototype.finish.calledOnce).to.be(true); + expect(ol.render.webgl.PolygonReplay.prototype.replay.calledOnce).to.be(true); + expect(ol.render.webgl.PolygonReplay.prototype.getDeleteResourcesFunction.calledOnce).to.be(true); + }); + + after(function() { + restore(ol.render.webgl.PolygonReplay, tmpObj); + }); + }); + + describe('#drawCircle', function() { + var tmpObj; + beforeEach(function() { + tmpObj = mock(ol.render.webgl.CircleReplay, 'drawCircle'); + }); + + it('draws a circle', function() { + context.drawGeometry(circle); + expect(ol.render.webgl.CircleReplay.prototype.setFillStrokeStyle.calledOnce).to.be(true); + expect(ol.render.webgl.CircleReplay.prototype.drawCircle.calledOnce).to.be(true); + expect(ol.render.webgl.CircleReplay.prototype.finish.calledOnce).to.be(true); + expect(ol.render.webgl.CircleReplay.prototype.replay.calledOnce).to.be(true); + expect(ol.render.webgl.CircleReplay.prototype.getDeleteResourcesFunction.calledOnce).to.be(true); + }); + + after(function() { + restore(ol.render.webgl.CircleReplay, tmpObj); + }); + }); + }); +}); diff --git a/test/spec/ol/render/webgl/index.test.js b/test/spec/ol/render/webgl/index.test.js new file mode 100644 index 0000000000..5426879bb3 --- /dev/null +++ b/test/spec/ol/render/webgl/index.test.js @@ -0,0 +1,62 @@ +goog.provide('ol.test.render.webgl.Replay'); + +goog.require('ol.render.webgl.Replay'); + +describe('ol.render.Replay', function() { + var replay; + beforeEach(function() { + replay = new ol.render.webgl.Replay(5, [-180, -90, 180, 90]); + }); + + + describe('constructor', function() { + it('stores view related data', function() { + expect(replay.tolerance).to.be(5); + expect(replay.maxExtent).to.eql([-180, -90, 180, 90]); + expect(replay.origin).to.eql([0, 0]); + }); + + it ('sets up the required matrices', function() { + var mat3 = [1, 0, 0, 1, 0, 0]; + var mat4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; + expect(replay.projectionMatrix_).to.eql(mat3); + expect(replay.offsetRotateMatrix_).to.eql(mat3); + expect(replay.offsetScaleMatrix_).to.eql(mat3); + expect(replay.tmpMat4_).to.eql(mat4); + }); + }); + + describe('#replay', function() { + var gl = { + uniformMatrix4fv: function() {}, + uniform1f: function() {} + }; + var context = { + bindBuffer: function() {}, + getGL: function() { + return gl; + } + }; + beforeEach(function() { + replay.setUpProgram = function() { + return { + u_projectionMatrix: true, + u_offsetScaleMatrix: true, + u_offsetRotateMatrix: true, + u_opacity: true + }; + }; + }); + + it('calculates the correct matrices', function() { + var sin = Math.sin(Math.PI); + replay.replay(context, [0, 0], 10, Math.PI, [10, 10], 1, 0, {}, undefined, + false, undefined); + + expect(replay.projectionMatrix_).to.eql([-0.02, -sin * 0.02, sin * 0.02, + -0.02, 0, 0]); + expect(replay.offsetRotateMatrix_).to.eql([-1, -sin, sin, -1, 0, 0]); + expect(replay.offsetScaleMatrix_).to.eql([0.2, 0, 0, 0.2, 0, 0]); + }); + }); +}); diff --git a/test/spec/ol/render/webgl/linestringreplay.test.js b/test/spec/ol/render/webgl/linestringreplay.test.js new file mode 100644 index 0000000000..219a26a1c9 --- /dev/null +++ b/test/spec/ol/render/webgl/linestringreplay.test.js @@ -0,0 +1,363 @@ +goog.provide('ol.test.render.webgl.LineStringReplay'); + +goog.require('ol'); +goog.require('ol.Feature'); +goog.require('ol.geom.LineString'); +goog.require('ol.geom.MultiLineString'); +goog.require('ol.render.webgl.linestringreplay.defaultshader'); +goog.require('ol.render.webgl.LineStringReplay'); +goog.require('ol.style.Stroke'); + +describe('ol.render.webgl.LineStringReplay', function() { + var replay; + + var strokeStyle1 = new ol.style.Stroke({ + color: [0, 255, 0, 0.4] + }); + + var strokeStyle2 = new ol.style.Stroke({ + color: [255, 0, 0, 1], + lineCap: 'square', + lineJoin: 'miter' + }); + + beforeEach(function() { + var tolerance = 0.1; + var maxExtent = [-10000, -20000, 10000, 20000]; + replay = new ol.render.webgl.LineStringReplay(tolerance, maxExtent); + }); + + describe('#setFillStrokeStyle', function() { + + it('set expected states', function() { + replay.setFillStrokeStyle(null, strokeStyle1); + expect(replay.state_).not.be(null); + expect(replay.state_.lineCap).to.be('round'); + expect(replay.state_.lineJoin).to.be('round'); + expect(replay.state_.strokeColor).to.eql([0, 1, 0, 0.4]); + expect(replay.state_.lineWidth).to.be(1); + expect(replay.state_.miterLimit).to.be(10); + expect(replay.state_.changed).to.be(true); + expect(replay.styles_).to.have.length(1); + + replay.setFillStrokeStyle(null, strokeStyle2); + expect(replay.state_.lineCap).to.be('square'); + expect(replay.state_.lineJoin).to.be('miter'); + expect(replay.state_.strokeColor).to.eql([1, 0, 0, 1]); + expect(replay.state_.lineWidth).to.be(1); + expect(replay.state_.miterLimit).to.be(10); + expect(replay.state_.changed).to.be(true); + expect(replay.styles_).to.have.length(2); + + }); + }); + + describe('#drawLineString', function() { + + it('sets the buffer data', function() { + var linestring; + + linestring = new ol.geom.LineString( + [[1000, 2000], [2000, 3000]]); + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(linestring, null); + expect(replay.vertices).to.have.length(56); + expect(replay.indices).to.have.length(18); + expect(replay.state_.changed).to.be(false); + expect(replay.startIndices).to.have.length(1); + expect(replay.startIndicesFeature).to.have.length(1); + + linestring = new ol.geom.LineString( + [[1000, 3000], [2000, 4000], [3000, 3000]]); + replay.drawLineString(linestring, null); + expect(replay.vertices).to.have.length(140); + expect(replay.indices).to.have.length(48); + expect(replay.state_.changed).to.be(false); + expect(replay.startIndices).to.have.length(2); + expect(replay.startIndicesFeature).to.have.length(2); + }); + }); + + describe('#drawMultiLineString', function() { + + it('sets the buffer data', function() { + var multilinestring; + + multilinestring = new ol.geom.MultiLineString( + [[[1000, 2000], [2000, 3000]], + [[1000, 3000], [2000, 4000], [3000, 3000]]]); + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawMultiLineString(multilinestring, null); + expect(replay.vertices).to.have.length(140); + expect(replay.indices).to.have.length(48); + expect(replay.state_.changed).to.be(false); + expect(replay.startIndices).to.have.length(1); + expect(replay.startIndicesFeature).to.have.length(1); + }); + }); + + describe('#drawCoordinates_', function() { + + it('triangulates linestrings', function() { + var linestring; + + var stroke = new ol.style.Stroke({ + color: [0, 255, 0, 1], + lineCap: 'butt', + lineJoin: 'bevel' + }); + + linestring = new ol.geom.LineString( + [[1000, 3000], [2000, 4000], [3000, 3000]]); + var flatCoordinates = linestring.getFlatCoordinates(); + replay.setFillStrokeStyle(null, stroke); + replay.drawCoordinates_(flatCoordinates, 0, + flatCoordinates.length, 2); + expect(replay.indices).to.eql( + [2, 0, 1, 4, 2, 1, + 2, 4, 3, + 5, 3, 4, 4, 6, 5]); + }); + + it('optionally creates miters', function() { + var linestring; + + var stroke = new ol.style.Stroke({ + color: [0, 255, 0, 1], + lineCap: 'butt' + }); + + linestring = new ol.geom.LineString( + [[1000, 3000], [2000, 4000], [3000, 3000]]); + var flatCoordinates = linestring.getFlatCoordinates(); + replay.setFillStrokeStyle(null, stroke); + replay.drawCoordinates_(flatCoordinates, 0, + flatCoordinates.length, 2); + expect(replay.indices).to.eql( + [2, 0, 1, 4, 2, 1, + 2, 4, 3, 3, 5, 2, + 6, 3, 4, 4, 7, 6]); + }); + + it('optionally creates caps', function() { + var linestring; + + var stroke = new ol.style.Stroke({ + color: [0, 255, 0, 1] + }); + + linestring = new ol.geom.LineString( + [[1000, 3000], [2000, 4000], [3000, 3000]]); + var flatCoordinates = linestring.getFlatCoordinates(); + replay.setFillStrokeStyle(null, stroke); + replay.drawCoordinates_(flatCoordinates, 0, + flatCoordinates.length, 2); + expect(replay.indices).to.eql( + [2, 0, 1, 1, 3, 2, + 4, 2, 3, 6, 4, 3, + 4, 6, 5, 5, 7, 4, + 8, 5, 6, 6, 9, 8, + 10, 8, 9, 9, 11, 10]); + }); + + it('respects segment orientation', function() { + var linestring; + + var stroke = new ol.style.Stroke({ + color: [0, 255, 0, 1], + lineCap: 'butt', + lineJoin: 'bevel' + }); + + linestring = new ol.geom.LineString( + [[1000, 3000], [2000, 2000], [3000, 3000]]); + var flatCoordinates = linestring.getFlatCoordinates(); + replay.setFillStrokeStyle(null, stroke); + replay.drawCoordinates_(flatCoordinates, 0, + flatCoordinates.length, 2); + expect(replay.indices).to.eql( + [2, 0, 1, 4, 2, 0, + 2, 4, 3, + 5, 3, 4, 4, 6, 5]); + }); + + it('closes boundaries', function() { + var linestring; + + var stroke = new ol.style.Stroke({ + color: [0, 255, 0, 1], + lineCap: 'butt', + lineJoin: 'bevel' + }); + + linestring = new ol.geom.LineString( + [[1000, 3000], [2000, 4000], [3000, 3000], [1000, 3000]]); + var flatCoordinates = linestring.getFlatCoordinates(); + replay.setFillStrokeStyle(null, stroke); + replay.drawCoordinates_(flatCoordinates, 0, + flatCoordinates.length, 2); + expect(replay.indices).to.eql( + [0, 2, 1, 3, 1, 2, + 5, 3, 2, + 3, 5, 4, 6, 4, 5, + 8, 6, 5, + 6, 8, 7, 9, 7, 8, + 10, 9, 8]); + expect(replay.vertices.slice(0, 7)).to.eql( + replay.vertices.slice(-14, -7)); + expect(replay.vertices.slice(14, 21)).to.eql( + replay.vertices.slice(-7)); + }); + }); + + 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.linestringreplay.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.linestringreplay.defaultshader.fragment, + ol.render.webgl.linestringreplay.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); + }); + }); + + describe('#drawReplay', function() { + var gl, context; + var feature1 = new ol.Feature({ + geometry: new ol.geom.LineString([[0, 0], [500, 500]]) + }); + var feature2 = new ol.Feature({ + geometry: new ol.geom.LineString([[0, 0], [500, 500]]) + }); + var feature3 = new ol.Feature({ + geometry: new ol.geom.LineString([[0, 0], [500, 500]]) + }); + beforeEach(function() { + gl = { + enable: function() {}, + disable: function() {}, + depthMask: function() {}, + depthFunc: function() {}, + clear: function() {}, + getParameter: function() {} + }; + context = {}; + replay.setStrokeStyle_ = function() {}; + replay.drawElements = function() {}; + sinon.spy(replay, 'setStrokeStyle_'); + sinon.spy(replay, 'drawElements'); + sinon.spy(gl, 'clear'); + }); + + it('draws the elements in a single call if they have the same style', function() { + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + + replay.drawReplay(gl, context, {}, false); + expect(replay.setStrokeStyle_.calledOnce).to.be(true); + expect(replay.drawElements.calledOnce).to.be(true); + expect(gl.clear.called).to.be(true); + }); + + it('draws the elements in batches if there are multiple styles', function() { + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(null, strokeStyle2); + replay.drawLineString(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + + replay.drawReplay(gl, context, {}, false); + expect(replay.setStrokeStyle_.calledThrice).to.be(true); + expect(replay.drawElements.calledThrice).to.be(true); + expect(gl.clear.called).to.be(true); + }); + + it('can skip elements if needed', function() { + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(null, strokeStyle1); + replay.drawLineString(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + var skippedFeatHash = {}; + skippedFeatHash[ol.getUid(feature2).toString()] = true; + + replay.drawReplay(gl, context, skippedFeatHash, false); + expect(replay.setStrokeStyle_.calledOnce).to.be(true); + expect(replay.drawElements.calledTwice).to.be(true); + expect(gl.clear.called).to.be(true); + }); + }); +}); diff --git a/test/spec/ol/render/webgl/polygonreplay.test.js b/test/spec/ol/render/webgl/polygonreplay.test.js new file mode 100644 index 0000000000..d67431fc59 --- /dev/null +++ b/test/spec/ol/render/webgl/polygonreplay.test.js @@ -0,0 +1,471 @@ +goog.provide('ol.test.render.webgl.PolygonReplay'); + +goog.require('ol'); +goog.require('ol.Feature'); +goog.require('ol.geom.MultiPolygon'); +goog.require('ol.geom.Polygon'); +goog.require('ol.render.webgl.polygonreplay.defaultshader'); +goog.require('ol.render.webgl.PolygonReplay'); +goog.require('ol.structs.LinkedList'); +goog.require('ol.structs.RBush'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Stroke'); + +describe('ol.render.webgl.PolygonReplay', function() { + var replay; + + var fillStyle = new ol.style.Fill({ + color: [0, 0, 255, 0.5] + }); + var strokeStyle = new ol.style.Stroke({ + color: [0, 255, 0, 0.4] + }); + + beforeEach(function() { + var tolerance = 0.1; + var maxExtent = [-10000, -20000, 10000, 20000]; + replay = new ol.render.webgl.PolygonReplay(tolerance, maxExtent); + }); + + describe('#drawPolygon', function() { + beforeEach(function() { + replay.setFillStrokeStyle(fillStyle, strokeStyle); + }); + + it('sets the buffer data', function() { + var polygon1 = new ol.geom.Polygon( + [[[1000, 2000], [1200, 2000], [1200, 3000]]] + ); + replay.drawPolygon(polygon1, null); + expect(replay.vertices).to.have.length(8); + expect(replay.indices).to.have.length(3); + + expect(replay.vertices).to.eql([ + 1000, 2000, 1200, 3000, 1200, 2000, 1000, 2000]); + expect(replay.indices).to.eql([2, 0, 1]); + + var polygon2 = new ol.geom.Polygon( + [[[4000, 2000], [4200, 2000], [4200, 3000]]] + ); + replay.drawPolygon(polygon2, null); + expect(replay.vertices).to.have.length(16); + expect(replay.indices).to.have.length(6); + + expect(replay.vertices).to.eql([ + 1000, 2000, 1200, 3000, 1200, 2000, 1000, 2000, + 4000, 2000, 4200, 3000, 4200, 2000, 4000, 2000 + ]); + expect(replay.indices).to.eql([2, 0, 1, 6, 4, 5]); + }); + }); + + describe('#drawMultiPolygon', function() { + beforeEach(function() { + replay.setFillStrokeStyle(fillStyle, strokeStyle); + }); + + it('sets the buffer data', function() { + var multiPolygon = new ol.geom.MultiPolygon([ + [[[1000, 2000], [1200, 2000], [1200, 3000]]], + [[[4000, 2000], [4200, 2000], [4200, 3000]]] + ]); + replay.drawMultiPolygon(multiPolygon, null); + expect(replay.vertices).to.have.length(16); + expect(replay.indices).to.have.length(6); + + expect(replay.vertices).to.eql([ + 1000, 2000, 1200, 3000, 1200, 2000, 1000, 2000, + 4000, 2000, 4200, 3000, 4200, 2000, 4000, 2000 + ]); + expect(replay.indices).to.eql([2, 0, 1, 6, 4, 5]); + }); + }); + + describe('triangulating functions', function() { + var list, rtree; + beforeEach(function() { + list = new ol.structs.LinkedList(); + rtree = new ol.structs.RBush(); + }); + + describe('#createPoint_', function() { + it('creates a WebGL polygon vertex', function() { + var p = replay.createPoint_(1, 1, 1); + expect(p.x).to.be(1); + expect(p.y).to.be(1); + expect(p.i).to.be(1); + expect(p.reflex).to.be(undefined); + }); + + it('adds the point to the vertex array', function() { + replay.createPoint_(1, 1, 1); + expect(replay.vertices.length).to.be(2); + expect(replay.vertices[0]).to.be(1); + expect(replay.vertices[1]).to.be(1); + }); + }); + + describe('#insertItem_', function() { + var p0, p1; + beforeEach(function() { + p0 = replay.createPoint_(1, 1, 1); + p1 = replay.createPoint_(2, 2, 2); + }); + + it('creates a WebGL polygon segment', function() { + var seg = replay.insertItem_(p0, p1, list, rtree); + expect(seg.p0).to.be(p0); + expect(seg.p1).to.be(p1); + }); + + it('inserts the segment into the provided linked list', function() { + var seg = replay.insertItem_(p0, p1, list, rtree); + expect(list.head_.data).to.be(seg); + }); + + it('inserts the segment into the R-Tree, if provided', function() { + replay.insertItem_(p0, p1, list); + expect(rtree.isEmpty()).to.be(true); + replay.insertItem_(p0, p1, list, rtree); + expect(rtree.isEmpty()).to.be(false); + }); + }); + + describe('#removeItem_', function() { + var s0, s1; + beforeEach(function() { + var p = replay.createPoint_(2, 2, 2); + s0 = replay.insertItem_(replay.createPoint_(1, 1, 1), + p, list, rtree); + s1 = replay.insertItem_(p, + replay.createPoint_(5, 2, 3), list, rtree); + }); + + it('removes the current item', function() { + replay.removeItem_(s0, s1, list, rtree); + expect(list.head_.data).not.to.be(s1); + expect(rtree.getAll().length).to.be(1); + }); + + it('updates the preceding segment', function() { + var dataExtent = rtree.getExtent(); + replay.removeItem_(s0, s1, list, rtree); + expect(s0.p1).to.be(s1.p1); + expect(rtree.getExtent()).to.eql(dataExtent); + }); + }); + + describe('#getPointsInTriangle_', function() { + var p0, p1, p2, p3; + beforeEach(function() { + p0 = replay.createPoint_(2, 0, 0); + p1 = replay.createPoint_(0, 5, 1); + p2 = replay.createPoint_(2, 3, 2); + p3 = replay.createPoint_(4, 5, 3); + replay.insertItem_(p0, p1, list, rtree); + replay.insertItem_(p1, p2, list, rtree); + replay.insertItem_(p2, p3, list, rtree); + replay.insertItem_(p3, p0, list, rtree); + replay.classifyPoints_(list, rtree, false); + }); + + it('gets every point in a triangle', function() { + var points = replay.getPointsInTriangle_({x: -3, y: 6}, {x: 7, y: 6}, + {x: 2, y: 2}, rtree); + expect(points).to.eql([p1, p2, p3]); + }); + + it('gets only reflex points in a triangle', function() { + var points = replay.getPointsInTriangle_({x: -3, y: 6}, {x: 7, y: 6}, + {x: 2, y: 2}, rtree, true); + expect(points).to.eql([p2]); + }); + }); + + describe('#getIntersections_', function() { + var p0, p1, p2, p3, s0, s1, s2, s3; + beforeEach(function() { + p0 = replay.createPoint_(2, 0, 0); + p1 = replay.createPoint_(0, 5, 1); + p2 = replay.createPoint_(2, 3, 2); + p3 = replay.createPoint_(4, 5, 3); + s0 = replay.insertItem_(p0, p1, list, rtree); + s1 = replay.insertItem_(p1, p2, list, rtree); + s2 = replay.insertItem_(p2, p3, list, rtree); + s3 = replay.insertItem_(p3, p0, list, rtree); + }); + + it('gets intersecting, but non touching segments', function() { + var segments = replay.getIntersections_({p0: {x: 0, y: 3}, p1: {x: 4, y: 5}}, + rtree); + expect(segments).to.eql([s0, s1]); + }); + + it('gets intersecting and touching segments', function() { + var segments = replay.getIntersections_({p0: {x: 0, y: 3}, p1: {x: 4, y: 5}}, + rtree, true); + expect(segments).to.eql([s0, s1, s2, s3]); + }); + }); + + describe('#calculateIntersection_', function() { + var p0 = {x: 0, y: 0}; + var p1 = {x: 4, y: 4}; + var p2 = {x: 0, y: 4}; + var p3 = {x: 4, y: 0}; + + it('calculates the intersection point of two intersecting segments', function() { + var i = replay.calculateIntersection_(p0, p1, p2, p3); + var t = replay.calculateIntersection_(p0, p1, p1, p2); + expect(i).to.eql([2, 2]); + expect(t).to.be(undefined); + }); + + it('calculates the intersection point of two touching segments', function() { + var t = replay.calculateIntersection_(p0, p1, p1, p2, true); + expect(t).to.eql([4, 4]); + }); + }); + + describe('#diagonalIsInside_', function() { + var p0, p1, p2, p3; + beforeEach(function() { + p0 = replay.createPoint_(2, 0, 0); + p1 = replay.createPoint_(0, 5, 1); + p2 = replay.createPoint_(2, 3, 2); + p3 = replay.createPoint_(4, 5, 3); + replay.insertItem_(p0, p1, list, rtree); + replay.insertItem_(p1, p2, list, rtree); + replay.insertItem_(p2, p3, list, rtree); + replay.insertItem_(p3, p0, list, rtree); + replay.classifyPoints_(list, rtree, false); + }); + + it('identifies if diagonal is inside the polygon', function() { + var inside = replay.diagonalIsInside_(p1, p2, p3, p0, p1); + expect(inside).to.be(true); + }); + + it('identifies if diagonal is outside the polygon', function() { + var inside = replay.diagonalIsInside_(p0, p1, p2, p3, p0); + expect(inside).to.be(false); + }); + }); + + describe('#classifyPoints_', function() { + var p0, p1, p2, p3; + beforeEach(function() { + p0 = replay.createPoint_(2, 0, 0); + p1 = replay.createPoint_(0, 5, 1); + p2 = replay.createPoint_(2, 3, 2); + p3 = replay.createPoint_(4, 5, 3); + replay.insertItem_(p0, p1, list, rtree); + replay.insertItem_(p1, p2, list, rtree); + replay.insertItem_(p2, p3, list, rtree); + replay.insertItem_(p3, p0, list, rtree); + }); + + it('classifies the points of clockwise polygons', function() { + replay.classifyPoints_(list, rtree, false); + expect(p0.reflex).to.be(false); + expect(p1.reflex).to.be(false); + expect(p2.reflex).to.be(true); + expect(p3.reflex).to.be(false); + }); + + it('classifies the points of counter-clockwise polygons', function() { + replay.classifyPoints_(list, rtree, true); + expect(p0.reflex).to.be(true); + expect(p1.reflex).to.be(true); + expect(p2.reflex).to.be(false); + expect(p3.reflex).to.be(true); + }); + + it('removes collinear points', function() { + replay.insertItem_(p3, p0, list, rtree); + replay.classifyPoints_(list, rtree, false); + expect(list.getLength()).to.be(4); + expect(rtree.getAll().length).to.be(4); + }); + }); + + describe('#isSimple_', function() { + var p0, p1, p2, p3; + beforeEach(function() { + p0 = replay.createPoint_(2, 0, 0); + p1 = replay.createPoint_(0, 5, 1); + p2 = replay.createPoint_(2, 3, 2); + p3 = replay.createPoint_(4, 5, 3); + replay.insertItem_(p0, p1, list, rtree); + replay.insertItem_(p1, p2, list, rtree); + replay.insertItem_(p2, p3, list, rtree); + replay.insertItem_(p3, p0, list, rtree); + }); + + it('identifies simple polygons', function() { + var simple = replay.isSimple_(list, rtree); + expect(simple).to.be(true); + }); + + it('identifies self-intersecting polygons', function() { + var p4 = replay.createPoint_(2, 5, 4); + var p5 = replay.createPoint_(4, 2, 5); + replay.insertItem_(p0, p4, list, rtree); + replay.insertItem_(p4, p5, list, rtree); + replay.insertItem_(p5, p0, list, rtree); + var simple = replay.isSimple_(list, rtree); + expect(simple).to.be(false); + }); + }); + }); + + 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.polygonreplay.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.polygonreplay.defaultshader.fragment, + ol.render.webgl.polygonreplay.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); + }); + }); + + describe('#drawReplay', function() { + var gl, context; + var feature1 = new ol.Feature({ + geometry: new ol.geom.Polygon([[[0, 0], [500, 500], [500, 0], [0, 0]]]) + }); + var feature2 = new ol.Feature({ + geometry: new ol.geom.Polygon([[[0, 0], [500, 500], [500, 0], [0, 0]]]) + }); + var feature3 = new ol.Feature({ + geometry: new ol.geom.Polygon([[[0, 0], [500, 500], [500, 0], [0, 0]]]) + }); + beforeEach(function() { + gl = { + getParameter: function() {}, + enable: function() {}, + disable: function() {}, + depthMask: function() {}, + depthFunc: function() {}, + clear: function() {} + }; + context = {}; + replay.setFillStyle_ = function() {}; + replay.drawElements = function() {}; + sinon.spy(replay, 'setFillStyle_'); + sinon.spy(replay, 'drawElements'); + }); + + it('draws the elements in a single call if they have the same style', function() { + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawPolygon(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawPolygon(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawPolygon(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + + replay.drawReplay(gl, context, {}, false); + expect(replay.setFillStyle_.calledOnce).to.be(true); + expect(replay.drawElements.calledOnce).to.be(true); + }); + + it('draws the elements in batches if there are multiple fill styles', function() { + var fillStyle2 = new ol.style.Fill({ + color: [0, 255, 0, 1] + }); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawPolygon(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(fillStyle2, strokeStyle); + replay.drawPolygon(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawPolygon(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + + replay.drawReplay(gl, context, {}, false); + expect(replay.setFillStyle_.calledThrice).to.be(true); + expect(replay.drawElements.calledThrice).to.be(true); + }); + + it('can skip elements if needed', function() { + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawPolygon(feature1.getGeometry(), feature1); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawPolygon(feature2.getGeometry(), feature2); + replay.setFillStrokeStyle(fillStyle, strokeStyle); + replay.drawPolygon(feature3.getGeometry(), feature3); + replay.startIndices.push(replay.indices.length); + var skippedFeatHash = {}; + skippedFeatHash[ol.getUid(feature2).toString()] = true; + + replay.drawReplay(gl, context, skippedFeatHash, false); + expect(replay.setFillStyle_.calledOnce).to.be(true); + expect(replay.drawElements.calledTwice).to.be(true); + }); + }); +}); diff --git a/test/spec/ol/render/webgl/replay.test.js b/test/spec/ol/render/webgl/replay.test.js deleted file mode 100644 index bf55e7ed20..0000000000 --- a/test/spec/ol/render/webgl/replay.test.js +++ /dev/null @@ -1,170 +0,0 @@ -goog.provide('ol.test.render.webgl.Replay'); - -goog.require('ol.geom.MultiPoint'); -goog.require('ol.geom.Point'); -goog.require('ol.render.webgl.ImageReplay'); -goog.require('ol.style.Image'); - - -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.getHitDetectionImage = function() { - return image; - }; - imageStyle.getImageSize = function() { - return [512, 512]; - }; - imageStyle.getHitDetectionImageSize = 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); - expect(replay.hitDetectionImages_).to.have.length(1); - 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.hitDetectionImages_).to.have.length(1); - 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.hitDetectionImages_).to.have.length(2); - expect(replay.hitDetectionGroupIndices_).to.have.length(1); - }); - }); - - describe('#drawPoint', 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.drawPoint(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.drawPoint(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('#drawMultiPoint', 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.drawMultiPoint(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.drawMultiPoint(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); - }); - }); -}); diff --git a/test/spec/ol/structs/linkedlist.test.js b/test/spec/ol/structs/linkedlist.test.js new file mode 100644 index 0000000000..661fdd66cf --- /dev/null +++ b/test/spec/ol/structs/linkedlist.test.js @@ -0,0 +1,267 @@ +goog.provide('ol.test.structs.LinkedList'); + +goog.require('ol.structs.LinkedList'); + +describe('ol.structs.LinkedList', function() { + var ll; + var item = {}; + var item2 = {}; + beforeEach(function() { + ll = new ol.structs.LinkedList(); + }); + + it('defaults to circular', function() { + expect(ll.circular_).to.be(true); + }); + + it('creates an empty list', function() { + expect(ll.length_).to.be(0); + expect(ll.first_).to.be(undefined); + expect(ll.last_).to.be(undefined); + expect(ll.head_).to.be(undefined); + }); + + describe('#insertItem', function() { + beforeEach(function() { + ll.insertItem(item); + }); + + it('inserts an item into the list', function() { + expect(ll.length_).to.be(1); + }); + + it('sets the cursor to the inserted item', function() { + expect(ll.head_.data).to.be(item); + }); + + it('links the previous item to the new one', function() { + ll.insertItem(item2); + expect(ll.head_.prev.data).to.be(item); + expect(ll.head_.prev.next.data).to.be(item2); + }); + }); + + describe('#removeItem', function() { + var item3 = {}; + beforeEach(function() { + ll.insertItem(item); + ll.insertItem(item2); + ll.insertItem(item3); + }); + + it('removes the current item', function() { + ll.removeItem(); + expect(ll.length_).to.be(2); + expect(ll.head_.data).not.to.be(item3); + }); + + it('sets the cursor to the next item if possible', function() { + ll.removeItem(); + expect(ll.head_.data).to.be(item); + }); + + it('otherwise sets the cursor to the prevous item', function() { + ll = new ol.structs.LinkedList(false); + ll.insertItem(item); + ll.insertItem(item2); + ll.insertItem(item3); + ll.removeItem(); + expect(ll.head_.data).to.be(item2); + }); + + it('empties a list with only one item', function() { + ll = new ol.structs.LinkedList(); + ll.insertItem(item); + ll.removeItem(); + expect(ll.length_).to.be(0); + expect(ll.head_).to.be(undefined); + expect(ll.first_).to.be(undefined); + expect(ll.last_).to.be(undefined); + }); + }); + + describe('#firstItem', function() { + it('sets the cursor to the first item and returns its data', function() { + ll.insertItem(item); + ll.insertItem(item2); + var i = ll.firstItem(); + expect(i).to.be(item); + expect(ll.head_.data).to.be(item); + }); + + it('returns undefined on empty list', function() { + var i = ll.firstItem(); + expect(i).to.be(undefined); + }); + }); + + describe('#lastItem', function() { + it('sets the cursor to the last item and returns its data', function() { + ll.insertItem(item); + ll.insertItem(item2); + ll.firstItem(); + var i = ll.lastItem(); + expect(i).to.be(item2); + expect(ll.head_.data).to.be(item2); + }); + + it('returns undefined on empty list', function() { + var i = ll.lastItem(); + expect(i).to.be(undefined); + }); + }); + + describe('#nextItem', function() { + it('sets the cursor to the next item and returns its data', function() { + ll.insertItem(item); + ll.insertItem(item2); + ll.firstItem(); + var i = ll.nextItem(); + expect(i).to.be(item2); + expect(ll.head_.data).to.be(item2); + }); + + it('returns undefined on empty list', function() { + var i = ll.nextItem(); + expect(i).to.be(undefined); + }); + }); + + describe('#prevItem', function() { + it('sets the cursor to the previous item and returns its data', function() { + ll.insertItem(item); + ll.insertItem(item2); + var i = ll.prevItem(); + expect(i).to.be(item); + expect(ll.head_.data).to.be(item); + }); + + it('returns undefined on empty list', function() { + var i = ll.prevItem(); + expect(i).to.be(undefined); + }); + }); + + describe('#getNextItem', function() { + it('returns the data of the next item without stepping the cursor', function() { + ll.insertItem(item); + ll.insertItem(item2); + ll.firstItem(); + var i = ll.getNextItem(); + expect(i).to.be(item2); + expect(ll.head_.data).to.be(item); + }); + + it('returns undefined on empty list', function() { + var i = ll.getNextItem(); + expect(i).to.be(undefined); + }); + }); + + describe('#getPrevItem', function() { + it('returns the data of the previous item without stepping the cursor', function() { + ll.insertItem(item); + ll.insertItem(item2); + var i = ll.getPrevItem(); + expect(i).to.be(item); + expect(ll.head_.data).to.be(item2); + }); + + it('returns undefined on empty list', function() { + var i = ll.getPrevItem(); + expect(i).to.be(undefined); + }); + }); + + describe('#getCurrItem', function() { + it('returns the data of the current item', function() { + var item3 = {}; + ll.insertItem(item); + ll.insertItem(item2); + ll.insertItem(item3); + ll.prevItem(); + var i = ll.getCurrItem(); + expect(i).to.be(item2); + expect(ll.head_.data).to.be(item2); + }); + + it('returns undefined on empty list', function() { + var i = ll.getCurrItem(); + expect(i).to.be(undefined); + }); + }); + + describe('#getLength', function() { + it('returns the length of the list', function() { + ll.insertItem(item); + ll.insertItem(item2); + var l = ll.getLength(); + expect(l).to.be(2); + }); + }); + + describe('#concat', function() { + var ll2, item3; + beforeEach(function() { + item3 = {}; + ll2 = new ol.structs.LinkedList(); + ll2.insertItem(item); + ll2.insertItem(item2); + ll2.insertItem(item3); + }); + + it('concatenates a second list with the current one', function() { + var item4 = {}; + var item5 = {}; + var item6 = {}; + ll.insertItem(item4); + ll.insertItem(item5); + ll.insertItem(item6); + ll.prevItem(); + ll.concat(ll2); + expect(ll.length_).to.be(6); + expect(ll.head_.data).to.be(item5); + expect(ll.head_.next.data).to.be(item); + expect(ll.head_.next.next.next.next.data).to.be(item6); + }); + + it('receives the second list if the current one is empty', function() { + ll.concat(ll2); + expect(ll.length_).to.be(3); + expect(ll.first_.data).to.be(item); + expect(ll.last_.data).to.be(item3); + expect(ll.head_.data).to.be(item3); + }); + + it('destroys the second list', function() { + ll.concat(ll2); + expect(ll2.length_).to.be(0); + expect(ll2.first_).to.be(undefined); + expect(ll2.last_).to.be(undefined); + expect(ll2.head_).to.be(undefined); + }); + }); + + describe('when circular', function() { + beforeEach(function() { + ll = new ol.structs.LinkedList(); + ll.insertItem(item); + }); + + describe('#insertItem', function() { + it('initializes the list in a circular way', function() { + expect(ll.head_.prev.data).to.be(item); + expect(ll.head_.next.data).to.be(item); + }); + }); + + describe('#setFirstItem', function() { + it('resets the first item to the current one', function() { + ll.insertItem(item2); + ll.setFirstItem(); + expect(ll.first_.data).to.be(item2); + expect(ll.last_.data).to.be(item); + }); + }); + }); +}); diff --git a/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png b/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png index 5781ffd7ed..dcdff4669b 100644 Binary files a/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png and b/test_rendering/spec/ol/layer/expected/vector-canvas-opaque.png differ diff --git a/test_rendering/spec/ol/layer/expected/vector-canvas.png b/test_rendering/spec/ol/layer/expected/vector-canvas.png index 69ed07ca4e..988080016e 100644 Binary files a/test_rendering/spec/ol/layer/expected/vector-canvas.png and b/test_rendering/spec/ol/layer/expected/vector-canvas.png differ diff --git a/test_rendering/spec/ol/layer/vector.test.js b/test_rendering/spec/ol/layer/vector.test.js index 5949ebfbd2..4045e064cc 100644 --- a/test_rendering/spec/ol/layer/vector.test.js +++ b/test_rendering/spec/ol/layer/vector.test.js @@ -139,7 +139,7 @@ describe('ol.rendering.layer.Vector', function() { })); map.once('postrender', function() { expectResemble(map, 'spec/ol/layer/expected/vector-canvas-opaque.png', - 17, done); + 24.34, done); }); }); diff --git a/test_rendering/spec/ol/style/expected/linestring-strokes-webgl.png b/test_rendering/spec/ol/style/expected/linestring-strokes-webgl.png new file mode 100644 index 0000000000..95e1a9a602 Binary files /dev/null and b/test_rendering/spec/ol/style/expected/linestring-strokes-webgl.png differ diff --git a/test_rendering/spec/ol/style/expected/polygon-fill-and-strokes-webgl.png b/test_rendering/spec/ol/style/expected/polygon-fill-and-strokes-webgl.png new file mode 100644 index 0000000000..da2f9000f4 Binary files /dev/null and b/test_rendering/spec/ol/style/expected/polygon-fill-and-strokes-webgl.png differ diff --git a/test_rendering/spec/ol/style/expected/polygon-types-webgl-stroke.png b/test_rendering/spec/ol/style/expected/polygon-types-webgl-stroke.png new file mode 100644 index 0000000000..624f096158 Binary files /dev/null and b/test_rendering/spec/ol/style/expected/polygon-types-webgl-stroke.png differ diff --git a/test_rendering/spec/ol/style/expected/polygon-types-webgl.png b/test_rendering/spec/ol/style/expected/polygon-types-webgl.png new file mode 100644 index 0000000000..4abfccb058 Binary files /dev/null and b/test_rendering/spec/ol/style/expected/polygon-types-webgl.png differ diff --git a/test_rendering/spec/ol/style/expected/polygon-zindex-webgl.png b/test_rendering/spec/ol/style/expected/polygon-zindex-webgl.png new file mode 100644 index 0000000000..931df4a072 Binary files /dev/null and b/test_rendering/spec/ol/style/expected/polygon-zindex-webgl.png differ diff --git a/test_rendering/spec/ol/style/linestring.test.js b/test_rendering/spec/ol/style/linestring.test.js index b3e14dbbdc..05a6e10a0d 100644 --- a/test_rendering/spec/ol/style/linestring.test.js +++ b/test_rendering/spec/ol/style/linestring.test.js @@ -98,5 +98,12 @@ describe('ol.rendering.style.LineString', function() { map, 'spec/ol/style/expected/linestring-strokes-canvas.png', 3.0, done); }); + it('tests the WebGL renderer', function(done) { + assertWebGL(); + map = createMap('webgl'); + createFeatures(); + expectResemble(map, 'spec/ol/style/expected/linestring-strokes-webgl.png', + 14.6, done); + }); }); }); diff --git a/test_rendering/spec/ol/style/polygon.test.js b/test_rendering/spec/ol/style/polygon.test.js index 21fd39e093..23843091c0 100644 --- a/test_rendering/spec/ol/style/polygon.test.js +++ b/test_rendering/spec/ol/style/polygon.test.js @@ -86,11 +86,18 @@ describe('ol.rendering.style.Polygon', function() { } it('tests the canvas renderer', function(done) { - map = createMap('canvas'); + map = createMap('webgl'); createFeatures(); expectResemble(map, 'spec/ol/style/expected/polygon-types-canvas.png', IMAGE_TOLERANCE, done); }); + + it('tests the webgl renderer', function(done) { + map = createMap('webgl'); + createFeatures(); + expectResemble(map, 'spec/ol/style/expected/polygon-types-webgl.png', + IMAGE_TOLERANCE, done); + }); }); describe('different types with stroke', function() { @@ -135,7 +142,7 @@ describe('ol.rendering.style.Polygon', function() { feature = new ol.Feature({ geometry: new ol.geom.Polygon([ [[-20, -20], [-20, 5], [20, 5], [20, -20], [-20, -20]], - [[-12, -12], [-8, -12], [-8, -3], [-12, -3], [-12, -3]], + [[-12, -3], [-12, -12], [-8, -12], [-8, -3], [-12, -3]], [[0, -12], [13, -12], [13, -3], [0, -3], [0, -12]] ]) @@ -153,6 +160,14 @@ describe('ol.rendering.style.Polygon', function() { expectResemble(map, 'spec/ol/style/expected/polygon-types-canvas-stroke.png', IMAGE_TOLERANCE, done); }); + + it('tests the webgl renderer', function(done) { + map = createMap('webgl', 100); + map.getView().setResolution(0.5); + createFeatures(); + expectResemble(map, 'spec/ol/style/expected/polygon-types-webgl-stroke.png', + IMAGE_TOLERANCE, done); + }); }); describe('z-index', function() { @@ -206,6 +221,13 @@ describe('ol.rendering.style.Polygon', function() { expectResemble(map, 'spec/ol/style/expected/polygon-zindex-canvas.png', IMAGE_TOLERANCE, done); }); + + it('tests the webgl renderer', function(done) { + map = createMap('webgl'); + createFeatures(); + expectResemble(map, 'spec/ol/style/expected/polygon-zindex-webgl.png', + IMAGE_TOLERANCE, done); + }); }); describe('different fills and strokes', function() { @@ -259,6 +281,14 @@ describe('ol.rendering.style.Polygon', function() { map, 'spec/ol/style/expected/polygon-fill-and-strokes-canvas.png', IMAGE_TOLERANCE, done); }); + + it('tests the webgl renderer', function(done) { + map = createMap('webgl'); + createFeatures(); + expectResemble( + map, 'spec/ol/style/expected/polygon-fill-and-strokes-webgl.png', + 5.76, done); + }); }); describe('CanvasPattern and LinearGradient as fills and strokes', function() {