/** * @module ol/render/webgl/LineStringReplay */ import {getUid, inherits} from '../../index.js'; import {equals} from '../../array.js'; import {asArray} from '../../color.js'; import {intersects} from '../../extent.js'; import {linearRingIsClockwise} from '../../geom/flat/orient.js'; import {translate} from '../../geom/flat/transform.js'; import {lineStringIsClosed} from '../../geom/flat/topology.js'; import {isEmpty} from '../../obj.js'; import {DEFAULT_LINECAP, DEFAULT_LINEDASH, DEFAULT_LINEDASHOFFSET, DEFAULT_LINEJOIN, DEFAULT_LINEWIDTH, DEFAULT_MITERLIMIT, DEFAULT_STROKESTYLE, triangleIsCounterClockwise} from '../webgl.js'; import WebGLReplay from '../webgl/Replay.js'; import {fragment, vertex} from '../webgl/linestringreplay/defaultshader.js'; import Locations from '../webgl/linestringreplay/defaultshader/Locations.js'; import {FLOAT} from '../../webgl.js'; import WebGLBuffer from '../../webgl/Buffer.js'; /** * @enum {number} */ const Instruction = { 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 }; /** * @constructor * @extends {module:ol/render/webgl/Replay} * @param {number} tolerance Tolerance. * @param {module:ol/extent~Extent} maxExtent Max extent. * @struct */ const WebGLLineStringReplay = function(tolerance, maxExtent) { WebGLReplay.call(this, tolerance, maxExtent); /** * @private * @type {module: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., * lineDashOffset: (number|undefined), * lineJoin: (string|undefined), * lineWidth: (number|undefined), * miterLimit: (number|undefined), * changed: boolean}|null} */ this.state_ = { strokeColor: null, lineCap: undefined, lineDash: null, lineDashOffset: undefined, lineJoin: undefined, lineWidth: undefined, miterLimit: undefined, changed: false }; }; inherits(WebGLLineStringReplay, WebGLReplay); /** * Draw one segment. * @private * @param {Array.} flatCoordinates Flat coordinates. * @param {number} offset Offset. * @param {number} end End. * @param {number} stride Stride. */ WebGLLineStringReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) { let i, ii; let numVertices = this.vertices.length; let 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 //Instruction, and a rounding factor (1 or 2). If the product is even, //we round it. If it is odd, we don't. const lineJoin = this.state_.lineJoin === 'bevel' ? 0 : this.state_.lineJoin === 'miter' ? 1 : 2; const lineCap = this.state_.lineCap === 'butt' ? 0 : this.state_.lineCap === 'square' ? 1 : 2; const closed = lineStringIsClosed(flatCoordinates, offset, end, stride); let startCoords, sign, n; let lastIndex = numIndices; let lastSign = 1; //We need the adjacent vertices to define normals in joins. p0 = last, p1 = current, p2 = next. let 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 && 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 * Instruction.BEGIN_LINE_CAP * lineCap, numVertices); numVertices = this.addVertices_([0, 0], p1, p2, -lastSign * Instruction.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 * Instruction.BEGIN_LINE * (lineCap || 1), numVertices); numVertices = this.addVertices_([0, 0], p1, p2, -lastSign * Instruction.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 { p0 = p0 || [0, 0]; numVertices = this.addVertices_(p0, p1, [0, 0], lastSign * Instruction.END_LINE * (lineCap || 1), numVertices); numVertices = this.addVertices_(p0, p1, [0, 0], -lastSign * Instruction.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 * Instruction.END_LINE_CAP * lineCap, numVertices); numVertices = this.addVertices_(p0, p1, [0, 0], -lastSign * Instruction.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 = triangleIsCounterClockwise(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]) ? -1 : 1; numVertices = this.addVertices_(p0, p1, p2, sign * Instruction.BEVEL_FIRST * (lineJoin || 1), numVertices); numVertices = this.addVertices_(p0, p1, p2, sign * Instruction.BEVEL_SECOND * (lineJoin || 1), numVertices); numVertices = this.addVertices_(p0, p1, p2, -sign * Instruction.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 * Instruction.MITER_TOP * lineJoin, numVertices); this.indices[numIndices++] = n + 1; this.indices[numIndices++] = n + 3; this.indices[numIndices++] = n; } } if (closed) { n = n || numVertices / 7; sign = 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 * Instruction.BEVEL_FIRST * (lineJoin || 1), numVertices); numVertices = this.addVertices_(p0, p1, p2, -sign * Instruction.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 */ WebGLLineStringReplay.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 */ WebGLLineStringReplay.prototype.isValid_ = function(flatCoordinates, offset, end, stride) { const range = end - offset; if (range < stride * 2) { return false; } else if (range === stride * 2) { const firstP = [flatCoordinates[offset], flatCoordinates[offset + 1]]; const lastP = [flatCoordinates[offset + stride], flatCoordinates[offset + stride + 1]]; return !equals(firstP, lastP); } return true; }; /** * @inheritDoc */ WebGLLineStringReplay.prototype.drawLineString = function(lineStringGeometry, feature) { let flatCoordinates = lineStringGeometry.getFlatCoordinates(); const stride = lineStringGeometry.getStride(); if (this.isValid_(flatCoordinates, 0, flatCoordinates.length, stride)) { flatCoordinates = 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 */ WebGLLineStringReplay.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) { const indexCount = this.indices.length; const ends = multiLineStringGeometry.getEnds(); ends.unshift(0); const flatCoordinates = multiLineStringGeometry.getFlatCoordinates(); const stride = multiLineStringGeometry.getStride(); let i, ii; if (ends.length > 1) { for (i = 1, ii = ends.length; i < ii; ++i) { if (this.isValid_(flatCoordinates, ends[i - 1], ends[i], stride)) { const lineString = translate(flatCoordinates, ends[i - 1], ends[i], stride, -this.origin[0], -this.origin[1]); this.drawCoordinates_( lineString, 0, lineString.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. */ WebGLLineStringReplay.prototype.drawPolygonCoordinates = function( flatCoordinates, holeFlatCoordinates, stride) { if (!lineStringIsClosed(flatCoordinates, 0, flatCoordinates.length, stride)) { flatCoordinates.push(flatCoordinates[0]); flatCoordinates.push(flatCoordinates[1]); } this.drawCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride); if (holeFlatCoordinates.length) { let i, ii; for (i = 0, ii = holeFlatCoordinates.length; i < ii; ++i) { if (!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 {module:ol/Feature|module:ol/render/Feature} feature Feature. * @param {number=} opt_index Index count. */ WebGLLineStringReplay.prototype.setPolygonStyle = function(feature, opt_index) { const 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. */ WebGLLineStringReplay.prototype.getCurrentIndex = function() { return this.indices.length; }; /** * @inheritDoc **/ WebGLLineStringReplay.prototype.finish = function(context) { // create, bind, and populate the vertices buffer this.verticesBuffer = new WebGLBuffer(this.vertices); // create, bind, and populate the indices buffer this.indicesBuffer = new WebGLBuffer(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 */ WebGLLineStringReplay.prototype.getDeleteResourcesFunction = function(context) { const verticesBuffer = this.verticesBuffer; const indicesBuffer = this.indicesBuffer; return function() { context.deleteBuffer(verticesBuffer); context.deleteBuffer(indicesBuffer); }; }; /** * @inheritDoc */ WebGLLineStringReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) { // get the program const program = context.getProgram(fragment, vertex); // get the locations let locations; if (!this.defaultLocations_) { locations = new 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, FLOAT, false, 28, 0); gl.enableVertexAttribArray(locations.a_position); gl.vertexAttribPointer(locations.a_position, 2, FLOAT, false, 28, 8); gl.enableVertexAttribArray(locations.a_nextPos); gl.vertexAttribPointer(locations.a_nextPos, 2, FLOAT, false, 28, 16); gl.enableVertexAttribArray(locations.a_direction); gl.vertexAttribPointer(locations.a_direction, 1, FLOAT, false, 28, 24); // Enable renderer specific uniforms. gl.uniform2fv(locations.u_size, size); gl.uniform1f(locations.u_pixelRatio, pixelRatio); return locations; }; /** * @inheritDoc */ WebGLLineStringReplay.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 */ WebGLLineStringReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) { //Save GL parameters. const tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC)); const tmpDepthMask = /** @type {boolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK)); if (!hitDetection) { gl.enable(gl.DEPTH_TEST); gl.depthMask(true); gl.depthFunc(gl.NOTEQUAL); } if (!isEmpty(skippedFeaturesHash)) { this.drawReplaySkipping_(gl, context, skippedFeaturesHash); } else { //Draw by style groups to minimize drawElements() calls. let 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 {module:ol/webgl/Context} context Context. * @param {Object} skippedFeaturesHash Ids of features to skip. */ WebGLLineStringReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) { let 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 = 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 */ WebGLLineStringReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) { let 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 = getUid(feature).toString(); if (skippedFeaturesHash[featureUid] === undefined && feature.getGeometry() && (opt_hitExtent === undefined || intersects( /** @type {Array} */ (opt_hitExtent), feature.getGeometry().getExtent()))) { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); this.drawElements(gl, context, start, end); const 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. */ WebGLLineStringReplay.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 */ WebGLLineStringReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) { const strokeStyleLineCap = strokeStyle.getLineCap(); this.state_.lineCap = strokeStyleLineCap !== undefined ? strokeStyleLineCap : DEFAULT_LINECAP; const strokeStyleLineDash = strokeStyle.getLineDash(); this.state_.lineDash = strokeStyleLineDash ? strokeStyleLineDash : DEFAULT_LINEDASH; const strokeStyleLineDashOffset = strokeStyle.getLineDashOffset(); this.state_.lineDashOffset = strokeStyleLineDashOffset ? strokeStyleLineDashOffset : DEFAULT_LINEDASHOFFSET; const strokeStyleLineJoin = strokeStyle.getLineJoin(); this.state_.lineJoin = strokeStyleLineJoin !== undefined ? strokeStyleLineJoin : DEFAULT_LINEJOIN; let strokeStyleColor = strokeStyle.getColor(); if (!(strokeStyleColor instanceof CanvasGradient) && !(strokeStyleColor instanceof CanvasPattern)) { strokeStyleColor = asArray(strokeStyleColor).map(function(c, i) { return i != 3 ? c / 255 : c; }) || DEFAULT_STROKESTYLE; } else { strokeStyleColor = DEFAULT_STROKESTYLE; } let strokeStyleWidth = strokeStyle.getWidth(); strokeStyleWidth = strokeStyleWidth !== undefined ? strokeStyleWidth : DEFAULT_LINEWIDTH; let strokeStyleMiterLimit = strokeStyle.getMiterLimit(); strokeStyleMiterLimit = strokeStyleMiterLimit !== undefined ? strokeStyleMiterLimit : DEFAULT_MITERLIMIT; if (!this.state_.strokeColor || !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]); } }; export default WebGLLineStringReplay;