mapbox-gl
Version:
A WebGL interactive maps library
300 lines (247 loc) • 11.1 kB
JavaScript
'use strict';
var Point = require('point-geometry');
var EXTENT = require('../data/bucket').EXTENT;
var Grid = require('grid-index');
module.exports = CollisionTile;
/**
* A collision tile used to prevent symbols from overlapping. It keep tracks of
* where previous symbols have been placed and is used to check if a new
* symbol overlaps with any previously added symbols.
*
* @class CollisionTile
* @param {number} angle
* @param {number} pitch
* @private
*/
function CollisionTile(angle, pitch, collisionBoxArray) {
if (typeof angle === 'object') {
var serialized = angle;
collisionBoxArray = pitch;
angle = serialized.angle;
pitch = serialized.pitch;
this.grid = new Grid(serialized.grid);
this.ignoredGrid = new Grid(serialized.ignoredGrid);
} else {
this.grid = new Grid(EXTENT, 12, 6);
this.ignoredGrid = new Grid(EXTENT, 12, 0);
}
this.angle = angle;
this.pitch = pitch;
var sin = Math.sin(angle),
cos = Math.cos(angle);
this.rotationMatrix = [cos, -sin, sin, cos];
this.reverseRotationMatrix = [cos, sin, -sin, cos];
// Stretch boxes in y direction to account for the map tilt.
this.yStretch = 1 / Math.cos(pitch / 180 * Math.PI);
// The amount the map is squished depends on the y position.
// Sort of account for this by making all boxes a bit bigger.
this.yStretch = Math.pow(this.yStretch, 1.3);
this.collisionBoxArray = collisionBoxArray;
if (collisionBoxArray.length === 0) {
// the first collisionBoxArray is passed to a CollisionTile
// tempCollisionBox
collisionBoxArray.emplaceBack();
var maxInt16 = 32767;
//left
collisionBoxArray.emplaceBack(0, 0, 0, -maxInt16, 0, maxInt16, maxInt16,
0, 0, 0, 0, 0, 0, 0, 0,
0);
// right
collisionBoxArray.emplaceBack(EXTENT, 0, 0, -maxInt16, 0, maxInt16, maxInt16,
0, 0, 0, 0, 0, 0, 0, 0,
0);
// top
collisionBoxArray.emplaceBack(0, 0, -maxInt16, 0, maxInt16, 0, maxInt16,
0, 0, 0, 0, 0, 0, 0, 0,
0);
// bottom
collisionBoxArray.emplaceBack(0, EXTENT, -maxInt16, 0, maxInt16, 0, maxInt16,
0, 0, 0, 0, 0, 0, 0, 0,
0);
}
this.tempCollisionBox = collisionBoxArray.get(0);
this.edges = [
collisionBoxArray.get(1),
collisionBoxArray.get(2),
collisionBoxArray.get(3),
collisionBoxArray.get(4)
];
}
CollisionTile.prototype.serialize = function() {
var data = {
angle: this.angle,
pitch: this.pitch,
grid: this.grid.toArrayBuffer(),
ignoredGrid: this.ignoredGrid.toArrayBuffer()
};
return {
data: data,
transferables: [data.grid, data.ignoredGrid]
};
};
CollisionTile.prototype.minScale = 0.25;
CollisionTile.prototype.maxScale = 2;
/**
* Find the scale at which the collisionFeature can be shown without
* overlapping with other features.
*
* @param {CollisionFeature} collisionFeature
* @returns {number} placementScale
* @private
*/
CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allowOverlap, avoidEdges) {
var collisionBoxArray = this.collisionBoxArray;
var minPlacementScale = this.minScale;
var rotationMatrix = this.rotationMatrix;
var yStretch = this.yStretch;
for (var b = collisionFeature.boxStartIndex; b < collisionFeature.boxEndIndex; b++) {
var box = collisionBoxArray.get(b);
var anchorPoint = box.anchorPoint._matMult(rotationMatrix);
var x = anchorPoint.x;
var y = anchorPoint.y;
var x1 = x + box.x1;
var y1 = y + box.y1 * yStretch;
var x2 = x + box.x2;
var y2 = y + box.y2 * yStretch;
box.bbox0 = x1;
box.bbox1 = y1;
box.bbox2 = x2;
box.bbox3 = y2;
if (!allowOverlap) {
var blockingBoxes = this.grid.query(x1, y1, x2, y2);
for (var i = 0; i < blockingBoxes.length; i++) {
var blocking = collisionBoxArray.get(blockingBoxes[i]);
var blockingAnchorPoint = blocking.anchorPoint._matMult(rotationMatrix);
minPlacementScale = this.getPlacementScale(minPlacementScale, anchorPoint, box, blockingAnchorPoint, blocking);
if (minPlacementScale >= this.maxScale) {
return minPlacementScale;
}
}
}
if (avoidEdges) {
var rotatedCollisionBox;
if (this.angle) {
var reverseRotationMatrix = this.reverseRotationMatrix;
var tl = new Point(box.x1, box.y1).matMult(reverseRotationMatrix);
var tr = new Point(box.x2, box.y1).matMult(reverseRotationMatrix);
var bl = new Point(box.x1, box.y2).matMult(reverseRotationMatrix);
var br = new Point(box.x2, box.y2).matMult(reverseRotationMatrix);
rotatedCollisionBox = this.tempCollisionBox;
rotatedCollisionBox.anchorPointX = box.anchorPoint.x;
rotatedCollisionBox.anchorPointY = box.anchorPoint.y;
rotatedCollisionBox.x1 = Math.min(tl.x, tr.x, bl.x, br.x);
rotatedCollisionBox.y1 = Math.min(tl.y, tr.x, bl.x, br.x);
rotatedCollisionBox.x2 = Math.max(tl.x, tr.x, bl.x, br.x);
rotatedCollisionBox.y2 = Math.max(tl.y, tr.x, bl.x, br.x);
rotatedCollisionBox.maxScale = box.maxScale;
} else {
rotatedCollisionBox = box;
}
for (var k = 0; k < this.edges.length; k++) {
var edgeBox = this.edges[k];
minPlacementScale = this.getPlacementScale(minPlacementScale, box.anchorPoint, rotatedCollisionBox, edgeBox.anchorPoint, edgeBox);
if (minPlacementScale >= this.maxScale) {
return minPlacementScale;
}
}
}
}
return minPlacementScale;
};
CollisionTile.prototype.queryRenderedSymbols = function(minX, minY, maxX, maxY, scale) {
var sourceLayerFeatures = {};
var result = [];
var collisionBoxArray = this.collisionBoxArray;
var rotationMatrix = this.rotationMatrix;
var anchorPoint = new Point(minX, minY)._matMult(rotationMatrix);
var queryBox = this.tempCollisionBox;
queryBox.anchorX = anchorPoint.x;
queryBox.anchorY = anchorPoint.y;
queryBox.x1 = 0;
queryBox.y1 = 0;
queryBox.x2 = maxX - minX;
queryBox.y2 = maxY - minY;
queryBox.maxScale = scale;
// maxScale is stored using a Float32. Convert `scale` to the stored Float32 value.
scale = queryBox.maxScale;
var searchBox = [
anchorPoint.x + queryBox.x1 / scale,
anchorPoint.y + queryBox.y1 / scale * this.yStretch,
anchorPoint.x + queryBox.x2 / scale,
anchorPoint.y + queryBox.y2 / scale * this.yStretch
];
var blockingBoxKeys = this.grid.query(searchBox[0], searchBox[1], searchBox[2], searchBox[3]);
var blockingBoxKeys2 = this.ignoredGrid.query(searchBox[0], searchBox[1], searchBox[2], searchBox[3]);
for (var k = 0; k < blockingBoxKeys2.length; k++) {
blockingBoxKeys.push(blockingBoxKeys2[k]);
}
for (var i = 0; i < blockingBoxKeys.length; i++) {
var blocking = collisionBoxArray.get(blockingBoxKeys[i]);
var sourceLayer = blocking.sourceLayerIndex;
var featureIndex = blocking.featureIndex;
if (sourceLayerFeatures[sourceLayer] === undefined) {
sourceLayerFeatures[sourceLayer] = {};
}
if (!sourceLayerFeatures[sourceLayer][featureIndex]) {
var blockingAnchorPoint = blocking.anchorPoint.matMult(rotationMatrix);
var minPlacementScale = this.getPlacementScale(this.minScale, anchorPoint, queryBox, blockingAnchorPoint, blocking);
if (minPlacementScale >= scale) {
sourceLayerFeatures[sourceLayer][featureIndex] = true;
result.push(blockingBoxKeys[i]);
}
}
}
return result;
};
CollisionTile.prototype.getPlacementScale = function(minPlacementScale, anchorPoint, box, blockingAnchorPoint, blocking) {
// Find the lowest scale at which the two boxes can fit side by side without overlapping.
// Original algorithm:
var anchorDiffX = anchorPoint.x - blockingAnchorPoint.x;
var anchorDiffY = anchorPoint.y - blockingAnchorPoint.y;
var s1 = (blocking.x1 - box.x2) / anchorDiffX; // scale at which new box is to the left of old box
var s2 = (blocking.x2 - box.x1) / anchorDiffX; // scale at which new box is to the right of old box
var s3 = (blocking.y1 - box.y2) * this.yStretch / anchorDiffY; // scale at which new box is to the top of old box
var s4 = (blocking.y2 - box.y1) * this.yStretch / anchorDiffY; // scale at which new box is to the bottom of old box
if (isNaN(s1) || isNaN(s2)) s1 = s2 = 1;
if (isNaN(s3) || isNaN(s4)) s3 = s4 = 1;
var collisionFreeScale = Math.min(Math.max(s1, s2), Math.max(s3, s4));
var blockingMaxScale = blocking.maxScale;
var boxMaxScale = box.maxScale;
if (collisionFreeScale > blockingMaxScale) {
// After a box's maxScale the label has shrunk enough that the box is no longer needed to cover it,
// so unblock the new box at the scale that the old box disappears.
collisionFreeScale = blockingMaxScale;
}
if (collisionFreeScale > boxMaxScale) {
// If the box can only be shown after it is visible, then the box can never be shown.
// But the label can be shown after this box is not visible.
collisionFreeScale = boxMaxScale;
}
if (collisionFreeScale > minPlacementScale &&
collisionFreeScale >= blocking.placementScale) {
// If this collision occurs at a lower scale than previously found collisions
// and the collision occurs while the other label is visible
// this this is the lowest scale at which the label won't collide with anything
minPlacementScale = collisionFreeScale;
}
return minPlacementScale;
};
/**
* Remember this collisionFeature and what scale it was placed at to block
* later features from overlapping with it.
*
* @param {CollisionFeature} collisionFeature
* @param {number} minPlacementScale
* @private
*/
CollisionTile.prototype.insertCollisionFeature = function(collisionFeature, minPlacementScale, ignorePlacement) {
var grid = ignorePlacement ? this.ignoredGrid : this.grid;
var collisionBoxArray = this.collisionBoxArray;
for (var k = collisionFeature.boxStartIndex; k < collisionFeature.boxEndIndex; k++) {
var box = collisionBoxArray.get(k);
box.placementScale = minPlacementScale;
if (minPlacementScale < this.maxScale) {
grid.insert(k, box.bbox0, box.bbox1, box.bbox2, box.bbox3);
}
}
};