Skip to content

Commit

Permalink
Add alfa-quadtree package
Browse files Browse the repository at this point in the history
  • Loading branch information
rcj-siteimprove committed Apr 8, 2024
1 parent b7fd020 commit f45b7df
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 1 deletion.
35 changes: 35 additions & 0 deletions packages/alfa-quadtree/package.json
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/"
}
}
1 change: 1 addition & 0 deletions packages/alfa-quadtree/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./quadtree";
183 changes: 183 additions & 0 deletions packages/alfa-quadtree/src/quadtree.ts
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;
}
}
17 changes: 17 additions & 0 deletions packages/alfa-quadtree/tsconfig.json
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" }
]
}
1 change: 0 additions & 1 deletion packages/alfa-rules/src/sia-r113/rule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Rule } from "@siteimprove/alfa-act";
import { Cache } from "@siteimprove/alfa-cache";
import { Device } from "@siteimprove/alfa-device";
import { Document, Element } from "@siteimprove/alfa-dom";
import { Either } from "@siteimprove/alfa-either";
Expand Down
1 change: 1 addition & 0 deletions packages/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
{ "path": "alfa-performance" },
{ "path": "alfa-predicate" },
{ "path": "alfa-promise" },
{ "path": "alfa-quadtree" },
{ "path": "alfa-record" },
{ "path": "alfa-rectangle" },
{ "path": "alfa-reducer" },
Expand Down

0 comments on commit f45b7df

Please sign in to comment.