diff --git a/.gitignore b/.gitignore index 29c6e6e..bfdf2c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.DS_Store .idea/ *.iml *.iws @@ -12,4 +13,4 @@ build .dart_tool # coverage -/coverage/ \ No newline at end of file +/coverage/ diff --git a/analysis_options.yaml b/analysis_options.yaml index df4a168..9b0ab1a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,5 @@ -linter: - rules: - - cancel_subscriptions - - close_sinks \ No newline at end of file +include: package:workiva_analysis_options/v2.recommended.yaml +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: true diff --git a/benchmark/benchmarks.dart b/benchmark/benchmarks.dart index e6fcb10..c9ca846 100644 --- a/benchmark/benchmarks.dart +++ b/benchmark/benchmarks.dart @@ -4,11 +4,11 @@ import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:r_tree/r_tree.dart'; -final int branchFactor = 16; -final int randomSeed = 3; -main() { +const int branchFactor = 16; +const int randomSeed = 3; +void main() { print('Running benchmarks...'); - var collector = ScoreCollector(); + final collector = ScoreCollector(); InsertBenchmark(collector, totalItems: 100).report(); InsertBenchmark(collector, totalItems: 1000).report(); InsertBenchmark(collector, totalItems: 10000).report(); @@ -27,19 +27,19 @@ main() { SearchBenchmark(collector, totalItems: 1000, iterateAll: true, useLoad: true).report(); SearchBenchmark(collector, totalItems: 10000, iterateAll: true, useLoad: true).report(); - var longestName = + final longestName = collector.collected.keys.reduce((value, element) => value.length > element.length ? value : element).length; - var longestValue = collector.collected.values + final longestValue = collector.collected.values .reduce((value, element) => value.toStringAsFixed(2).length > element.toStringAsFixed(2).length ? value : element) .toStringAsFixed(2); - var nameHeading = 'Name'; - var heading = '$nameHeading${' ' * (longestName - nameHeading.length)}\tResult (microseconds)'; - var separator = '-' * (heading.length + 5); + const nameHeading = 'Name'; + final heading = '$nameHeading${' ' * (longestName - nameHeading.length)}\tResult (microseconds)'; + final separator = '-' * (heading.length + 5); var output = '\n$heading\n$separator\n'; - collector.collected.forEach((String name, double value) { - name += (' ' * (longestName - name.length)); - var valueString = value.toStringAsFixed(2); - output += '$name\t${' ' * (longestValue.length - valueString.length)}${valueString}\n'; + collector.collected.forEach((name, value) { + name += ' ' * (longestName - name.length); + final valueString = value.toStringAsFixed(2); + output += '$name\t${' ' * (longestValue.length - valueString.length)}$valueString\n'; }); print(output); @@ -48,95 +48,104 @@ main() { class InsertBenchmark extends RTreeBenchmarkBase { final int totalItems; - InsertBenchmark(ScoreCollector collector, {this.totalItems = 500}) : super("Insert $totalItems", collector); + InsertBenchmark(ScoreCollector collector, {this.totalItems = 500}) : super('Insert $totalItems', collector); late RTree tree; late List> datum; + @override void run() { tree = RTree(branchFactor); - for (var data in datum) { - tree.insert(data); + for (final data in datum) { + tree.add([data]); } } + @override void setup() { - Random rand = Random(randomSeed); + final rand = Random(randomSeed); datum = >[]; - for (int i = 0; i < totalItems; i++) { - int x = rand.nextInt(1000); - int y = rand.nextInt(1000); - int height = rand.nextInt(100); - int width = rand.nextInt(100); + for (var i = 0; i < totalItems; i++) { + final x = rand.nextInt(1000); + final y = rand.nextInt(1000); + final height = rand.nextInt(100); + final width = rand.nextInt(100); final item = RTreeDatum(Rectangle(x, y, width, height), 'item $i'); datum.add(item); } } + @override void teardown() {} } class LoadBenchmark extends RTreeBenchmarkBase { final int totalItems; - LoadBenchmark(ScoreCollector collector, {required this.totalItems}) : super("Load $totalItems ", collector); + LoadBenchmark(ScoreCollector collector, {required this.totalItems}) : super('Load $totalItems ', collector); late RTree tree; late List> datum; + @override void run() { tree = RTree(branchFactor); - tree.load(datum); + tree.add(datum); } + @override void setup() { - Random rand = Random(randomSeed); + final rand = Random(randomSeed); datum = >[]; - for (int i = 0; i < totalItems; i++) { - int x = rand.nextInt(1000); - int y = rand.nextInt(1000); - int height = rand.nextInt(100); - int width = rand.nextInt(100); + for (var i = 0; i < totalItems; i++) { + final x = rand.nextInt(1000); + final y = rand.nextInt(1000); + final height = rand.nextInt(100); + final width = rand.nextInt(100); final item = RTreeDatum(Rectangle(x, y, width, height), 'item $i'); datum.add(item); } datum.shuffle(); } + @override void teardown() {} } class RemoveBenchmark extends RTreeBenchmarkBase { - RemoveBenchmark(ScoreCollector collector) : super("Remove 5k", collector); + RemoveBenchmark(ScoreCollector collector) : super('Remove 5k', collector); late RTree tree; final items = >>[]; + @override void run() { - for (int i = 0; i < 100; i++) { - for (int j = 0; j < 50; j++) { + for (var i = 0; i < 100; i++) { + for (var j = 0; j < 50; j++) { tree.remove(items[i][j]); } } } + @override void setup() { tree = RTree(branchFactor); - for (int i = 0; i < 100; i++) { - for (int j = 0; j < 100; j++) { + for (var i = 0; i < 100; i++) { + for (var j = 0; j < 100; j++) { if (items.length <= i) { items.add([]); } - Rectangle rect = Rectangle(i, j, 1, 1); + final rect = Rectangle(i, j, 1, 1); final datum = RTreeDatum(rect, 'item $i:$j'); items[i].add(datum); - tree.insert(datum); + tree.add([datum]); } } } + @override void teardown() {} } @@ -154,19 +163,20 @@ class SearchBenchmark extends RTreeBenchmarkBase { required this.totalItems, this.iterateAll = false, this.useLoad = false, - }) : super("Search${iterateAll ? '/Iterate' : ''} ${useLoad ? '(using Load)' : '(using Insert)'} ${totalItems}", + }) : super("Search${iterateAll ? '/Iterate' : ''} ${useLoad ? '(using Load)' : '(using Insert)'} $totalItems", collector); late RTree tree; late int size; + @override void run() { - for (int x = 0; x < size; x++) { - for (int y = 0; y < size; y++) { - var results = tree.search(Rectangle(x, y, 1, 1)); + for (var x = 0; x < size; x++) { + for (var y = 0; y < size; y++) { + final results = tree.search(Rectangle(x, y, 1, 1)); if (iterateAll) { // ignore: unused_local_variable - for (var result in results) { + for (final result in results) { // nothing to do here, just iterating over every result once } } @@ -174,14 +184,15 @@ class SearchBenchmark extends RTreeBenchmarkBase { } } + @override void setup() { size = sqrt(totalItems).ceil(); tree = RTree(branchFactor); - var datum = >[]; - for (int i = 0; i < 10; i++) { - for (int j = 0; j < 50; j++) { - Rectangle rect = Rectangle(i, j, 1, 1); + final datum = >[]; + for (var i = 0; i < 10; i++) { + for (var j = 0; j < 50; j++) { + final rect = Rectangle(i, j, 1, 1); datum.add(RTreeDatum(rect, 'item1')); datum.add(RTreeDatum(rect, 'item2')); datum.add(RTreeDatum(rect, 'item3')); @@ -196,12 +207,15 @@ class SearchBenchmark extends RTreeBenchmarkBase { } if (useLoad) { - tree.load(datum); + tree.add(datum); } else { - datum.forEach(tree.insert); + for (final item in datum) { + tree.add([item]); + } } } + @override void teardown() {} } @@ -212,7 +226,7 @@ class RTreeBenchmarkBase extends BenchmarkBase { @override void exercise() { - for (int i = 0; i < iterations; i++) { + for (var i = 0; i < iterations; i++) { run(); } } diff --git a/benchmark/web_benchmarks.dart b/benchmark/web_benchmarks.dart index 07f7f6a..9443a94 100644 --- a/benchmark/web_benchmarks.dart +++ b/benchmark/web_benchmarks.dart @@ -2,7 +2,7 @@ import 'dart:html'; import 'benchmarks.dart' as benchmarks; -main() { +void main() { final button = querySelector('#runButton')! as ButtonElement; button.onClick.listen((_) async { button.disabled = true; diff --git a/example/main.dart b/example/main.dart index 3ef003a..9ab20a1 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,19 +1,23 @@ import 'dart:async'; -import 'dart:html'; +import 'dart:html' hide Node; import 'dart:math'; import 'package:r_tree/r_tree.dart'; +import 'package:r_tree/src/r_tree/leaf_node.dart'; +import 'package:r_tree/src/r_tree/node.dart'; +import 'package:r_tree/src/r_tree/non_leaf_node.dart'; +import 'package:r_tree/src/r_tree/r_tree.dart'; Future main() async { var rtree = RTree(); - var app = querySelector('#app')!; - var canvas = CanvasElement(width: 640, height: 480); + final app = querySelector('#app')!; + final canvas = CanvasElement(width: 640, height: 480); app.append(canvas); canvas.context2D ..fillStyle = '#ccc' ..fillRect(0, 0, 640, 480); int? startX, startY, proposedX, proposedY; - final draw = () { + void draw() { canvas.context2D.clearRect(0, 0, 700, 500); canvas.context2D.strokeStyle = ''; rtree.search(Rectangle(0, 0, 700, 500)).forEach((node) { @@ -26,12 +30,12 @@ Future main() async { canvas.context2D.strokeStyle = 'black'; canvas.context2D.strokeRect(startX!, startY!, proposedX! - startX!, proposedY! - startY!); } - }; + } var isDrawing = false; - canvas.onMouseDown.listen((MouseEvent event) { - var target = event.currentTarget as HtmlElement; - var boundingRect = target.getBoundingClientRect(); + canvas.onMouseDown.listen((event) { + final target = event.currentTarget! as HtmlElement; + final boundingRect = target.getBoundingClientRect(); isDrawing = true; proposedX = null; proposedY = null; @@ -40,29 +44,29 @@ Future main() async { startY = ((event.client.y - boundingRect.top) + target.scrollTop).floor(); }); - canvas.onMouseMove.listen((MouseEvent event) { + canvas.onMouseMove.listen((event) { if (!isDrawing || startX == null || startY == null) return; - var target = event.currentTarget as HtmlElement; - var boundingRect = target.getBoundingClientRect(); + final target = event.currentTarget! as HtmlElement; + final boundingRect = target.getBoundingClientRect(); proposedX = ((event.client.x - boundingRect.left) + target.scrollLeft).floor(); proposedY = ((event.client.y - boundingRect.top) + target.scrollTop).floor(); draw(); }); - canvas.onMouseUp.listen((MouseEvent event) { + canvas.onMouseUp.listen((event) { isDrawing = false; if (startX == null || startY == null) return; - var target = event.currentTarget as HtmlElement; - var boundingRect = target.getBoundingClientRect(); - var endX = ((event.client.x - boundingRect.left) + target.scrollLeft).floor(); - var endY = ((event.client.y - boundingRect.top) + target.scrollTop).floor(); + final target = event.currentTarget! as HtmlElement; + final boundingRect = target.getBoundingClientRect(); + final endX = ((event.client.x - boundingRect.left) + target.scrollLeft).floor(); + final endY = ((event.client.y - boundingRect.top) + target.scrollTop).floor(); - var rectangle = Rectangle.fromPoints(Point(startX!, startY!), Point(endX, endY)); + final rectangle = Rectangle.fromPoints(Point(startX!, startY!), Point(endX, endY)); if (currentBrush == 'search') { - var resultList = querySelector('#results')!; + final resultList = querySelector('#results')!; resultList.children = []; for (final match in rtree.search(rectangle)) { var color = ''; @@ -85,7 +89,7 @@ Future main() async { resultList.append(LIElement()..innerHtml = 'No results in $rectangle'); } } else { - rtree.insert(RTreeDatum(rectangle, currentBrush)); + rtree.add([RTreeDatum(rectangle, currentBrush)]); } draw(); @@ -96,22 +100,25 @@ Future main() async { final blueButton = querySelector('#blue')!; final searchButton = querySelector('#search')!; final allButtons = [redButton, greenButton, blueButton, searchButton]; - final resetAllButtons = () => allButtons.forEach((element) { - element.style.background = ''; - }); + void resetAllButtons() { + for (final element in allButtons) { + element.style.background = ''; + } + } + redButton.onClick.listen((_) { resetAllButtons(); - currentBrush = '$red'; + currentBrush = red; redButton.style.background = 'darkgray'; }); greenButton.onClick.listen((_) { resetAllButtons(); - currentBrush = '$green'; + currentBrush = green; greenButton.style.background = 'darkgray'; }); blueButton.onClick.listen((_) { resetAllButtons(); - currentBrush = '$blue'; + currentBrush = blue; blueButton.style.background = 'darkgray'; }); searchButton.onClick.listen((_) { @@ -120,28 +127,28 @@ Future main() async { searchButton.style.background = 'darkgray'; }); - final makeDataset = () { - Random rand = Random(); - var datum = >[]; - for (int i = 0; i < 300; i++) { - int startX = rand.nextInt((canvas.width! / 2).floor()); - int endX = rand.nextInt((canvas.width! / 2).floor()) * 2; - int startY = rand.nextInt((canvas.height! / 2).floor()); - int endY = rand.nextInt((canvas.width! / 2).floor()) * 2; - int color = rand.nextInt(2); - var item = RTreeDatum(Rectangle.fromPoints(Point(startX, startY), Point(endX, endY)), colors[color]); + List> makeDataset() { + final rand = Random(); + final datum = >[]; + for (var i = 0; i < 300; i++) { + final startX = rand.nextInt((canvas.width! / 2).floor()); + final endX = rand.nextInt((canvas.width! / 2).floor()) * 2; + final startY = rand.nextInt((canvas.height! / 2).floor()); + final endY = rand.nextInt((canvas.width! / 2).floor()) * 2; + final color = rand.nextInt(2); + final item = RTreeDatum(Rectangle.fromPoints(Point(startX, startY), Point(endX, endY)), colors[color]); datum.add(item); } return datum; - }; + } querySelector('#insert')!.onClick.listen((_) { - makeDataset().forEach(rtree.insert); + makeDataset().forEach((item) => rtree.add([item])); draw(); }); querySelector('#load')!.onClick.listen((_) { - rtree.load(makeDataset()); + rtree.add(makeDataset()); draw(); }); @@ -151,14 +158,14 @@ Future main() async { }); querySelector('#graphviz')!.onClick.listen((_) { - var output = querySelector('#output') as PreElement; + final output = querySelector('#output')! as PreElement; - output.innerHtml = toGraphViz(rtree.currentRootNode); + output.innerHtml = toGraphViz(getCurrentRootNode(rtree)); }); querySelector('#copy')!.onClick.listen((_) async { try { - await window.navigator.clipboard?.writeText((querySelector('#output') as PreElement).innerText); + await window.navigator.clipboard?.writeText((querySelector('#output')! as PreElement).innerText); querySelector('#copy')!.style.background = 'green'; await Future.delayed(Duration(milliseconds: 350)); querySelector('#copy')!.style.background = ''; @@ -173,10 +180,10 @@ const String red = '#ff0000$alpha'; const String green = '#00ff00$alpha'; const String blue = '#0000ff$alpha'; const colors = [red, green, blue]; -String currentBrush = '$red'; +String currentBrush = red; String toGraphViz(Node root) { - var output = StringBuffer('''digraph r_tree { + final output = StringBuffer('''digraph r_tree { root [ color="gray" label="root" @@ -191,9 +198,9 @@ String toGraphViz(Node root) { void _graphVizRecurse(Node node, String parent, String identifierPrefix, StringBuffer buffer) { for (var i = 0; i < node.children.length; i++) { - var child = node.children[i]; + final child = node.children[i]; if (child is LeafNode) { - var id = "${identifierPrefix}LeafNode$i"; + final id = '${identifierPrefix}LeafNode$i'; buffer.write(''' $id [ color="green" @@ -202,8 +209,8 @@ void _graphVizRecurse(Node node, String parent, String identifierPrefix, StringB $parent -> $id '''); for (var j = 0; j < child.children.length; j++) { - var leafChild = child.children[j]; - var childId = "${id}LeafChild$j"; + final leafChild = child.children[j]; + final childId = '${id}LeafChild$j'; buffer.write(''' "$childId" [ color="orange" @@ -213,7 +220,7 @@ $id -> "$childId" '''); } } else if (child is NonLeafNode) { - var id = "${identifierPrefix}ChildNode$i"; + final id = '${identifierPrefix}ChildNode$i'; buffer.write(''' $id [ color="brown" @@ -223,7 +230,7 @@ $id -> "$childId" '''); _graphVizRecurse(child, id, id, buffer); } else if (child is RTreeDatum) { - var id = "${identifierPrefix}Datum$i"; + final id = '${identifierPrefix}Datum$i'; buffer.write(''' "$id" [ color="orange" diff --git a/lib/r_tree.dart b/lib/r_tree.dart index 5e17cdf..d4a63db 100644 --- a/lib/r_tree.dart +++ b/lib/r_tree.dart @@ -26,12 +26,6 @@ /// rectangles or polygons." - http://en.wikipedia.org/wiki/R-tree library r_tree; -import 'dart:math'; - -part 'src/r_tree/leaf_node.dart'; -part 'src/r_tree/non_leaf_node.dart'; -part 'src/r_tree/node.dart'; -part 'src/r_tree/quickselect.dart'; -part 'src/r_tree/r_tree.dart'; -part 'src/r_tree/r_tree_datum.dart'; -part 'src/r_tree/r_tree_contributor.dart'; +export 'src/r_tree/r_tree.dart' show RTree; +export 'src/r_tree/r_tree_contributor.dart' show RTreeContributor; +export 'src/r_tree/r_tree_datum.dart' show RTreeDatum; diff --git a/lib/src/r_tree/leaf_node.dart b/lib/src/r_tree/leaf_node.dart index 5104f34..07e0d5c 100644 --- a/lib/src/r_tree/leaf_node.dart +++ b/lib/src/r_tree/leaf_node.dart @@ -14,13 +14,17 @@ * limitations under the License. */ -part of r_tree; +import 'dart:math'; + +import 'package:r_tree/src/r_tree/node.dart'; +import 'package:r_tree/src/r_tree/r_tree_datum.dart'; +import 'package:r_tree/src/r_tree/rectangle_helper.dart'; /// A [Node] that is a leaf node of the tree. These are created automatically -/// by [RTree] when inserting/removing items from the tree. -@Deprecated('For internal use only, removed in next major release') +/// when inserting/removing items from the tree. class LeafNode extends Node { final List> _items = []; + @override List> get children => _items; LeafNode(int branchFactor, {List> initialItems = const []}) : super(branchFactor) { @@ -35,26 +39,32 @@ class LeafNode extends Node { @override int get height => 1; + @override Node createNewNode() { return LeafNode(branchFactor); } - Iterable> search(Rectangle searchRect, bool Function(E item)? shouldInclude) { - return _items.where( - (RTreeDatum item) => item.rect.overlaps(searchRect) && (shouldInclude == null || shouldInclude(item.value))); + @override + List> search(Rectangle searchRect, bool Function(E item)? shouldInclude) { + return _items + .where((item) => item.rect.overlaps(searchRect) && (shouldInclude == null || shouldInclude(item.value))) + .toList(); } + @override Node? insert(RTreeDatum item) { addChild(item); return splitIfNecessary(); } - remove(RTreeDatum item) { + @override + void remove(RTreeDatum item) { removeChild(item); } - clearChildren() { + @override + void clearChildren() { + super.clearChildren(); _items.clear(); - _minimumBoundingRect = noMBR; } } diff --git a/lib/src/r_tree/node.dart b/lib/src/r_tree/node.dart index cadc584..9ee30db 100644 --- a/lib/src/r_tree/node.dart +++ b/lib/src/r_tree/node.dart @@ -14,14 +14,16 @@ * limitations under the License. */ -part of r_tree; +import 'dart:math'; + +import 'package:r_tree/src/r_tree/r_tree_contributor.dart'; +import 'package:r_tree/src/r_tree/r_tree_datum.dart'; +import 'package:r_tree/src/r_tree/rectangle_helper.dart'; -@Deprecated('For internal use only, removed in next major release') const noMBR = Rectangle(0, 0, 0, 0); -/// A [Node] is an entry in the [RTree] for a particular rectangle. This is an -/// abstract class, see [LeafNode] and [NonLeafNode] for more information. -@Deprecated('For internal use only, removed in next major release') +/// A [Node] is an entry in a tree for a particular rectangle. This is an +/// abstract class, see LeafNode and NonLeafNode for more information. abstract class Node implements RTreeContributor { /// The branch factor this node is configured with, which determines when the node should split final int branchFactor; @@ -35,21 +37,24 @@ abstract class Node implements RTreeContributor { Rectangle _minimumBoundingRect = noMBR; /// Returns the rectangle this Node covers + @override Rectangle get rect => _minimumBoundingRect; Node(this.branchFactor); /// Returns an iterable of all items within [searchRect] - Iterable> search(Rectangle searchRect, bool Function(E item)? shouldInclude); + List> search(Rectangle searchRect, bool Function(E item)? shouldInclude); /// Inserts [item] into the node. If the insertion causes a split to occur, the split node will be returned, otherwise null is returned. Node? insert(RTreeDatum item); /// Removes [item] from this node - remove(RTreeDatum item); + void remove(RTreeDatum item); /// Remove all children from this node - clearChildren(); + void clearChildren() { + _minimumBoundingRect = noMBR; + } /// Returns a list of all items in this node List get children; @@ -61,13 +66,13 @@ abstract class Node implements RTreeContributor { int get size => children.length; /// Adds [child] to this node - addChild(covariant RTreeContributor child) { + void addChild(covariant RTreeContributor child) { include(child); children.add(child); } /// Removes [child] from this node - removeChild(covariant RTreeContributor child) { + void removeChild(covariant RTreeContributor child) { children.remove(child); updateBoundingRect(); } @@ -76,35 +81,32 @@ abstract class Node implements RTreeContributor { /// of adding a new @item to this Node num expansionCost(RTreeContributor item) { if (_minimumBoundingRect == noMBR) { - return _area(item.rect); + return item.rect.area(); } - Rectangle newRect = rect.boundingBox(item.rect); - return _area(newRect) - _area(rect); + final newRect = rect.boundingBox(item.rect); + return newRect.area() - rect.area(); } - num area() => _area(rect); - - num get margin => (rect.right - rect.left) + (rect.bottom - rect.top); + num area() => rect.area(); /// Adds the rectangle containing [item] to this node's covered rectangle - include(RTreeContributor item) { + void include(RTreeContributor item) { _minimumBoundingRect = _minimumBoundingRect == noMBR ? item.rect : rect.boundingBox(item.rect); } /// Recalculated the bounding rectangle of this node Rectangle updateBoundingRect() { + _minimumBoundingRect = noMBR; if (children.isEmpty) { - _minimumBoundingRect = noMBR; return _minimumBoundingRect; } - var updatedBoundingRect = children[0].rect; + _minimumBoundingRect = children[0].rect; for (var i = 1; i < children.length; i++) { - updatedBoundingRect = updatedBoundingRect.boundingBox(children[i].rect); + _minimumBoundingRect = _minimumBoundingRect.boundingBox(children[i].rect); } - _minimumBoundingRect = updatedBoundingRect; return _minimumBoundingRect; } @@ -116,16 +118,16 @@ abstract class Node implements RTreeContributor { Node? splitIfNecessary() => size > branchFactor ? _split() : null; Node _split() { - _Seeds seeds = _pickSeeds(); + final seeds = _pickSeeds(); removeChild(seeds.seed1); removeChild(seeds.seed2); - List remainingChildren = children.toList(); + final remainingChildren = children.toList(); clearChildren(); addChild(seeds.seed1); - Node splitNode = createNewNode(); + final splitNode = createNewNode(); splitNode.height = height; splitNode.addChild(seeds.seed2); @@ -135,16 +137,16 @@ abstract class Node implements RTreeContributor { } void _reassignRemainingChildren(List remainingChildren, Node splitNode) { - for (var child in remainingChildren) { - num thisExpansionCost = expansionCost(child); - num splitExpansionCost = splitNode.expansionCost(child); + for (final child in remainingChildren) { + final thisExpansionCost = expansionCost(child); + final splitExpansionCost = splitNode.expansionCost(child); if (thisExpansionCost < splitExpansionCost) { - this.addChild(child); + addChild(child); } else if (splitExpansionCost < thisExpansionCost) { splitNode.addChild(child); } else if (size < splitNode.size) { - this.addChild(child); + addChild(child); } else { splitNode.addChild(child); } @@ -155,12 +157,12 @@ abstract class Node implements RTreeContributor { RTreeContributor seed1; RTreeContributor seed2; - RTreeContributor leftmost = children.elementAt(0); - RTreeContributor rightmost = children.elementAt(0); - RTreeContributor topmost = children.elementAt(0); - RTreeContributor bottommost = children.elementAt(0); + var leftmost = children.elementAt(0); + var rightmost = children.elementAt(0); + var topmost = children.elementAt(0); + var bottommost = children.elementAt(0); - for (var child in children) { + for (final child in children) { if (child.rect.right < leftmost.rect.right) leftmost = child; if (child.rect.left > rightmost.rect.left) rightmost = child; if (child.rect.top > bottommost.rect.top) bottommost = child; @@ -207,5 +209,3 @@ class _Seeds { const _Seeds(this.seed1, this.seed2); } - -num _area(Rectangle rect) => rect.width * rect.height; diff --git a/lib/src/r_tree/non_leaf_node.dart b/lib/src/r_tree/non_leaf_node.dart index f43b43b..a89ada2 100644 --- a/lib/src/r_tree/non_leaf_node.dart +++ b/lib/src/r_tree/non_leaf_node.dart @@ -14,13 +14,17 @@ * limitations under the License. */ -part of r_tree; +import 'dart:math'; -/// A [Node] that is not a leaf end of the [RTree]. These are created automatically -/// by [RTree] when inserting/removing items from the tree. -@Deprecated('For internal use only, removed in next major release') +import 'package:r_tree/src/r_tree/node.dart'; +import 'package:r_tree/src/r_tree/r_tree_datum.dart'; +import 'package:r_tree/src/r_tree/rectangle_helper.dart'; + +/// A [Node] that is not a leaf end of the tree. These are created automatically +/// when inserting/removing items from the tree. class NonLeafNode extends Node { final List> _childNodes = []; + @override List> get children => _childNodes; NonLeafNode(int branchFactor, {List> initialChildNodes = const []}) : super(branchFactor) { @@ -33,14 +37,16 @@ class NonLeafNode extends Node { } } + @override Node createNewNode() { return NonLeafNode(branchFactor); } - Iterable> search(Rectangle searchRect, bool Function(E item)? shouldInclude) { - List> overlappingLeafs = []; + @override + List> search(Rectangle searchRect, bool Function(E item)? shouldInclude) { + final overlappingLeafs = >[]; - for (var childNode in _childNodes) { + for (final childNode in _childNodes) { if (childNode.rect.overlaps(searchRect)) { overlappingLeafs.addAll(childNode.search(searchRect, shouldInclude)); } @@ -49,11 +55,12 @@ class NonLeafNode extends Node { return overlappingLeafs; } + @override Node? insert(RTreeDatum item) { include(item); - Node bestNode = _getBestNodeForInsert(item); - Node? splitNode = bestNode.insert(item); + final bestNode = _getBestNodeForInsert(item); + final splitNode = bestNode.insert(item); if (splitNode != null) { addChild(splitNode); @@ -62,10 +69,11 @@ class NonLeafNode extends Node { return splitIfNecessary(); } - remove(RTreeDatum item) { - List> childrenToRemove = []; + @override + void remove(RTreeDatum item) { + final childrenToRemove = >[]; - for (var childNode in _childNodes) { + for (final childNode in _childNodes) { if (childNode.rect.overlaps(item.rect)) { childNode.remove(item); @@ -75,33 +83,36 @@ class NonLeafNode extends Node { } } - for (var child in childrenToRemove) { + for (final child in childrenToRemove) { removeChild(child); } _updateHeightAndBounds(); } - addChild(Node child) { + @override + void addChild(Node child) { super.addChild(child); child.parent = this; } - removeChild(Node child) { + @override + void removeChild(Node child) { super.removeChild(child); child.parent = null; _updateHeightAndBounds(); } - clearChildren() { + @override + void clearChildren() { + super.clearChildren(); _childNodes.clear(); - _minimumBoundingRect = noMBR; } Node _getBestNodeForInsert(RTreeDatum item) { - Node bestNode = _childNodes[0]; - num bestCost = bestNode.expansionCost(item); + var bestNode = _childNodes[0]; + var bestCost = bestNode.expansionCost(item); for (var i = 1; i < _childNodes.length; i++) { final child = _childNodes[i]; @@ -115,12 +126,12 @@ class NonLeafNode extends Node { return bestNode; } - _updateHeightAndBounds() { + void _updateHeightAndBounds() { var maxChildHeight = 0; for (final childNode in _childNodes) { maxChildHeight = max(maxChildHeight, childNode.height); } - this.height = 1 + maxChildHeight; + height = 1 + maxChildHeight; updateBoundingRect(); } diff --git a/lib/src/r_tree/quickselect.dart b/lib/src/r_tree/quickselect.dart index 9cd302a..ca6079b 100644 --- a/lib/src/r_tree/quickselect.dart +++ b/lib/src/r_tree/quickselect.dart @@ -1,10 +1,9 @@ -part of r_tree; -// Port of https://github.com/mourner/quickselect. +import 'dart:math'; -// sort an array so that items come in groups of n unsorted items, with groups sorted between each other; -// combines selection algorithm with binary divide & conquer approach -@Deprecated('For internal use only, removed in next major release') -multiSelect(List arr, int left, int right, int n, int Function(E a, E b) compare) { +/// Port of https://github.com/mourner/quickselect. +/// sort an array so that items come in groups of n unsorted items, with groups sorted between each other; +/// combines selection algorithm with binary divide & conquer approach +void multiSelect(List arr, int left, int right, int n, int Function(E a, E b) compare) { final stack = [left, right]; while (stack.isNotEmpty) { @@ -44,7 +43,6 @@ multiSelect(List arr, int left, int right, int n, int Function(E a, E b) c /// // arr is [39, 28, 28, 33, 21, 12, 22, 50, 53, 56, 59, 65, 90, 77, 95] /// // ^^ middle index /// ``` -@Deprecated('For internal use only, removed in next major release') void quickSelect(List arr, int k, int left, int right, Comparator compare) { if (arr.isEmpty) { return; @@ -108,7 +106,7 @@ void _quickSelectStep(List arr, int k, int left, int right, Comparator } } -void _swap(List arr, i, j) { +void _swap(List arr, int i, int j) { final tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; diff --git a/lib/src/r_tree/r_tree.dart b/lib/src/r_tree/r_tree.dart index b7b23ee..218c976 100644 --- a/lib/src/r_tree/r_tree.dart +++ b/lib/src/r_tree/r_tree.dart @@ -14,7 +14,14 @@ * limitations under the License. */ -part of r_tree; +import 'dart:math'; + +import 'package:r_tree/src/r_tree/leaf_node.dart'; +import 'package:r_tree/src/r_tree/node.dart'; +import 'package:r_tree/src/r_tree/non_leaf_node.dart'; +import 'package:r_tree/src/r_tree/quickselect.dart'; +import 'package:r_tree/src/r_tree/r_tree_datum.dart'; +import 'package:r_tree/src/r_tree/rectangle_helper.dart'; /// A two dimensional index of data that allows querying by rectangular areas class RTree { @@ -30,31 +37,27 @@ class RTree { _resetRoot(); } - @Deprecated('For internal use only, removed in next major release') - Node get currentRootNode => _root; - /// Adds all [items] to the rtree void add(List> items) { if (items.length == 1) { - insert(items.first); + _insert(items.first); return; } - load(items); + _load(items); } /// Removes [item] from the rtree - remove(RTreeDatum item) { + void remove(RTreeDatum item) { _root.remove(item); - if (_root.children.length == 0) { + if (_root.children.isEmpty) { _resetRoot(); } } /// Adds [item] to the rtree - @Deprecated('Use add') - insert(RTreeDatum item) { + void _insert(RTreeDatum item) { final splitNode = _root.insert(item); if (splitNode != null) { @@ -62,13 +65,16 @@ class RTree { } } - // Returns all items whose rectangles overlap [searchRect] - // Note: Rectangles that share only a border are not considered to overlap - Iterable> search(Rectangle searchRect, {bool Function(E item)? shouldInclude}) { + /// Returns all items whose rectangles overlap [searchRect] + /// If [shouldInclude] is specified, each item will be passed to the + /// method and excluded if [shouldInclude] evaluates to false. + /// + /// Note: Rectangles that share only a border are not considered to overlap + List> search(Rectangle searchRect, {bool Function(E item)? shouldInclude}) { shouldInclude ??= (_) => true; if (_root is LeafNode) { - return _root.search(searchRect, shouldInclude).toList(); + return _root.search(searchRect, shouldInclude); } return _root.search(searchRect, shouldInclude); @@ -76,23 +82,22 @@ class RTree { /// Bulk adds all [items] to the rtree. This implementation draws heavily from /// https://github.com/mourner/rbush and https://github.com/Zverik/dart_rbush. - @Deprecated('Use add') - void load(List> items) { + void _load(List> items) { if (items.isEmpty) { return; } if (items.length < _minEntries) { for (final item in items) { - insert(item); + _insert(item); } return; } // recursively build the tree with the given data from scratch using OMT algorithm - Node node = _build(items, 0, items.length - 1, 0); + var node = _build(items, 0, items.length - 1, 0); - if (_root.children.length == 0) { + if (_root.children.isEmpty) { // save as is if tree is empty _root = node; } else if (_root.height == node.height) { @@ -114,7 +119,7 @@ class RTree { } void _insertTree(int level, Node inode) { - final List> insertPath = []; + final insertPath = >[]; // find the best node for accommodating the item, saving all nodes along the path too final node = _chooseSubtree(inode, _root, level, insertPath); @@ -134,7 +139,9 @@ class RTree { } // fix all the bounding rectangles along the insertion path - insertPath.reversed.forEach((e) => e.updateBoundingRect()); + for (final e in insertPath.reversed) { + e.updateBoundingRect(); + } } Node _chooseSubtree(Node inode, Node node, int level, List> path) { @@ -205,18 +212,18 @@ class RTree { // split the items into M mostly square tiles - final N2 = (N.toDouble() / M).ceil(); - final N1 = N2 * sqrt(M).ceil(); + final n2 = (N.toDouble() / M).ceil(); + final n1 = n2 * sqrt(M).ceil(); - multiSelect(items, left, right, N1, _compareRectLeft); + multiSelect(items, left, right, n1, _compareRectLeft); - for (int i = left; i <= right; i += N1) { - final right2 = min(i + N1 - 1, right); + for (var i = left; i <= right; i += n1) { + final right2 = min(i + n1 - 1, right); - multiSelect(items, i, right2, N2, _compareRectTop); + multiSelect(items, i, right2, n2, _compareRectTop); - for (int j = i; j <= right2; j += N2) { - final right3 = min(j + N2 - 1, right2); + for (var j = i; j <= right2; j += n2) { + final right3 = min(j + n2 - 1, right2); // pack each entry recursively node.children.add(_build(items, j, right3, height - 1, node)); @@ -263,7 +270,7 @@ class RTree { _root.height = node.height + 1; } - int _chooseSplitIndex(Node node, m, M) { + int _chooseSplitIndex(Node node, int m, int M) { int? index; num minOverlap = double.infinity; num minArea = double.infinity; @@ -272,8 +279,8 @@ class RTree { final bbox1 = _boundingBoxForDistribution(node, 0, i); final bbox2 = _boundingBoxForDistribution(node, i, M); - final intersection = bbox1.rect.intersection(bbox2.rect); - final overlap = intersection != null ? _area(intersection) : 0; + final intersection = bbox1.intersection(bbox2); + final overlap = intersection != null ? intersection.area() : 0; final area = bbox1.area() + bbox2.area(); // choose distribution with minimum overlap @@ -294,7 +301,7 @@ class RTree { return index ?? M - m; } - void _chooseSplitAxis(node, m, M) { + void _chooseSplitAxis(Node node, int m, int M) { final xMargin = _allDistributionMargins(node, m, M, true); final yMargin = _allDistributionMargins(node, m, M, false); @@ -319,29 +326,28 @@ class RTree { final leftBoundingBox = _boundingBoxForDistribution(node, 0, m); final rightBoundingBox = _boundingBoxForDistribution(node, M - m, M); - var margin = leftBoundingBox.margin + rightBoundingBox.margin; + num calculateMargin(Rectangle rect) => (rect.right - rect.left) + (rect.bottom - rect.top); + + var margin = calculateMargin(leftBoundingBox) + calculateMargin(rightBoundingBox); for (var i = m; i < M - m; i++) { - leftBoundingBox.extend(node is LeafNode ? node.children[i].rect : node.children[i].rect); - margin += leftBoundingBox.margin; + leftBoundingBox.boundingBox(node is LeafNode ? node.children[i].rect : node.children[i].rect); + margin += calculateMargin(leftBoundingBox); } for (var i = M - m - 1; i >= m; i--) { - rightBoundingBox.extend(node.children[i].rect); - margin += rightBoundingBox.margin; + rightBoundingBox.boundingBox(node.children[i].rect); + margin += calculateMargin(rightBoundingBox); } return margin; } - Node _boundingBoxForDistribution(Node node, int startChild, int stopChild) { - final destNode = LeafNode(_branchFactor); - destNode._minimumBoundingRect = node.children[0].rect; - - for (int i = startChild; i < stopChild; i++) { - destNode.extend(node.children[i].rect); - } - return destNode; + Rectangle _boundingBoxForDistribution(Node node, int startChild, int stopChild) { + return node.children.sublist(startChild, stopChild).fold( + node.children[startChild].rect, + (previousValue, element) => previousValue.boundingBox(element.rect), + ); } void _resetRoot() { @@ -349,7 +355,7 @@ class RTree { } void _growTree(Node node1, Node node2) { - NonLeafNode newRoot = NonLeafNode(_branchFactor, initialChildNodes: [node1, node2]); + final newRoot = NonLeafNode(_branchFactor, initialChildNodes: [node1, node2]); newRoot.height = _root.height + 1; _root = newRoot; node1.parent = _root; @@ -371,3 +377,6 @@ int _compareRectTop(RTreeDatum a, RTreeDatum b) => _compareNumber(a.rect.top, b. @pragma('vm:prefer-inline') int _compareRectLeft(RTreeDatum a, RTreeDatum b) => _compareNumber(a.rect.left, b.rect.left); + +/// Helper for example app to generate GraphViz +Node getCurrentRootNode(RTree tree) => tree._root; diff --git a/lib/src/r_tree/r_tree_contributor.dart b/lib/src/r_tree/r_tree_contributor.dart index c1a4ef3..b3fcf7b 100644 --- a/lib/src/r_tree/r_tree_contributor.dart +++ b/lib/src/r_tree/r_tree_contributor.dart @@ -14,25 +14,11 @@ * limitations under the License. */ -part of r_tree; +import 'dart:math'; -/// The base definition of an object that exists in an [RTree] +/// The base definition of an object that exists in a tree abstract class RTreeContributor { const RTreeContributor(); Rectangle get rect; } - -extension on Rectangle { - // Calculate if otherRect overlaps with the current rectangle - // - // This function is a replication of Rectangle.intersects. It differs in that - // the inequalities are strict and do not allow for equivalences. This means - // that the two rectangles are not considered overlapping if they share an edge. - bool overlaps(Rectangle otherRect) { - return left < otherRect.left + otherRect.width && - otherRect.left < left + width && - top < otherRect.top + otherRect.height && - otherRect.top < top + height; - } -} diff --git a/lib/src/r_tree/r_tree_datum.dart b/lib/src/r_tree/r_tree_datum.dart index baf4da4..ba8b41e 100644 --- a/lib/src/r_tree/r_tree_datum.dart +++ b/lib/src/r_tree/r_tree_datum.dart @@ -14,12 +14,15 @@ * limitations under the License. */ -part of r_tree; +import 'dart:math'; + +import 'package:r_tree/src/r_tree/r_tree_contributor.dart'; /// An [RTreeContributor] that has a piece of data attached to it class RTreeDatum implements RTreeContributor { + @override final Rectangle rect; final E value; - RTreeDatum(Rectangle this.rect, E this.value); + RTreeDatum(this.rect, this.value); } diff --git a/lib/src/r_tree/rectangle_helper.dart b/lib/src/r_tree/rectangle_helper.dart new file mode 100644 index 0000000..e655661 --- /dev/null +++ b/lib/src/r_tree/rectangle_helper.dart @@ -0,0 +1,17 @@ +import 'dart:math'; + +extension RectangleHelper on Rectangle { + /// Calculate if otherRect overlaps with the current rectangle + /// + /// This function is a replication of Rectangle.intersects. It differs in that + /// the inequalities are strict and do not allow for equivalences. This means + /// that the two rectangles are not considered overlapping if they share an edge. + bool overlaps(Rectangle otherRect) { + return left < otherRect.left + otherRect.width && + otherRect.left < left + width && + top < otherRect.top + otherRect.height && + otherRect.top < top + height; + } + + num area() => width * height; +} diff --git a/pubspec.yaml b/pubspec.yaml index 8b4dcdc..7d2aa49 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,3 +14,4 @@ dev_dependencies: dart_style: ^2.2.4 dependency_validator: ^3.0.0 test: ^1.15.7 + workiva_analysis_options: ^1.4.0 diff --git a/test/r_tree/leaf_node_test.dart b/test/r_tree/leaf_node_test.dart index 5b8d7cb..16106af 100644 --- a/test/r_tree/leaf_node_test.dart +++ b/test/r_tree/leaf_node_test.dart @@ -1,18 +1,18 @@ -library leaf_node; - import 'dart:math'; import 'package:r_tree/r_tree.dart'; +import 'package:r_tree/src/r_tree/leaf_node.dart'; +import 'package:r_tree/src/r_tree/node.dart'; import 'package:test/test.dart'; -main() { +void main() { group('LeafNode', () { group('createNewNode', () { test('test that the right type of Node is created', () { - LeafNode leafNode = LeafNode(10); + final leafNode = LeafNode(10); leafNode.addChild(RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 1')); - Node newNode = leafNode.createNewNode(); + final newNode = leafNode.createNewNode(); expect(newNode is LeafNode, equals(true)); expect(newNode.size, equals(0)); expect(newNode.branchFactor, equals(10)); @@ -21,7 +21,7 @@ main() { group('addChild/removeChild', () { test('adding/clearing children updates the rect', () { - LeafNode leaf = LeafNode(3); + final leaf = LeafNode(3); expect(leaf.rect, equals(noMBR)); expect(leaf.size, equals(0)); @@ -31,7 +31,7 @@ main() { expect(leaf.rect, equals(Rectangle(0, 0, 1, 1))); expect(leaf.size, equals(1)); - RTreeDatum nextChild = RTreeDatum(Rectangle(1, 1, 1, 1), 'Item 1'); + final nextChild = RTreeDatum(Rectangle(1, 1, 1, 1), 'Item 1'); leaf.addChild(nextChild); expect(leaf.rect, equals(Rectangle(0, 0, 2, 2))); diff --git a/test/r_tree/node_test.dart b/test/r_tree/node_test.dart index dc69de7..c8733d1 100644 --- a/test/r_tree/node_test.dart +++ b/test/r_tree/node_test.dart @@ -1,21 +1,21 @@ -library node_test; - import 'dart:math'; import 'package:r_tree/r_tree.dart'; +import 'package:r_tree/src/r_tree/leaf_node.dart'; import 'package:test/test.dart'; -main() { +void main() { group('Node', () { group('splitIfNecessary', () { test('split should not occur until branchFactor is exceeded', () { - LeafNode leafNode = LeafNode(10); - Map itemMap = Map(); - - for (int i = 0; i < 4; i++) { - String itemId = 'Item $i'; - itemMap[itemId] = RTreeDatum(Rectangle(0, i, 1, 1), itemId); - leafNode.addChild(itemMap[itemId]); + final leafNode = LeafNode(10); + final itemMap = {}; + + for (var i = 0; i < 4; i++) { + final itemId = 'Item $i'; + final item = RTreeDatum(Rectangle(0, i, 1, 1), itemId); + itemMap[itemId] = item; + leafNode.addChild(item); } expect(leafNode.size, equals(4)); @@ -23,18 +23,19 @@ main() { }); test('test that split correctly splits a column', () { - LeafNode leafNode = LeafNode(3); - Map itemMap = Map(); - - for (int i = 0; i < 4; i++) { - String itemId = 'Item $i'; - itemMap[itemId] = RTreeDatum(Rectangle(0, i, 1, 1), itemId); - leafNode.addChild(itemMap[itemId]); + final leafNode = LeafNode(3); + final itemMap = {}; + + for (var i = 0; i < 4; i++) { + final itemId = 'Item $i'; + final item = RTreeDatum(Rectangle(0, i, 1, 1), itemId); + itemMap[itemId] = item; + leafNode.addChild(item); } expect(leafNode.size, equals(4)); - LeafNode splitNode = leafNode.splitIfNecessary() as LeafNode; + final splitNode = leafNode.splitIfNecessary()! as LeafNode; Iterable items = leafNode.search(Rectangle(0, 0, 1, 10), (_) => true); expect(items.length, equals(leafNode.size)); @@ -50,18 +51,19 @@ main() { }); test('test that split correctly splits a row', () { - LeafNode leafNode = LeafNode(3); - Map itemMap = Map(); - - for (int i = 0; i < 4; i++) { - String itemId = 'Item $i'; - itemMap[itemId] = RTreeDatum(Rectangle(i, 0, 1, 1), itemId); - leafNode.addChild(itemMap[itemId]); + final leafNode = LeafNode(3); + final itemMap = {}; + + for (var i = 0; i < 4; i++) { + final itemId = 'Item $i'; + final item = RTreeDatum(Rectangle(i, 0, 1, 1), itemId); + itemMap[itemId] = item; + leafNode.addChild(item); } expect(leafNode.size, equals(4)); - LeafNode splitNode = leafNode.splitIfNecessary() as LeafNode; + final splitNode = leafNode.splitIfNecessary()! as LeafNode; Iterable items = leafNode.search(Rectangle(0, 0, 10, 1), (_) => true); expect(items.length, equals(leafNode.size)); @@ -77,18 +79,19 @@ main() { }); test('test that split correctly splits a random cluster', () { - LeafNode leafNode = LeafNode(3); - Map itemMap = Map(); - - for (int i = 0; i < 4; i++) { - String itemId = 'Item $i'; - itemMap[itemId] = RTreeDatum(Rectangle(i, 0, 1, 1), itemId); - leafNode.addChild(itemMap[itemId]); + final leafNode = LeafNode(3); + final itemMap = {}; + + for (var i = 0; i < 4; i++) { + final itemId = 'Item $i'; + final item = RTreeDatum(Rectangle(i, 0, 1, 1), itemId); + itemMap[itemId] = item; + leafNode.addChild(item); } expect(leafNode.size, equals(4)); - LeafNode splitNode = leafNode.splitIfNecessary() as LeafNode; + final splitNode = leafNode.splitIfNecessary()! as LeafNode; Iterable items = leafNode.search(Rectangle(0, 0, 10, 1), (_) => true); expect(items.length, equals(leafNode.size)); @@ -106,7 +109,7 @@ main() { group('expansionCost', () { test('expansionCost correctly calculated', () { - LeafNode node = LeafNode(3); + final node = LeafNode(3); expect(node.expansionCost(RTreeDatum(Rectangle(0, 0, 1, 1), '')), equals(1)); diff --git a/test/r_tree/non_leaf_node_test.dart b/test/r_tree/non_leaf_node_test.dart index b1abe24..f785aa6 100644 --- a/test/r_tree/non_leaf_node_test.dart +++ b/test/r_tree/non_leaf_node_test.dart @@ -3,16 +3,19 @@ library non_leaf_node; import 'dart:math'; import 'package:r_tree/r_tree.dart'; +import 'package:r_tree/src/r_tree/leaf_node.dart'; +import 'package:r_tree/src/r_tree/node.dart'; +import 'package:r_tree/src/r_tree/non_leaf_node.dart'; import 'package:test/test.dart'; -main() { +void main() { group('NonLeafNode', () { group('createNewNode', () { test('test that the right type of Node is created', () { - NonLeafNode node = NonLeafNode(10); + final node = NonLeafNode(10); node.addChild(LeafNode(10)); - Node newNode = node.createNewNode(); + final newNode = node.createNewNode(); expect(newNode is NonLeafNode, equals(true)); expect(newNode.size, equals(0)); expect(newNode.branchFactor, equals(10)); @@ -21,19 +24,19 @@ main() { group('addChild/removeChild', () { test('adding/clearing children updates the rect', () { - NonLeafNode node = NonLeafNode(3); + final node = NonLeafNode(3); expect(node.rect, equals(noMBR)); expect(node.size, equals(0)); - LeafNode leaf = LeafNode(3); + final leaf = LeafNode(3); leaf.addChild(RTreeDatum(Rectangle(0, 0, 1, 1), '')); node.addChild(leaf); expect(node.rect, equals(Rectangle(0, 0, 1, 1))); expect(node.size, equals(1)); - LeafNode nextChild = LeafNode(3); + final nextChild = LeafNode(3); nextChild.addChild(RTreeDatum(Rectangle(1, 1, 1, 1), '')); node.addChild(nextChild); diff --git a/test/r_tree/r_tree_test.dart b/test/r_tree/r_tree_test.dart index 0a07ba8..38e94a8 100644 --- a/test/r_tree/r_tree_test.dart +++ b/test/r_tree/r_tree_test.dart @@ -3,16 +3,20 @@ library r_tree; import 'dart:math'; import 'package:r_tree/r_tree.dart'; +import 'package:r_tree/src/r_tree/leaf_node.dart'; +import 'package:r_tree/src/r_tree/node.dart'; +import 'package:r_tree/src/r_tree/non_leaf_node.dart'; +import 'package:r_tree/src/r_tree/r_tree.dart'; import 'package:test/test.dart'; -main() { +void main() { group('RTree', () { group('Insert/Search', () { test('insert 1 item', () { - RTree tree = RTree(3); - RTreeDatum item = RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 1'); + final tree = RTree(3); + final item = RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 1'); - tree.insert(item); + tree.add([item]); assertTreeValidity(tree); var items = tree.search(item.rect, shouldInclude: (_) => false); @@ -22,130 +26,121 @@ main() { expect(items.length, equals(1)); expect(items.elementAt(0).value, equals('Item 1')); - items.forEach((item) { - tree.insert(RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 2')); - tree.insert(RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 3')); - tree.insert(RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 4')); - tree.insert(RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 5')); - }); + for (var i = 0; i < items.length; i++) { + tree.add([RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 2')]); + tree.add([RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 3')]); + tree.add([RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 4')]); + tree.add([RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 5')]); + } assertTreeValidity(tree); items = tree.search(item.rect); expect(items.length, equals(5)); - items.forEach((item) { + for (final item in items) { tree.remove(item); - }); + } assertTreeValidity(tree); - items = tree.search((item.rect)); + items = tree.search(item.rect); expect(items.isEmpty, isTrue); }); - final addMethods = [ - _InsertCase('insert', (RTree tree, Iterable> toAdd) { - toAdd.forEach(tree.insert); - }), - _InsertCase('load', (RTree tree, Iterable> toAdd) { - tree.load(toAdd.toList()); - }) - ]; - - for (final addMethod in addMethods) { - test('search for 1 cell in large format ranges (${addMethod.name})', () { - RTree tree = RTree(3); - Map itemMap = Map(); - List> itemsToInsert = []; - - for (int i = 0; i < 10; i++) { - String itemId = 'Item $i'; - itemMap[itemId] = RTreeDatum(Rectangle(i, 0, 10 - i, 10), itemId); - itemsToInsert.add(itemMap[itemId]); - } + test('search for 1 cell in large format ranges ', () { + final tree = RTree(3); + final itemMap = {}; + final itemsToInsert = >[]; + + for (var i = 0; i < 10; i++) { + final itemId = 'Item $i'; + final item = RTreeDatum(Rectangle(i, 0, 10 - i, 10), itemId); + itemMap[itemId] = item; + itemsToInsert.add(item); + } - addMethod.method(tree, itemsToInsert); - assertTreeValidity(tree); - - var items = tree.search(Rectangle(0, 0, 1, 3)); // A1:A3 - expect(items.length, equals(1)); - expect(items.contains(itemMap['Item 0']), equals(true)); - - items = tree.search(Rectangle(0, 3, 1, 10)); // A3:A13 - expect(items.length, equals(1)); - expect(items.contains(itemMap['Item 0']), equals(true)); - - items = tree.search(Rectangle(4, 4, 1, 1)); // E5 - expect(items.length, equals(5)); - expect(items.contains(itemMap['Item 0']), equals(true)); - expect(items.contains(itemMap['Item 1']), equals(true)); - expect(items.contains(itemMap['Item 2']), equals(true)); - expect(items.contains(itemMap['Item 3']), equals(true)); - expect(items.contains(itemMap['Item 4']), equals(true)); - }); - - test('insert enough items to cause split (${addMethod.name})', () { - RTree tree = RTree(3); - Map itemMap = Map(); - List> itemsToInsert = []; - - for (int i = 0; i < 5; i++) { - String itemId = 'Item $i'; - itemMap[itemId] = RTreeDatum(Rectangle(0, i, 1, 1), itemId); - itemsToInsert.add(itemMap[itemId]); - } + tree.add(itemsToInsert); + assertTreeValidity(tree); + + var items = tree.search(Rectangle(0, 0, 1, 3)); // A1:A3 + expect(items.length, equals(1)); + expect(items.contains(itemMap['Item 0']), equals(true)); + + items = tree.search(Rectangle(0, 3, 1, 10)); // A3:A13 + expect(items.length, equals(1)); + expect(items.contains(itemMap['Item 0']), equals(true)); + + items = tree.search(Rectangle(4, 4, 1, 1)); // E5 + expect(items.length, equals(5)); + expect(items.contains(itemMap['Item 0']), equals(true)); + expect(items.contains(itemMap['Item 1']), equals(true)); + expect(items.contains(itemMap['Item 2']), equals(true)); + expect(items.contains(itemMap['Item 3']), equals(true)); + expect(items.contains(itemMap['Item 4']), equals(true)); + }); + + test('insert enough items to cause split', () { + final tree = RTree(3); + final itemMap = {}; + final itemsToInsert = >[]; + + for (var i = 0; i < 5; i++) { + final itemId = 'Item $i'; + final item = RTreeDatum(Rectangle(0, i, 1, 1), itemId); + itemMap[itemId] = item; + itemsToInsert.add(item); + } + + tree.add(itemsToInsert); + assertTreeValidity(tree); + + var items = tree.search(Rectangle(0, 2, 1, 1)); + expect(items.length, equals(1)); + expect(items.contains(itemMap['Item 2']), equals(true)); - addMethod.method(tree, itemsToInsert); - assertTreeValidity(tree); - - var items = tree.search(Rectangle(0, 2, 1, 1)); - expect(items.length, equals(1)); - expect(items.contains(itemMap['Item 2']), equals(true)); - - items = tree.search(Rectangle(0, 1, 1, 2)); - expect(items.length, equals(2)); - expect(items.contains(itemMap['Item 1']), equals(true)); - expect(items.contains(itemMap['Item 2']), equals(true)); - - items = tree.search(Rectangle(0, 0, 1, 5)); - expect(items.length, equals(5)); - expect(items.contains(itemMap['Item 0']), equals(true)); - expect(items.contains(itemMap['Item 1']), equals(true)); - expect(items.contains(itemMap['Item 2']), equals(true)); - expect(items.contains(itemMap['Item 3']), equals(true)); - expect(items.contains(itemMap['Item 4']), equals(true)); - }); - - test('insert large amount of items (${addMethod.name})', () { - RTree tree = RTree(16); - List> itemsToInsert = []; - - for (int i = 0; i < 50; i++) { - for (int j = 0; j < 50; j++) { - RTreeDatum item = RTreeDatum(Rectangle(i, j, 1, 1), 'Item $i:$j'); - itemsToInsert.add(item); - } + items = tree.search(Rectangle(0, 1, 1, 2)); + expect(items.length, equals(2)); + expect(items.contains(itemMap['Item 1']), equals(true)); + expect(items.contains(itemMap['Item 2']), equals(true)); + + items = tree.search(Rectangle(0, 0, 1, 5)); + expect(items.length, equals(5)); + expect(items.contains(itemMap['Item 0']), equals(true)); + expect(items.contains(itemMap['Item 1']), equals(true)); + expect(items.contains(itemMap['Item 2']), equals(true)); + expect(items.contains(itemMap['Item 3']), equals(true)); + expect(items.contains(itemMap['Item 4']), equals(true)); + }); + + test('insert large amount of items', () { + final tree = RTree(16); + final itemsToInsert = >[]; + + for (var i = 0; i < 50; i++) { + for (var j = 0; j < 50; j++) { + final item = RTreeDatum(Rectangle(i, j, 1, 1), 'Item $i:$j'); + itemsToInsert.add(item); } + } - addMethod.method(tree, itemsToInsert); - assertTreeValidity(tree); + tree.add(itemsToInsert); + assertTreeValidity(tree); - var items = tree.search(Rectangle(31, 27, 1, 1)); - expect(items.length, equals(1)); - expect(items.elementAt(0).value, equals('Item 31:27')); + var items = tree.search(Rectangle(31, 27, 1, 1)); + expect(items.length, equals(1)); + expect(items.elementAt(0).value, equals('Item 31:27')); - items = tree.search(Rectangle(0, 0, 2, 50)); - expect(items.length, equals(100)); - }); - } + items = tree.search(Rectangle(0, 0, 2, 50)); + expect(items.length, equals(100)); + }); }); group('Remove', () { test('remove should only remove first occurrence of item', () { - RTree tree = RTree(3); - RTreeDatum item = RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 1'); + final tree = RTree(3); + final item = RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 1'); - tree.insert(item); - tree.insert(item); + tree.add([item]); + tree.add([item]); assertTreeValidity(tree); var items = tree.search(item.rect); @@ -163,7 +158,7 @@ main() { items = tree.search(item.rect); expect(items.length, equals(0)); - tree.insert(item); + tree.add([item]); assertTreeValidity(tree); items = tree.search(item.rect); @@ -171,84 +166,85 @@ main() { }); test('remove from large tree', () { - RTree tree = RTree(16); - Map itemMap = Map(); - - for (int i = 0; i < 50; i++) { - for (int j = 0; j < 50; j++) { - String itemId = 'Item $i:$j'; - itemMap[itemId] = RTreeDatum(Rectangle(i, j, 1, 1), itemId); - tree.insert(itemMap[itemId]); + final tree = RTree(16); + final itemMap = >{}; + + for (var i = 0; i < 50; i++) { + for (var j = 0; j < 50; j++) { + final itemId = 'Item $i:$j'; + final item = RTreeDatum(Rectangle(i, j, 1, 1), itemId); + itemMap[itemId] = item; + tree.add([item]); } } assertTreeValidity(tree); - var items = tree.search(itemMap['Item 0:0'].rect); + var items = tree.search(itemMap['Item 0:0']!.rect); expect(items.length, equals(1)); - tree.remove(itemMap['Item 0:0']); + tree.remove(itemMap['Item 0:0']!); assertTreeValidity(tree); - items = tree.search(itemMap['Item 0:0'].rect); + items = tree.search(itemMap['Item 0:0']!.rect); expect(items.length, equals(0)); - items = tree.search(itemMap['Item 13:41'].rect); + items = tree.search(itemMap['Item 13:41']!.rect); expect(items.length, equals(1)); - tree.remove(itemMap['Item 13:41']); + tree.remove(itemMap['Item 13:41']!); assertTreeValidity(tree); - items = tree.search(itemMap['Item 13:41'].rect); + items = tree.search(itemMap['Item 13:41']!.rect); expect(items.length, equals(0)); }); test('remove all items from tree', () { - RTree tree = RTree(12); - List data = []; + final tree = RTree(12); + final data = []; - for (int i = 0; i < 50; i++) { - for (int j = 0; j < 50; j++) { - RTreeDatum item = RTreeDatum(Rectangle(i, j, 1, 1), 'Item $i:$j'); + for (var i = 0; i < 50; i++) { + for (var j = 0; j < 50; j++) { + final item = RTreeDatum(Rectangle(i, j, 1, 1), 'Item $i:$j'); data.add(item); - tree.insert(item); + tree.add([item]); } } assertTreeValidity(tree); - expect(tree.currentRootNode, isA>()); + expect(getCurrentRootNode(tree), isA>()); var items = tree.search(Rectangle(0, 0, 50, 50)); expect(items.length, equals(2500)); - data.forEach((RTreeDatum item) { + for (final item in data) { tree.remove(item); - }); + } assertTreeValidity(tree); items = tree.search(Rectangle(0, 0, 50, 50)); expect(items.length, equals(0)); - expect(tree.currentRootNode, isA>()); + expect(getCurrentRootNode(tree), isA>()); //test inserting after removal to ensure new root leaf node functions correctly - tree.insert(RTreeDatum(Rectangle(0, 0, 1, 1), 'New Initial Item')); + tree.add([RTreeDatum(Rectangle(0, 0, 1, 1), 'New Initial Item')]); assertTreeValidity(tree); items = tree.search(Rectangle(0, 0, 50, 50)); - items.forEach((datum) { + for (final datum in items) { expect(datum.value, equals('New Initial Item')); - }); + } }); test('remove all items and then reload', () { final tree = RTree(3); - var items = >[]; + final items = >[]; for (var i = 0; i < 20; i++) { final item = RTreeDatum(Rectangle(0, i, 1, 1), 'Item $i'); items.add(item); - tree.insert(item); + tree.add([item]); } assertTreeValidity(tree); @@ -263,7 +259,7 @@ main() { searchResult = tree.search(Rectangle(0, 0, 1, 20)); expect(searchResult, isEmpty); - tree.load(items.sublist(0, 3)); + tree.add(items.sublist(0, 3)); assertTreeValidity(tree); searchResult = tree.search(Rectangle(0, 0, 1, 20)); @@ -273,20 +269,20 @@ main() { test('has correct parents after _split', () { final tree = RTree(3); - var items = >[]; + final items = >[]; for (var i = 0; i < 1; i++) { final item = RTreeDatum(Rectangle(0, i, 1, 1), 'Item $i'); items.add(item); } - tree.load(items); + tree.add(items); assertTreeValidity(tree); - var otherItems = >[]; + final otherItems = >[]; for (var i = 0; i < 20; i++) { final item = RTreeDatum(Rectangle(i + 10, 0, 1, 1), 'Item $i'); otherItems.add(item); } - tree.load(otherItems); + tree.add(otherItems); assertTreeValidity(tree); }); @@ -294,7 +290,7 @@ main() { final tree = RTree(3); var items = >[RTreeDatum(Rectangle(0, 0, 1, 1), 'Item 0')]; - tree.load(items); + tree.add(items); assertTreeValidity(tree); items = List>.generate( @@ -304,8 +300,7 @@ main() { 'Item $index', ), ); - ; - tree.load(items); + tree.add(items); assertTreeValidity(tree); items = List>.generate( @@ -315,7 +310,7 @@ main() { 'Item $index', ), ); - tree.load(items); + tree.add(items); expect(tree.search(Rectangle(0, 0, 50, 50)), hasLength(24)); assertTreeValidity(tree); }); @@ -329,7 +324,7 @@ main() { 'Item 0', ) ]; - tree.load(items); + tree.add(items); items = List>.generate( 20, @@ -338,7 +333,7 @@ main() { 'Item $i', ), ); - tree.load(items); + tree.add(items); items = List>.generate( 30, @@ -347,7 +342,7 @@ main() { 'Item $i', ), ); - tree.load(items); + tree.add(items); items = List>.generate( 3, @@ -356,7 +351,7 @@ main() { 'Item $i', ), ); - tree.load(items); + tree.add(items); // the test is a bit convoluted but the key here is the search rectangle // intersects what the subtree's rectangle should be but not what it is @@ -372,7 +367,7 @@ main() { /// rectangles. void assertTreeValidity(RTree tree) { try { - assertNodeValidity(tree, tree.currentRootNode); + assertNodeValidity(tree, getCurrentRootNode(tree)); } on StateError catch (e) { fail('${e.message}\nTree:\n${stringifyTree(tree)}'); } @@ -380,7 +375,7 @@ void assertTreeValidity(RTree tree) { /// Comprehensively assert the consistency of the specified subtree, including node height, parent references, and /// bounding rectangles. -_SubtreeValidationData assertNodeValidity(RTree tree, RTreeContributor contributor) { +SubtreeValidationData assertNodeValidity(RTree tree, RTreeContributor contributor) { if (contributor is LeafNode) { return assertLeafNodeValidity(tree, contributor); } else if (contributor is NonLeafNode) { @@ -388,12 +383,12 @@ _SubtreeValidationData assertNodeValidity(RTree tree, RTreeContributor con } // This is a datum - return _SubtreeValidationData(0, contributor.rect); + return SubtreeValidationData(0, contributor.rect); } /// Comprehensively assert the consistency of the subtree rooted at the specified leaf node, including node height, /// parent references, and bounding rectangles. -_SubtreeValidationData assertLeafNodeValidity(RTree tree, LeafNode node) { +SubtreeValidationData assertLeafNodeValidity(RTree tree, LeafNode node) { if (node.height != 1) { throw StateError('Leaf height of ${node.height} should be 1.'); } @@ -408,22 +403,22 @@ _SubtreeValidationData assertLeafNodeValidity(RTree tree, LeafNode node throw StateError('Leaf rect ${node.rect} should be $actualRect.'); } - return _SubtreeValidationData(1, actualRect); + return SubtreeValidationData(1, actualRect); } /// Comprehensively assert the consistency of the subtree rooted at the specified non-leaf node, including node height, /// parent references, and bounding rectangles. -_SubtreeValidationData assertNonLeafNodeValidity(RTree tree, NonLeafNode node) { +SubtreeValidationData assertNonLeafNodeValidity(RTree tree, NonLeafNode node) { if (node.children.isEmpty) { throw StateError('Non-leaf nodes must have at least one leaf.'); } // Assert parent references for children point back to this node - node.children.forEach((child) { + for (final child in node.children) { if (child.parent != node) { throw StateError("Non-leaf child's parent reference is incorrect."); } - }); + } // Traverse the tree from this child and collect validation data to propagate upwards final childrenValidationData = node.children.map((child) => assertNodeValidity(tree, child)).toList(); @@ -433,7 +428,7 @@ _SubtreeValidationData assertNonLeafNodeValidity(RTree tree, NonLeafNode(0, 0, 0, 0); // Recalculate the actual height for this subtree using validation data - final compareMaxWithChild = (int maxHeight, _SubtreeValidationData child) => max(maxHeight, child.height); + int compareMaxWithChild(int maxHeight, SubtreeValidationData child) => max(maxHeight, child.height); final maxChildHeight = childrenValidationData.fold(0, compareMaxWithChild); // Assert this node's height matches its actual structure @@ -447,20 +442,20 @@ _SubtreeValidationData assertNonLeafNodeValidity(RTree tree, NonLeafNode rect; - _SubtreeValidationData(this.height, this.rect); + SubtreeValidationData(this.height, this.rect); } /// Serializes the tree in a human-readable form for debugging. String stringifyTree(RTree tree) { final buffer = StringBuffer(); - stringifyNode(buffer, tree.currentRootNode, 0); + stringifyNode(buffer, getCurrentRootNode(tree), 0); return buffer.toString(); } @@ -477,13 +472,6 @@ void stringifyNode(StringBuffer buffer, RTreeContributor contributor, int lev } } -class _InsertCase { - final Function(RTree tree, Iterable> toAdd) method; - final String name; - - _InsertCase(this.name, this.method); -} - /// Compute the minimum bounding rectangles of the specified rectangles. Returns null if no rectangles provided. Rectangle? getMinimumBoundingRectangle(Iterable> rectangles) { if (rectangles.isEmpty) {