UNPKG

mapbox-gl

Version:
1,405 lines (1,095 loc) 1.79 MB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.mapboxgl = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ 'use strict'; // a simple wrapper around a single arraybuffer module.exports = Buffer; function Buffer(buffer) { if (!buffer) { this.array = new ArrayBuffer(this.defaultLength); this.length = this.defaultLength; this.setupViews(); } else { // we only recreate buffers after receiving them from workers for binding to gl, // so we only need these 2 properties this.array = buffer.array; this.pos = buffer.pos; } } Buffer.prototype = { pos: 0, itemSize: 4, // bytes in one item defaultLength: 8192, // initial buffer size arrayType: 'ARRAY_BUFFER', // gl buffer type get index() { return this.pos / this.itemSize; }, setupViews: function() { // set up views for each type to add data of different types to the same buffer this.ubytes = new Uint8Array(this.array); this.bytes = new Int8Array(this.array); this.ushorts = new Uint16Array(this.array); this.shorts = new Int16Array(this.array); }, // binds the buffer to a webgl context bind: function(gl) { var type = gl[this.arrayType]; if (!this.buffer) { this.buffer = gl.createBuffer(); gl.bindBuffer(type, this.buffer); gl.bufferData(type, this.array.slice(0, this.pos), gl.STATIC_DRAW); // dump array buffer once it's bound to gl this.array = null; } else { gl.bindBuffer(type, this.buffer); } }, destroy: function(gl) { if (this.buffer) { gl.deleteBuffer(this.buffer); } }, // increase the buffer size by 50% if a new item doesn't fit resize: function() { if (this.length < this.pos + this.itemSize) { while (this.length < this.pos + this.itemSize) { // increase the length by 50% but keep it even this.length = Math.round(this.length * 1.5 / 2) * 2; } // array buffers can't be resized, so we create a new one and reset all bytes there this.array = new ArrayBuffer(this.length); var ubytes = new Uint8Array(this.array); ubytes.set(this.ubytes); this.setupViews(); } } }; },{}],2:[function(require,module,exports){ 'use strict'; var LineVertexBuffer = require('./line_vertex_buffer'); var LineElementBuffer = require('./line_element_buffer'); var FillVertexBuffer = require('./fill_vertex_buffer'); var FillElementBuffer = require('./triangle_element_buffer'); var OutlineElementBuffer = require('./outline_elements_buffer'); var GlyphVertexBuffer = require('./glyph_vertex_buffer'); var GlyphElementBuffer = require('./triangle_element_buffer'); var IconVertexBuffer = require('./icon_vertex_buffer'); var IconElementBuffer = require('./triangle_element_buffer'); var CollisionBoxVertexBuffer = require('./collision_box_vertex_buffer'); module.exports = function(bufferset) { bufferset = bufferset || {}; return { glyphVertex: new GlyphVertexBuffer(bufferset.glyphVertex), glyphElement: new GlyphElementBuffer(bufferset.glyphElement), iconVertex: new IconVertexBuffer(bufferset.iconVertex), iconElement: new IconElementBuffer(bufferset.iconElement), fillVertex: new FillVertexBuffer(bufferset.fillVertex), fillElement: new FillElementBuffer(bufferset.fillElement), outlineElement: new OutlineElementBuffer(bufferset.outlineElement), lineVertex: new LineVertexBuffer(bufferset.lineVertex), lineElement: new LineElementBuffer(bufferset.lineElement), collisionBoxVertex: new CollisionBoxVertexBuffer(bufferset.collisionBoxVertex) }; }; },{"./collision_box_vertex_buffer":3,"./fill_vertex_buffer":4,"./glyph_vertex_buffer":5,"./icon_vertex_buffer":6,"./line_element_buffer":7,"./line_vertex_buffer":8,"./outline_elements_buffer":9,"./triangle_element_buffer":10}],3:[function(require,module,exports){ 'use strict'; var util = require('../../util/util'); var Buffer = require('./buffer'); module.exports = CollisionBoxVertexBuffer; function CollisionBoxVertexBuffer(buffer) { Buffer.call(this, buffer); } CollisionBoxVertexBuffer.prototype = util.inherit(Buffer, { itemSize: 12, // bytes per vertex (2 * short + 1 * short + 2 * byte = 8 bytes) defaultLength: 32768, // add a vertex to this buffer; // x, y - vertex position // ex, ey - extrude normal add: function(point, extrude, maxZoom, placementZoom) { var pos = this.pos, pos2 = pos / 2, index = this.index; this.resize(); this.shorts[pos2 + 0] = point.x; this.shorts[pos2 + 1] = point.y; this.shorts[pos2 + 2] = Math.round(extrude.x); this.shorts[pos2 + 3] = Math.round(extrude.y); this.ubytes[pos + 8] = Math.floor(maxZoom * 10); this.ubytes[pos + 9] = Math.floor(placementZoom * 10); this.pos += this.itemSize; return index; } }); },{"../../util/util":96,"./buffer":1}],4:[function(require,module,exports){ 'use strict'; var util = require('../../util/util'); var Buffer = require('./buffer'); module.exports = FillVertexBuffer; function FillVertexBuffer(buffer) { Buffer.call(this, buffer); } FillVertexBuffer.prototype = util.inherit(Buffer, { itemSize: 4, // bytes per vertex (2 * short == 4 bytes) add: function(x, y) { var pos2 = this.pos / 2; this.resize(); this.shorts[pos2 + 0] = x; this.shorts[pos2 + 1] = y; this.pos += this.itemSize; } }); },{"../../util/util":96,"./buffer":1}],5:[function(require,module,exports){ 'use strict'; var util = require('../../util/util'); var Buffer = require('./buffer'); module.exports = GlyphVertexBuffer; function GlyphVertexBuffer(buffer) { Buffer.call(this, buffer); } GlyphVertexBuffer.prototype = util.inherit(Buffer, { defaultLength: 2048 * 16, itemSize: 16, add: function(x, y, ox, oy, tx, ty, minzoom, maxzoom, labelminzoom) { var pos = this.pos, pos2 = pos / 2; this.resize(); this.shorts[pos2 + 0] = x; this.shorts[pos2 + 1] = y; this.shorts[pos2 + 2] = Math.round(ox * 64); // use 1/64 pixels for placement this.shorts[pos2 + 3] = Math.round(oy * 64); // a_data1 this.ubytes[pos + 8] /* tex */ = Math.floor(tx / 4); this.ubytes[pos + 9] /* tex */ = Math.floor(ty / 4); this.ubytes[pos + 10] /* labelminzoom */ = Math.floor((labelminzoom) * 10); // a_data2 this.ubytes[pos + 12] /* minzoom */ = Math.floor((minzoom) * 10); // 1/10 zoom levels: z16 == 160. this.ubytes[pos + 13] /* maxzoom */ = Math.floor(Math.min(maxzoom, 25) * 10); // 1/10 zoom levels: z16 == 160. this.pos += this.itemSize; }, bind: function(gl, shader, offset) { Buffer.prototype.bind.call(this, gl); var stride = this.itemSize; gl.vertexAttribPointer(shader.a_pos, 2, gl.SHORT, false, stride, offset + 0); gl.vertexAttribPointer(shader.a_offset, 2, gl.SHORT, false, stride, offset + 4); gl.vertexAttribPointer(shader.a_data1, 4, gl.UNSIGNED_BYTE, false, stride, offset + 8); gl.vertexAttribPointer(shader.a_data2, 2, gl.UNSIGNED_BYTE, false, stride, offset + 12); } }); },{"../../util/util":96,"./buffer":1}],6:[function(require,module,exports){ 'use strict'; var util = require('../../util/util'); var Buffer = require('./buffer'); module.exports = GlyphVertexBuffer; function GlyphVertexBuffer(buffer) { Buffer.call(this, buffer); } GlyphVertexBuffer.prototype = util.inherit(Buffer, { defaultLength: 2048 * 16, itemSize: 16, add: function(x, y, ox, oy, tx, ty, minzoom, maxzoom, labelminzoom) { var pos = this.pos, pos2 = pos / 2; this.resize(); this.shorts[pos2 + 0] = x; this.shorts[pos2 + 1] = y; this.shorts[pos2 + 2] = Math.round(ox * 64); // use 1/64 pixels for placement this.shorts[pos2 + 3] = Math.round(oy * 64); // a_data1 this.ubytes[pos + 8] /* tex */ = tx / 4; this.ubytes[pos + 9] /* tex */ = ty / 4; this.ubytes[pos + 10] /* labelminzoom */ = Math.floor((labelminzoom || 0) * 10); // a_data2 this.ubytes[pos + 12] /* minzoom */ = Math.floor((minzoom || 0) * 10); // 1/10 zoom levels: z16 == 160. this.ubytes[pos + 13] /* maxzoom */ = Math.floor(Math.min(maxzoom || 25, 25) * 10); // 1/10 zoom levels: z16 == 160. this.pos += this.itemSize; }, bind: function(gl, shader, offset) { Buffer.prototype.bind.call(this, gl); var stride = this.itemSize; gl.vertexAttribPointer(shader.a_pos, 2, gl.SHORT, false, stride, offset + 0); gl.vertexAttribPointer(shader.a_offset, 2, gl.SHORT, false, stride, offset + 4); gl.vertexAttribPointer(shader.a_data1, 4, gl.UNSIGNED_BYTE, false, stride, offset + 8); gl.vertexAttribPointer(shader.a_data2, 2, gl.UNSIGNED_BYTE, false, stride, offset + 12); } }); },{"../../util/util":96,"./buffer":1}],7:[function(require,module,exports){ 'use strict'; var util = require('../../util/util'); var Buffer = require('./buffer'); module.exports = LineElementBuffer; function LineElementBuffer(buffer) { Buffer.call(this, buffer); } LineElementBuffer.prototype = util.inherit(Buffer, { itemSize: 6, // bytes per triangle (3 * unsigned short == 6 bytes) arrayType: 'ELEMENT_ARRAY_BUFFER', add: function(a, b, c) { var pos2 = this.pos / 2; this.resize(); this.ushorts[pos2 + 0] = a; this.ushorts[pos2 + 1] = b; this.ushorts[pos2 + 2] = c; this.pos += this.itemSize; } }); },{"../../util/util":96,"./buffer":1}],8:[function(require,module,exports){ 'use strict'; var util = require('../../util/util'); var Buffer = require('./buffer'); module.exports = LineVertexBuffer; function LineVertexBuffer(buffer) { Buffer.call(this, buffer); } // scale the extrusion vector so that the normal length is this value. // contains the "texture" normals (-1..1). this is distinct from the extrude // normals for line joins, because the x-value remains 0 for the texture // normal array, while the extrude normal actually moves the vertex to create // the acute/bevelled line join. LineVertexBuffer.extrudeScale = 63; LineVertexBuffer.prototype = util.inherit(Buffer, { itemSize: 8, // bytes per vertex (2 * short + 1 * short + 2 * byte = 8 bytes) defaultLength: 32768, // add a vertex to this buffer; // x, y - vertex position // ex, ey - extrude normal // tx, ty - texture normal add: function(point, extrude, tx, ty, linesofar) { var pos = this.pos, pos2 = pos / 2, index = this.index, extrudeScale = LineVertexBuffer.extrudeScale; this.resize(); this.shorts[pos2 + 0] = (Math.floor(point.x) * 2) | tx; this.shorts[pos2 + 1] = (Math.floor(point.y) * 2) | ty; this.bytes[pos + 4] = Math.round(extrudeScale * extrude.x); this.bytes[pos + 5] = Math.round(extrudeScale * extrude.y); this.bytes[pos + 6] = (linesofar || 0) / 128; this.bytes[pos + 7] = (linesofar || 0) % 128; this.pos += this.itemSize; return index; } }); },{"../../util/util":96,"./buffer":1}],9:[function(require,module,exports){ 'use strict'; var util = require('../../util/util'); var Buffer = require('./buffer'); module.exports = OutlineElementsBuffer; function OutlineElementsBuffer(buffer) { Buffer.call(this, buffer); } OutlineElementsBuffer.prototype = util.inherit(Buffer, { itemSize: 4, // bytes per line (2 * unsigned short == 4 bytes) arrayType: 'ELEMENT_ARRAY_BUFFER', add: function(a, b) { var pos2 = this.pos / 2; this.resize(); this.ushorts[pos2 + 0] = a; this.ushorts[pos2 + 1] = b; this.pos += this.itemSize; } }); },{"../../util/util":96,"./buffer":1}],10:[function(require,module,exports){ 'use strict'; var util = require('../../util/util'); var Buffer = require('./buffer'); module.exports = TriangleElementsBuffer; function TriangleElementsBuffer(buffer) { Buffer.call(this, buffer); } TriangleElementsBuffer.prototype = util.inherit(Buffer, { itemSize: 6, // bytes per triangle (3 * unsigned short == 6 bytes) arrayType: 'ELEMENT_ARRAY_BUFFER', add: function(a, b, c) { var pos2 = this.pos / 2; this.resize(); this.ushorts[pos2 + 0] = a; this.ushorts[pos2 + 1] = b; this.ushorts[pos2 + 2] = c; this.pos += this.itemSize; } }); },{"../../util/util":96,"./buffer":1}],11:[function(require,module,exports){ 'use strict'; module.exports = createBucket; var LineBucket = require('./line_bucket'); var FillBucket = require('./fill_bucket'); var SymbolBucket = require('./symbol_bucket'); var LayoutProperties = require('../style/layout_properties'); var featureFilter = require('feature-filter'); var StyleDeclarationSet = require('../style/style_declaration_set'); function createBucket(layer, buffers, z, overscaling, collisionDebug) { var values = new StyleDeclarationSet('layout', layer.type, layer.layout, {}).values(), fakeZoomHistory = { lastIntegerZoom: Infinity, lastIntegerZoomTime: 0, lastZoom: 0 }, layout = {}; for (var k in values) { layout[k] = values[k].calculate(z, fakeZoomHistory); } var BucketClass = layer.type === 'line' ? LineBucket : layer.type === 'fill' ? FillBucket : layer.type === 'symbol' ? SymbolBucket : null; var bucket = new BucketClass(buffers, new LayoutProperties[layer.type](layout), overscaling, z, collisionDebug); bucket.id = layer.id; bucket.type = layer.type; bucket['source-layer'] = layer['source-layer']; bucket.interactive = layer.interactive; bucket.minZoom = layer.minzoom; bucket.maxZoom = layer.maxzoom; bucket.filter = featureFilter(layer.filter); bucket.features = []; return bucket; } },{"../style/layout_properties":49,"../style/style_declaration_set":55,"./fill_bucket":14,"./line_bucket":15,"./symbol_bucket":16,"feature-filter":102}],12:[function(require,module,exports){ 'use strict'; module.exports = ElementGroups; function ElementGroups(vertexBuffer, elementBuffer, secondElementBuffer) { this.vertexBuffer = vertexBuffer; this.elementBuffer = elementBuffer; this.secondElementBuffer = secondElementBuffer; this.groups = []; } ElementGroups.prototype.makeRoomFor = function(numVertices) { if (!this.current || this.current.vertexLength + numVertices > 65535) { this.current = new ElementGroup(this.vertexBuffer.index, this.elementBuffer && this.elementBuffer.index, this.secondElementBuffer && this.secondElementBuffer.index); this.groups.push(this.current); } }; function ElementGroup(vertexStartIndex, elementStartIndex, secondElementStartIndex) { // the offset into the vertex buffer of the first vertex in this group this.vertexStartIndex = vertexStartIndex; this.elementStartIndex = elementStartIndex; this.secondElementStartIndex = secondElementStartIndex; this.elementLength = 0; this.vertexLength = 0; this.secondElementLength = 0; } },{}],13:[function(require,module,exports){ 'use strict'; var rbush = require('rbush'); var Point = require('point-geometry'); var vt = require('vector-tile'); var util = require('../util/util'); module.exports = FeatureTree; function FeatureTree(coord, overscaling) { this.x = coord.x; this.y = coord.y; this.z = coord.z - Math.log(overscaling) / Math.LN2; this.rtree = rbush(9); this.toBeInserted = []; } FeatureTree.prototype.insert = function(bbox, layers, feature) { bbox.layers = layers; bbox.feature = feature; this.toBeInserted.push(bbox); }; // bulk insert into tree FeatureTree.prototype._load = function() { this.rtree.load(this.toBeInserted); this.toBeInserted = []; }; // Finds features in this tile at a particular position. FeatureTree.prototype.query = function(args, callback) { if (this.toBeInserted.length) this._load(); var params = args.params || {}, radius = (params.radius || 0) * 4096 / args.scale, x = args.x, y = args.y, result = []; var matching = this.rtree.search([ x - radius, y - radius, x + radius, y + radius ]); for (var i = 0; i < matching.length; i++) { var feature = matching[i].feature, layers = matching[i].layers, type = vt.VectorTileFeature.types[feature.type]; if (params.$type && type !== params.$type) continue; if (!geometryContainsPoint(feature.loadGeometry(), type, new Point(x, y), radius)) continue; var geoJSON = feature.toGeoJSON(this.x, this.y, this.z); for (var l = 0; l < layers.length; l++) { var layer = layers[l]; if (params.layer && layer !== params.layer.id) continue; result.push(util.extend({layer: layer}, geoJSON)); } } callback(null, result); }; function geometryContainsPoint(rings, type, p, radius) { return type === 'Point' ? pointContainsPoint(rings, p, radius) : type === 'LineString' ? lineContainsPoint(rings, p, radius) : type === 'Polygon' ? polyContainsPoint(rings, p) || lineContainsPoint(rings, p, radius) : false; } // Code from http://stackoverflow.com/a/1501725/331379. function distToSegmentSquared(p, v, w) { var l2 = v.distSqr(w); if (l2 === 0) return p.distSqr(v); var t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; if (t < 0) return p.distSqr(v); if (t > 1) return p.distSqr(w); return p.distSqr(w.sub(v)._mult(t)._add(v)); } function lineContainsPoint(rings, p, radius) { var r = radius * radius; for (var i = 0; i < rings.length; i++) { var ring = rings[i]; for (var j = 1; j < ring.length; j++) { // Find line segments that have a distance <= radius^2 to p // In that case, we treat the line as "containing point p". var v = ring[j - 1], w = ring[j]; if (distToSegmentSquared(p, v, w) < r) return true; } } return false; } // point in polygon ray casting algorithm function polyContainsPoint(rings, p) { var c = false, ring, p1, p2; for (var k = 0; k < rings.length; k++) { ring = rings[k]; for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) { p1 = ring[i]; p2 = ring[j]; if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { c = !c; } } } return c; } function pointContainsPoint(rings, p, radius) { var r = radius * radius; for (var i = 0; i < rings.length; i++) { var ring = rings[i]; for (var j = 0; j < ring.length; j++) { if (ring[j].distSqr(p) <= r) return true; } } return false; } },{"../util/util":96,"point-geometry":127,"rbush":128,"vector-tile":131}],14:[function(require,module,exports){ 'use strict'; var ElementGroups = require('./element_groups'); module.exports = FillBucket; function FillBucket(buffers) { this.buffers = buffers; this.elementGroups = new ElementGroups(buffers.fillVertex, buffers.fillElement, buffers.outlineElement); } FillBucket.prototype.addFeatures = function() { var features = this.features; for (var i = 0; i < features.length; i++) { var feature = features[i]; this.addFeature(feature.loadGeometry()); } }; FillBucket.prototype.addFeature = function(lines) { for (var i = 0; i < lines.length; i++) { this.addFill(lines[i]); } }; FillBucket.prototype.addFill = function(vertices) { if (vertices.length < 3) { //console.warn('a fill must have at least three vertices'); return; } // Calculate the total number of vertices we're going to produce so that we // can resize the buffer beforehand, or detect whether the current line // won't fit into the buffer anymore. // In order to be able to use the vertex buffer for drawing the antialiased // outlines, we separate all polygon vertices with a degenerate (out-of- // viewplane) vertex. var len = vertices.length; // Check whether this geometry buffer can hold all the required vertices. this.elementGroups.makeRoomFor(len + 1); var elementGroup = this.elementGroups.current; var fillVertex = this.buffers.fillVertex; var fillElement = this.buffers.fillElement; var outlineElement = this.buffers.outlineElement; // We're generating triangle fans, so we always start with the first coordinate in this polygon. var firstIndex = fillVertex.index - elementGroup.vertexStartIndex, prevIndex, currentIndex, currentVertex; for (var i = 0; i < vertices.length; i++) { currentIndex = fillVertex.index - elementGroup.vertexStartIndex; currentVertex = vertices[i]; fillVertex.add(currentVertex.x, currentVertex.y); elementGroup.vertexLength++; // Only add triangles that have distinct vertices. if (i >= 2 && (currentVertex.x !== vertices[0].x || currentVertex.y !== vertices[0].y)) { fillElement.add(firstIndex, prevIndex, currentIndex); elementGroup.elementLength++; } if (i >= 1) { outlineElement.add(prevIndex, currentIndex); elementGroup.secondElementLength++; } prevIndex = currentIndex; } }; },{"./element_groups":12}],15:[function(require,module,exports){ 'use strict'; var ElementGroups = require('./element_groups'); module.exports = LineBucket; /** * @class LineBucket * @private */ function LineBucket(buffers, layoutProperties) { this.buffers = buffers; this.elementGroups = new ElementGroups(buffers.lineVertex, buffers.lineElement); this.layoutProperties = layoutProperties; } LineBucket.prototype.addFeatures = function() { var features = this.features; for (var i = 0; i < features.length; i++) { var feature = features[i]; this.addFeature(feature.loadGeometry()); } }; LineBucket.prototype.addFeature = function(lines) { var layoutProperties = this.layoutProperties; for (var i = 0; i < lines.length; i++) { this.addLine(lines[i], layoutProperties['line-join'], layoutProperties['line-cap'], layoutProperties['line-miter-limit'], layoutProperties['line-round-limit']); } }; LineBucket.prototype.addLine = function(vertices, join, cap, miterLimit, roundLimit) { var len = vertices.length; // If the line has duplicate vertices at the end, adjust length to remove them. while (len > 2 && vertices[len - 1].equals(vertices[len - 2])) { len--; } if (vertices.length < 2) { //console.warn('a line must have at least two vertices'); return; } if (join === 'bevel') miterLimit = 1.05; var firstVertex = vertices[0], lastVertex = vertices[len - 1], closed = firstVertex.equals(lastVertex); // we could be more precise, but it would only save a negligible amount of space this.elementGroups.makeRoomFor(len * 4); if (len === 2 && closed) { // console.warn('a line may not have coincident points'); return; } var beginCap = cap, endCap = closed ? 'butt' : cap, flip = 1, distance = 0, startOfLine = true, currentVertex, prevVertex, nextVertex, prevNormal, nextNormal, offsetA, offsetB; // the last three vertices added this.e1 = this.e2 = this.e3 = -1; 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; // The join if a middle vertex, otherwise the cap. var middleVertex = prevVertex && nextVertex; var currentJoin = middleVertex ? join : nextVertex ? beginCap : endCap; if (middleVertex && currentJoin === 'round' && miterLength < roundLimit) { currentJoin = 'miter'; } if (currentJoin === 'miter' && miterLength > miterLimit) { currentJoin = 'bevel'; } if (currentJoin === 'bevel') { // The maximum extrude length is 128 / 63 = 2 times the width of the line // so if miterLength >= 2 we need to draw a different type of bevel where. if (miterLength > 2) currentJoin = 'flipbevel'; // If the miterLength is really small and the line bevel wouldn't be visible, // just draw a miter join to save a triangle. if (miterLength < miterLimit) currentJoin = 'miter'; } if (currentJoin === 'miter') { joinNormal._mult(miterLength); this.addCurrentVertex(currentVertex, flip, distance, joinNormal, 0, 0, false); } else if (currentJoin === 'flipbevel') { // miter is too big, flip the direction to make a beveled join if (miterLength > 100) { // Almost parallel lines joinNormal = nextNormal.clone(); } else { var direction = prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x > 0 ? -1 : 1; var bevelLength = miterLength * prevNormal.add(nextNormal).mag() / prevNormal.sub(nextNormal).mag(); joinNormal._perp()._mult(bevelLength * direction); } this.addCurrentVertex(currentVertex, flip, distance, joinNormal, 0, 0, false); flip = -flip; } else if (currentJoin === 'bevel') { var dir = prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x; var offset = -Math.sqrt(miterLength * miterLength - 1); if (flip * dir > 0) { offsetB = 0; offsetA = offset; } else { offsetA = 0; offsetB = offset; } // Close previous segment with a bevel if (!startOfLine) { this.addCurrentVertex(currentVertex, flip, distance, prevNormal, offsetA, offsetB, false); } // Start next segment if (nextVertex) { this.addCurrentVertex(currentVertex, flip, distance, nextNormal, -offsetA, -offsetB, false); } } else if (currentJoin === 'butt') { if (!startOfLine) { // Close previous segment with a butt this.addCurrentVertex(currentVertex, flip, distance, prevNormal, 0, 0, false); } // Start next segment with a butt if (nextVertex) { this.addCurrentVertex(currentVertex, flip, distance, nextNormal, 0, 0, false); } } else if (currentJoin === 'square') { if (!startOfLine) { // Close previous segment with a square cap this.addCurrentVertex(currentVertex, flip, distance, prevNormal, 1, 1, false); // The segment is done. Unset vertices to disconnect segments. this.e1 = this.e2 = -1; flip = 1; } // Start next segment if (nextVertex) { this.addCurrentVertex(currentVertex, flip, distance, nextNormal, -1, -1, false); } } else if (currentJoin === 'round') { if (!startOfLine) { // Close previous segment with butt this.addCurrentVertex(currentVertex, flip, distance, prevNormal, 0, 0, false); // Add round cap or linejoin at end of segment this.addCurrentVertex(currentVertex, flip, distance, prevNormal, 1, 1, true); // The segment is done. Unset vertices to disconnect segments. this.e1 = this.e2 = -1; flip = 1; } else if (beginCap === 'round') { // Add round cap before first segment this.addCurrentVertex(currentVertex, flip, distance, nextNormal, -1, -1, true); } // Start next segment with a butt if (nextVertex) { this.addCurrentVertex(currentVertex, flip, distance, nextNormal, 0, 0, false); } } startOfLine = false; } }; /** * Add two vertices to the buffers. * * @param {Object} currentVertex the line vertex to add buffer vertices for * @param {number} flip -1 if the vertices should be flipped, 1 otherwise * @param {number} distance the distance from the beggining of the line to the vertex * @param {number} endLeft extrude to shift the left vertex along the line * @param {number} endRight extrude to shift the left vertex along the line * @param {boolean} round whether this is a round cap * @private */ LineBucket.prototype.addCurrentVertex = function(currentVertex, flip, distance, normal, endLeft, endRight, round) { var tx = round ? 1 : 0; var extrude; var lineVertex = this.buffers.lineVertex; var lineElement = this.buffers.lineElement; var elementGroup = this.elementGroups.current; var vertexStartIndex = this.elementGroups.current.vertexStartIndex; extrude = normal.mult(flip); if (endLeft) extrude._sub(normal.perp()._mult(endLeft)); this.e3 = lineVertex.add(currentVertex, extrude, tx, 0, distance) - vertexStartIndex; if (this.e1 >= 0 && this.e2 >= 0) { lineElement.add(this.e1, this.e2, this.e3); elementGroup.elementLength++; } this.e1 = this.e2; this.e2 = this.e3; extrude = normal.mult(-flip); if (endRight) extrude._sub(normal.perp()._mult(endRight)); this.e3 = lineVertex.add(currentVertex, extrude, tx, 1, distance) - vertexStartIndex; if (this.e1 >= 0 && this.e2 >= 0) { lineElement.add(this.e1, this.e2, this.e3); elementGroup.elementLength++; } this.e1 = this.e2; this.e2 = this.e3; elementGroup.vertexLength += 2; }; },{"./element_groups":12}],16:[function(require,module,exports){ 'use strict'; var ElementGroups = require('./element_groups'); var Anchor = require('../symbol/anchor'); var getAnchors = require('../symbol/get_anchors'); var resolveTokens = require('../util/token'); var Quads = require('../symbol/quads'); var Shaping = require('../symbol/shaping'); var resolveText = require('../symbol/resolve_text'); var resolveIcons = require('../symbol/resolve_icons'); var mergeLines = require('../symbol/mergelines'); var shapeText = Shaping.shapeText; var shapeIcon = Shaping.shapeIcon; var getGlyphQuads = Quads.getGlyphQuads; var getIconQuads = Quads.getIconQuads; var clipLine = require('../symbol/clip_line'); var Point = require('point-geometry'); var CollisionFeature = require('../symbol/collision_feature'); module.exports = SymbolBucket; function SymbolBucket(buffers, layoutProperties, overscaling, zoom, collisionDebug) { this.buffers = buffers; this.layoutProperties = layoutProperties; this.overscaling = overscaling; this.zoom = zoom; this.collisionDebug = collisionDebug; var tileSize = 512 * overscaling; var tileExtent = 4096; this.tilePixelRatio = tileExtent / tileSize; this.symbolInstances = []; } SymbolBucket.prototype.needsPlacement = true; SymbolBucket.prototype.addFeatures = function(collisionTile) { var layout = this.layoutProperties; var features = this.features; var textFeatures = this.textFeatures; var horizontalAlign = 0.5, verticalAlign = 0.5; switch (layout['text-anchor']) { case 'right': case 'top-right': case 'bottom-right': horizontalAlign = 1; break; case 'left': case 'top-left': case 'bottom-left': horizontalAlign = 0; break; } switch (layout['text-anchor']) { case 'bottom': case 'bottom-right': case 'bottom-left': verticalAlign = 1; break; case 'top': case 'top-right': case 'top-left': verticalAlign = 0; break; } var justify = layout['text-justify'] === 'right' ? 1 : layout['text-justify'] === 'left' ? 0 : 0.5; var oneEm = 24; var lineHeight = layout['text-line-height'] * oneEm; var maxWidth = layout['symbol-placement'] !== 'line' ? layout['text-max-width'] * oneEm : 0; var spacing = layout['text-letter-spacing'] * oneEm; var textOffset = [layout['text-offset'][0] * oneEm, layout['text-offset'][1] * oneEm]; var fontstack = layout['text-font']; var geometries = []; for (var g = 0; g < features.length; g++) { geometries.push(features[g].loadGeometry()); } if (layout['symbol-placement'] === 'line') { // Merge adjacent lines with the same text to improve labelling. // It's better to place labels on one long line than on many short segments. var merged = mergeLines(features, textFeatures, geometries); geometries = merged.geometries; features = merged.features; textFeatures = merged.textFeatures; } var shapedText, shapedIcon; for (var k = 0; k < features.length; k++) { if (!geometries[k]) continue; if (textFeatures[k]) { shapedText = shapeText(textFeatures[k], this.stacks[fontstack], maxWidth, lineHeight, horizontalAlign, verticalAlign, justify, spacing, textOffset); } else { shapedText = null; } if (layout['icon-image']) { var iconName = resolveTokens(features[k].properties, layout['icon-image']); var image = this.icons[iconName]; shapedIcon = shapeIcon(image, layout); if (image) { if (this.sdfIcons === undefined) { this.sdfIcons = image.sdf; } else if (this.sdfIcons !== image.sdf) { console.warn('Style sheet warning: Cannot mix SDF and non-SDF icons in one bucket'); } } } else { shapedIcon = null; } if (shapedText || shapedIcon) { this.addFeature(geometries[k], shapedText, shapedIcon); } } this.placeFeatures(collisionTile, this.buffers, this.collisionDebug); }; SymbolBucket.prototype.addFeature = function(lines, shapedText, shapedIcon) { var layout = this.layoutProperties; var glyphSize = 24; var fontScale = layout['text-max-size'] / glyphSize, textBoxScale = this.tilePixelRatio * fontScale, iconBoxScale = this.tilePixelRatio * layout['icon-max-size'], symbolMinDistance = this.tilePixelRatio * layout['symbol-min-distance'], avoidEdges = layout['symbol-avoid-edges'], textPadding = layout['text-padding'] * this.tilePixelRatio, iconPadding = layout['icon-padding'] * this.tilePixelRatio, textMaxAngle = layout['text-max-angle'] / 180 * Math.PI, textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line', iconAlongLine = layout['icon-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line', mayOverlap = layout['text-allow-overlap'] || layout['icon-allow-overlap'] || layout['text-ignore-placement'] || layout['icon-ignore-placement']; if (layout['symbol-placement'] === 'line') { lines = clipLine(lines, 0, 0, 4096, 4096); } for (var i = 0; i < lines.length; i++) { var line = lines[i]; // Calculate the anchor points around which you want to place labels var anchors = layout['symbol-placement'] === 'line' ? getAnchors(line, symbolMinDistance, textMaxAngle, shapedText, glyphSize, textBoxScale, this.overscaling) : [ new Anchor(line[0].x, line[0].y, 0) ]; // For each potential label, create the placement features used to check for collisions, and the quads use for rendering. for (var j = 0, len = anchors.length; j < len; j++) { var anchor = anchors[j]; var inside = !(anchor.x < 0 || anchor.x > 4096 || anchor.y < 0 || anchor.y > 4096); if (avoidEdges && !inside) continue; // Normally symbol layers are drawn across tile boundaries. Only symbols // with their anchors within the tile boundaries are added to the buffers // to prevent symbols from being drawn twice. // // Symbols in layers with overlap are sorted in the y direction so that // symbols lower on the canvas are drawn on top of symbols near the top. // To preserve this order across tile boundaries these symbols can't // be drawn across tile boundaries. Instead they need to be included in // the buffers for both tiles and clipped to tile boundaries at draw time. var addToBuffers = inside || mayOverlap; this.symbolInstances.push(new SymbolInstance(anchor, line, shapedText, shapedIcon, layout, addToBuffers, textBoxScale, textPadding, textAlongLine, iconBoxScale, iconPadding, iconAlongLine)); } } }; SymbolBucket.prototype.placeFeatures = function(collisionTile, buffers, collisionDebug) { // Calculate which labels can be shown and when they can be shown and // create the bufers used for rendering. this.buffers = buffers; var elementGroups = this.elementGroups = { text: new ElementGroups(buffers.glyphVertex, buffers.glyphElement), icon: new ElementGroups(buffers.iconVertex, buffers.iconElement), sdfIcons: this.sdfIcons }; var layout = this.layoutProperties; var maxScale = collisionTile.maxScale; var textAlongLine = layout['text-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; var iconAlongLine = layout['icon-rotation-alignment'] === 'map' && layout['symbol-placement'] === 'line'; var mayOverlap = layout['text-allow-overlap'] || layout['icon-allow-overlap'] || layout['text-ignore-placement'] || layout['icon-ignore-placement']; // Sort symbols by their y position on the canvas so that they lower symbols // are drawn on top of higher symbols. // Don't sort symbols that won't overlap because it isn't necessary and // because it causes more labels to pop in and out when rotating. if (mayOverlap) { var angle = collisionTile.angle; var sin = Math.sin(angle), cos = Math.cos(angle); this.symbolInstances.sort(function(a, b) { var aRotated = sin * a.x + cos * a.y; var bRotated = sin * b.x + cos * b.y; return bRotated - aRotated; }); } for (var p = 0; p < this.symbolInstances.length; p++) { var symbolInstance = this.symbolInstances[p]; var hasText = symbolInstance.hasText; var hasIcon = symbolInstance.hasIcon; var iconWithoutText = layout['text-optional'] || !hasText, textWithoutIcon = layout['icon-optional'] || !hasIcon; // Calculate the scales at which the text and icon can be placed without collision. var glyphScale = hasText && !layout['text-allow-overlap'] ? collisionTile.placeCollisionFeature(symbolInstance.textCollisionFeature) : collisionTile.minScale; var iconScale = hasIcon && !layout['icon-allow-overlap'] ? collisionTile.placeCollisionFeature(symbolInstance.iconCollisionFeature) : collisionTile.minScale; // Combine the scales for icons and text. if (!iconWithoutText && !textWithoutIcon) { iconScale = glyphScale = Math.max(iconScale, glyphScale); } else if (!textWithoutIcon && glyphScale) { glyphScale = Math.max(iconScale, glyphScale); } else if (!iconWithoutText && iconScale) { iconScale = Math.max(iconScale, glyphScale); } // Insert final placement into collision tree and add glyphs/icons to buffers if (hasText) { if (!layout['text-ignore-placement']) { collisionTile.insertCollisionFeature(symbolInstance.textCollisionFeature, glyphScale); } if (glyphScale <= maxScale) { this.addSymbols(buffers.glyphVertex, buffers.glyphElement, elementGroups.text, symbolInstance.glyphQuads, glyphScale, layout['text-keep-upright'], textAlongLine, collisionTile.angle); } } if (hasIcon) { if (!layout['icon-ignore-placement']) { collisionTile.insertCollisionFeature(symbolInstance.iconCollisionFeature, iconScale); } if (iconScale <= maxScale) { this.addSymbols(buffers.iconVertex, buffers.iconElement, elementGroups.icon, symbolInstance.iconQuads, iconScale, layout['icon-keep-upright'], iconAlongLine, collisionTile.angle); } } } if (collisionDebug) this.addToDebugBuffers(collisionTile); }; SymbolBucket.prototype.addSymbols = function(vertex, element, elementGroups, quads, scale, keepUpright, alongLine, placementAngle) { elementGroups.makeRoomFor(4 * quads.length); var elementGroup = elementGroups.current; var zoom = this.zoom; var placementZoom = Math.max(Math.log(scale) / Math.LN2 + zoom, 0); for (var k = 0; k < quads.length; k++) { var symbol = quads[k], angle = symbol.angle; // drop upside down versions of glyphs var a = (angle + placementAngle + Math.PI) % (Math.PI * 2); if (keepUpright && alongLine && (a <= Math.PI / 2 || a > Math.PI * 3 / 2)) continue; var tl = symbol.tl, tr = symbol.tr, bl = symbol.bl, br = symbol.br, tex = symbol.tex, anchorPoint = symbol.anchorPoint, minZoom = Math.max(zoom + Math.log(symbol.minScale) / Math.LN2, placementZoom), maxZoom = Math.min(zoom + Math.log(symbol.maxScale) / Math.LN2, 25); if (maxZoom <= minZoom) continue; // Lower min zoom so that while fading out the label it can be shown outside of collision-free zoom levels if (minZoom === placementZoom) minZoom = 0; var triangleIndex = vertex.index - elementGroup.vertexStartIndex; vertex.add(anchorPoint.x, anchorPoint.y, tl.x, tl.y, tex.x, tex.y, minZoom, maxZoom, placementZoom); vertex.add(anchorPoint.x, anchorPoint.y, tr.x, tr.y, tex.x + tex.w, tex.y, minZoom, maxZoom, placementZoom); vertex.add(anchorPoint.x, anchorPoint.y, bl.x, bl.y, tex.x, tex.y + tex.h, minZoom, maxZoom, placementZoom); vertex.add(anchorPoint.x, anchorPoint.y, br.x, br.y, tex.x + tex.w, tex.y + tex.h, minZoom, maxZoom, placementZoom); elementGroup.vertexLength += 4; element.add(triangleIndex, triangleIndex + 1, triangleIndex + 2); element.add(triangleIndex + 1, triangleIndex + 2, triangleIndex + 3); elementGroup.elementLength += 2; } }; SymbolBucket.prototype.getDependencies = function(tile, actor, callback) { var firstdone = false; this.getTextDependencies(tile, actor, done); this.getIconDependencies(tile, actor, done); function done(err) { if (err || firstdone) return callback(err); firstdone = true; } }; SymbolBucket.prototype.getIconDependencies = function(tile, actor, callback) { if (this.layoutProperties['icon-image']) { var features = this.features; var icons = resolveIcons(features, this.layoutProperties); if (icons.length) { actor.send('get icons', { icons: icons }, setIcons.bind(this)); } else { callback(); } } else { callback(); } function setIcons(err, newicons) { if (err) return callback(err); this.icons = newicons; callback(); } }; SymbolBucket.prototype.getTextDependencies = function(tile, actor, callback) { var features = this.features; var fontstack = this.layoutProperties['text-font']; var stacks = this.stacks = tile.stacks; if (stacks[fontstack] === undefined) { stacks[fontstack] = {}; } var stack = stacks[fontstack]; var data = resolveText(features, this.layoutProperties, stack); this.textFeatures = data.textFeatures; actor.send('get glyphs', { uid: tile.uid, fontstack: fontstack, codepoints: data.codepoints }, function(err, newstack) { if (err) return callback(err); for (var codepoint in newstack) { stack[codepoint] = newstack[codepoint]; } callback(); }); }; SymbolBucket.prototype.addToDebugBuffers = function(collisionTile) { this.elementGroups.collisionBox = new ElementGroups(this.buffers.collisionBoxVertex); this.elementGroups.collisionBox.makeRoomFor(0); var buffer = this.buffers.collisionBoxVertex; var angle = -collisionTile.angle; var yStretch = collisionTile.yStretch; for (var j = 0; j < this.symbolInstances.length; j++) { for (var i = 0; i < 2; i++) { var feature = this.symbolInstances[j][i === 0 ? 'textCollisionFeature' : 'iconCollisionFeature']; if (!feature) continue; var boxes = feature.boxes; for (var b = 0; b < boxes.length; b++) { var box = boxes[b]; var anchorPoint = box.anchorPoint; var tl = new Point(box.x1, box.y1 * yStretch)._rotate(angle); var tr = new Point(box.x2, box.y1 * yStretch)._rotate(angle); var bl = new Point(box.x1, box.y2 * yStretch)._rotate(angle); var br = new Point(box.x2, box.y2 * yStretch)._rotate(angle); var maxZoom = Math.max(0, Math.min(25, this.zoom + Math.log(box.maxScale) / Math.LN2)); var placementZoom = Math.max(0, Math.min(25, this.zoom + Math.log(box.placementScale) / Math.LN2)); buffer.add(anchorPoint, tl, maxZoom, placementZoom); buffer.add(anchorPoint, tr, maxZoom, placementZoom); buffer.add(anchorPoint, tr, maxZoom, placementZoom); buffer.add(anchorPoint, br, maxZoom, placementZoom); buffer.add(anchorPoint, br, maxZoom, placementZoom); buffer.add(anchorPoint, bl, maxZoom, placementZoom); buffer.add(anchorPoint, bl, maxZoom, placementZoom); buffer.add(anchorPoint, tl, maxZoom, placementZoom); this.elementGroups.collisionBox.current.vertexLength +=