├── .gitignore ├── lib ├── rbush.dart └── src │ ├── quickselect.dart │ ├── tinyqueue.dart │ └── rbush.dart ├── pubspec.yaml ├── CHANGELOG.md ├── test ├── quickselect.dart ├── tinyqueue.dart ├── rbush_knn.dart └── rbush.dart ├── LICENSE ├── analysis_options.yaml ├── README.md └── pubspec.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub. 2 | .dart_tool/ 3 | .packages 4 | 5 | # Conventional directory for build output. 6 | build/ 7 | 8 | .idea/ 9 | *.iml 10 | *.swp 11 | -------------------------------------------------------------------------------- /lib/rbush.dart: -------------------------------------------------------------------------------- 1 | export 'src/rbush.dart' show RBush, RBushBase, RBushBox, RBushElement, RBushDirect; 2 | export 'src/quickselect.dart' show quickSelect; 3 | export 'src/tinyqueue.dart' show TinyQueue; -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rbush 2 | description: RBush — a high-performance R-tree-based 2D spatial index for points and rectangles. 3 | version: 1.1.1 4 | homepage: https://github.com/Zverik/dart_rbush 5 | 6 | environment: 7 | sdk: '>=2.12.0 <4.0.0' 8 | 9 | dev_dependencies: 10 | lints: ^2.1.1 11 | test: ^1.19.5 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.1 2 | 3 | * Better node children sorting (thanks @brand17). 4 | * Upgraded dependencies and enabled Dart 3. 5 | 6 | ## 1.1.0 7 | 8 | * `RBushElement` is now templated and requires a non-null data. 9 | * Added `RBushDirect` for skipping bounding boxes management. 10 | 11 | ## 1.0.0 12 | 13 | * Initial version. 14 | -------------------------------------------------------------------------------- /test/quickselect.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:rbush/rbush.dart' show quickSelect; 3 | 4 | void main() { 5 | test('selection', () { 6 | var arr = [65, 28, 59, 33, 21, 56, 22, 95, 50, 12, 90, 53, 28, 77, 39]; 7 | quickSelect(arr, 8); 8 | expect(arr, equals([39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95])); 9 | }); 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Ilya Zverev 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /test/tinyqueue.dart: -------------------------------------------------------------------------------- 1 | import 'package:rbush/rbush.dart' show TinyQueue; 2 | import 'package:test/test.dart'; 3 | import 'dart:math' show Random; 4 | 5 | void main() { 6 | final rand = Random(); 7 | final data = [for (var i = 0; i < 100; i++) rand.nextInt(100)]; 8 | final sorted = List.of(data); 9 | sorted.sort(); 10 | 11 | test('maintains a priority queue', () { 12 | final queue = TinyQueue(); 13 | for (final item in data) queue.push(item); 14 | 15 | expect(queue.peek(), equals(sorted.first)); 16 | 17 | final result = []; 18 | while (queue.isNotEmpty) result.add(queue.pop()); 19 | 20 | expect(result, equals(sorted)); 21 | }); 22 | 23 | test('accepts data in constructor', () { 24 | final queue = TinyQueue(data); 25 | 26 | final result = []; 27 | while (queue.isNotEmpty) result.add(queue.pop()); 28 | 29 | expect(result, equals(sorted)); 30 | }); 31 | 32 | test('handles edge cases with few elements', () { 33 | final queue = TinyQueue(); 34 | 35 | queue.push(2); 36 | queue.push(1); 37 | queue.pop(); 38 | queue.pop(); 39 | 40 | expect(() => queue.pop(), throwsStateError); 41 | 42 | queue.push(2); 43 | queue.push(1); 44 | 45 | expect(queue.pop(), equals(1)); 46 | expect(queue.pop(), equals(2)); 47 | }); 48 | 49 | test('handles init with empty array', () { 50 | final queue = TinyQueue([]); 51 | expect(queue.data, equals([])); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RBush 2 | 3 | RBush is a high-performance Dart library for 2D **spatial indexing** of points and rectangles. 4 | It's based on an optimized **R-tree** data structure with **bulk insertion** support. 5 | 6 | *Spatial index* is a special data structure for points and rectangles 7 | that allows you to perform queries like "all items within this bounding box" very efficiently 8 | (e.g. hundreds of times faster than looping over all items). 9 | It's most commonly used in maps and data visualizations. 10 | 11 | ## Usage 12 | 13 | ```dart 14 | // Create a tree with up to 16 elements in a leaf. 15 | final tree = RBush(16); 16 | 17 | // Bulk load elements (empty in this case, so here it's a no-op). 18 | tree.load([]); 19 | 20 | // Insert a single element. 21 | tree.insert(RBushElement( 22 | minX: 10, minY: 10, 23 | maxX: 20, maxY: 30, 24 | data: 'sample data' 25 | )); 26 | 27 | // Find the element we've inserted. 28 | final List found = tree.search( 29 | RBushBox(minX: 5, minY: 5, maxX: 25, maxY: 25)); 30 | 31 | // Remove all elements from the tree. 32 | tree.clear(); 33 | ``` 34 | 35 | An optional argument to `RBush` defines the maximum number of entries in a tree node. 36 | `9` (used by default) is a reasonable choice for most applications. 37 | Higher value means faster insertion and slower search, and vice versa. 38 | 39 | To store items of a different type, extend from (or instantiate) `RBushBase`. 40 | For example: 41 | 42 | ```dart 43 | class MyItem { 44 | final String id; 45 | final LatLng location; 46 | 47 | const MyItem(this.id, this.location); 48 | } 49 | 50 | final tree = RBushBase( 51 | maxEntries: 4, 52 | toBBox: (item) => RBushBox( 53 | minX: item.location.longitude, 54 | maxX: item.location.longitude, 55 | minY: item.location.latitude, 56 | maxY: item.location.latitude, 57 | ), 58 | getMinX: (item) => item.location.longitude, 59 | getMinY: (item) => item.location.latitude, 60 | ); 61 | ``` 62 | 63 | ### K Nearest Neighbours 64 | 65 | The `RBushBase` class also includes a `knn()` method for the nearest neighbours 66 | search. This is especially useful when using the r-tree to store point features, 67 | like in the example above. 68 | 69 | Note that for larger geographical areas the distance would be wrong, since the 70 | class uses pythagorean distances (`dx² + dy²`), not Haversine or great circle. 71 | 72 | ## Tiny Queue and Quick Select 73 | 74 | This package also includes ported fast versions of a priority queue and 75 | a selection algorithm. These are used internally by the r-tree, but might 76 | be useful for you as well. 77 | 78 | ## Upstream 79 | 80 | This library is a straight-up port of several JavaScript libraries written 81 | by Vladimir Agafonkin: 82 | 83 | * [RBush 3.0.1](https://github.com/mourner/rbush) 84 | * [RBush-knn 3.0.1](https://github.com/mourner/rbush-knn) 85 | * [QuickSelect 2.0.0](https://github.com/mourner/quickselect) 86 | * [TinyQueue 2.0.3](https://github.com/mourner/tinyqueue) 87 | 88 | All of these are published under MIT or ISC licenses. -------------------------------------------------------------------------------- /lib/src/quickselect.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Ilya Zverev, (c) 2018 Vladimir Agafonkin. 2 | // Port of https://github.com/mourner/quickselect. 3 | // Use of this code is governed by an ISC license, see the LICENSE file. 4 | 5 | import 'dart:math' as math; 6 | 7 | /// This function implements a fast 8 | /// [selection algorithm](https://en.wikipedia.org/wiki/Selection_algorithm) 9 | /// (specifically, [Floyd-Rivest selection](https://en.wikipedia.org/wiki/Floyd%E2%80%93Rivest_algorithm)). 10 | /// 11 | /// Rearranges items so that all items in the `[left, k]` are the smallest. 12 | /// The `k`-th element will have the `(k - left + 1)`-th smallest value in `[left, right]`. 13 | /// 14 | /// - [arr]: the list to partially sort (in place) 15 | /// - [k]: middle index for partial sorting (as defined above) 16 | /// - [left]: left index of the range to sort (`0` by default) 17 | /// - [right]: right index (last index of the array by default) 18 | /// - [compare]: compare function, if items in the list are not `Comparable`. 19 | /// 20 | /// Example: 21 | /// 22 | /// ```dart 23 | /// var arr = [65, 28, 59, 33, 21, 56, 22, 95, 50, 12, 90, 53, 28, 77, 39]; 24 | /// 25 | /// quickSelect(arr, 8); 26 | /// 27 | /// // arr is [39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95] 28 | /// // ^^ middle index 29 | /// ``` 30 | quickSelect(List arr, int k, 31 | [int left = 0, int? right, Comparator? compare]) { 32 | if (arr.isEmpty) return; 33 | if (compare == null && arr.first is! Comparable) { 34 | throw ArgumentError( 35 | 'Please either provide a comparator or use a list of Comparable elements.'); 36 | } 37 | _quickSelectStep(arr, k, left, right ?? arr.length - 1, 38 | compare ?? (T a, T b) => (a as Comparable).compareTo(b)); 39 | } 40 | 41 | _quickSelectStep( 42 | List arr, int k, int left, int right, Comparator compare) { 43 | while (right > left) { 44 | if (right - left > 600) { 45 | final n = right - left + 1; 46 | final m = k - left + 1; 47 | final z = math.log(n); 48 | final s = 0.5 * math.exp(2 * z / 3); 49 | final sd = 0.5 * math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); 50 | final newLeft = math.max(left, (k - m * s / n + sd).floor()); 51 | final newRight = math.min(right, (k + (n - m) * s / n + sd).floor()); 52 | _quickSelectStep(arr, k, newLeft, newRight, compare); 53 | } 54 | 55 | final t = arr[k]; 56 | var i = left; 57 | var j = right; 58 | 59 | _swap(arr, left, k); 60 | if (compare(arr[right], t) > 0) _swap(arr, left, right); 61 | 62 | while (i < j) { 63 | _swap(arr, i, j); 64 | i++; 65 | j--; 66 | while (compare(arr[i], t) < 0) i++; 67 | while (compare(arr[j], t) > 0) j--; 68 | } 69 | 70 | if (compare(arr[left], t) == 0) { 71 | _swap(arr, left, j); 72 | } else { 73 | j++; 74 | _swap(arr, j, right); 75 | } 76 | 77 | if (j <= k) left = j + 1; 78 | if (k <= j) right = j - 1; 79 | } 80 | } 81 | 82 | _swap(List arr, i, j) { 83 | final tmp = arr[i]; 84 | arr[i] = arr[j]; 85 | arr[j] = tmp; 86 | } -------------------------------------------------------------------------------- /lib/src/tinyqueue.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Ilya Zverev, (c) 2019 Vladimir Agafonkin. 2 | /// Port of https://github.com/mourner/tinyqueue. 3 | // Use of this code is governed by an ISC license, see the LICENSE file. 4 | 5 | /// The smallest and simplest binary heap priority queue. 6 | /// 7 | /// ```dart 8 | /// // create an empty priority queue 9 | /// var queue = new TinyQueue(); 10 | /// 11 | /// // add some items 12 | /// queue.push(7); 13 | /// queue.push(5); 14 | /// queue.push(10); 15 | /// 16 | /// // remove the top item 17 | /// var top = queue.pop(); // returns 5 18 | /// 19 | /// // return the top item (without removal) 20 | /// top = queue.peek(); // returns 7 21 | /// 22 | /// // get queue length 23 | /// queue.length; // returns 2 24 | /// 25 | /// // create a priority queue from an existing array (modifies the array) 26 | /// queue = new TinyQueue([7, 5, 10]); 27 | /// 28 | /// // pass a custom item comparator as a second argument 29 | /// queue = new TinyQueue([{value: 5}, {value: 7}], function (a, b) { 30 | /// return a.value - b.value; 31 | /// }); 32 | /// 33 | /// // turn a queue into a sorted array 34 | /// var array = []; 35 | /// while (queue.length) array.push(queue.pop()); 36 | /// ``` 37 | class TinyQueue { 38 | final List data; 39 | int length; 40 | final Comparator compare; 41 | 42 | /// Constructs a new queue. 43 | /// 44 | /// Adding all elements at once would be faster than using [push] 45 | /// for each one. If elements are not `Comparable`, specify the 46 | /// [compare] function. 47 | TinyQueue([Iterable? data, Comparator? compare]) 48 | : data = data?.toList() ?? [], 49 | length = data?.length ?? 0, 50 | compare = compare ?? ((a, b) => (a as Comparable).compareTo(b)) { 51 | if (length > 0) { 52 | for (var i = (length >> 1) - 1; i >= 0; i--) { 53 | _down(i); 54 | } 55 | } 56 | } 57 | 58 | bool get isEmpty => length == 0; 59 | bool get isNotEmpty => length > 0; 60 | 61 | /// Adds an element to this queue. 62 | push(T item) { 63 | data.add(item); 64 | _up(length++); 65 | } 66 | 67 | /// Removes the smallest element from the queue and returns it. 68 | T pop() { 69 | final top = data.first; 70 | final bottom = data.removeLast(); 71 | 72 | if (--length > 0) { 73 | data[0] = bottom; 74 | _down(0); 75 | } 76 | 77 | return top; 78 | } 79 | 80 | /// Returns the smallest queue element without removing it. 81 | T peek() => data.first; 82 | 83 | _up(int pos) { 84 | final item = data[pos]; 85 | while (pos > 0) { 86 | final parent = (pos - 1) >> 1; 87 | final current = data[parent]; 88 | if (compare(item, current) >= 0) break; 89 | data[pos] = current; 90 | pos = parent; 91 | } 92 | data[pos] = item; 93 | } 94 | 95 | _down(int pos) { 96 | final halfLength = length >> 1; 97 | final item = data[pos]; 98 | 99 | while (pos < halfLength) { 100 | int bestChild = (pos << 1) + 1; // initially it is the left child 101 | final right = bestChild + 1; 102 | 103 | if (right < length && compare(data[right], data[bestChild]) < 0) { 104 | bestChild = right; 105 | } 106 | if (compare(data[bestChild], item) >= 0) break; 107 | 108 | data[pos] = data[bestChild]; 109 | pos = bestChild; 110 | } 111 | 112 | data[pos] = item; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/rbush_knn.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:rbush/rbush.dart'; 3 | 4 | RBushElement listToBBox(List list) { 5 | return RBushElement(minX: list[0], minY: list[1], maxX: list[2], maxY: list[3], data: 0); 6 | } 7 | 8 | final data = >[ 9 | [87,55,87,56],[38,13,39,16],[7,47,8,47],[89,9,91,12],[4,58,5,60],[0,11,1,12],[0,5,0,6],[69,78,73,78], 10 | [56,77,57,81],[23,7,24,9],[68,24,70,26],[31,47,33,50],[11,13,14,15],[1,80,1,80],[72,90,72,91],[59,79,61,83], 11 | [98,77,101,77],[11,55,14,56],[98,4,100,6],[21,54,23,58],[44,74,48,74],[70,57,70,61],[32,9,33,12],[43,87,44,91], 12 | [38,60,38,60],[62,48,66,50],[16,87,19,91],[5,98,9,99],[9,89,10,90],[89,2,92,6],[41,95,45,98],[57,36,61,40], 13 | [50,1,52,1],[93,87,96,88],[29,42,33,42],[34,43,36,44],[41,64,42,65],[87,3,88,4],[56,50,56,52],[32,13,35,15], 14 | [3,8,5,11],[16,33,18,33],[35,39,38,40],[74,54,78,56],[92,87,95,90],[12,97,16,98],[76,39,78,40],[16,93,18,95], 15 | [62,40,64,42],[71,87,71,88],[60,85,63,86],[39,52,39,56],[15,18,19,18],[91,62,94,63],[10,16,10,18],[5,86,8,87], 16 | [85,85,88,86],[44,84,44,88],[3,94,3,97],[79,74,81,78],[21,63,24,66],[16,22,16,22],[68,97,72,97],[39,65,42,65], 17 | [51,68,52,69],[61,38,61,42],[31,65,31,65],[16,6,19,6],[66,39,66,41],[57,32,59,35],[54,80,58,84],[5,67,7,71], 18 | [49,96,51,98],[29,45,31,47],[31,72,33,74],[94,25,95,26],[14,7,18,8],[29,0,31,1],[48,38,48,40],[34,29,34,32], 19 | [99,21,100,25],[79,3,79,4],[87,1,87,5],[9,77,9,81],[23,25,25,29],[83,48,86,51],[79,94,79,95],[33,95,33,99], 20 | [1,14,1,14],[33,77,34,77],[94,56,98,59],[75,25,78,26],[17,73,20,74],[11,3,12,4],[45,12,47,12],[38,39,39,39], 21 | [99,3,103,5],[41,92,44,96],[79,40,79,41],[29,2,29,4], 22 | ].map((list) => listToBBox(list)).toList(); 23 | 24 | // Copied from `collections` package to reduce dependencies. 25 | extension IndexedListExtension on List { 26 | /// Maps each element and its index to a new value. 27 | Iterable mapIndexed(R Function(int index, E element) convert) sync* { 28 | for (var index = 0; index < length; index++) { 29 | yield convert(index, this[index]); 30 | } 31 | } 32 | } 33 | 34 | void main() { 35 | test('finds n neighbours', () { 36 | final tree = RBush().load(data); 37 | final result = tree.knn(40, 40, 10); 38 | 39 | expect(result, equals(>[ 40 | [38,39,39,39],[35,39,38,40],[34,43,36,44],[29,42,33,42],[48,38,48,40],[31,47,33,50],[34,29,34,32], 41 | [29,45,31,47],[39,52,39,56],[57,36,61,40] 42 | ].map((list) => listToBBox(list)).toList())); 43 | }); 44 | 45 | test('does not throw if requesting too many items', () { 46 | final tree = RBush().load(data); 47 | expect(() => tree.knn(40, 40, 1000), returnsNormally); 48 | List result = []; 49 | try { 50 | result = tree.knn(40, 40, 1000); 51 | } on Error {}; 52 | expect(result.length, equals(data.length)); 53 | }); 54 | 55 | test('finds all neighbors for maxDistance', () { 56 | final tree = RBush().load(data); 57 | final result = tree.knn(40, 40, 1, maxDistance: 10); 58 | 59 | expect(result, equals([listToBBox([38, 39, 39, 39])])); 60 | }); 61 | 62 | test('does not throw if requesting too many items for maxDistance', () { 63 | final tree = RBush().load(data); 64 | expect(() => tree.knn(40, 40, 1000, maxDistance: 10), returnsNormally); 65 | List result = []; 66 | try { 67 | result = tree.knn(40, 40, 1000, maxDistance: 10); 68 | } on Error {}; 69 | expect(result, equals(>[ 70 | [38,39,39,39],[35,39,38,40],[34,43,36,44],[29,42,33,42],[48,38,48,40],[31,47,33,50],[34,29,34,32] 71 | ].map((list) => listToBBox(list)).toList())); 72 | }); 73 | 74 | final pythData = >[[0,0,0,0],[9,9,9,9],[12,12,12,12],[13,14,19,11]].map((list) => listToBBox(list)); 75 | 76 | test('verify maxDistance excludes items too far away, in order to adhere to pythagoras theorem a^2+b^2=c^2', () { 77 | final tree = RBush().load(pythData); 78 | // sqrt(9^2+9^2)~=12.727 79 | final result = tree.knn(0, 0, 1000, maxDistance: 12.6); 80 | expect(result, equals([listToBBox([0, 0, 0, 0])])); 81 | }); 82 | 83 | test('verify maxDistance includes all items within range, in order to adhere to pythagoras theorem a^2+b^2=c^2', () { 84 | final tree = RBush().load(pythData); 85 | // sqrt(9^2+9^2)~=12.727 86 | final result = tree.knn(0, 0, 1000, maxDistance: 12.8); 87 | expect(result, equals([listToBBox([0, 0, 0, 0]), listToBBox([9, 9, 9, 9])])); 88 | }); 89 | 90 | final richData = >[[1,2,1,2],[3,3,3,3],[5,5,5,5],[4,2,4,2],[2,4,2,4],[5,3,5,3]].mapIndexed((index, list) { 91 | return RBushElement(minX: list[0], minY: list[1], maxX: list[2], maxY: list[3], data: index + 1); 92 | }); 93 | 94 | test('find n neighbours that do satisfy a given predicate', () { 95 | final tree = RBush().load(richData); 96 | final result = tree.knn(2, 4, 1, predicate: (item) => item.data < 5); 97 | expect(result.length, equals(1)); 98 | 99 | final item = result.first; 100 | expect(item.minX, equals(3)); 101 | expect(item.minY, equals(3)); 102 | expect(item.maxX, equals(3)); 103 | expect(item.maxY, equals(3)); 104 | expect(item.data, equals(2)); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "0816708f5fbcacca324d811297153fe3c8e047beb5c6752e12292d2974c17045" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "62.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "21862995c9932cd082f89d72ae5f5e2c110d1a0204ad06e4ebaee8307b76b834" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.0.0" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.4.2" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | boolean_selector: 37 | dependency: transitive 38 | description: 39 | name: boolean_selector 40 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.1.1" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.17.2" 52 | convert: 53 | dependency: transitive 54 | description: 55 | name: convert 56 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "3.1.1" 60 | coverage: 61 | dependency: transitive 62 | description: 63 | name: coverage 64 | sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.6.3" 68 | crypto: 69 | dependency: transitive 70 | description: 71 | name: crypto 72 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "3.0.3" 76 | dart_internal: 77 | dependency: transitive 78 | description: 79 | name: dart_internal 80 | sha256: "689dccc3d5f62affd339534cca548dce12b3a6b32f0f10861569d3025efc0567" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "0.2.9" 84 | file: 85 | dependency: transitive 86 | description: 87 | name: file 88 | sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "7.0.0" 92 | frontend_server_client: 93 | dependency: transitive 94 | description: 95 | name: frontend_server_client 96 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "3.2.0" 100 | glob: 101 | dependency: transitive 102 | description: 103 | name: glob 104 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "2.1.2" 108 | http_multi_server: 109 | dependency: transitive 110 | description: 111 | name: http_multi_server 112 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "3.2.1" 116 | http_parser: 117 | dependency: transitive 118 | description: 119 | name: http_parser 120 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "4.0.2" 124 | io: 125 | dependency: transitive 126 | description: 127 | name: io 128 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "1.0.4" 132 | js: 133 | dependency: transitive 134 | description: 135 | name: js 136 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "0.6.7" 140 | lints: 141 | dependency: "direct dev" 142 | description: 143 | name: lints 144 | sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "2.1.1" 148 | logging: 149 | dependency: transitive 150 | description: 151 | name: logging 152 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "1.2.0" 156 | matcher: 157 | dependency: transitive 158 | description: 159 | name: matcher 160 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "0.12.16" 164 | meta: 165 | dependency: transitive 166 | description: 167 | name: meta 168 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.9.1" 172 | mime: 173 | dependency: transitive 174 | description: 175 | name: mime 176 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "1.0.4" 180 | node_preamble: 181 | dependency: transitive 182 | description: 183 | name: node_preamble 184 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "2.0.2" 188 | package_config: 189 | dependency: transitive 190 | description: 191 | name: package_config 192 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "2.1.0" 196 | path: 197 | dependency: transitive 198 | description: 199 | name: path 200 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "1.8.3" 204 | pool: 205 | dependency: transitive 206 | description: 207 | name: pool 208 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "1.5.1" 212 | pub_semver: 213 | dependency: transitive 214 | description: 215 | name: pub_semver 216 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "2.1.4" 220 | shelf: 221 | dependency: transitive 222 | description: 223 | name: shelf 224 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "1.4.1" 228 | shelf_packages_handler: 229 | dependency: transitive 230 | description: 231 | name: shelf_packages_handler 232 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "3.0.2" 236 | shelf_static: 237 | dependency: transitive 238 | description: 239 | name: shelf_static 240 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "1.1.2" 244 | shelf_web_socket: 245 | dependency: transitive 246 | description: 247 | name: shelf_web_socket 248 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "1.0.4" 252 | source_map_stack_trace: 253 | dependency: transitive 254 | description: 255 | name: source_map_stack_trace 256 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "2.1.1" 260 | source_maps: 261 | dependency: transitive 262 | description: 263 | name: source_maps 264 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "0.10.12" 268 | source_span: 269 | dependency: transitive 270 | description: 271 | name: source_span 272 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "1.10.0" 276 | stack_trace: 277 | dependency: transitive 278 | description: 279 | name: stack_trace 280 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "1.11.1" 284 | stream_channel: 285 | dependency: transitive 286 | description: 287 | name: stream_channel 288 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "2.1.2" 292 | string_scanner: 293 | dependency: transitive 294 | description: 295 | name: string_scanner 296 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "1.2.0" 300 | term_glyph: 301 | dependency: transitive 302 | description: 303 | name: term_glyph 304 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "1.2.1" 308 | test: 309 | dependency: "direct dev" 310 | description: 311 | name: test 312 | sha256: "67ec5684c7a19b2aba91d2831f3d305a6fd8e1504629c5818f8d64478abf4f38" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "1.24.4" 316 | test_api: 317 | dependency: transitive 318 | description: 319 | name: test_api 320 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "0.6.1" 324 | test_core: 325 | dependency: transitive 326 | description: 327 | name: test_core 328 | sha256: "6b753899253c38ca0523bb0eccff3934ec83d011705dae717c61ecf209e333c9" 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "0.5.4" 332 | typed_data: 333 | dependency: transitive 334 | description: 335 | name: typed_data 336 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "1.3.2" 340 | vm_service: 341 | dependency: transitive 342 | description: 343 | name: vm_service 344 | sha256: ada49637c27973c183dad90beb6bd781eea4c9f5f955d35da172de0af7bd3440 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "11.8.0" 348 | watcher: 349 | dependency: transitive 350 | description: 351 | name: watcher 352 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "1.1.0" 356 | web_socket_channel: 357 | dependency: transitive 358 | description: 359 | name: web_socket_channel 360 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "2.4.0" 364 | webkit_inspection_protocol: 365 | dependency: transitive 366 | description: 367 | name: webkit_inspection_protocol 368 | sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "1.2.0" 372 | yaml: 373 | dependency: transitive 374 | description: 375 | name: yaml 376 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "3.1.2" 380 | sdks: 381 | dart: ">=3.0.0 <3.3.0" 382 | -------------------------------------------------------------------------------- /test/rbush.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:rbush/rbush.dart'; 3 | 4 | List someData(int n) { 5 | final List data = []; 6 | for (double i = 0; i < n; i++) { 7 | data.add(RBushElement(minX: i, minY: i, maxX: i, maxY: i, data: i)); 8 | } 9 | return data; 10 | } 11 | 12 | RBushElement listToBBox(List list) => RBushElement.fromList(list, 0); 13 | 14 | final data = >[ 15 | [0, 0, 0, 0], [10, 10, 10, 10], [20, 20, 20, 20], [25, 0, 25, 0], [35, 10, 35, 10], 16 | [45, 20, 45, 20], [0, 25, 0, 25], [10, 35, 10, 35], [20, 45, 20, 45], [25, 25, 25, 25], 17 | [35, 35, 35, 35], [45, 45, 45, 45], [50, 0, 50, 0], [60, 10, 60, 10], [70, 20, 70, 20], 18 | [75, 0, 75, 0], [85, 10, 85, 10], [95, 20, 95, 20], [50, 25, 50, 25], [60, 35, 60, 35], 19 | [70, 45, 70, 45], [75, 25, 75, 25], [85, 35, 85, 35], [95, 45, 95, 45], [0, 50, 0, 50], 20 | [10, 60, 10, 60], [20, 70, 20, 70], [25, 50, 25, 50], [35, 60, 35, 60], [45, 70, 45, 70], 21 | [0, 75, 0, 75], [10, 85, 10, 85], [20, 95, 20, 95], [25, 75, 25, 75], [35, 85, 35, 85], 22 | [45, 95, 45, 95], [50, 50, 50, 50], [60, 60, 60, 60], [70, 70, 70, 70], [75, 50, 75, 50], 23 | [85, 60, 85, 60], [95, 70, 95, 70], [50, 75, 50, 75], [60, 85, 60, 85], [70, 95, 70, 95], 24 | [75, 75, 75, 75], [85, 85, 85, 85], [95, 95, 95, 95], 25 | ].map((list) => listToBBox(list)).toList(); 26 | 27 | final emptyData = >[ 28 | [double.negativeInfinity, double.negativeInfinity, double.infinity, double.infinity], 29 | [double.negativeInfinity, double.negativeInfinity, double.infinity, double.infinity], 30 | [double.negativeInfinity, double.negativeInfinity, double.infinity, double.infinity], 31 | [double.negativeInfinity, double.negativeInfinity, double.infinity, double.infinity], 32 | [double.negativeInfinity, double.negativeInfinity, double.infinity, double.infinity], 33 | [double.negativeInfinity, double.negativeInfinity, double.infinity, double.infinity], 34 | ].map((list) => listToBBox(list)); 35 | 36 | class MyItem { 37 | final double minLng; 38 | final double minLat; 39 | final double maxLng; 40 | final double maxLat; 41 | 42 | const MyItem( 43 | {required this.minLng, 44 | required this.minLat, 45 | required this.maxLng, 46 | required this.maxLat}); 47 | 48 | @override 49 | bool operator ==(Object other) { 50 | if (other is! MyItem) return false; 51 | return minLng == other.minLng && minLat == other.minLat 52 | && maxLng == other.maxLng && maxLat == other.maxLat; 53 | } 54 | } 55 | 56 | void main() { 57 | test('allows custom formats by overriding some methods', () { 58 | final tree = RBushBase( 59 | toBBox: (item) => RBushBox( 60 | minX: item.minLng, 61 | minY: item.minLat, 62 | maxX: item.maxLng, 63 | maxY: item.maxLat, 64 | ), 65 | getMinX: (item) => item.minLng, 66 | getMinY: (item) => item.minLat, 67 | ); 68 | expect(tree.toBBox(MyItem(minLng: 1, minLat: 2, maxLng: 3, maxLat: 4)), 69 | equals(RBushBox(minX: 1, minY: 2, maxX: 3, maxY: 4))); 70 | }); 71 | 72 | test('constructor uses 9 max entries by default', () { 73 | final tree = RBush().load(someData(9)); 74 | expect(tree.data.height, equals(1)); 75 | 76 | final tree2 = RBush().load(someData(10)); 77 | expect(tree2.data.height, equals(2)); 78 | }); 79 | 80 | test('#toBBox, #compareMinX, #compareMinY can be overriden to allow custom data structures', () { 81 | final tree = RBushBase( 82 | maxEntries: 4, 83 | toBBox: (item) => RBushBox( 84 | minX: item.minLng, 85 | minY: item.minLat, 86 | maxX: item.maxLng, 87 | maxY: item.maxLat, 88 | ), 89 | getMinX: (item) => item.minLng, 90 | getMinY: (item) => item.minLat, 91 | ); 92 | 93 | const data = [ 94 | MyItem(minLng: -115, minLat: 45, maxLng: -105, maxLat: 55), 95 | MyItem(minLng: 105, minLat: 45, maxLng: 115, maxLat: 55), 96 | MyItem(minLng: 105, minLat: -55, maxLng: 115, maxLat: -45), 97 | MyItem(minLng: -115, minLat: -55, maxLng: -105, maxLat: -45), 98 | ]; 99 | 100 | tree.load(data); 101 | 102 | expect( 103 | tree.search(RBushBox(minX: -180, minY: -90, maxX: 180, maxY: 90)), 104 | unorderedEquals([ 105 | MyItem(minLng: -115, minLat: 45, maxLng: -105, maxLat: 55), 106 | MyItem(minLng: 105, minLat: 45, maxLng: 115, maxLat: 55), 107 | MyItem(minLng: 105, minLat: -55, maxLng: 115, maxLat: -45), 108 | MyItem(minLng: -115, minLat: -55, maxLng: -105, maxLat: -45), 109 | ]), 110 | ); 111 | 112 | expect( 113 | tree.search(RBushBox(minX: -180, minY: -90, maxX: 0, maxY: 90)), 114 | unorderedEquals([ 115 | MyItem(minLng: -115, minLat: 45, maxLng: -105, maxLat: 55), 116 | MyItem(minLng: -115, minLat: -55, maxLng: -105, maxLat: -45), 117 | ]), 118 | ); 119 | 120 | expect( 121 | tree.search(RBushBox(minX: 0, minY: -90, maxX: 180, maxY: 90)), 122 | unorderedEquals([ 123 | MyItem(minLng: 105, minLat: 45, maxLng: 115, maxLat: 55), 124 | MyItem(minLng: 105, minLat: -55, maxLng: 115, maxLat: -45), 125 | ]), 126 | ); 127 | 128 | expect( 129 | tree.search(RBushBox(minX: -180, minY: 0, maxX: 180, maxY: 90)), 130 | unorderedEquals([ 131 | MyItem(minLng: -115, minLat: 45, maxLng: -105, maxLat: 55), 132 | MyItem(minLng: 105, minLat: 45, maxLng: 115, maxLat: 55), 133 | ]), 134 | ); 135 | 136 | expect( 137 | tree.search(RBushBox(minX: -180, minY: -90, maxX: 180, maxY: 0)), 138 | unorderedEquals([ 139 | MyItem(minLng: 105, minLat: -55, maxLng: 115, maxLat: -45), 140 | MyItem(minLng: -115, minLat: -55, maxLng: -105, maxLat: -45), 141 | ]), 142 | ); 143 | }); 144 | 145 | test('RBushDirect returns custom data types', () { 146 | final tree = RBushDirect(4); 147 | 148 | final data = [ 149 | RBushElement.fromList([-115, 45, -105, 55], 1), 150 | RBushElement.fromList([105, 45, 115, 55], 2), 151 | RBushElement.fromList([105, -55, 115, -45], 3), 152 | ]; 153 | 154 | tree.load(data); 155 | tree.insert(RBushBox.fromList([-115, -55, -105, -45]), 4); 156 | 157 | expect(tree.all(), unorderedEquals([1, 2, 3, 4])); 158 | 159 | expect( 160 | tree.search(RBushBox(minX: -180, minY: -90, maxX: 180, maxY: 90)), 161 | unorderedEquals([1, 2, 3, 4]), 162 | ); 163 | 164 | expect( 165 | tree.search(RBushBox(minX: -180, minY: -90, maxX: 0, maxY: 90)), 166 | unorderedEquals([1, 4]), 167 | ); 168 | 169 | expect( 170 | tree.collides(RBushBox(minX: -180, minY: -90, maxX: 0, maxY: 90)), 171 | isTrue, 172 | ); 173 | 174 | tree.remove(3); 175 | expect(tree.all(), unorderedEquals([1, 2, 4])); 176 | 177 | expect(() => tree.insert(RBushBox.fromList([0, 0, 1, 1]), 1), throwsStateError, 178 | reason: 'no duplicates in RBushDirect'); 179 | 180 | tree.clear(); 181 | expect(tree.all(), isEmpty); 182 | }); 183 | 184 | test('#load bulk-loads the given data given max node entries and forms a proper search tree', () { 185 | final tree = RBush(4).load(data); 186 | expect(tree.all(), unorderedEquals(data)); 187 | }); 188 | 189 | test('#load uses standard insertion when given a low number of items', () { 190 | final tree = RBush(8).load(data).load(data.sublist(0, 3)); 191 | final tree2 = RBush(8).load(data); 192 | 193 | tree2.insert(data[0]); 194 | tree2.insert(data[1]); 195 | tree2.insert(data[2]); 196 | 197 | expect(tree.data, equals(tree2.data)); 198 | }); 199 | 200 | test('#load does nothing if loading empty data', () { 201 | final tree = RBush().load([]); 202 | expect(tree.data, equals(RBush().data)); 203 | }); 204 | 205 | test('#load handles the insertion of maxEntries + 2 empty bboxes', () { 206 | final tree = RBush(4).load(emptyData); 207 | expect(tree.data.height, equals(2)); 208 | expect(tree.all(), unorderedEquals(emptyData)); 209 | }); 210 | 211 | test('#insert handles the insertion of maxEntries + 2 empty bboxes', () { 212 | final tree = RBush(4); 213 | for (final element in emptyData) { tree.insert(element); } 214 | 215 | expect(tree.data.height, equals(2), reason: 'height'); 216 | expect(tree.all(), unorderedEquals(emptyData), reason: 'all'); 217 | expect(tree.data.children[0].childrenLength, equals(4), reason: 'children 0'); 218 | expect(tree.data.children[1].childrenLength, equals(2), reason: 'children 1'); 219 | }); 220 | 221 | test('#load properly splits tree root when merging trees of the same height', () { 222 | final tree = RBush(4).load(data).load(data); 223 | expect(tree.data.height, equals(4)); 224 | expect(tree.all(), unorderedEquals(data + data)); 225 | }); 226 | 227 | test('#load properly merges data of smaller or bigger tree heights', () { 228 | final smaller = someData(10); 229 | final tree1 = RBush(4).load(data).load(smaller); 230 | final tree2 = RBush(4).load(smaller).load(data); 231 | 232 | expect(tree1.data.height, equals(tree2.data.height)); 233 | expect(tree1.all(), unorderedEquals(data + smaller)); 234 | expect(tree2.all(), unorderedEquals(data + smaller)); 235 | }); 236 | 237 | test('#search finds matching points in the tree given a bbox', () { 238 | final tree = RBush(4).load(data); 239 | final result = tree.search(RBushBox(minX: 40, minY: 20, maxX: 80, maxY: 70)); 240 | 241 | expect(result, unorderedEquals(>[ 242 | [70,20,70,20],[75,25,75,25],[45,45,45,45],[50,50,50,50],[60,60,60,60],[70,70,70,70], 243 | [45,20,45,20],[45,70,45,70],[75,50,75,50],[50,25,50,25],[60,35,60,35],[70,45,70,45], 244 | ].map((list) => listToBBox(list)))); 245 | }); 246 | 247 | test('#collides returns true when search finds matching points', () { 248 | final tree = RBush(4).load(data); 249 | final result = tree.collides(RBushBox(minX: 40, minY: 20, maxX: 80, maxY: 70)); 250 | 251 | expect(result, isTrue); 252 | }); 253 | 254 | test('#search returns an empty array if nothing found', () { 255 | final result = RBush(4).load(data).search(RBushBox(minX: 200, minY: 200, maxX: 200, maxY: 200)); 256 | expect(result, equals([])); 257 | }); 258 | 259 | test('#collides returns false if nothing found', () { 260 | final result = RBush(4).load(data).collides(RBushBox(minX: 200, minY: 200, maxX: 200, maxY: 200)); 261 | expect(result, isFalse); 262 | }); 263 | 264 | test('#all returns all points in the tree', () { 265 | final tree = RBush(4).load(data); 266 | final result = tree.all(); 267 | 268 | expect(result, unorderedEquals(data)); 269 | expect(tree.search(RBushBox(minX: 0, minY: 0, maxX: 100, maxY: 100)), unorderedEquals(data)); 270 | }); 271 | 272 | test('#insert adds an item to an existing tree correctly', () { 273 | final items = >[ 274 | [0, 0, 0, 0], 275 | [1, 1, 1, 1], 276 | [2, 2, 2, 2], 277 | [3, 3, 3, 3], 278 | [1, 1, 2, 2], 279 | ].map((list) => listToBBox(list)).toList(); 280 | 281 | final tree = RBush(4).load(items.sublist(0, 3)); 282 | 283 | tree.insert(items[3]); 284 | expect(tree.data.height, equals(1), reason: 'height 3'); 285 | expect(tree.all(), unorderedEquals(items.sublist(0, 4)), reason: 'all 3'); 286 | 287 | tree.insert(items[4]); 288 | expect(tree.data.height, equals(2), reason: 'height 4'); 289 | expect(tree.all(), unorderedEquals(items), reason: 'all 4'); 290 | }); 291 | 292 | test('#insert forms a valid tree if items are inserted one by one', () { 293 | final tree = RBush(4); 294 | 295 | for (var i = 0; i < data.length; i++) { 296 | tree.insert(data[i]); 297 | } 298 | 299 | final tree2 = RBush(4).load(data); 300 | 301 | expect((tree.data.height - tree2.data.height).abs(), lessThanOrEqualTo(1)); 302 | expect(tree.all(), unorderedEquals(tree2.all())); 303 | }); 304 | 305 | test('#remove removes items correctly', () { 306 | final tree = RBush(4).load(data); 307 | final len = data.length; 308 | 309 | tree.remove(data[0]); 310 | tree.remove(data[1]); 311 | tree.remove(data[2]); 312 | 313 | tree.remove(data[len - 1]); 314 | tree.remove(data[len - 2]); 315 | tree.remove(data[len - 3]); 316 | 317 | expect(data.sublist(3, len - 3), unorderedEquals(tree.all())); 318 | }); 319 | 320 | test('#remove does nothing if nothing found', () { 321 | final removed = RBush().load(data); 322 | removed.remove(listToBBox([13, 13, 13, 13])); 323 | 324 | expect(RBush().load(data).data, equals(removed.data)); 325 | }); 326 | 327 | test('#remove brings the tree to a clear state when removing everything one by one', () { 328 | final tree = RBush(4).load(data); 329 | 330 | for (final item in data) tree.remove(item); 331 | 332 | expect(tree.data, equals(RBush(4).data)); 333 | }); 334 | 335 | test('#clear should clear all the data in the tree', () { 336 | final tree = RBush(4).load(data); 337 | tree.clear(); 338 | 339 | expect(tree.data, equals(RBush(4).data)); 340 | }); 341 | } 342 | -------------------------------------------------------------------------------- /lib/src/rbush.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Ilya Zverev, (c) 2020 Vladimir Agafonkin. 2 | // Port of https://github.com/mourner/rbush and https://github.com/mourner/rbush-knn. 3 | // Use of this code is governed by an ISC license, see the LICENSE file. 4 | import 'dart:math' show min, max, log, pow, sqrt; 5 | 6 | import 'quickselect.dart'; 7 | import 'tinyqueue.dart'; 8 | 9 | typedef MinXYGetter = double Function(T item); 10 | 11 | /// RBush — a high-performance R-tree-based 2D spatial index for points and rectangles. 12 | class RBushBase { 13 | final int _minEntries; 14 | final int _maxEntries; 15 | _RBushNode data; 16 | 17 | final RBushBox Function(T item) toBBox; 18 | final MinXYGetter getMinX; 19 | final MinXYGetter getMinY; 20 | 21 | /// Constructs a new r-tree with items of type [T]. 22 | /// Each leaf would have at most [maxEntries] items. 23 | /// 24 | /// Use [load] to bulk load items into this tree, or 25 | /// [insert] to append one by one. After that use [search] 26 | /// and [knn] for searching inside the tree. 27 | /// 28 | /// Specify [toBBox], [getMinX], and [getMinY] to extract 29 | /// needed information from objects of type [T]. Alternatively, 30 | /// see [RBush] class for a simpler option. 31 | RBushBase( 32 | {int maxEntries = 9, 33 | required this.toBBox, 34 | required this.getMinX, 35 | required this.getMinY}) 36 | : _maxEntries = max(4, maxEntries), 37 | _minEntries = max(2, (max(4, maxEntries) * 0.4).ceil()), 38 | data = _RBushNode([]); 39 | 40 | /// Returns all items inside this tree. 41 | List all() => _all(data, []); 42 | 43 | /// Looks for all items that intersect with [bbox]. 44 | List search(RBushBox bbox) { 45 | _RBushNode node = data; 46 | final List result = []; 47 | 48 | if (!bbox.intersects(node)) return result; 49 | 50 | final List<_RBushNode> nodesToSearch = []; 51 | 52 | while (true) { 53 | if (node.leaf) { 54 | for (final child in node.leafChildren) { 55 | if (bbox.intersects(toBBox(child))) { 56 | result.add(child); 57 | } 58 | } 59 | } else { 60 | for (final child in node.children) { 61 | if (bbox.intersects(child)) { 62 | if (bbox.contains(child)) { 63 | _all(child, result); 64 | } else { 65 | nodesToSearch.add(child); 66 | } 67 | } 68 | } 69 | } 70 | if (nodesToSearch.isEmpty) break; 71 | node = nodesToSearch.removeLast(); 72 | } 73 | 74 | return result; 75 | } 76 | 77 | /// Tests if any items intersect with [bbox]. 78 | /// Use [search] if you need the list. 79 | bool collides(RBushBox bbox) { 80 | _RBushNode node = data; 81 | 82 | if (!bbox.intersects(node)) return false; 83 | 84 | final List<_RBushNode> nodesToSearch = []; 85 | 86 | while (true) { 87 | if (node.leaf) { 88 | for (final child in node.leafChildren) { 89 | if (bbox.intersects(toBBox(child))) { 90 | return true; 91 | } 92 | } 93 | } else { 94 | for (final child in node.children) { 95 | if (bbox.intersects(child)) { 96 | if (bbox.contains(child)) return true; 97 | nodesToSearch.add(child); 98 | } 99 | } 100 | } 101 | if (nodesToSearch.isEmpty) break; 102 | node = nodesToSearch.removeLast(); 103 | } 104 | 105 | return false; 106 | } 107 | 108 | /// Bulk loads items into this r-tree. 109 | /// This method returns `this` to allow for this chaining: 110 | /// `RBushBase().load([...])` 111 | RBushBase load(Iterable items) { 112 | if (items.isEmpty) return this; 113 | 114 | if (items.length < _minEntries) { 115 | for (final item in items) insert(item); 116 | return this; 117 | } 118 | 119 | // recursively build the tree with the given data from scratch using OMT algorithm 120 | _RBushNode node = _build(List.of(items), 0, items.length - 1, 0); 121 | 122 | if (data.childrenLength == 0) { 123 | // save as is if tree is empty 124 | data = node; 125 | } else if (data.height == node.height) { 126 | // split root if trees have the same height 127 | _splitRoot(data, node); 128 | } else { 129 | if (data.height < node.height) { 130 | // swap trees if inserted one is bigger 131 | final tmpNode = data; 132 | data = node; 133 | node = tmpNode; 134 | } 135 | 136 | // insert the small tree into the large tree at appropriate level 137 | _insert(data.height - node.height - 1, inode: node); 138 | } 139 | 140 | return this; 141 | } 142 | 143 | /// Inserts a single item into the tree. 144 | insert(T item) { 145 | _insert(data.height - 1, item: item); 146 | } 147 | 148 | /// Removes all items from the tree. 149 | clear() { 150 | data = _RBushNode([]); 151 | } 152 | 153 | /// Removes a single item from the tree. 154 | /// Does nothing if the item is not there. 155 | remove(T? item) { 156 | if (item == null) return; 157 | 158 | _RBushNode? node = data; 159 | final bbox = toBBox(item); 160 | List<_RBushNode> path = []; 161 | List indexes = []; 162 | int i = 0; 163 | _RBushNode? parent; 164 | bool goingUp = false; 165 | 166 | // depth-first iterative tree traversal 167 | while (node != null || path.isNotEmpty) { 168 | if (node == null) { 169 | // go up 170 | node = path.removeLast(); 171 | parent = path.isEmpty ? null : path.last; 172 | i = indexes.removeLast(); 173 | goingUp = true; 174 | } 175 | 176 | if (node.leaf) { 177 | // check current node 178 | final index = node.leafChildren.indexOf(item); 179 | if (index != -1) { 180 | // item found, remove the item and condense tree upwards 181 | node.leafChildren.removeAt(index); 182 | path.add(node); 183 | _condense(path); 184 | return; 185 | } 186 | } 187 | 188 | if (!goingUp && !node.leaf && node.contains(bbox)) { 189 | // go down 190 | path.add(node); 191 | indexes.add(i); 192 | i = 0; 193 | parent = node; 194 | node = node.children.first; 195 | } else if (parent != null) { 196 | // go right 197 | i++; 198 | node = i >= parent.children.length ? null : parent.children[i]; 199 | goingUp = false; 200 | } else { 201 | // nothing found 202 | node = null; 203 | } 204 | } 205 | } 206 | 207 | /// K-nearest neighbors search. 208 | /// 209 | /// For a given ([x], [y]) location, returns [k] nearest items, 210 | /// sorted by distance to their bounding boxes. 211 | /// 212 | /// Use [maxDistance] to filter by distance as well. 213 | /// Use [predicate] function to filter by item properties. 214 | List knn(double x, double y, int k, 215 | {bool Function(T item)? predicate, double? maxDistance}) { 216 | final List result = []; 217 | if (k <= 0) return result; 218 | 219 | _RBushNode node = data; 220 | final queue = TinyQueue<_KnnElement>([]); 221 | 222 | while (true) { 223 | if (node.leaf) { 224 | for (final child in node.leafChildren) { 225 | final dist = toBBox(child).distanceSq(x, y); 226 | if (maxDistance == null || dist <= maxDistance * maxDistance) { 227 | queue.push(_KnnElement(item: child, dist: dist)); 228 | } 229 | } 230 | } else { 231 | for (final child in node.children) { 232 | final dist = child.distanceSq(x, y); 233 | if (maxDistance == null || dist <= maxDistance * maxDistance) { 234 | queue.push(_KnnElement(node: child, dist: dist)); 235 | } 236 | } 237 | } 238 | 239 | while (queue.isNotEmpty && queue.peek().item != null) { 240 | T candidate = queue.pop().item!; 241 | if (predicate == null || predicate(candidate)) { 242 | result.add(candidate); 243 | } 244 | if (result.length == k) return result; 245 | } 246 | 247 | if (queue.isEmpty) break; 248 | if (queue.peek().node == null) break; 249 | node = queue.pop().node!; 250 | } 251 | 252 | return result; 253 | } 254 | 255 | /// K-nearest neighbors search. 256 | /// 257 | /// Given distance() function, returns [k] nearest items, 258 | /// sorted by distance to items. 259 | /// 260 | /// Use [distance] to filter by distance. It gets an item and its 261 | /// bounding box for the input, with null item for non leaf node. 262 | /// [distance] returns distance to the item or bbox or null if 263 | /// distance is not within acceptable range. 264 | /// Use [predicate] function to filter by item properties. 265 | List knnGeneric(int k, { 266 | required double? Function(T? item, RBushBox bbox) distance, 267 | bool Function(T item, double dist)? predicate, 268 | }) { 269 | final List result = []; 270 | if (k <= 0) return result; 271 | 272 | _RBushNode node = data; 273 | final queue = TinyQueue<_KnnElement>([]); 274 | 275 | while (true) { 276 | if (node.leaf) { 277 | for (final child in node.leafChildren) { 278 | final dist = distance(child, toBBox(child)); 279 | if (dist != null) { 280 | queue.push(_KnnElement(item: child, dist: dist)); 281 | } 282 | } 283 | } else { 284 | for (final child in node.children) { 285 | final dist = distance(null, child); 286 | if (dist != null) { 287 | queue.push(_KnnElement(node: child, dist: dist)); 288 | } 289 | } 290 | } 291 | 292 | while (queue.isNotEmpty && queue.peek().item != null) { 293 | final elem = queue.pop(); 294 | // ignore: null_check_on_nullable_type_parameter 295 | final candidate = elem.item!; 296 | if (predicate == null || predicate(candidate, elem.dist)) { 297 | result.add(candidate); 298 | } 299 | if (result.length == k) return result; 300 | } 301 | 302 | if (queue.isEmpty) break; 303 | if (queue.peek().node == null) break; 304 | node = queue.pop().node!; 305 | } 306 | 307 | return result; 308 | } 309 | 310 | List _all(_RBushNode node, List result) { 311 | final List<_RBushNode> nodesToSearch = []; 312 | while (true) { 313 | if (node.leaf) { 314 | result.addAll(node.leafChildren); 315 | } else { 316 | nodesToSearch.addAll(node.children); 317 | } 318 | if (nodesToSearch.isEmpty) break; 319 | node = nodesToSearch.removeLast(); 320 | } 321 | return result; 322 | } 323 | 324 | _RBushNode _build(List items, int left, int right, int height) { 325 | final N = right - left + 1; 326 | var M = _maxEntries; 327 | _RBushNode node; 328 | 329 | if (N <= M) { 330 | // reached leaf level; return leaf 331 | node = _RBushNode([], items.sublist(left, right + 1)); 332 | _calcBBox(node); 333 | return node; 334 | } 335 | 336 | if (height == 0) { 337 | // target height of the bulk-loaded tree 338 | height = (log(N) / log(M)).ceil(); 339 | 340 | // target number of root entries to maximize storage utilization 341 | M = (N / pow(M, height - 1)).ceil(); 342 | } 343 | 344 | node = _RBushNode([]); 345 | node.leaf = false; 346 | node.height = height; 347 | 348 | // split the items into M mostly square tiles 349 | 350 | final N2 = (N.toDouble() / M).ceil(); 351 | final N1 = N2 * sqrt(M).ceil(); 352 | 353 | _multiSelect(items, left, right, N1, getMinX); 354 | 355 | for (int i = left; i <= right; i += N1) { 356 | final right2 = min(i + N1 - 1, right); 357 | 358 | _multiSelect(items, i, right2, N2, getMinY); 359 | 360 | for (int j = i; j <= right2; j += N2) { 361 | final right3 = min(j + N2 - 1, right2); 362 | 363 | // pack each entry recursively 364 | node.children.add(_build(items, j, right3, height - 1)); 365 | } 366 | } 367 | _calcBBox(node); 368 | return node; 369 | } 370 | 371 | _RBushNode _chooseSubtree( 372 | RBushBox bbox, _RBushNode node, int level, List<_RBushNode> path) { 373 | while (true) { 374 | path.add(node); 375 | 376 | if (node.leaf || path.length - 1 == level) break; 377 | 378 | var minArea = double.infinity; 379 | var minEnlargement = double.infinity; 380 | _RBushNode? targetNode; 381 | 382 | // no leaves here 383 | for (final child in node.children) { 384 | final area = child.area; 385 | final enlargement = bbox.enlargedArea(child) - area; 386 | 387 | // choose entry with the least area enlargement 388 | if (enlargement < minEnlargement) { 389 | minEnlargement = enlargement; 390 | minArea = area < minArea ? area : minArea; 391 | targetNode = child; 392 | } else if (enlargement == minEnlargement) { 393 | // otherwise choose one with the smallest area 394 | if (area < minArea) { 395 | minArea = area; 396 | targetNode = child; 397 | } 398 | } 399 | } 400 | 401 | node = targetNode ?? node.children.first; 402 | } 403 | 404 | return node; 405 | } 406 | 407 | _insert(int level, {T? item, _RBushNode? inode}) { 408 | RBushBox bbox = item != null ? toBBox(item) : inode!; 409 | final List<_RBushNode> insertPath = []; 410 | 411 | // find the best node for accommodating the item, saving all nodes along the path too 412 | final node = _chooseSubtree(bbox, data, level, insertPath); 413 | 414 | // put the item into the node 415 | if (item != null) { 416 | node.leafChildren.add(item); 417 | } else { 418 | node.children.add(inode!); 419 | } 420 | node.extend(bbox); 421 | 422 | // split on node overflow; propagate upwards if necessary 423 | while (level >= 0) { 424 | if (insertPath[level].childrenLength > _maxEntries) { 425 | _split(insertPath, level); 426 | level--; 427 | } else { 428 | break; 429 | } 430 | } 431 | 432 | // adjust bboxes along the insertion path 433 | _adjustParentBBoxes(bbox, insertPath, level); 434 | } 435 | 436 | /// split overflowed node into two 437 | _split(List<_RBushNode> insertPath, int level) { 438 | final node = insertPath[level]; 439 | final M = node.childrenLength; 440 | final m = _minEntries; 441 | 442 | _chooseSplitAxis(node, m, M); 443 | 444 | final splitIndex = _chooseSplitIndex(node, m, M); 445 | 446 | _RBushNode newNode; 447 | if (node.leaf) { 448 | newNode = _RBushNode([], node.leafChildren.sublist(splitIndex)); 449 | node.leafChildren.removeRange(splitIndex, node.leafChildren.length); 450 | } else { 451 | newNode = _RBushNode(node.children.sublist(splitIndex)); 452 | node.children.removeRange(splitIndex, node.children.length); 453 | } 454 | newNode.height = node.height; 455 | 456 | _calcBBox(node); 457 | _calcBBox(newNode); 458 | 459 | if (level > 0) { 460 | insertPath[level - 1].children.add(newNode); 461 | } else { 462 | _splitRoot(node, newNode); 463 | } 464 | } 465 | 466 | /// Split root node 467 | _splitRoot(_RBushNode node, _RBushNode newNode) { 468 | data = _RBushNode([node, newNode]); 469 | data.height = node.height + 1; 470 | _calcBBox(data); 471 | } 472 | 473 | int _chooseSplitIndex(_RBushNode node, m, M) { 474 | int? index; 475 | double minOverlap = double.infinity; 476 | double minArea = double.infinity; 477 | 478 | for (var i = m; i <= M - m; i++) { 479 | final bbox1 = _distBBox(node, 0, i); 480 | final bbox2 = _distBBox(node, i, M); 481 | 482 | final overlap = bbox1.intersectionArea(bbox2); 483 | final area = bbox1.area + bbox2.area; 484 | 485 | // choose distribution with minimum overlap 486 | if (overlap < minOverlap) { 487 | minOverlap = overlap; 488 | index = i; 489 | 490 | minArea = area < minArea ? area : minArea; 491 | } else if (overlap == minOverlap) { 492 | // otherwise choose distribution with minimum area 493 | if (area < minArea) { 494 | minArea = area; 495 | index = i; 496 | } 497 | } 498 | } 499 | 500 | return index ?? M - m; 501 | } 502 | 503 | _chooseSplitAxis(node, m, M) { 504 | final xMargin = _addDistMargin(node, m, M, true); 505 | final yMargin = _addDistMargin(node, m, M, false); 506 | 507 | // if total distributions margin value is minimal for x, sort by minX, 508 | // otherwise it's already sorted by minY 509 | if (xMargin < yMargin) _sortChildrenBy(node, true); 510 | } 511 | 512 | _sortChildrenBy(_RBushNode node, bool sortByMinX) { 513 | final getter = sortByMinX ? getMinX : getMinY; 514 | if (sortByMinX) { 515 | node.children.sort((a, b) => a.minX.compareTo(b.minX)); 516 | } else { 517 | node.children.sort((a, b) => a.minY.compareTo(b.minY)); 518 | } 519 | node.leafChildren.sort((a, b) => getter(a).compareTo(getter(b))); 520 | } 521 | 522 | double _addDistMargin(_RBushNode node, int m, int M, bool sortByMinX) { 523 | _sortChildrenBy(node, sortByMinX); 524 | 525 | final leftBBox = _distBBox(node, 0, m); 526 | final rightBBox = _distBBox(node, M - m, M); 527 | var margin = leftBBox.margin + rightBBox.margin; 528 | 529 | for (var i = m; i < M - m; i++) { 530 | leftBBox 531 | .extend(node.leaf ? toBBox(node.leafChildren[i]) : node.children[i]); 532 | margin += leftBBox.margin; 533 | } 534 | 535 | for (var i = M - m - 1; i >= m; i--) { 536 | rightBBox 537 | .extend(node.leaf ? toBBox(node.leafChildren[i]) : node.children[i]); 538 | margin += rightBBox.margin; 539 | } 540 | 541 | return margin; 542 | } 543 | 544 | /// adjust bboxes along the given tree path 545 | _adjustParentBBoxes(RBushBox bbox, List path, int level) { 546 | for (var i = level; i >= 0; i--) { 547 | path[i].extend(bbox); 548 | } 549 | } 550 | 551 | // go through the path, removing empty nodes and updating bboxes 552 | _condense(List<_RBushNode> path) { 553 | for (var i = path.length - 1; i >= 0; i--) { 554 | if (path[i].childrenLength == 0) { 555 | if (i > 0) { 556 | if (path[i - 1].leaf) { 557 | path[i - 1].leafChildren.remove(path[i]); 558 | } else { 559 | path[i - 1].children.remove(path[i]); 560 | } 561 | } else { 562 | clear(); 563 | } 564 | } else { 565 | _calcBBox(path[i]); 566 | } 567 | } 568 | } 569 | 570 | _calcBBox(_RBushNode node) { 571 | _distBBox(node, 0, 572 | node.leaf ? node.leafChildren.length : node.children.length, node); 573 | } 574 | 575 | _RBushNode _distBBox(_RBushNode node, int k, int p, 576 | [_RBushNode? destNode]) { 577 | destNode ??= _RBushNode([]); 578 | destNode.minX = double.infinity; 579 | destNode.minY = double.infinity; 580 | destNode.maxX = double.negativeInfinity; 581 | destNode.maxY = double.negativeInfinity; 582 | 583 | for (int i = k; i < p; i++) { 584 | if (node.leaf) { 585 | destNode.extend(toBBox(node.leafChildren[i])); 586 | } else { 587 | destNode.extend(node.children[i]); 588 | } 589 | } 590 | return destNode; 591 | } 592 | 593 | _multiSelect(List arr, int left, int right, int n, MinXYGetter getter) { 594 | final stack = [left, right]; 595 | final compare = (T a, T b) => getter(a).compareTo(getter(b)); 596 | while (stack.isNotEmpty) { 597 | right = stack.removeLast(); 598 | left = stack.removeLast(); 599 | if (right - left <= n) continue; 600 | final mid = left + ((right - left).toDouble() / n / 2).ceil() * n; 601 | quickSelect(arr, mid, left, right, compare); 602 | stack.addAll([left, mid, mid, right]); 603 | } 604 | } 605 | } 606 | 607 | /// Bounding box for an r-tree item. 608 | /// 609 | /// If your item class extends this one, writing `toBBox` function 610 | /// would be easier. Also it's got some useful methods like [contains] 611 | /// and [area]. 612 | class RBushBox { 613 | double minX; 614 | double minY; 615 | double maxX; 616 | double maxY; 617 | 618 | RBushBox({ 619 | this.minX = double.infinity, 620 | this.minY = double.infinity, 621 | this.maxX = double.negativeInfinity, 622 | this.maxY = double.negativeInfinity, 623 | }); 624 | 625 | RBushBox.fromList(List bbox) 626 | : minX = bbox[0].toDouble(), 627 | minY = bbox[1].toDouble(), 628 | maxX = bbox[2].toDouble(), 629 | maxY = bbox[3].toDouble(); 630 | 631 | /// Extends this box's bounds to cover [b]. 632 | extend(RBushBox b) { 633 | minX = min(minX, b.minX); 634 | minY = min(minY, b.minY); 635 | maxX = max(maxX, b.maxX); 636 | maxY = max(maxY, b.maxY); 637 | } 638 | 639 | /// Calculates area: `dx * dy`. 640 | double get area => (maxX - minX) * (maxY - minY); 641 | 642 | /// Calculates the box's half-perimeter: `dx + dy`. 643 | double get margin => (maxX - minX) + (maxY - minY); 644 | 645 | /// Calculates area for an extendes bounding box that 646 | /// would cover both this one and [b]. See [extend] for 647 | /// the extension method. 648 | double enlargedArea(RBushBox b) { 649 | return (max(b.maxX, maxX) - min(b.minX, minX)) * 650 | (max(b.maxY, maxY) - min(b.minY, minY)); 651 | } 652 | 653 | /// Calculates area for an intersection box of this 654 | /// and [b]. 655 | double intersectionArea(RBushBox b) { 656 | final minX = max(this.minX, b.minX); 657 | final minY = max(this.minY, b.minY); 658 | final maxX = min(this.maxX, b.maxX); 659 | final maxY = min(this.maxY, b.maxY); 660 | return max(0, maxX - minX) * max(0, maxY - minY); 661 | } 662 | 663 | bool contains(RBushBox b) { 664 | return minX <= b.minX && minY <= b.minY && b.maxX <= maxX && b.maxY <= maxY; 665 | } 666 | 667 | bool intersects(RBushBox b) { 668 | return b.minX <= maxX && b.minY <= maxY && b.maxX >= minX && b.maxY >= minY; 669 | } 670 | 671 | /// Calculates squared distance from ([x], [y]) to this box. 672 | double distanceSq(double x, double y) { 673 | final dx = _axisDist(x, minX, maxX); 674 | final dy = _axisDist(y, minY, maxY); 675 | return dx * dx + dy * dy; 676 | } 677 | 678 | double _axisDist(double k, double min, double max) { 679 | return k < min 680 | ? min - k 681 | : k <= max 682 | ? 0 683 | : k - max; 684 | } 685 | 686 | @override 687 | bool operator ==(Object other) { 688 | if (other is! RBushBox) return false; 689 | return minX == other.minX && 690 | minY == other.minY && 691 | maxX == other.maxX && 692 | maxY == other.maxY; 693 | } 694 | 695 | @override 696 | int get hashCode => 697 | minX.hashCode + maxX.hashCode + minY.hashCode + maxY.hashCode; 698 | 699 | @override 700 | String toString() => '{$minX, $minY, $maxX, $maxY}'; 701 | } 702 | 703 | /// Internal class for nodes inside the r-tree. 704 | class _RBushNode extends RBushBox { 705 | final List<_RBushNode> children; 706 | final List leafChildren; 707 | int height = 1; 708 | bool leaf; 709 | 710 | _RBushNode(this.children, [List? leafChildren]) 711 | : leaf = children.isEmpty, 712 | leafChildren = leafChildren ?? []; 713 | 714 | int get childrenLength => leaf ? leafChildren.length : children.length; 715 | 716 | bool _listsEqual(List a, List b) { 717 | if (a == b) return true; 718 | if (a.length != b.length) return false; 719 | for (var i = 0; i < a.length; i++) { 720 | if (a[i] != b[i]) return false; 721 | } 722 | return true; 723 | } 724 | 725 | @override 726 | bool operator ==(Object other) { 727 | if (other is! _RBushNode) return false; 728 | return height == other.height && 729 | leaf == other.leaf && 730 | _listsEqual(children, other.children) && 731 | _listsEqual(leafChildren, other.leafChildren); 732 | } 733 | } 734 | 735 | /// Internal class for a queue elements for the `RBushBase.knn` method. 736 | class _KnnElement implements Comparable<_KnnElement> { 737 | _RBushNode? node; 738 | T? item; 739 | double dist; 740 | 741 | _KnnElement({this.node, this.item, required this.dist}) { 742 | if (node == null && item == null) { 743 | throw ArgumentError('Either node or item should be not null'); 744 | } 745 | } 746 | 747 | @override 748 | int compareTo(other) => dist.compareTo(other.dist); 749 | } 750 | 751 | /// An r-tree for [RBushElement]: a convenience class 752 | /// that does not make you write accessor functions. 753 | class RBush extends RBushBase> { 754 | RBush([int maxEntries = 9]) 755 | : super( 756 | maxEntries: maxEntries, 757 | toBBox: (item) => item, 758 | getMinX: (item) => item.minX, 759 | getMinY: (item) => item.minY, 760 | ); 761 | } 762 | 763 | /// A convenient r-tree for working directly with data objects, uncoupling 764 | /// these from bounding boxes. Encapsulates [RBush], so for bulk inserts 765 | /// this class still needs a list of [RBushElement]s. 766 | class RBushDirect { 767 | final RBush _tree; 768 | final Map> _boxes = {}; 769 | 770 | RBushDirect([int maxEntries = 9]) : _tree = RBush(maxEntries); 771 | 772 | List all() => _tree.all().map((e) => e.data).toList(); 773 | List search(RBushBox bbox) => 774 | _tree.search(bbox).map((e) => e.data).toList(); 775 | bool collides(RBushBox bbox) => _tree.collides(bbox); 776 | clear() => _tree.clear(); 777 | remove(T? item) => _tree.remove(_boxes[item]); 778 | 779 | List knn(double x, double y, int k, 780 | {bool Function(T item)? predicate, double? maxDistance}) => 781 | _tree 782 | .knn(x, y, k, 783 | predicate: (e) => predicate == null || predicate(e.data), 784 | maxDistance: maxDistance) 785 | .map((e) => e.data) 786 | .toList(); 787 | 788 | RBushDirect load(Iterable> items) { 789 | if (items.any((item) => _boxes.containsKey(item))) { 790 | throw StateError( 791 | 'Cannot have duplicates in the tree, use RBush class for that.'); 792 | } 793 | _tree.load(items); 794 | _boxes.addAll({for (final i in items) i.data: i}); 795 | return this; 796 | } 797 | 798 | insert(RBushBox bbox, T item) { 799 | if (_boxes.containsKey(item)) { 800 | throw StateError( 801 | 'Cannot have duplicate $item in the tree, use RBush class for that.'); 802 | } 803 | final element = RBushElement( 804 | minX: bbox.minX, 805 | minY: bbox.minY, 806 | maxX: bbox.maxX, 807 | maxY: bbox.maxY, 808 | data: item); 809 | _tree.insert(element); 810 | _boxes[item] = element; 811 | } 812 | } 813 | 814 | /// A container for your data, to be used with [RBush]. 815 | class RBushElement extends RBushBox { 816 | final T data; 817 | 818 | RBushElement({ 819 | required double minX, 820 | required double minY, 821 | required double maxX, 822 | required double maxY, 823 | required this.data, 824 | }) : super(minX: minX, maxX: maxX, minY: minY, maxY: maxY); 825 | 826 | RBushElement.fromList(List bbox, this.data) : super.fromList(bbox); 827 | } 828 | --------------------------------------------------------------------------------