Quad Tree

// Author: Jerold Albertson
// Profile: https://github.com/jerold
// Algorithm: https://en.wikipedia.org/wiki/Quadtree

import 'dart:math';
import 'package:test/test.dart';

// defaults should almost never be used, tune the quad tree to fit your problem
int default_max_depth = 1000;
int default_max_items = 100;

// names reflect a coordinate system where values increase as one goes left or down
const _upperLeftIndex = 0;
const _upperRightIndex = 1;
const _lowerLeftIndex = 2;
const _lowerRightIndex = 3;

class Node<T> extends Rectangle<num> {
  final int maxDepth;
  final int maxItems;

  final int _depth;
  final Point<num> _center;
  final List<_ItemAtPoint<T>> _items = <_ItemAtPoint<T>>[];
  final List<Node<T>> _children = <Node<T>>[];

  factory Node(num left, num top, num width, num height,
          {int maxDepth, int maxItems}) =>
      Node._(left, top, width, height, maxDepth, maxItems, 0);

  Node._(num left, num top, num width, num height, int maxDepth, int maxItems,
      int depth)
      : maxDepth = maxDepth ?? default_max_depth,
        maxItems = maxItems ?? default_max_items,
        _depth = depth,
        _center = Point<num>(left + width / 2.0, top + height / 2.0),
        super(left, top, width, height);

  bool insert(T item, Point<num> atPoint) {
    if (!containsPoint(atPoint)) return false;

    if (_children.isEmpty) {
      if (_items.length + 1 <= maxItems || _depth + 1 > maxDepth) {
        _items.add(_ItemAtPoint<T>(item, atPoint));
        return true;
      }
      _splitItemsBetweenChildren();
    }
    return _insertItemIntoChildren(item, atPoint);
  }

  List<T> query(Rectangle range) {
    if (_children.isEmpty) {
      return _items
          .where((item) => range.containsPoint(item.point))
          .map((item) => item.item)
          .toList();
    }
    return _children
        .where((child) => child.intersects(range))
        .expand((child) => child.query(range))
        .toList();
  }

  String toString() {
    return '[$_depth](${_items.map((item) => item.item).toList()}:$_children)';
  }

  bool _insertItemIntoChildren(T item, Point<num> atPoint) {
    if (atPoint.x > _center.x) {
      if (atPoint.y > _center.y) {
        return _children[_lowerRightIndex].insert(item, atPoint);
      }
      return _children[_upperRightIndex].insert(item, atPoint);
    } else {
      if (atPoint.y > _center.y) {
        return _children[_lowerLeftIndex].insert(item, atPoint);
      } else {
        return _children[_upperLeftIndex].insert(item, atPoint);
      }
    }
  }

  void _splitItemsBetweenChildren() {
    _children.addAll([
      _newUpperLeft, // _upperLeftIndex = 0
      _newUpperRight, // _upperRightIndex = 1
      _newLowerLeft, // _lowerLeftIndex = 2
      _newLowerRight // _lowerRightIndex = 3
    ]);
    for (final item in _items) {
      _insertItemIntoChildren(item.item, item.point);
    }
    _items.clear();
  }

  Node<T> get _newUpperLeft => Node<T>._(
      left, top, width / 2.0, height / 2.0, maxDepth, maxItems, _depth + 1);

  Node<T> get _newUpperRight => Node<T>._(_center.x, top, width / 2.0,
      height / 2.0, maxDepth, maxItems, _depth + 1);

  Node<T> get _newLowerLeft => Node<T>._(left, _center.y, width / 2.0,
      height / 2.0, maxDepth, maxItems, _depth + 1);

  Node<T> get _newLowerRight => Node<T>._(_center.x, _center.y, width / 2.0,
      height / 2.0, maxDepth, maxItems, _depth + 1);
}

class _ItemAtPoint<T> {
  final T item;
  final Point<num> point;

  _ItemAtPoint(this.item, this.point);
}

