2d-quadtree
Version:
Quadtree for games and simulations
726 lines (556 loc) • 20.3 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: quadtree.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: quadtree.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>'use strict';
var _ = require('underscore');
var DEFAULT_MAX_CHILDREN = 4,
DEFAULT_DEPTH = 4,
DEFAULT_WIDTH = 1000,
DEFAULT_HEIGHT = 1000,
NORTH_WEST = 1,
NORTH_EAST = 2,
SOUTH_WEST = 4,
SOUTH_EAST = 8;
/**
* Rectangles inserted into the quadtree are extended with this object literal
*/
var rectPrototype = {
/**
* Moves the rectangle in the quadtree to a new position (x, y)
* @method
* @param {Number} x The x coordinate as defined by the quadtree coordinate system
* @param {Number} y The y coordinate as defined by the quadtree coordinate system
*/
'move': function (x, y) {
this.x = x;
this.y = y;
if (this.parent.orphans.indexOf(this) !== -1 || !isWithinBounds(this.parent, this)) {
this.parent.remove(this);
this.parent.insert(this);
}
},
/**
* Returns an array of the rectangles within the quadtree that intersect with this rectangle
* @method
* @return {Array} The rectangles that intersect with this rectangle
*/
'getCollisions': function () {
return this.parent.getCollisions(this);
},
/**
* Comepletely removes this rectangle from the quadtree
* @method
*/
'remove': function () {
this.parent.remove(this);
}
};
/**
* Quadtree contstructor function. Use to initialize the Quadtree. Also, whenever the quadtree splits,
* this constructor is used to initialize the new nodes of the quadtree.
* @constructor
* @param {Object} options The only options of concern to you: width, height, maxChildren, depth
*/
function Quadtree (options) {
options = options || {};
this.maxChildren = options.maxChildren || DEFAULT_MAX_CHILDREN;
this.depth = options.depth || DEFAULT_DEPTH;
this.height = options.height || DEFAULT_HEIGHT;
this.width = options.width || DEFAULT_WIDTH;
this.halfHeight = this.height / 2;
this.halfWidth = this.width / 2;
this.x = options.x || 0;
this.y = options.y || 0;
this.parent = options.parent || null;
this.children = [];
this.orphans = [];
this.isLeaf = true;
this.quadrant = options.quadrant || (NORTH_WEST + NORTH_EAST + SOUTH_WEST + SOUTH_EAST);
}
/**
*
* Inserts an object into the quadTree
* @param {Object} An arbitrary object with rectangle properties (x, y, width, height)
*/
Quadtree.prototype.insert = function (object) {
var numberOfChildren = this.children.length;
if (!hasRectProps(object)) {
throw 'Inserting an object into the Quadtree requires a height, width, x, and y property';
}
if (!object.move) {
_.extend(object, rectPrototype);
}
if (!isWithinBounds(this, object)) {
if (this.parent) {
this.parent.insert(object);
return;
} else {
forceObjectWithinBounds(object, this);
}
}
object.parent = this;
object.quadrant = undefined;
// This quadTree does not contain quadTrees
if (this.isLeaf) {
this.children.push(object);
setQuadrant(object, this);
if (this.children.length > this.maxChildren && this.depth) {
this.divide();
return;
}
// This quadTree contains quadTrees
// We should check if the object we are inserting can be completely contained within
// one of these quadTrees. If it can't, it must be an orphan.
} else {
for (var i = 0; i < numberOfChildren; i++) {
if (isWithinBounds(this.children[i], object)) {
this.children[i].insert(object);
return;
}
}
// Object does not fit within any of the sub-quadTrees. It's an orphan.
setQuadrant(object, this);
this.orphans.push(object);
}
};
/**
* Removes the object and potentially collapses the quadTree
* @method remove
* @param {Object} Item that was inserted into the quadTree
*/
Quadtree.prototype.remove = function (object) {
var parent = object.parent,
children = parent.children,
orphans = parent.orphans,
newParent = parent;
if (_.contains(children, object)) {
children.splice(children.indexOf(object), 1);
} else if (_.contains(orphans, object)) {
orphans.splice(orphans.indexOf(object), 1);
} else {
throw 'Object not found in quadTree when attempting to remove';
}
while (newParent.parent) {
newParent = newParent.parent;
}
object.parent = newParent;
parent.collapse();
};
/**
* Partitions the quadTree into 4 equal sized quadTrees.
* It also re-inserts all of the children that the leaf contained.
*/
Quadtree.prototype.divide = function () {
var children = this.children,
quarterWidth = this.width / 4,
quarterHeight = this.height / 4,
options = {
'depth' : this.depth - 1,
'width' : this.width / 2,
'height': this.height / 2,
'parent': this
};
this.isLeaf = false;
this.children = [
new Quadtree(_.extend(options, {
'x' : this.x - quarterWidth,
'y' : this.y - quarterHeight,
'quadrant': NORTH_WEST
})),
new Quadtree(_.extend(options, {
'x' : this.x + quarterWidth,
'y' : this.y - quarterHeight,
'quadrant': NORTH_EAST
})),
new Quadtree(_.extend(options, {
'x' : this.x - quarterWidth,
'y' : this.y + quarterHeight,
'quadrant': SOUTH_WEST
})),
new Quadtree(_.extend(options, {
'x' : this.x + quarterWidth,
'y' : this.y + quarterHeight,
'quadrant': SOUTH_EAST
}))
];
for (var i = 0, l = children.length; i < l; i++) {
this.insert(children[i]);
}
};
/**
* Collapses the quadTree
*/
Quadtree.prototype.collapse = function () {
if (this.parent) {
if (this !== this.parent.children[0] && this !== this.parent.children[1] && this !== this.parent.children[2] && this !== this.parent.children[3]) {
throw 'This was a bug that was fixed, but I am paranoid this will get hit so I left it...';
}
}
if (this.parent && this.parent.canCollapse()) {
this.parent.collapse();
return;
}
if (this.canCollapse() && !this.isLeaf) {
var allChildrenAndOrphans = this.getOrphansAndChildren();
this.children = [];
this.orphans = [];
this.isLeaf = true;
for (var i = 0; i < allChildrenAndOrphans.length; i++) {
this.insert(allChildrenAndOrphans[i]);
}
}
};
/**
* Helper method that determines if the quadtree should collapse
*/
Quadtree.prototype.canCollapse = function () {
return this.getOrphanAndChildCount() <= this.maxChildren;
}
/**
* getOrphanCount returns the number of orphans in the quadTree
* @return {Array} number of orphans in the quadTree
*/
Quadtree.prototype.getOrphanCount = function () {
var numberOfOrphans = this.orphans.length,
numberOfChildren = this.children.length,
count = numberOfOrphans;
if (this.isLeaf) {
if (count !== 0) {
throw 'Why does this leaf have orphans?!';
}
return count; // should be 0.
} else {
for (var i = 0; i < numberOfChildren; i++) {
count += this.children[i].getOrphanCount();
}
}
return count;
};
/**
* Returns the number of children in the quadTree
* @return {Number} The number of children in the quadTree
*/
Quadtree.prototype.getChildCount = function () {
var count = 0,
numberOfChildren = this.children.length;
if (!this.isLeaf) {
for (var i = 0; i < numberOfChildren; i++) {
count += this.children[i].getChildCount();
}
} else {
count += numberOfChildren;
}
return count;
};
/**
* getOrphanAndChildCount returns all rectangles that have been inserted into the quadtree
* @return {Number} The number of all inserted objects in the quadtree
*/
Quadtree.prototype.getOrphanAndChildCount = function () {
return this.getOrphanCount() + this.getChildCount();
};
/**
* getOrphans return all the orphans of the quadTree
* @return {Array} all the orphans of the quadTree
*/
Quadtree.prototype.getOrphans = function () {
var orphans = [];
if (!this.isLeaf) {
orphans = this.orphans;
for (var i = 0; i < this.children.length; i++) {
orphans = orphans.concat(this.children[i].getOrphans());
}
}
return orphans;
};
/**
* getChildren returns an array of all the children of the quadTree
* @return {Array} all the children of the quadTree
*/
Quadtree.prototype.getChildren = function () {
var children = [];
if (this.isLeaf) {
return this.children;
} else {
for (var i = 0; i < this.children.length; i++) {
children = children.concat(this.children[i].getChildren());
}
}
return children;
};
/**
* getOrphansAndChildren returns an array of all the children and orphans of the quadTree
* @return {Array} all the children and orphans of the quadTree
*/
Quadtree.prototype.getOrphansAndChildren = function () {
return this.getChildren().concat(this.getOrphans());
};
/**
* getQuadtreeCount returns the number of divisions within the quadtree.
* @return {Number} The number of divisions within the quadtree.
*/
Quadtree.prototype.getQuadtreeCount = function () {
var count = this.children.length;
if (this.isLeaf) {
return 0;
}
for (var i = 0; i < this.children.length; i++) {
count += this.children[i].getQuadtreeCount();
}
return count;
};
Quadtree.prototype.getEntireQuadtreesOrphansAndChildren = function () {
var originalParent = this;
while (originalParent.parent) {
originalParent = originalParent.parent;
}
return originalParent.getOrphansAndChildren();
}
Quadtree.prototype.getParentOrphanComparisons = function () {
var comparisonList = [],
orphans = this.parent && this.parent.orphans;
if (!orphans) {
return comparisonList;
}
for (var i = 0; i < orphans.length; i++) {
if ((orphans[i].quadrant & this.quadrant)) {
comparisonList.push(orphans[i]);
}
}
return comparisonList.concat(this.parent.getParentOrphanComparisons());
};
Quadtree.prototype.getCollisions = function (rect) {
if (!hasRectProps(rect)) {
throw 'Collsion must be a rect';
}
return getCollisions(this.getComparisons(rect), rect);
};
// This might be an area to optimized. A rectangle that is an orphans of the parent-most quadtree
// that overlaps all quadrants will be the same as a brute force collision detector.
Quadtree.prototype.getOrphansAndChildrenInQuadrants = function (rect) {
var orphansAndChildren = [],
quadrant = rect.quadrant;
if (quadrant & NORTH_WEST) {
orphansAndChildren = orphansAndChildren.concat(this.children[0].getOrphansAndChildren());
}
if (quadrant & NORTH_EAST) {
orphansAndChildren = orphansAndChildren.concat(this.children[1].getOrphansAndChildren());
}
if (quadrant & SOUTH_WEST) {
orphansAndChildren = orphansAndChildren.concat(this.children[2].getOrphansAndChildren());
}
if (quadrant & SOUTH_EAST) {
orphansAndChildren = orphansAndChildren.concat(this.children[3].getOrphansAndChildren());
}
return orphansAndChildren;
};
Quadtree.prototype.getComparisons = function (rect) {
if (!hasRectProps(rect)) {
throw 'Collsion must be a rect';
}
if (!rect.quadrant) {
throw 'Rect does not have a quadrant property';
}
var comparisonList = rect.parent.isLeaf ? rect.parent.getChildren() : rect.parent.getOrphansAndChildrenInQuadrants(rect),
directOrphans = rect.parent.orphans,
parentOrphanComparisons = rect.parent.getParentOrphanComparisons(rect);
for (var i = 0; i < directOrphans.length; i++) {
if ((directOrphans[i].quadrant & rect.quadrant)) {
comparisonList.push(directOrphans[i]);
}
}
comparisonList = comparisonList.concat(parentOrphanComparisons);
if (_.contains(comparisonList, rect)) {
comparisonList.splice(comparisonList.indexOf(rect), 1);
}
return comparisonList;
};
Quadtree.prototype.getBruteForceCollisions = function (rect) {
if (!hasRectProps(rect)) {
throw 'Collsion must be a rect';
}
var comparisonList,
currentQuadTree = this;
while (currentQuadTree.parent) {
currentQuadTree = currentQuadTree.parent;
}
comparisonList = currentQuadTree.getOrphansAndChildren();
if (_.contains(comparisonList, rect)) {
comparisonList.splice(comparisonList.indexOf(rect), 1);
}
return getCollisions(comparisonList, rect);
};
// Helper functions
/**
* setQuadrant sets the overlapping quadrants (quadtrees) given an object
* @param {Object} object A rectangle that is inserted in the quadtree
* @param {Object} quadtree A quadtree
*/
function setQuadrant (object, quadtree) {
if (quadtree.isLeaf) {
if (quadtree.parent) {
object.quadrant = (isIntersecting(quadtree.parent.children[0], object) * NORTH_WEST) +
(isIntersecting(quadtree.parent.children[1], object) * NORTH_EAST) +
(isIntersecting(quadtree.parent.children[2], object) * SOUTH_WEST) +
(isIntersecting(quadtree.parent.children[3], object) * SOUTH_EAST);
} else {
object.quadrant = 15;
}
} else {
object.quadrant = (isIntersecting(quadtree.children[0], object) * NORTH_WEST) +
(isIntersecting(quadtree.children[1], object) * NORTH_EAST) +
(isIntersecting(quadtree.children[2], object) * SOUTH_WEST) +
(isIntersecting(quadtree.children[3], object) * SOUTH_EAST);
}
}
/**
* [hasRectProps determines if the object has the necessary properties to be considered a rectangle]
* @param {Object} object [The object questioned for rect props]
* @return {Boolean} [True if it is a rectangle]
*/
function hasRectProps (object) {
return typeof object.width !== 'undefined' && object.height !== 'undefined' && object.x !== 'undefined' && object.y !== 'undefined';
}
/**
* [getBounds returns the bounds of a rectangle]
* @param {Object} r [x, y, width, height]
* @return {Object} [left, right, top, bottom]
*/
function getBounds (r) {
return {
'left' : r.x - r.width / 2,
'right' : r.x + r.width / 2,
'top' : r.y - r.height / 2,
'bottom': r.y + r.height / 2
};
}
/**
* [isWithinBounds retuns true if rect2 is completely within rect1]
* @param {Object} r1 [x, y, width, height]
* @param {Object} r2 [x, y, width, height]
* @return {Boolean} [true if rect2 is completely within rect1]
*/
function isWithinBounds (r1, r2) {
var r1Bounds = getBounds(r1),
r2Bounds = getBounds(r2);
return (r2Bounds.left >= r1Bounds.left &&
r2Bounds.right <= r1Bounds.right &&
r2Bounds.top >= r1Bounds.top &&
r2Bounds.bottom <= r1Bounds.bottom);
}
/**
* [isIntersecting returns true if two rectangles intersect]
* @param {Object} r1 [rectangle]
* @param {Object} r2 [rectangle]
* @return {Boolean} [True if two rectangles isIntersecting]
* @diagram
*
* * * * * * *
* * r2 *
* * * * * *
* * * * * * * *
* * r1 *
* * * * *
*
* * * * * * *
* * r2 *
* * * * * * *
* * * * * * * * r1 *
* * *
* * * * *
*/
function isIntersecting (r1, r2) {
if (r1.radius && r2.radius) {
return isIntersectingCircles(r1, r2);
} else {
return isIntersectingSquares(r1, r2);
}
}
function isIntersectingSquares (r1, r2) {
var r1Bounds = getBounds(r1),
r2Bounds = getBounds(r2);
return (r1Bounds.left < r2Bounds.right &&
r1Bounds.right > r2Bounds.left &&
r1Bounds.top < r2Bounds.bottom &&
r1Bounds.bottom > r2Bounds.top);
}
function isIntersectingCircles (c1, c2) {
var dx = c1.x - c2.x,
dy = c1.y - c2.y,
distance = Math.sqrt(dx * dx + dy * dy);
return distance < c1.radius + c2.radius;
}
function getCollisions (comparisonList, rect) {
var collisionList = [];
for (var i = 0; i < comparisonList.length; i++) {
if (isIntersecting(comparisonList[i], rect)) {
collisionList.push(comparisonList[i]);
}
}
return collisionList;
}
/**
* [forceObjectWithinBounds forces the inserted object into the quadtree bounds.
* This makes the quadtree behave like pac-man when he goes into the opening on
* the side of the map]
* @param {Object} object [This is the parent-most quadtree]
* @param {Object} rect [The inserted rectangle]
*/
function forceObjectWithinBounds (object, rect) {
var objectBounds = getBounds(object),
containerBounds = getBounds(rect),
isTooFarLeft = objectBounds.left < containerBounds.left,
isTooFarRight = objectBounds.left > containerBounds.right,
isTooFarAbove = objectBounds.top < containerBounds.top,
isTooFarBelow = objectBounds.top > containerBounds.bottom;
if (isTooFarLeft) {
while (object.x < containerBounds.left) {
object.x = containerBounds.right + object.x + rect.halfWidth;
}
}
if (isTooFarRight) {
while (object.x > containerBounds.right) {
object.x = containerBounds.left + object.x - rect.halfWidth;
}
}
if (isTooFarAbove) {
while (object.y < containerBounds.top) {
object.y = containerBounds.bottom + object.y + rect.halfHeight;
}
}
if (isTooFarBelow) {
while (object.y > containerBounds.bottom) {
object.y = containerBounds.top + object.y - rect.halfHeight;
}
}
}
module.exports = Quadtree;</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Quadtree.html">Quadtree</a></li></ul><h3>Global</h3><ul><li><a href="global.html#forceObjectWithinBounds">forceObjectWithinBounds</a></li><li><a href="global.html#getBounds">getBounds</a></li><li><a href="global.html#hasRectProps">hasRectProps</a></li><li><a href="global.html#isIntersecting">isIntersecting</a></li><li><a href="global.html#isWithinBounds">isWithinBounds</a></li><li><a href="global.html#rectPrototype">rectPrototype</a></li><li><a href="global.html#remove">remove</a></li><li><a href="global.html#setQuadrant">setQuadrant</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.3.2</a> on Tue Aug 04 2015 14:09:02 GMT-0400 (Eastern Daylight Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>