-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b7fd020
commit f45b7df
Showing
6 changed files
with
237 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
{ | ||
"$schema": "http://json.schemastore.org/package", | ||
"name": "@siteimprove/alfa-quadtree", | ||
"homepage": "https://alfa.siteimprove.com", | ||
"version": "0.77.0", | ||
"license": "MIT", | ||
"description": "Implementation of a quadtree data structure for storing and querying spatial data.", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/siteimprove/alfa.git", | ||
"directory": "packages/alfa-quadtree" | ||
}, | ||
"bugs": "https://github.com/siteimprove/alfa/issues", | ||
"main": "src/index.js", | ||
"types": "src/index.d.ts", | ||
"files": [ | ||
"src/**/*.js", | ||
"src/**/*.d.ts" | ||
], | ||
"dependencies": { | ||
"@siteimprove/alfa-equatable": "workspace:^0.77.0", | ||
"@siteimprove/alfa-hash": "workspace:^0.77.0", | ||
"@siteimprove/alfa-iterable": "workspace:^0.77.0", | ||
"@siteimprove/alfa-json": "workspace:^0.77.0", | ||
"@siteimprove/alfa-rectangle": "workspace:^0.77.0", | ||
"@siteimprove/alfa-sequence": "workspace:^0.77.0" | ||
}, | ||
"devDependencies": { | ||
"@siteimprove/alfa-test": "workspace:^0.77.0" | ||
}, | ||
"publishConfig": { | ||
"access": "public", | ||
"registry": "https://npm.pkg.github.com/" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./quadtree"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
import { Iterable } from "@siteimprove/alfa-iterable"; | ||
import { Rectangle } from "@siteimprove/alfa-rectangle"; | ||
|
||
/** | ||
* Implementation of a quadtree that can store any type of item that has a bounding rectangle associated with it. | ||
* In this implementation, the items are only stored in the leaves of the quadtree and in all leaves | ||
* they intersect with. | ||
* | ||
* @privateRemarks | ||
* An alternative implementation could allow items at all levels of the quadtree, not just the leaves, and insert them into | ||
* the first quad that fully contains the item's rectangle. That would remove the need to store the same item multiple times, | ||
* but the search would have to compare with items at all levels, which would mean comparing with the items lying | ||
* on the boundaries of the quads. It's not clear if that would be more or less efficient than the current implementation. | ||
* It would also remove the need to move items after subdivision. | ||
*/ | ||
export class QuadTree<T> { | ||
static readonly CAPACITY = 4; | ||
static readonly MAX_DEPTH = 8; | ||
|
||
public static of<T>(boundary: Rectangle, capacity: number): QuadTree<T> { | ||
return new QuadTree(boundary, capacity); | ||
} | ||
|
||
private readonly _boundary: Rectangle; | ||
private readonly _depth: number; | ||
private _items: Array<[T, Rectangle]> = []; | ||
private _children: Array<QuadTree<T>> = []; | ||
private _divided = false; | ||
|
||
private constructor(bounds: Rectangle, depth = 0) { | ||
this._boundary = bounds; | ||
this._depth = depth; | ||
} | ||
|
||
private subdivide(): void { | ||
const halfWidth = this._boundary.width / 2; | ||
const halfHeight = this._boundary.height / 2; | ||
|
||
// Top-left | ||
this._children.push( | ||
QuadTree.of<T>( | ||
Rectangle.of(this._boundary.x, this._boundary.y, halfWidth, halfHeight), | ||
this._depth + 1, | ||
), | ||
); | ||
|
||
// Top-right | ||
this._children.push( | ||
QuadTree.of<T>( | ||
Rectangle.of( | ||
this._boundary.x + halfWidth, | ||
this._boundary.y, | ||
halfWidth, | ||
halfHeight, | ||
), | ||
this._depth + 1, | ||
), | ||
); | ||
|
||
// Bottom-left | ||
this._children.push( | ||
QuadTree.of<T>( | ||
Rectangle.of( | ||
this._boundary.x, | ||
this._boundary.y + halfHeight, | ||
halfWidth, | ||
halfHeight, | ||
), | ||
this._depth + 1, | ||
), | ||
); | ||
|
||
// Bottom-right | ||
this._children.push( | ||
QuadTree.of<T>( | ||
Rectangle.of( | ||
this._boundary.x + halfWidth, | ||
this._boundary.y + halfHeight, | ||
halfWidth, | ||
halfHeight, | ||
), | ||
this._depth + 1, | ||
), | ||
); | ||
|
||
this._divided = true; | ||
} | ||
|
||
/** | ||
* Try inserting an item into the quadtree. | ||
* | ||
* @remarks | ||
* If the item's rectangle does not intersect the boundary of the quadtree, it won't be inserted. | ||
*/ | ||
public insert(itemRectPair: [T, Rectangle]): void { | ||
const [, rect] = itemRectPair; | ||
|
||
// If the item's rectangle does not intersect the boundary of the quadtree, don't insert it | ||
if (!this._boundary.intersects(rect)) { | ||
return; | ||
} | ||
|
||
// If the max depth has been reached, insert the item into the current quad | ||
if (this._depth >= QuadTree.MAX_DEPTH) { | ||
this._items.push(itemRectPair); | ||
return; | ||
} | ||
|
||
// If the quadtree has been divided, try to insert in all children | ||
if (this._divided) { | ||
for (const child of this._children) { | ||
child.insert(itemRectPair); | ||
} | ||
return; | ||
} | ||
|
||
// If the capacity has not been reached, insert the item into the current quad | ||
if (this._items.length < QuadTree.CAPACITY) { | ||
this._items.push(itemRectPair); | ||
return; | ||
} | ||
|
||
// If the capacity has been reached, subdivide and move all items into the children | ||
// before inserting the new item | ||
|
||
this.subdivide(); | ||
|
||
for (const child of this._children) { | ||
for (const existing of this._items) { | ||
child.insert(existing); | ||
} | ||
|
||
child.insert(itemRectPair); | ||
} | ||
|
||
// Clear the items in the current quad since they have been moved to the children | ||
this._items = []; | ||
} | ||
|
||
/** | ||
* Returns all items in the current quad and all descendants | ||
*/ | ||
private getAllItems(result = new Set<T>()): Iterable<T> { | ||
for (const [item] of this._items) { | ||
result.add(item); | ||
} | ||
|
||
for (const child of this._children) { | ||
child.getAllItems(result); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
/** | ||
* Query the quadtree for all items that intersect with the given range. | ||
*/ | ||
public query(range: Rectangle, result = new Set<T>()): Iterable<T> { | ||
// If the range doesn't intersect with the quad, don't search it | ||
if (!range.intersects(this._boundary)) { | ||
return result; | ||
} | ||
|
||
// If the range is fully contained in the quad, return all items in the quad and descendants | ||
if (this._boundary.contains(range)) { | ||
return this.getAllItems(result); | ||
} | ||
|
||
// Compare with all items in the quad (in case it's a leaf) | ||
for (const [item, rect] of this._items) { | ||
if (range.intersects(rect)) { | ||
result.add(item); | ||
} | ||
} | ||
|
||
// Search the children | ||
for (const child of this._children) { | ||
child.query(range, result); | ||
} | ||
|
||
return result; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"$schema": "http://json.schemastore.org/tsconfig", | ||
"extends": "../tsconfig.json", | ||
"files": [ | ||
"src/index.ts", | ||
"src/quadtree.ts", | ||
], | ||
"references": [ | ||
{ "path": "../alfa-equatable" }, | ||
{ "path": "../alfa-hash" }, | ||
{ "path": "../alfa-iterable" }, | ||
{ "path": "../alfa-json" }, | ||
{ "path": "../alfa-rectangle" }, | ||
{ "path": "../alfa-sequence" }, | ||
{ "path": "../alfa-test" } | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters