From 0ec60ff96749276f32710ec43f26ccff03f50096 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Mon, 29 Feb 2016 16:56:20 -0800 Subject: [PATCH] index collision boxes with a grid instead of rtree 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. --- js/symbol/collision_tile.js | 43 +++++++++--- js/util/grid.js | 130 ++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 js/util/grid.js diff --git a/js/symbol/collision_tile.js b/js/symbol/collision_tile.js index b317a525531..7ceb1691f8d 100644 --- a/js/symbol/collision_tile.js +++ b/js/symbol/collision_tile.js @@ -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; @@ -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), @@ -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); @@ -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]; @@ -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); + } +}; diff --git a/js/util/grid.js b/js/util/grid.js new file mode 100644 index 00000000000..cbd1761b6b9 --- /dev/null +++ b/js/util/grid.js @@ -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; +};