void main() {
  group('QuadTree', () {
    test('items will not insert at points outside the tree\'s bounds', () {
      final tree = Node<String>(-50, -50, 100, 100);
      expect(tree.insert("a", Point(-75, 0)), isFalse);
      expect(tree.insert("b", Point(75, 0)), isFalse);
      expect(tree.insert("c", Point(0, -75)), isFalse);
      expect(tree.insert("d", Point(0, 75)), isFalse);
      expect(tree.toString(), equals("[0]([]:[])"));
    });

    test('maxItems is honored until maxDepth is hit', () {
      final tree = Node<String>(0, 0, 100, 100, maxItems: 2, maxDepth: 2);

      expect(tree.insert("a", Point(0, 0)), isTrue);
      expect(tree.toString(), equals("[0]([a]:[])"));

      expect(tree.insert("b", Point(100, 0)), isTrue);
      expect(tree.toString(), equals("[0]([a, b]:[])"));

      expect(tree.insert("c", Point(0, 100)), isTrue);
      expect(
          tree.toString(),
          equals(
              "[0]([]:[[1]([a]:[]), [1]([b]:[]), [1]([c]:[]), [1]([]:[])])"));

      expect(tree.insert("d", Point(100, 100)), isTrue);
      expect(
          tree.toString(),
          equals(
              "[0]([]:[[1]([a]:[]), [1]([b]:[]), [1]([c]:[]), [1]([d]:[])])"));

      expect(tree.insert("e", Point(99, 99)), isTrue);
      expect(tree.insert("f", Point(99, 99)), isTrue);
      expect(tree.insert("g", Point(99, 99)), isTrue);
      expect(tree.insert("h", Point(99, 99)), isTrue);
      expect(
          tree.toString(),
          equals(
              "[0]([]:[[1]([a]:[]), [1]([b]:[]), [1]([c]:[]), [1]([]:[[2]([]:[]), [2]([]:[]), [2]([]:[]), [2]([d, e, f, g, h]:[])])])"));
    });

    test(
        'better at finding local points within a large space than simple iteration',
        () {
      final rand = Random.secure();
      final items = <int, Point>{};

      final width = 1000;
      final height = 1000;

      var timesBetter = 0;
      final numberOfRuns = 100;
      // run the same test x number of times
      for (int j = 0; j < numberOfRuns; j++) {
        // test consists of setting up x items, distributing them randomly
        // within a space, and then searching for a small subset of those
        // using simple rect comparison on all items, or a quad tree query
        final tree = Node(0, 0, width, height);
        final numberOfItems = 50000;
        for (int i = 0; i < numberOfItems; i++) {
          items[i] =
              Point(rand.nextDouble() * width, rand.nextDouble() * height);
          expect(tree.insert(i, items[i]), isTrue);
        }

        // set up a box that is 1/10th the size of the total space
        final rangeSizePercentage = .1;
        final rangeWidth = width * rangeSizePercentage;
        final rangeHeight = height * rangeSizePercentage;
        final rangeLeft = rand.nextDouble() * (width - rangeWidth);
        final rangeTop = rand.nextDouble() * (height - rangeHeight);

        final range = Rectangle(rangeLeft, rangeTop, rangeWidth, rangeHeight);

        // simple iteration over all items, comparing each to the given range
        var startTime = DateTime.now();
        final foundA =
            items.keys.where((key) => range.containsPoint(items[key])).toList();
        var iterationTime = DateTime.now().difference(startTime);

        // quad tree query rules out whole quadrants full of points when possible
        startTime = DateTime.now();
        final foundB = tree.query(range);
        var quadTreeTime = DateTime.now().difference(startTime);

        if (iterationTime.compareTo(quadTreeTime) > 0) {
          timesBetter++;
        }

        // every time, quad tree query results should equal brute force results
        expect(foundA.toSet().containsAll(foundB), isTrue,
            reason: "not all items were found");
        expect(foundB.toSet().containsAll(foundA), isTrue,
            reason: "not all items were found");
      }

      expect(timesBetter / numberOfRuns > 0.5, isTrue,
          reason:
              "tree query was only better ${timesBetter / numberOfRuns * 100}% of the time");
    });
  });
}
Algerlogo

Β© Alger 2022

About us

We are a group of programmers helping each other build new things, whether it be writing complex encryption programs, or simple ciphers. Our goal is to work together to document and model beautiful, helpful and interesting algorithms using code. We are an open-source community - anyone can contribute. We check each other's work, communicate and collaborate to solve problems. We strive to be welcoming, respectful, yet make sure that our code follows the latest programming guidelines.