Skip to content

Commit

Permalink
index collision boxes with a grid instead of rtree
Browse files Browse the repository at this point in the history
The combined running time of placeCollisionFeature and
insertCollisionFeature is now 2/3rds of what it used to be.

The grid index divids a square into n x n cells. When a bbox is
inserted, it is added to the array of every cell it intersects. When
querying, look in all the cells that intersect the query box and then
compare all the individual bboxes contained in that cell against the
query box.

This kind of index is faster in this specific case because it's
characteristics better match the work we're using it for.

Queries can take 1.3x as long (slower) with the grid index.
But insertions often take < 0.1x as long with the grid index.
Our collision detection code does a lot of insertions so this is a
worthwhile tradeoff.

The grid index also takes advantage of the fact that label boxes are
fairly evenly sized and evenly distributed.

The grid index can also be serialized to an ArrayBuffer. The ArrayBuffer
version can be queried without deserializing all the individual objects
first. This is not currently used.
  • Loading branch information
ansis committed Mar 1, 2016
1 parent 4deefd3 commit 0ec60ff
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 8 deletions.
43 changes: 35 additions & 8 deletions js/symbol/collision_tile.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use strict';

var rbush = require('rbush');
var CollisionBox = require('./collision_box');
var Point = require('point-geometry');
var EXTENT = require('../data/buffer').EXTENT;
var Grid = require('../util/grid');

module.exports = CollisionTile;

Expand All @@ -18,8 +18,11 @@ module.exports = CollisionTile;
* @private
*/
function CollisionTile(angle, pitch) {
this.tree = rbush();
this.ignoredTree = rbush();
this.grid = new Grid(12, EXTENT, 6);
this.gridFeatures = [];
this.ignoredGrid = new Grid(12, EXTENT, 0);
this.ignoredGridFeatures = [];

this.angle = angle;

var sin = Math.sin(angle),
Expand Down Expand Up @@ -78,10 +81,10 @@ CollisionTile.prototype.placeCollisionFeature = function(collisionFeature, allow
box[2] = x + box.x2;
box[3] = y + box.y2 * yStretch;

var blockingBoxes = this.tree.search(box);
var blockingBoxes = this.grid.query(box);

for (var i = 0; i < blockingBoxes.length; i++) {
var blocking = blockingBoxes[i];
var blocking = this.gridFeatures[blockingBoxes[i]];
var blockingAnchorPoint = blocking.anchorPoint.matMult(rotationMatrix);

minPlacementScale = this.getPlacementScale(minPlacementScale, anchorPoint, box, blockingAnchorPoint, blocking);
Expand Down Expand Up @@ -131,7 +134,15 @@ CollisionTile.prototype.getFeaturesAt = function(queryBox, scale) {
anchorPoint.y + queryBox.y2 / scale * this.yStretch
];

var blockingBoxes = this.tree.search(searchBox).concat(this.ignoredTree.search(searchBox));
var blockingBoxes = [];
var blockingBoxKeys = this.grid.query(searchBox);
for (var j = 0; j < blockingBoxKeys.length; j++) {
blockingBoxes.push(this.gridFeatures[blockingBoxKeys[j]]);
}
blockingBoxKeys = this.ignoredGrid.query(searchBox);
for (var k = 0; k < blockingBoxKeys.length; k++) {
blockingBoxes.push(this.ignoredGridFeatures[blockingBoxKeys[k]]);
}

for (var i = 0; i < blockingBoxes.length; i++) {
var blocking = blockingBoxes[i];
Expand Down Expand Up @@ -207,9 +218,25 @@ CollisionTile.prototype.insertCollisionFeature = function(collisionFeature, minP

if (minPlacementScale < this.maxScale) {
if (ignorePlacement) {
this.ignoredTree.load(boxes);
this.insertIgnoredGrid(boxes);
} else {
this.tree.load(boxes);
this.insertGrid(boxes);
}
}
};

CollisionTile.prototype.insertGrid = function(boxes) {
for (var i = 0; i < boxes.length; i++) {
var box = boxes[i];
this.grid.insert(box, this.gridFeatures.length);
this.gridFeatures.push(box);
}
};

CollisionTile.prototype.insertIgnoredGrid = function(boxes) {
for (var i = 0; i < boxes.length; i++) {
var box = boxes[i];
this.ignoredGrid.insert(box, this.ignoredGridFeatures.length);
this.ignoredGridFeatures.push(box);
}
};
130 changes: 130 additions & 0 deletions js/util/grid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use strict';

module.exports = Grid;

var NUM_PARAMS = 3;

function Grid(n, extent, padding) {
var cells = this.cells = [];

if (n instanceof ArrayBuffer) {
var array = new Int32Array(n);
n = array[0];
extent = array[1];
padding = array[2];

this.d = n + 2 * padding;
for (var k = 0; k < this.d * this.d; k++) {
cells.push(array.subarray(array[NUM_PARAMS + k], array[NUM_PARAMS + k + 1]));
}
var keysOffset = array[NUM_PARAMS + cells.length];
var bboxesOffset = array[NUM_PARAMS + cells.length + 1];
this.keys = array.subarray(keysOffset, bboxesOffset);
this.bboxes = array.subarray(bboxesOffset);
} else {
this.d = n + 2 * padding;
for (var i = 0; i < this.d * this.d; i++) {
cells.push([]);
}
this.keys = [];
this.bboxes = [];
}

this.n = n;
this.extent = extent;
this.padding = padding;
this.scale = n / extent;
this.uid = 0;
}


Grid.prototype.insert = function(bbox, key) {
this._forEachCell(bbox, this._insertCell, this.uid++);
this.keys.push(key);
this.bboxes.push(bbox[0]);
this.bboxes.push(bbox[1]);
this.bboxes.push(bbox[2]);
this.bboxes.push(bbox[3]);
};

Grid.prototype._insertCell = function(bbox, cellIndex, uid) {
this.cells[cellIndex].push(uid);
};

Grid.prototype.query = function(bbox) {
var result = [];
var seenUids = {};
this._forEachCell(bbox, this._queryCell, result, seenUids);
return result;
};

Grid.prototype._queryCell = function(bbox, cellIndex, result, seenUids) {
var cell = this.cells[cellIndex];
var keys = this.keys;
var bboxes = this.bboxes;
for (var u = 0; u < cell.length; u++) {
var uid = cell[u];
if (seenUids[uid] === undefined) {
var offset = uid * 4;
if ((bbox[0] <= bboxes[offset + 2]) &&
(bbox[1] <= bboxes[offset + 3]) &&
(bbox[2] >= bboxes[offset + 0]) &&
(bbox[3] >= bboxes[offset + 1])) {
seenUids[uid] = true;
result.push(keys[uid]);
} else {
seenUids[uid] = false;
}
}
}
};

Grid.prototype._forEachCell = function(bbox, fn, arg1, arg2) {
var x1 = this._convertToCellCoord(bbox[0]);
var y1 = this._convertToCellCoord(bbox[1]);
var x2 = this._convertToCellCoord(bbox[2]);
var y2 = this._convertToCellCoord(bbox[3]);
for (var x = x1; x <= x2; x++) {
for (var y = y1; y <= y2; y++) {
var cellIndex = this.d * y + x;
if (fn.call(this, bbox, cellIndex, arg1, arg2)) return;
}
}
};

Grid.prototype._convertToCellCoord = function(x) {
return Math.max(0, Math.min(this.d - 1, Math.floor(x * this.scale) + this.padding));
};

Grid.prototype.toArrayBuffer = function() {
var cells = this.cells;

var metadataLength = NUM_PARAMS + this.cells.length + 1 + 1;
var totalCellLength = 0;
for (var i = 0; i < this.cells.length; i++) {
totalCellLength += this.cells[i].length;
}

var array = new Int32Array(metadataLength + totalCellLength + this.keys.length + this.bboxes.length);
array[0] = this.n;
array[1] = this.extent;
array[2] = this.padding;

var offset = metadataLength;
for (var k = 0; k < cells.length; k++) {
var cell = cells[k];
array[NUM_PARAMS + k] = offset;
array.set(cell, offset);
offset += cell.length;
}

array[NUM_PARAMS + cells.length] = offset;
array.set(this.keys, offset);
offset += this.keys.length;

array[NUM_PARAMS + cells.length + 1] = offset;
array.set(this.bboxes, offset);
offset += this.bboxes.length;

return array.buffer;
};

0 comments on commit 0ec60ff

Please sign in to comment.