diff --git a/js/geometry/createbucket.js b/js/geometry/createbucket.js new file mode 100644 index 00000000000..8f9ea76d8db --- /dev/null +++ b/js/geometry/createbucket.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = createBucket; + +var Bucket = require('./bucket.js'); +var LineBucket = require('./linebucket.js'); + +function createBucket(info, geometry, placement, indices, buffers) { + if (info.line) { + return new LineBucket(info, buffers, placement, indices); + } else { + return new Bucket(info, geometry, placement, indices); + } +} diff --git a/js/geometry/elementgroups.js b/js/geometry/elementgroups.js new file mode 100644 index 00000000000..6c49673120d --- /dev/null +++ b/js/geometry/elementgroups.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = ElementGroups; + +function ElementGroups(vertexBuffer, elementBuffer, groups) { + + this.vertexBuffer = vertexBuffer; + this.elementBuffer = elementBuffer; + + if (groups) { + this.groups = groups; + } else { + this.groups = []; + } +} + +ElementGroups.prototype.makeRoomFor = function(numVertices) { + if (!this.current || this.current.vertexLength + numVertices > 65535) { + this.current = new ElementGroup(this.vertexBuffer.index, this.elementBuffer.index); + this.groups.push(this.current); + } +}; + +function ElementGroup(vertexStartIndex, elementStartIndex) { + // the offset into the vertex buffer of the first vertex in this group + this.vertexStartIndex = vertexStartIndex; + this.elementStartIndex = elementStartIndex; + this.elementLength = 0; +} diff --git a/js/geometry/linebucket.js b/js/geometry/linebucket.js new file mode 100644 index 00000000000..02652c17829 --- /dev/null +++ b/js/geometry/linebucket.js @@ -0,0 +1,224 @@ +'use strict'; + +var ElementGroups = require('./elementgroups.js'); + +module.exports = LineBucket; + +function LineBucket(info, buffers, placement, elementGroups) { + this.info = info; + this.buffers = buffers; + this.elementGroups = elementGroups || new ElementGroups(buffers.lineVertex, buffers.lineElement); +} + +LineBucket.prototype.addFeature = function(lines) { + var info = this.info; + for (var i = 0; i < lines.length; i++) { + this.addLine(lines[i], info['line-join'], info['line-cap'], + info['line-miter-limit'], info['line-round-limit']); + } +}; + +LineBucket.prototype.addLine = function(vertices, join, cap, miterLimit, roundLimit) { + if (vertices.length < 2) { + console.warn('a line must have at least two vertices'); + return; + } + + var len = vertices.length, + firstVertex = vertices[0], + lastVertex = vertices[len - 1], + closed = firstVertex.equals(lastVertex); + + var lineVertex = this.buffers.lineVertex; + var lineElement = this.buffers.lineElement; + + // we could be more precies, but it would only save a negligible amount of space + this.elementGroups.makeRoomFor(len * 4); + var elementGroup = this.elementGroups.current; + var vertexStartIndex = elementGroup.vertexStartIndex; + + if (len == 2 && closed) { + // console.warn('a line may not have coincident points'); + return; + } + + join = join || 'miter'; + cap = cap || 'butt'; + miterLimit = miterLimit || 2; + roundLimit = roundLimit || 1; + + var beginCap = cap, + endCap = closed ? 'butt' : cap, + flip = 1, + distance = 0, + currentVertex, prevVertex, nextVertex, prevNormal, nextNormal; + + // the last three vertices added + var e1, e2, e3; + + if (closed) { + currentVertex = vertices[len - 2]; + nextNormal = firstVertex.sub(currentVertex)._unit()._perp(); + } + + for (var i = 0; i < len; i++) { + + nextVertex = closed && i === len - 1 ? + vertices[1] : // if the line is closed, we treat the last vertex like the first + vertices[i + 1]; // just the next vertex + + // if two consecutive vertices exist, skip the current one + if (nextVertex && vertices[i].equals(nextVertex)) continue; + + if (nextNormal) prevNormal = nextNormal; + if (currentVertex) prevVertex = currentVertex; + + currentVertex = vertices[i]; + + // Calculate how far along the line the currentVertex is + if (prevVertex) distance += currentVertex.dist(prevVertex); + + // Calculate the normal towards the next vertex in this line. In case + // there is no next vertex, pretend that the line is continuing straight, + // meaning that we are just using the previous normal. + nextNormal = nextVertex ? nextVertex.sub(currentVertex)._unit()._perp() : prevNormal; + + // If we still don't have a previous normal, this is the beginning of a + // non-closed line, so we're doing a straight "join". + prevNormal = prevNormal || nextNormal; + + // Determine the normal of the join extrusion. It is the angle bisector + // of the segments between the previous line and the next line. + var joinNormal = prevNormal.add(nextNormal)._unit(); + + /* joinNormal prevNormal + * ↖ ↑ + * .________. prevVertex + * | + * nextNormal ← | currentVertex + * | + * nextVertex ! + * + */ + + // Calculate the length of the miter (the ratio of the miter to the width). + // Find the cosine of the angle between the next and join normals + // using dot product. The inverse of that is the miter length. + var cosHalfAngle = joinNormal.x * nextNormal.x + joinNormal.y * nextNormal.y; + var miterLength = 1 / cosHalfAngle; + + // Whether any vertices have been + var startOfLine = e1 === undefined || e2 === undefined; + + // The join if a middle vertex, otherwise the cap. + var currentJoin = (prevVertex && nextVertex) ? join : + nextVertex ? beginCap : endCap; + + if (currentJoin === 'round' && miterLength < roundLimit) { + currentJoin = 'miter'; + } + + if (currentJoin === 'miter' && miterLength > miterLimit && miterLength < Math.SQRT2) { + currentJoin = 'bevel'; + } + + // Mitered joins + if (currentJoin === 'miter') { + + if (miterLength > 100) { + // Almost parallel lines + flip = -flip; + joinNormal = nextNormal; + + } else if (miterLength > miterLimit) { + flip = -flip; + // miter is too big, flip the direction to make a beveled join + var bevelLength = miterLength * prevNormal.add(nextNormal).mag() / prevNormal.sub(nextNormal).mag(); + joinNormal._perp()._mult(flip * bevelLength); + + } else { + // scale the unit vector by the miter length + joinNormal._mult(miterLength); + } + + addCurrentVertex(joinNormal, 0, false); + + // All other types of joins + } else { + + // Close previous segment with a butt or a square cap + if (!startOfLine) { + addCurrentVertex(prevNormal, currentJoin === 'square' ? 1 : 0, false); + } + + // Add round cap or linejoin at end of segment + if (!startOfLine && currentJoin === 'round') { + addCurrentVertex(prevNormal, 1, true); + } + + // Segment include cap are done, unset vertices to disconnect segments. + // Or leave them to create a bevel. + if (startOfLine || currentJoin !== 'bevel') { + e1 = e2 = -1; + flip = 1; + } + + // Add round cap before first segment + if (startOfLine && beginCap === 'round') { + addCurrentVertex(nextNormal, -1, true); + } + + // Start next segment with a butt or square cap + if (nextVertex) { + addCurrentVertex(nextNormal, currentJoin === 'square' ? -1 : 0, false); + } + } + + } + + + /* + * Adds two vertices to the buffer that are + * normal and -normal from the currentVertex. + * + * endBox moves the extrude one unit in the direction of the line + * to create square or round cap. + * + * endBox === 1 moves the extrude in the direction of the line + * endBox === -1 moves the extrude in the reverse direction + */ + function addCurrentVertex(normal, endBox, round) { + + var tx = round ? 1 : 0; + var extrude; + + extrude = normal.mult(flip); + if (endBox) extrude._sub(normal.perp()._mult(endBox)); + e3 = lineVertex.add(currentVertex, extrude, tx, 0, distance) - vertexStartIndex; + if (e1 >= 0 && e2 >= 0) { + lineElement.add(e1, e2, e3); + elementGroup.elementLength++; + } + e1 = e2; e2 = e3; + + extrude = normal.mult(-flip); + if (endBox) extrude._sub(normal.perp()._mult(endBox)); + e3 = lineVertex.add(currentVertex, extrude, tx, 1, distance) - vertexStartIndex; + if (e1 >= 0 && e2 >= 0) { + lineElement.add(e1, e2, e3); + elementGroup.elementLength++; + } + e1 = e2; e2 = e3; + } +}; + +LineBucket.prototype.toJSON = function() { + return { + indices: this.elementGroups + }; +}; + +LineBucket.prototype.start = function() { +}; +LineBucket.prototype.end = function() { +}; diff --git a/js/render/drawline.js b/js/render/drawline.js index 4c4acd01b68..2c09a947b2d 100644 --- a/js/render/drawline.js +++ b/js/render/drawline.js @@ -42,23 +42,21 @@ module.exports = function drawLine(gl, painter, bucket, layerStyle, posMatrix, p } gl.uniform4fv(shader.u_color, color); - var buffer = bucket.indices.lineBufferIndex; - while (buffer <= bucket.indices.lineBufferIndexEnd) { - var vertex = bucket.geometry.lineBuffers[buffer].vertex; - vertex.bind(gl); - var elements = bucket.geometry.lineBuffers[buffer].element; - elements.bind(gl); + var vertex = bucket.buffers.lineVertex; + vertex.bind(gl); + var element = bucket.buffers.lineElement; + element.bind(gl); - gl.vertexAttribPointer(shader.a_pos, 4, gl.SHORT, false, 8, 0); - gl.vertexAttribPointer(shader.a_extrude, 2, gl.BYTE, false, 8, 6); - gl.vertexAttribPointer(shader.a_linesofar, 2, gl.SHORT, false, 8, 4); + bucket.elementGroups.groups.forEach(function(group) { + var offset = group.vertexStartIndex * vertex.itemSize; + gl.vertexAttribPointer(shader.a_pos, 4, gl.SHORT, false, 8, offset + 0); + gl.vertexAttribPointer(shader.a_extrude, 2, gl.BYTE, false, 8, offset + 6); + gl.vertexAttribPointer(shader.a_linesofar, 2, gl.SHORT, false, 8, offset + 4); - var begin = buffer == bucket.indices.lineBufferIndex ? bucket.indices.lineElementIndex : 0; - var end = buffer == bucket.indices.lineBufferIndexEnd ? bucket.indices.lineElementIndexEnd : elements.index; + var count = group.elementLength * 3; + var elementOffset = group.elementStartIndex * 6; + gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); + }); - gl.drawElements(gl.TRIANGLES, (end - begin) * 3, gl.UNSIGNED_SHORT, begin * 6); - - buffer++; - } }; diff --git a/js/render/painter.js b/js/render/painter.js index 697fabf27fa..11f161c39df 100644 --- a/js/render/painter.js +++ b/js/render/painter.js @@ -306,7 +306,7 @@ GLPainter.prototype.applyStyle = function(layer, style, buckets, params) { var bucket = buckets[layer.bucket]; // There are no vertices yet for this layer. - if (!bucket || !bucket.indices) return; + if (!bucket || !(bucket.indices || bucket.elementGroups)) return; var info = bucket.info; diff --git a/js/ui/vectortile.js b/js/ui/vectortile.js index 4528bc1a02b..2c6c1a72aed 100644 --- a/js/ui/vectortile.js +++ b/js/ui/vectortile.js @@ -7,9 +7,10 @@ var Tile = require('./tile.js'), FillElementsBuffer = require('../geometry/fillelementsbuffer.js'), GlyphVertexBuffer = require('../geometry/glyphvertexbuffer.js'), PointVertexBuffer = require('../geometry/pointvertexbuffer.js'), - Bucket = require('../geometry/bucket.js'), util = require('../util/util.js'); +var createBucket = require('../geometry/createbucket.js'); + module.exports = VectorTile; function VectorTile(id, source, url, callback) { @@ -72,9 +73,15 @@ VectorTile.prototype.onTileLoad = function(data) { d.elements = new FillElementsBuffer(d.elements); }); + this.buffers = data.buffers; + this.buffers.glyphVertex = new GlyphVertexBuffer(this.buffers.glyphVertex); + this.buffers.pointVertex = new PointVertexBuffer(this.buffers.pointVertex); + this.buffers.lineVertex = new LineVertexBuffer(this.buffers.lineVertex); + this.buffers.lineElement = new LineElementBuffer(this.buffers.lineElement); + this.buckets = {}; for (var b in data.buckets) { - this.buckets[b] = new Bucket(this.map.style.stylesheet.buckets[b], this.geometry, undefined, data.buckets[b].indices); + this.buckets[b] = createBucket(this.map.style.stylesheet.buckets[b], this.geometry, undefined, data.buckets[b].indices, this.buffers); } this.loaded = true; diff --git a/js/worker/workertile.js b/js/worker/workertile.js index 6a96e7f19bb..7bf5859733d 100644 --- a/js/worker/workertile.js +++ b/js/worker/workertile.js @@ -1,7 +1,6 @@ 'use strict'; var Geometry = require('../geometry/geometry.js'); -var Bucket = require('../geometry/bucket.js'); var FeatureTree = require('../geometry/featuretree.js'); var Protobuf = require('pbf'); var VectorTile = require('../format/vectortile.js'); @@ -17,6 +16,14 @@ var getArrayBuffer = require('../util/util.js').getArrayBuffer; // self.console = require('./console.js'); // } +var LineVertexBuffer = require('../geometry/linevertexbuffer.js'); +var LineElementBuffer = require('../geometry/lineelementbuffer.js'); +var FillVertexBuffer = require('../geometry/fillvertexbuffer.js'); +var FillElementBuffer = require('../geometry/fillelementsbuffer.js'); +var GlyphVertexBuffer = require('../geometry/glyphvertexbuffer.js'); +var PointVertexBuffer = require('../geometry/pointvertexbuffer.js'); + +var createBucket = require('../geometry/createbucket.js'); var actor = require('./worker.js'); module.exports = WorkerTile; @@ -28,6 +35,15 @@ function WorkerTile(url, id, zoom, tileSize, glyphs, callback) { this.tileSize = tileSize; this.glyphs = glyphs; + this.buffers = { + glyphVertex: new GlyphVertexBuffer(), + pointVertex: new PointVertexBuffer(), + fillVertex: new FillVertexBuffer(), + fillElement: new FillElementBuffer(), + lineVertex: new LineVertexBuffer(), + lineElement: new LineElementBuffer() + }; + WorkerTile.loading[id] = getArrayBuffer(url, function(err, data) { delete WorkerTile.loading[id]; if (err) { @@ -92,7 +108,7 @@ function sortFeaturesIntoBuckets(layer, mapping) { WorkerTile.prototype.parseBucket = function(tile, bucket_name, features, info, layer, layerDone, callback) { var geometry = tile.geometry; - var bucket = new Bucket(info, geometry, tile.placement); + var bucket = createBucket(info, geometry, tile.placement, undefined, tile.buffers); if (info.text) { tile.parseTextBucket(tile, bucket_name, features, bucket, info, layer, done); @@ -277,13 +293,18 @@ WorkerTile.prototype.parse = function(tile, callback) { // Collect all buffers to mark them as transferable object. var buffers = self.geometry.bufferList(); + for (var type in self.buffers) { + buffers.push(self.buffers[type].array); + } + // Convert buckets to a transferable format var bucketJSON = {}; for (var b in layers) bucketJSON[b] = layers[b].toJSON(); callback(null, { geometry: self.geometry, - buckets: bucketJSON + buckets: bucketJSON, + buffers: self.buffers }, buffers); // we don't need anything except featureTree at this point, so we mark it for GC