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; +};