mapbox-gl
Version:
A WebGL interactive maps library
1,436 lines (1,194 loc) • 2.24 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';
var featureFilter = require('feature-filter');
var ElementGroups = require('./element_groups');
var Buffer = require('./buffer');
var StyleLayer = require('../style/style_layer');
module.exports = Bucket;
/**
* Instantiate the appropriate subclass of `Bucket` for `options`.
* @private
* @param options See `Bucket` constructor options
* @returns {Bucket}
*/
Bucket.create = function(options) {
var Classes = {
fill: require('./fill_bucket'),
line: require('./line_bucket'),
circle: require('./circle_bucket'),
symbol: require('./symbol_bucket')
};
return new Classes[options.layer.type](options);
};
Bucket.AttributeType = Buffer.AttributeType;
/**
* The `Bucket` class builds a set of `Buffer`s for a set of vector tile
* features.
*
* `Bucket` is an abstract class. A subclass exists for each Mapbox GL
* style spec layer type. Because `Bucket` is an abstract class,
* instances should be created via the `Bucket.create` method.
*
* For performance reasons, `Bucket` creates its "add"s methods at
* runtime using `new Function(...)`.
*
* @class Bucket
* @private
* @param options
* @param {number} options.zoom Zoom level of the buffers being built. May be
* a fractional zoom level.
* @param options.layer A Mapbox GL style layer object
* @param {Object.<string, Buffer>} options.buffers The set of `Buffer`s being
* built for this tile. This object facilitates sharing of `Buffer`s be
between `Bucket`s.
*/
function Bucket(options) {
this.zoom = options.zoom;
this.overscaling = options.overscaling;
this.layer = StyleLayer.create(options.layer);
this.layer.recalculate(this.zoom, { lastIntegerZoom: Infinity, lastIntegerZoomTime: 0, lastZoom: 0 });
this.layers = [this.layer.id];
this.type = this.layer.type;
this.features = [];
this.id = this.layer.id;
this['source-layer'] = this.layer['source-layer'];
this.interactive = this.layer.interactive;
this.minZoom = this.layer.minzoom;
this.maxZoom = this.layer.maxzoom;
this.filter = featureFilter(this.layer.filter);
this.resetBuffers(options.buffers);
for (var shaderName in this.shaders) {
var shader = this.shaders[shaderName];
this[this.getAddMethodName(shaderName, 'vertex')] = createVertexAddMethod(
shaderName,
shader,
this.getBufferName(shaderName, 'vertex')
);
}
}
/**
* Build the buffers! Features are set directly to the `features` property.
* @private
*/
Bucket.prototype.addFeatures = function() {
for (var i = 0; i < this.features.length; i++) {
this.addFeature(this.features[i]);
}
};
/**
* Check if there is enough space available in the current element group for
* `vertexLength` vertices. If not, append a new elementGroup. Should be called
* by `addFeatures` and its callees.
* @private
* @param {string} shaderName the name of the shader associated with the buffer that will receive the vertices
* @param {number} vertexLength The number of vertices that will be inserted to the buffer.
*/
Bucket.prototype.makeRoomFor = function(shaderName, vertexLength) {
return this.elementGroups[shaderName].makeRoomFor(vertexLength);
};
/**
* Start using a new shared `buffers` object and recreate instances of `Buffer`
* as necessary.
* @private
* @param {Object.<string, Buffer>} buffers
*/
Bucket.prototype.resetBuffers = function(buffers) {
this.buffers = buffers;
this.elementGroups = {};
for (var shaderName in this.shaders) {
var shader = this.shaders[shaderName];
var vertexBufferName = this.getBufferName(shaderName, 'vertex');
if (shader.vertexBuffer && !buffers[vertexBufferName]) {
buffers[vertexBufferName] = new Buffer({
type: Buffer.BufferType.VERTEX,
attributes: shader.attributes
});
}
if (shader.elementBuffer) {
var elementBufferName = this.getBufferName(shaderName, 'element');
if (!buffers[elementBufferName]) {
buffers[elementBufferName] = createElementBuffer(shader.elementBufferComponents);
}
this[this.getAddMethodName(shaderName, 'element')] = createElementAddMethod(this.buffers[elementBufferName]);
}
if (shader.secondElementBuffer) {
var secondElementBufferName = this.getBufferName(shaderName, 'secondElement');
if (!buffers[secondElementBufferName]) {
buffers[secondElementBufferName] = createElementBuffer(shader.secondElementBufferComponents);
}
this[this.getAddMethodName(shaderName, 'secondElement')] = createElementAddMethod(this.buffers[secondElementBufferName]);
}
this.elementGroups[shaderName] = new ElementGroups(
buffers[this.getBufferName(shaderName, 'vertex')],
buffers[this.getBufferName(shaderName, 'element')],
buffers[this.getBufferName(shaderName, 'secondElement')]
);
}
};
/**
* Get the name of the method used to add an item to a buffer.
* @param {string} shaderName The name of the shader that will use the buffer
* @param {string} type One of "vertex", "element", or "secondElement"
* @returns {string}
*/
Bucket.prototype.getAddMethodName = function(shaderName, type) {
return 'add' + capitalize(shaderName) + capitalize(type);
};
/**
* Get the name of a buffer.
* @param {string} shaderName The name of the shader that will use the buffer
* @param {string} type One of "vertex", "element", or "secondElement"
* @returns {string}
*/
Bucket.prototype.getBufferName = function(shaderName, type) {
return shaderName + capitalize(type);
};
var createVertexAddMethodCache = {};
function createVertexAddMethod(shaderName, shader, bufferName) {
var pushArgs = [];
for (var i = 0; i < shader.attributes.length; i++) {
pushArgs = pushArgs.concat(shader.attributes[i].value);
}
var body = 'return this.buffers.' + bufferName + '.push(' + pushArgs.join(', ') + ');';
if (!createVertexAddMethodCache[body]) {
createVertexAddMethodCache[body] = new Function(shader.attributeArgs, body);
}
return createVertexAddMethodCache[body];
}
function createElementAddMethod(buffer) {
return function(one, two, three) {
return buffer.push(one, two, three);
};
}
function createElementBuffer(components) {
return new Buffer({
type: Buffer.BufferType.ELEMENT,
attributes: [{
name: 'vertices',
components: components || 3,
type: Buffer.ELEMENT_ATTRIBUTE_TYPE
}]
});
}
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
},{"../style/style_layer":47,"./buffer":2,"./circle_bucket":3,"./element_groups":4,"./fill_bucket":6,"./line_bucket":7,"./symbol_bucket":9,"feature-filter":107}],2:[function(require,module,exports){
'use strict';
// Note: all "sizes" are measured in bytes
var assert = require('assert');
/**
* The `Buffer` class is responsible for managing one instance of `ArrayBuffer`. `ArrayBuffer`s
* provide low-level read/write access to a chunk of memory. `ArrayBuffer`s are populated with
* per-vertex data, uploaded to the GPU, and used in rendering.
*
* `Buffer` provides an abstraction over `ArrayBuffer`, making it behave like an array of
* statically typed structs. A buffer is comprised of items. An item is comprised of a set of
* attributes. Attributes are defined when the class is constructed.
*
* @class Buffer
* @private
* @param options
* @param {BufferType} options.type
* @param {Array.<BufferAttribute>} options.attributes
*/
function Buffer(options) {
this.type = options.type;
// Clone an existing Buffer
if (options.arrayBuffer) {
this.capacity = options.capacity;
this.arrayBuffer = options.arrayBuffer;
this.attributes = options.attributes;
this.itemSize = options.itemSize;
this.length = options.length;
// Create a new Buffer
} else {
this.capacity = align(Buffer.CAPACITY_DEFAULT, Buffer.CAPACITY_ALIGNMENT);
this.arrayBuffer = new ArrayBuffer(this.capacity);
this.attributes = [];
this.itemSize = 0;
this.length = 0;
// Vertex buffer attributes must be aligned to word boundaries but
// element buffer attributes do not need to be aligned.
var attributeAlignment = this.type === Buffer.BufferType.VERTEX ? Buffer.VERTEX_ATTRIBUTE_ALIGNMENT : 1;
this.attributes = options.attributes.map(function(attributeOptions) {
var attribute = {};
attribute.name = attributeOptions.name;
attribute.components = attributeOptions.components || 1;
attribute.type = attributeOptions.type || Buffer.AttributeType.UNSIGNED_BYTE;
attribute.size = attribute.type.size * attribute.components;
attribute.offset = this.itemSize;
this.itemSize = align(attribute.offset + attribute.size, attributeAlignment);
assert(!isNaN(this.itemSize));
assert(!isNaN(attribute.size));
assert(attribute.type.name in Buffer.AttributeType);
return attribute;
}, this);
// These are expensive calls. Because we only push things to buffers in
// the worker thread, we can skip in the "clone an existing buffer" case.
this._createPushMethod();
this._refreshViews();
}
}
/**
* Bind this buffer to a WebGL context.
* @private
* @param gl The WebGL context
*/
Buffer.prototype.bind = function(gl) {
var type = gl[this.type];
if (!this.buffer) {
this.buffer = gl.createBuffer();
gl.bindBuffer(type, this.buffer);
gl.bufferData(type, this.arrayBuffer.slice(0, this.length * this.itemSize), gl.STATIC_DRAW);
// dump array buffer once it's bound to gl
this.arrayBuffer = null;
} else {
gl.bindBuffer(type, this.buffer);
}
};
/**
* Destroy the GL buffer bound to the given WebGL context
* @private
* @param gl The WebGL context
*/
Buffer.prototype.destroy = function(gl) {
if (this.buffer) {
gl.deleteBuffer(this.buffer);
}
};
/**
* Set the attribute pointers in a WebGL context according to the buffer's attribute layout
* @private
* @param gl The WebGL context
* @param shader The active WebGL shader
* @param {number} offset The offset of the attribute data in the currently bound GL buffer.
*/
Buffer.prototype.setAttribPointers = function(gl, shader, offset) {
for (var i = 0; i < this.attributes.length; i++) {
var attrib = this.attributes[i];
gl.vertexAttribPointer(
shader['a_' + attrib.name], attrib.components, gl[attrib.type.name],
false, this.itemSize, offset + attrib.offset);
}
};
/**
* Get an item from the `ArrayBuffer`. Only used for debugging.
* @private
* @param {number} index The index of the item to get
* @returns {Object.<string, Array.<number>>}
*/
Buffer.prototype.get = function(index) {
this._refreshViews();
var item = {};
var offset = index * this.itemSize;
for (var i = 0; i < this.attributes.length; i++) {
var attribute = this.attributes[i];
var values = item[attribute.name] = [];
for (var j = 0; j < attribute.components; j++) {
var componentOffset = ((offset + attribute.offset) / attribute.type.size) + j;
values.push(this.views[attribute.type.name][componentOffset]);
}
}
return item;
};
/**
* Check that a buffer item is well formed and throw an error if not. Only
* used for debugging.
* @private
* @param {number} args The "arguments" object from Buffer::push
*/
Buffer.prototype.validate = function(args) {
var argIndex = 0;
for (var i = 0; i < this.attributes.length; i++) {
for (var j = 0; j < this.attributes[i].components; j++) {
assert(!isNaN(args[argIndex++]));
}
}
assert(argIndex === args.length);
};
Buffer.prototype._resize = function(capacity) {
var old = this.views.UNSIGNED_BYTE;
this.capacity = align(capacity, Buffer.CAPACITY_ALIGNMENT);
this.arrayBuffer = new ArrayBuffer(this.capacity);
this._refreshViews();
this.views.UNSIGNED_BYTE.set(old);
};
Buffer.prototype._refreshViews = function() {
this.views = {
UNSIGNED_BYTE: new Uint8Array(this.arrayBuffer),
BYTE: new Int8Array(this.arrayBuffer),
UNSIGNED_SHORT: new Uint16Array(this.arrayBuffer),
SHORT: new Int16Array(this.arrayBuffer)
};
};
var createPushMethodCache = {};
Buffer.prototype._createPushMethod = function() {
var body = '';
var argNames = [];
body += 'var i = this.length++;\n';
body += 'var o = i * ' + this.itemSize + ';\n';
body += 'if (o + ' + this.itemSize + ' > this.capacity) { this._resize(this.capacity * 1.5); }\n';
for (var i = 0; i < this.attributes.length; i++) {
var attribute = this.attributes[i];
var offsetId = 'o' + i;
body += '\nvar ' + offsetId + ' = (o + ' + attribute.offset + ') / ' + attribute.type.size + ';\n';
for (var j = 0; j < attribute.components; j++) {
var rvalue = 'v' + argNames.length;
var lvalue = 'this.views.' + attribute.type.name + '[' + offsetId + ' + ' + j + ']';
body += lvalue + ' = ' + rvalue + ';\n';
argNames.push(rvalue);
}
}
body += '\nreturn i;\n';
if (!createPushMethodCache[body]) {
createPushMethodCache[body] = new Function(argNames, body);
}
this.push = createPushMethodCache[body];
};
/**
* @typedef BufferAttribute
* @private
* @property {string} name
* @property {number} components
* @property {BufferAttributeType} type
* @property {number} size
* @property {number} offset
*/
/**
* @enum {string} BufferType
* @private
* @readonly
*/
Buffer.BufferType = {
VERTEX: 'ARRAY_BUFFER',
ELEMENT: 'ELEMENT_ARRAY_BUFFER'
};
/**
* @enum {{size: number, name: string}} BufferAttributeType
* @private
* @readonly
*/
Buffer.AttributeType = {
BYTE: { size: 1, name: 'BYTE' },
UNSIGNED_BYTE: { size: 1, name: 'UNSIGNED_BYTE' },
SHORT: { size: 2, name: 'SHORT' },
UNSIGNED_SHORT: { size: 2, name: 'UNSIGNED_SHORT' }
};
/**
* An `BufferType.ELEMENT` buffer holds indicies of a corresponding `BufferType.VERTEX` buffer.
* These indicies are stored in the `BufferType.ELEMENT` buffer as `UNSIGNED_SHORT`s.
*
* @property {BufferAttributeType}
* @private
* @readonly
*/
Buffer.ELEMENT_ATTRIBUTE_TYPE = Buffer.AttributeType.UNSIGNED_SHORT;
/**
* The maximum extent of a feature that can be safely stored in the buffer.
* In practice, all features are converted to this extent before being added.
*
* Positions are stored as signed 16bit integers.
* One bit is lost for signedness to support featuers extending past the left edge of the tile.
* One bit is lost because the line vertex buffer packs 1 bit of other data into the int.
* One bit is lost to support features extending past the extent on the right edge of the tile.
* This leaves us with 2^13 = 8192
*
* @property {number}
* @private
* @readonly
*/
Buffer.EXTENT = 8192;
/**
* @property {number}
* @private
* @readonly
*/
Buffer.CAPACITY_DEFAULT = 8192;
/**
* WebGL performs best if buffer sizes are aligned to 2 byte boundaries.
* @property {number}
* @private
* @readonly
*/
Buffer.CAPACITY_ALIGNMENT = 2;
/**
* WebGL performs best if vertex attribute offsets are aligned to 4 byte boundaries.
* @property {number}
* @private
* @readonly
*/
Buffer.VERTEX_ATTRIBUTE_ALIGNMENT = 4;
function align(value, alignment) {
alignment = alignment || 1;
var remainder = value % alignment;
if (alignment !== 1 && remainder !== 0) {
value += (alignment - remainder);
}
return value;
}
module.exports = Buffer;
},{"assert":100}],3:[function(require,module,exports){
'use strict';
var Bucket = require('./bucket');
var util = require('../util/util');
var loadGeometry = require('./load_geometry');
var EXTENT = require('./buffer').EXTENT;
module.exports = CircleBucket;
/**
* Circles are represented by two triangles.
*
* Each corner has a pos that is the center of the circle and an extrusion
* vector that is where it points.
* @private
*/
function CircleBucket() {
Bucket.apply(this, arguments);
}
CircleBucket.prototype = util.inherit(Bucket, {});
CircleBucket.prototype.shaders = {
circle: {
vertexBuffer: true,
elementBuffer: true,
attributeArgs: ['x', 'y', 'extrudeX', 'extrudeY'],
attributes: [{
name: 'pos',
components: 2,
type: Bucket.AttributeType.SHORT,
value: [
'(x * 2) + ((extrudeX + 1) / 2)',
'(y * 2) + ((extrudeY + 1) / 2)'
]
}]
}
};
CircleBucket.prototype.addFeature = function(feature) {
var geometries = loadGeometry(feature);
for (var j = 0; j < geometries.length; j++) {
var geometry = geometries[j];
for (var k = 0; k < geometry.length; k++) {
var group = this.makeRoomFor('circle', 4);
var x = geometry[k].x;
var y = geometry[k].y;
// Do not include points that are outside the tile boundaries.
if (x < 0 || x >= EXTENT || y < 0 || y >= EXTENT) continue;
// this geometry will be of the Point type, and we'll derive
// two triangles from it.
//
// ┌─────────┐
// │ 3 2 │
// │ │
// │ 0 1 │
// └─────────┘
var index = this.addCircleVertex(x, y, -1, -1) - group.vertexStartIndex;
this.addCircleVertex(x, y, 1, -1);
this.addCircleVertex(x, y, 1, 1);
this.addCircleVertex(x, y, -1, 1);
group.vertexLength += 4;
this.addCircleElement(index, index + 1, index + 2);
this.addCircleElement(index, index + 3, index + 2);
group.elementLength += 2;
}
}
};
},{"../util/util":99,"./bucket":1,"./buffer":2,"./load_geometry":8}],4:[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.length,
this.elementBuffer && this.elementBuffer.length,
this.secondElementBuffer && this.secondElementBuffer.length);
this.groups.push(this.current);
}
return 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;
}
},{}],5:[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');
var loadGeometry = require('./load_geometry');
var EXTENT = require('./buffer').EXTENT;
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) {
var scale = EXTENT / feature.extent;
bbox[0] *= scale;
bbox[1] *= scale;
bbox[2] *= scale;
bbox[3] *= scale;
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 || {},
x = args.x,
y = args.y,
result = [];
var radius, bounds;
if (typeof x !== 'undefined' && typeof y !== 'undefined') {
// a point (or point+radius) query
radius = (params.radius || 0) * EXTENT / args.tileSize / args.scale;
bounds = [x - radius, y - radius, x + radius, y + radius];
} else {
// a rectangle query
bounds = [ args.minX, args.minY, args.maxX, args.maxY ];
}
var matching = this.rtree.search(bounds);
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 (radius && !geometryContainsPoint(loadGeometry(feature), type, new Point(x, y), radius))
continue;
else if (!geometryIntersectsBox(loadGeometry(feature), type, bounds))
continue;
var geoJSON = feature.toGeoJSON(this.x, this.y, this.z);
if (!params.includeGeometry) {
geoJSON.geometry = null;
}
for (var l = 0; l < layers.length; l++) {
var layer = layers[l];
if (params.layerIds && params.layerIds.indexOf(layer) < 0)
continue;
result.push(util.extend({layer: layer}, geoJSON));
}
}
callback(null, result);
};
function geometryIntersectsBox(rings, type, bounds) {
return type === 'Point' ? pointIntersectsBox(rings, bounds) :
type === 'LineString' ? lineIntersectsBox(rings, bounds) :
type === 'Polygon' ? polyIntersectsBox(rings, bounds) || lineIntersectsBox(rings, bounds) : false;
}
// Tests whether any of the four corners of the bbox are contained in the
// interior of the polygon. Otherwise, defers to lineIntersectsBox.
function polyIntersectsBox(rings, bounds) {
if (polyContainsPoint(rings, new Point(bounds[0], bounds[1])) ||
polyContainsPoint(rings, new Point(bounds[0], bounds[3])) ||
polyContainsPoint(rings, new Point(bounds[2], bounds[1])) ||
polyContainsPoint(rings, new Point(bounds[2], bounds[3])))
return true;
return lineIntersectsBox(rings, bounds);
}
// Only needs to cover the case where the line crosses the bbox boundary.
// Otherwise, pointIntersectsBox should have us covered.
function lineIntersectsBox(rings, bounds) {
for (var k = 0; k < rings.length; k++) {
var ring = rings[k];
for (var i = 0, j = ring.length - 1; i < ring.length; j = i++) {
var p0 = ring[i];
var p1 = ring[j];
// invert the segment so as to reuse segmentCrossesHorizontal for
// checking whether it crosses the vertical sides of the bbox.
var i0 = new Point(p0.y, p0.x);
var i1 = new Point(p1.y, p1.x);
if (segmentCrossesHorizontal(p0, p1, bounds[0], bounds[2], bounds[1]) ||
segmentCrossesHorizontal(p0, p1, bounds[0], bounds[2], bounds[3]) ||
segmentCrossesHorizontal(i0, i1, bounds[1], bounds[3], bounds[0]) ||
segmentCrossesHorizontal(i0, i1, bounds[1], bounds[3], bounds[2]))
return true;
}
}
return pointIntersectsBox(rings, bounds);
}
/*
* Answer whether segment p1-p2 intersects with (x1, y)-(x2, y)
* Assumes x2 >= x1
*/
function segmentCrossesHorizontal(p0, p1, x1, x2, y) {
if (p1.y === p0.y)
return p1.y === y &&
Math.min(p0.x, p1.x) <= x2 &&
Math.max(p0.x, p1.x) >= x1;
var r = (y - p0.y) / (p1.y - p0.y);
var x = p0.x + r * (p1.x - p0.x);
return (x >= x1 && x <= x2 && r <= 1 && r >= 0);
}
function pointIntersectsBox(rings, bounds) {
for (var i = 0; i < rings.length; i++) {
var ring = rings[i];
for (var j = 0; j < ring.length; j++) {
if (ring[j].x >= bounds[0] &&
ring[j].y >= bounds[1] &&
ring[j].x <= bounds[2] &&
ring[j].y <= bounds[3]) return true;
}
}
return false;
}
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":99,"./buffer":2,"./load_geometry":8,"point-geometry":153,"rbush":154,"vector-tile":159}],6:[function(require,module,exports){
'use strict';
var Bucket = require('./bucket');
var util = require('../util/util');
var loadGeometry = require('./load_geometry');
module.exports = FillBucket;
function FillBucket() {
Bucket.apply(this, arguments);
}
FillBucket.prototype = util.inherit(Bucket, {});
FillBucket.prototype.shaders = {
fill: {
vertexBuffer: true,
elementBuffer: true,
secondElementBuffer: true,
secondElementBufferComponents: 2,
attributeArgs: ['x', 'y'],
attributes: [{
name: 'pos',
components: 2,
type: Bucket.AttributeType.SHORT,
value: ['x', 'y']
}]
}
};
FillBucket.prototype.addFeature = function(feature) {
var lines = loadGeometry(feature);
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;
// Expand this geometry buffer to hold all the required vertices.
var group = this.makeRoomFor('fill', len + 1);
// We're generating triangle fans, so we always start with the first coordinate in this polygon.
var firstIndex, prevIndex;
for (var i = 0; i < vertices.length; i++) {
var currentVertex = vertices[i];
var currentIndex = this.addFillVertex(currentVertex.x, currentVertex.y) - group.vertexStartIndex;
group.vertexLength++;
if (i === 0) firstIndex = currentIndex;
// Only add triangles that have distinct vertices.
if (i >= 2 && (currentVertex.x !== vertices[0].x || currentVertex.y !== vertices[0].y)) {
this.addFillElement(firstIndex, prevIndex, currentIndex);
group.elementLength++;
}
if (i >= 1) {
this.addFillSecondElement(prevIndex, currentIndex);
group.secondElementLength++;
}
prevIndex = currentIndex;
}
};
},{"../util/util":99,"./bucket":1,"./load_geometry":8}],7:[function(require,module,exports){
'use strict';
var Bucket = require('./bucket');
var util = require('../util/util');
var loadGeometry = require('./load_geometry');
var EXTENT = require('./buffer').EXTENT;
// NOTE ON EXTRUDE SCALE:
// 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.
var EXTRUDE_SCALE = 63;
/*
* Sharp corners cause dashed lines to tilt because the distance along the line
* is the same at both the inner and outer corners. To improve the appearance of
* dashed lines we add extra points near sharp corners so that a smaller part
* of the line is tilted.
*
* COS_HALF_SHARP_CORNER controls how sharp a corner has to be for us to add an
* extra vertex. The default is 75 degrees.
*
* The newly created vertices are placed SHARP_CORNER_OFFSET pixels from the corner.
*/
var COS_HALF_SHARP_CORNER = Math.cos(75 / 2 * (Math.PI / 180));
var SHARP_CORNER_OFFSET = 15;
module.exports = LineBucket;
function LineBucket() {
Bucket.apply(this, arguments);
}
LineBucket.prototype = util.inherit(Bucket, {});
LineBucket.prototype.shaders = {
line: {
vertexBuffer: true,
elementBuffer: true,
attributeArgs: ['point', 'extrude', 'tx', 'ty', 'dir', 'linesofar'],
attributes: [{
name: 'pos',
components: 2,
type: Bucket.AttributeType.SHORT,
value: [
'(point.x << 1) | tx',
'(point.y << 1) | ty'
]
}, {
name: 'data',
components: 4,
type: Bucket.AttributeType.BYTE,
value: [
'Math.round(' + EXTRUDE_SCALE + ' * extrude.x)',
'Math.round(' + EXTRUDE_SCALE + ' * extrude.y)',
// Encode the -1/0/1 direction value into .zw coordinates of a_data, which is normally covered
// by linesofar, so we need to merge them.
// The z component's first bit, as well as the sign bit is reserved for the direction,
// so we need to shift the linesofar.
'((dir < 0) ? -1 : 1) * ((dir ? 1 : 0) | ((linesofar << 1) & 0x7F))',
'(linesofar >> 6) & 0x7F'
]
}]
}
};
LineBucket.prototype.addFeature = function(feature) {
var lines = loadGeometry(feature);
for (var i = 0; i < lines.length; i++) {
this.addLine(
lines[i],
this.layer.layout['line-join'],
this.layer.layout['line-cap'],
this.layer.layout['line-miter-limit'],
this.layer.layout['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 sharpCornerOffset = SHARP_CORNER_OFFSET * (EXTENT / (512 * this.overscaling));
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.makeRoomFor('line', len * 10);
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 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;
var isSharpCorner = cosHalfAngle < COS_HALF_SHARP_CORNER && prevVertex && nextVertex;
if (isSharpCorner && i > 0) {
var prevSegmentLength = currentVertex.dist(prevVertex);
if (prevSegmentLength > 2 * sharpCornerOffset) {
var newPrevVertex = currentVertex.sub(currentVertex.sub(prevVertex)._mult(sharpCornerOffset / prevSegmentLength)._round());
distance += newPrevVertex.dist(prevVertex);
this.addCurrentVertex(newPrevVertex, flip, distance, prevNormal.mult(1), 0, 0, false);
prevVertex = newPrevVertex;
}
}
// The join if a middle vertex, otherwise the cap.
var middleVertex = prevVertex && nextVertex;
var currentJoin = middleVertex ? join : nextVertex ? beginCap : endCap;
if (middleVertex && currentJoin === 'round') {
if (miterLength < roundLimit) {
currentJoin = 'miter';
} else if (miterLength <= 2) {
currentJoin = 'fakeround';
}
}
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';
}
// Calculate how far along the line the currentVertex is
if (prevVertex) distance += currentVertex.dist(prevVertex);
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);
this.addCurrentVertex(currentVertex, -flip, distance, joinNormal, 0, 0, false);
} else if (currentJoin === 'bevel' || currentJoin === 'fakeround') {
var lineTurnsLeft = flip * (prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x) > 0;
var offset = -Math.sqrt(miterLength * miterLength - 1);
if (lineTurnsLeft) {
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);
}
if (currentJoin === 'fakeround') {
// The join angle is sharp enough that a round join would be visible.
// Bevel joins fill the gap between segments with a single pie slice triangle.
// Create a round join by adding multiple pie slices. The join isn't actually round, but
// it looks like it is at the sizes we render lines at.
// Add more triangles for sharper angles.
// This math is just a good enough approximation. It isn't "correct".
var n = Math.floor((0.5 - (cosHalfAngle - 0.5)) * 8);
var approxFractionalJoinNormal;
for (var m = 0; m < n; m++) {
approxFractionalJoinNormal = nextNormal.mult((m + 1) / (n + 1))._add(prevNormal)._unit();
this.addPieSliceVertex(currentVertex, flip, distance, approxFractionalJoinNormal, lineTurnsLeft);
}
this.addPieSliceVertex(currentVertex, flip, distance, joinNormal, lineTurnsLeft);
for (var k = n - 1; k >= 0; k--) {
approxFractionalJoinNormal = prevNormal.mult((k + 1) / (n + 1))._add(nextNormal)._unit();
this.addPieSliceVertex(currentVertex, flip, distance, approxFractionalJoinNormal, lineTurnsLeft);
}
}
// 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;
}
// Start next segment with a butt
if (nextVertex) {
// Add round cap before first segment
this.addCurrentVertex(currentVertex, flip, distance, nextNormal, -1, -1, true);
this.addCurrentVertex(currentVertex, flip, distance, nextNormal, 0, 0, false);
}
}
if (isSharpCorner && i < len - 1) {
var nextSegmentLength = currentVertex.dist(nextVertex);
if (nextSegmentLength > 2 * sharpCornerOffset) {
var newCurrentVertex = currentVertex.add(nextVertex.sub(currentVertex)._mult(sharpCornerOffset / nextSegmentLength)._round());
distance += newCurrentVertex.dist(currentVertex);
this.addCurrentVertex(newCurrentVertex, flip, distance, nextNormal.mult(1), 0, 0, false);
currentVertex = newCurrentVertex;
}
}
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 beginning 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 group = this.elementGroups.line.current;
group.vertexLength += 2;
extrude = normal.mult(flip);
if (endLeft) extrude._sub(normal.perp()._mult(endLeft));
this.e3 = this.addLineVertex(currentVertex, extrude, tx, 0, endLeft, distance) - group.vertexStartIndex;
if (this.e1 >= 0 && this.e2 >= 0) {
this.addLineElement(this.e1, this.e2, this.e3);
group.elementLength++;
}
this.e1 = this.e2;
this.e2 = this.e3;
extrude = normal.mult(-flip);
if (endRight) extrude._sub(normal.perp()._mult(endRight));
this.e3 = this.addLineVertex(currentVertex, extrude, tx, 1, -endRight, distance) - group.vertexStartIndex;
if (this.e1 >= 0 && this.e2 >= 0) {
this.addLineElement(this.e1, this.e2, this.e3);
group.elementLength++;
}
this.e1 = this.e2;
this.e2 = this.e3;
};
/**
* Add a single new vertex and a triangle using two previous vertices.
* This adds a pie slice triangle near a join to simulate round joins
*
* @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 {Object} extrude the offset of the new vertex from the currentVertex
* @param {boolean} whether the line is turning left or right at this angle
* @private
*/
LineBucket.prototype.addPieSliceVertex = function(currentVertex, flip, distance, extrude, lineTurnsLeft) {
var ty = lineTurnsLeft ? 1 : 0;
extrude = extrude.mult(flip * (lineTurnsLeft ? -1 : 1));
var group = this.elementGroups.line.current;
this.e3 = this.addLineVertex(currentVertex, extrude, 0, ty, 0, distance) - group.vertexStartIndex;
group.vertexLength++;
if (this.e1 >= 0 && this.e2 >= 0) {
this.addLineElement(this.e1, this.e2, this.e3);
group.elementLength++;
}
if (lineTurnsLeft) {
this.e2 = this.e3;
} else {
this.e1 = this.e3;
}
};
},{"../util/util":99,"./bucket":1,"./buffer":2,"./load_geometry":8}],8:[function(require,module,exports){
'use strict';
var EXTENT = require('./buffer').EXTENT;
/**
* Loads a geometry from a VectorTileFeature and scales it to the common extent
* used internally.
* @private
*/
module.exports = function loadGeometry(feature) {
var scale = EXTENT / feature.extent;
var geometry = feature.loadGeometry();
for (var r = 0; r < geometry.length; r++) {
var ring = geometry[r];
for (var p = 0; p < ring.length; p++) {
var point = ring[p];
// round here because mapbox-gl-native uses integers to represent
// points and we need to do the same to avoid renering differences.
point.x = Math.round(point.x * scale);
point.y = Math.round(point.y * scale);
}
}
return geometry;
};
},{"./buffer":2}],9:[function(require,module,exports){
'use strict';
var Point = require('point-geometry');
var Bucket = require('./bucket');
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 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 util = require('../util/util');
var loadGeometry = require('./load_geometry');
var EXTENT = require('./buffer').EXTENT;
var CollisionFeature = require('../symbol/collision_feature');
module.exports = SymbolBucket;
function SymbolBucket(options) {
Bucket.apply(this, arguments);
this.collisionDebug = options.collisionDebug;
this.overscaling = options.overscaling;
// To reduce the number of labels that jump around when zooming we need
// to use a text-size value that is the same for all zoom levels.
// This calculates text-size at a high zoom level so that all tiles can
// use the same value when calculating anchor positions.
var zoomHistory = { lastIntegerZoom: Infinity, lastIntegerZoomTime: 0, lastZoom: 0 };
this.adjustedTextMaxSize = this.layer.getLayoutValue('text-size', 18, zoomHistory);
this.adjustedTextSize = this.layer.getLayoutValue('text-size', this.zoom + 1, zoomHistory);
this.adjustedIconMaxSize = this.layer.getLayoutValue('icon-size', 18, zoomHistory);
this.adjustedIconSize = this.layer.getLayoutValue('icon-size', this.zoom + 1, zoomHistory);
}
SymbolBucket.prototype = util.inherit(Bucket, {});
var shaderAttributeArgs = ['x', 'y', 'ox', 'oy', 'tx', 'ty', 'minzoom', 'maxzoom', 'labelminzoom'];
var shaderAttributes = [{
name: 'pos',
components: 2,
type: Bucket.AttributeType.SHORT,
value: ['x', 'y']
}, {
name: 'offset',
components: 2,
type: Bucket.AttributeType.SHORT,
value: [
'Math.round(ox * 64)', // use 1/64 pixels for placement
'Math.round(oy * 64)'
]
}, {
name: 'data1',
components: 4,
type: Bucket.AttributeType.UNSIGNED_BYTE,
value: [
'tx / 4', // tex
'ty / 4', // tex
'(labelminzoom || 0) * 10', // labelminzoom
'0'
]
}, {
name: 'data2',
components: 2,
type: Bucket.AttributeType.UNSIGNED_BYTE,
value: [