mapbox-gl
Version:
A WebGL interactive maps library
1,405 lines (1,095 loc) • 1.79 MB
JavaScript
(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 +=