From 7a6ac463731bf807c79dc8840c06734b68bc9ae5 Mon Sep 17 00:00:00 2001 From: xivk Date: Thu, 5 Oct 2023 12:07:06 +0200 Subject: [PATCH] Added a tile tree set. --- TilesMath.sln.DotSettings | 2 + src/TilesMath/Collections/TileTreeSet.cs | 146 ++++++++++++++++++ .../Collections/TileTreeSetExtensions.cs | 34 ++++ src/TilesMath/Tile.cs | 17 +- src/TilesMath/TileExtensions.cs | 22 +++ src/TilesMath/TilesMath.csproj | 3 +- .../Collections/TileTreeSetTests.cs | 104 +++++++++++++ test/TilesMath.Tests/TileExtensionTests.cs | 18 +++ test/TilesMath.Tests/TilesMath.Tests.csproj | 1 + 9 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 TilesMath.sln.DotSettings create mode 100644 src/TilesMath/Collections/TileTreeSet.cs create mode 100644 src/TilesMath/Collections/TileTreeSetExtensions.cs create mode 100644 test/TilesMath.Tests/Collections/TileTreeSetTests.cs create mode 100644 test/TilesMath.Tests/TileExtensionTests.cs diff --git a/TilesMath.sln.DotSettings b/TilesMath.sln.DotSettings new file mode 100644 index 0000000..1d09b05 --- /dev/null +++ b/TilesMath.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/TilesMath/Collections/TileTreeSet.cs b/src/TilesMath/Collections/TileTreeSet.cs new file mode 100644 index 0000000..aa52e28 --- /dev/null +++ b/src/TilesMath/Collections/TileTreeSet.cs @@ -0,0 +1,146 @@ +using System.Collections; + +namespace TilesMath.Collections; + +/// +/// A tile tree set that keeps a collection of tiles using leaf tiles. If all children of a tile are included only the parent is stored. +/// +public class TileTreeSet : IEnumerable +{ + private readonly HashSet _tiles = new(); + + /// + /// Adds the given tile. + /// + /// The tile. + /// True if the tile was added, false if it was already present. + public bool Add(Tile tile) + { + while (true) + { + if (_tiles.Contains(tile)) return false; + + // add the tile. + _tiles.Add(tile); + + // check if this leads to a new 'leaf'. + var parent = tile.Parent; + if (parent == null) + { + // tile was added and it is now a new leaf. + // this is the top level tile that was added. + return true; + } + var hasAllChildren = parent.Value.Children.All(x => _tiles.Contains(x)); + if (!hasAllChildren) + { + // tile was added and it is now a new leaf. + return true; + } + else + { + // remove all the children, the leaf will cover them. + _tiles.ExceptWith(parent.Value.Children); + + // the parent needs to be added. + tile = parent.Value; + } + } + } + + /// + /// Checks if a tile is covered by this tree. + /// + /// The tile. + /// True if the tile is covered, false otherwise. + public bool Contains(Tile tile) + { + return this.ContainsInternal(tile) != null; + } + + private Tile? ContainsInternal(Tile tile) + { + while (true) + { + if (_tiles.Contains(tile)) return tile; + + // check parent. + var parent = tile.Parent; + if (parent == null) return null; + + tile = parent.Value; + } + } + + /// + /// Removes a tile from the set. + /// + /// The tile to remove. + /// True if the tile was removed, false if not. + public bool Remove(Tile tile) + { + if (_tiles.Remove(tile)) return true; + + // find the parent that is there. + var parent = this.ContainsInternal(tile); + + // compose blacklist of the entire parent queue. + if (parent == null) return false; // tile is not in this set, no need to remove it. + _tiles.Remove(parent.Value); // we are already sure this tile is not a leaf anymore. + + // add all new leaves one by one. + _tiles.UnionWith(EnumerateTreeExceptAncestors(parent.Value)); + return true; + + IEnumerable EnumerateTreeExceptAncestors(Tile p) + { + foreach (var child in p.Children) + { + if (child == tile) continue; // the tile itself we do not want to add again. + if (child.IsAncestor(tile)) + { + // we do not want to add this ancestor again, but perhaps the children. + foreach (var grandChild in EnumerateTreeExceptAncestors(child)) + { + yield return grandChild; + } + } + else + { + yield return child; + } + } + } + } + + /// + /// True if the set is empty. + /// + public bool IsEmpty => _tiles.Count == 0; + + /// + /// Gets the inverted set. + /// + /// + public TileTreeSet GetInvertedSet() + { + // we start full and just remove all tiles in this set. + var invertedSet = new TileTreeSet() { Tile.Create(0, 0, 0) }; + foreach (var tile in _tiles) + { + invertedSet.Remove(tile); + } + + return invertedSet; + } + + public IEnumerator GetEnumerator() + { + return _tiles.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } +} diff --git a/src/TilesMath/Collections/TileTreeSetExtensions.cs b/src/TilesMath/Collections/TileTreeSetExtensions.cs new file mode 100644 index 0000000..2827706 --- /dev/null +++ b/src/TilesMath/Collections/TileTreeSetExtensions.cs @@ -0,0 +1,34 @@ +namespace TilesMath.Collections; + +public static class TileTreeSetExtensions +{ + /// + /// Enumerates all the tiles in the set at the given zoom level. + /// + /// The set. + /// The zoom level. + /// An enumerable with all tiles covered by the set in the given zoom level. + /// When tiles are found at a higher zoom level in the set it is not possible to enumerate. + public static IEnumerable ToEnumerableAtZoom(this TileTreeSet set, int zoom) + { + foreach (var tile in set) + { + if (tile.Zoom == zoom) + { + yield return tile; + } + else if (tile.Zoom < zoom) + { + foreach (var child in tile.ChildrenAtZoom(zoom)) + { + yield return child; + } + } + else + { + throw new Exception( + $"Cannot enumerate this set at {zoom}, found at tile at a higher zoom level: {tile.Zoom}"); + } + } + } +} diff --git a/src/TilesMath/Tile.cs b/src/TilesMath/Tile.cs index 94daa2a..51a63e3 100644 --- a/src/TilesMath/Tile.cs +++ b/src/TilesMath/Tile.cs @@ -72,13 +72,20 @@ public Tile? Parent /// public TileChildren Children => new TileChildren(this); - public IEnumerable ChildrenAtZoom(int zoom) + /// + /// Enumerates the children at the given zoom level. + /// + /// The zoom to enumerate at. + /// A callback to exclude children. + /// + /// + public IEnumerable ChildrenAtZoom(int zoom, Func? exclude = null) { if (zoom < this.Zoom) throw new Exception("Cannot calculate sub tiles for a smaller zoom level"); if (zoom == this.Zoom) { - yield return this; + if (exclude == null || !exclude(this)) yield return this; yield break; } @@ -86,16 +93,18 @@ public IEnumerable ChildrenAtZoom(int zoom) { foreach (var child in this.Children) { - yield return child; + if (exclude == null || !exclude(child)) yield return child; } yield break; } foreach (var childOneLevelLess in this.ChildrenAtZoom(zoom - 1)) { + if (exclude != null && !exclude(childOneLevelLess)) continue; + foreach (var child in childOneLevelLess.Children) { - yield return child; + if (exclude == null || !exclude(child)) yield return child; } } } diff --git a/src/TilesMath/TileExtensions.cs b/src/TilesMath/TileExtensions.cs index ec47b12..45f589f 100644 --- a/src/TilesMath/TileExtensions.cs +++ b/src/TilesMath/TileExtensions.cs @@ -5,6 +5,28 @@ namespace TilesMath; /// public static class TileExtensions { + /// + /// Checks if the tile is an ancestor of the given decendant. + /// + /// The potential ancestor. + /// The potential decendant. + /// True if the given tile is a decendant, false otherwise. + public static bool IsAncestor(this Tile tile, Tile decendant) + { + if (tile.Zoom >= decendant.Zoom) return false; + + var parent = decendant.Parent; + while (parent != null) + { + if (parent.Value == tile) return true; + if (tile.Zoom >= parent.Value.Zoom) return false; + + parent = parent.Value.Parent; + } + + return false; + } + /// /// Enumerates all tiles between the top left and bottom right tile. /// diff --git a/src/TilesMath/TilesMath.csproj b/src/TilesMath/TilesMath.csproj index 0d1bfc5..188a16f 100644 --- a/src/TilesMath/TilesMath.csproj +++ b/src/TilesMath/TilesMath.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 0.0.6 + 0.0.7 TilesMath ANYWAYS BV A tiny library for tiles math. @@ -15,4 +15,5 @@ tiles + diff --git a/test/TilesMath.Tests/Collections/TileTreeSetTests.cs b/test/TilesMath.Tests/Collections/TileTreeSetTests.cs new file mode 100644 index 0000000..9b7d023 --- /dev/null +++ b/test/TilesMath.Tests/Collections/TileTreeSetTests.cs @@ -0,0 +1,104 @@ +using TilesMath.Collections; + +namespace TilesMath.Tests.Collections; + +public class TileTreeSetTests +{ + [Fact] + public void TileTreeSet_NewSet_ShouldBeEmpty() + { + var set = new TileTreeSet(); + + Assert.True(set.IsEmpty); + } + + [Fact] + public void TileTreeSet_OneTile_ShouldNotBeEmpty() + { + var set = new TileTreeSet { Tile.Create(1025, 4511, 14) }; + + Assert.False(set.IsEmpty); + } + + [Fact] + public void TileTreeSet_OneTile_ShouldEnumerateOneTile() + { + var set = new TileTreeSet { Tile.Create(1025, 4511, 14) }; + + var leaves = set.ToList(); + Assert.Single(leaves); + Assert.Equal(Tile.Create(1025, 4511, 14), leaves[0]); + } + + [Fact] + public void TileTreeSet_AllChildren_OneZoomLower_ShouldEnumerateOneLeaf() + { + var set = new TileTreeSet(); + var expectedLeaf = Tile.Create(102, 451, 13); + foreach (var tile in expectedLeaf.Children) + { + set.Add(tile); + } + + var leaves = set.ToList(); + Assert.Single(leaves); + Assert.Equal(expectedLeaf, leaves[0]); + } + + [Fact] + public void TileTreeSet_AllChildren_ThreeZoomsLower_ShouldEnumerateOneLeaf() + { + var set = new TileTreeSet(); + var expectedLeaf = Tile.Create(2, 4, 4); + foreach (var tile in expectedLeaf.ChildrenAtZoom(7)) + { + set.Add(tile); + } + + var leaves = set.ToList(); + Assert.Single(leaves); + Assert.Equal(expectedLeaf, leaves[0]); + } + + [Fact] + public void TileTreeSet_SetWithTileZero_ShouldContainAllTiles() + { + var set = new TileTreeSet { Tile.Create(0, 0, 0) }; + + Assert.True(set.Contains(Tile.Create(2, 4, 4))); + Assert.True(set.Contains(Tile.Create(102, 451, 13))); + Assert.True(set.Contains(Tile.Create(1025, 4511, 14))); + } + + [Fact] + public void TileTreeSet_SetWithTileZero_RemoveChildTile_ShouldContainAllExceptRemovedTile() + { + var set = new TileTreeSet { Tile.Create(0, 0, 0) }; + + var removedTile = Tile.Create(1, 1, 1); + set.Remove(removedTile); + + Assert.True(set.Contains(Tile.Create(0, 0, 1))); + Assert.True(set.Contains(Tile.Create(1, 0, 1))); + Assert.True(set.Contains(Tile.Create(0, 1, 1))); + Assert.False(set.Contains(removedTile)); + } + + [Fact] + public void TileTreeSet_SetWithTileZero_RemoveGranChildTile_ShouldContainAllExceptRemovedTile() + { + var set = new TileTreeSet { Tile.Create(0, 0, 0) }; + + var removedTile = Tile.Create(2, 2, 2); + set.Remove(removedTile); + + Assert.True(set.Contains(Tile.Create(0, 0, 1))); + Assert.True(set.Contains(Tile.Create(1, 0, 1))); + Assert.True(set.Contains(Tile.Create(0, 1, 1))); + foreach (var leaf in removedTile.Parent.Value.Children.Where(x => x != removedTile)) + { + Assert.True(set.Contains(leaf)); + } + Assert.False(set.Contains(removedTile)); + } +} diff --git a/test/TilesMath.Tests/TileExtensionTests.cs b/test/TilesMath.Tests/TileExtensionTests.cs new file mode 100644 index 0000000..6e37ed9 --- /dev/null +++ b/test/TilesMath.Tests/TileExtensionTests.cs @@ -0,0 +1,18 @@ +namespace TilesMath.Tests; + +public class TileExtensionTests +{ + [Fact] + public void Tile_IsAncestor_WhenParent_ShouldBeTrue() + { + var tile = Tile.Create(1025, 4511, 14); + Assert.True(tile.Parent != null && tile.Parent.Value.IsAncestor(tile)); + } + + [Fact] + public void Tile_IsAncestor_WhenGranParent_ShouldBeTrue() + { + var tile = Tile.Create(1025, 4511, 14); + Assert.True(tile.Parent?.Parent is not null && tile.Parent.Value.Parent.Value.IsAncestor(tile)); + } +} diff --git a/test/TilesMath.Tests/TilesMath.Tests.csproj b/test/TilesMath.Tests/TilesMath.Tests.csproj index 3a8cb88..c9a6def 100644 --- a/test/TilesMath.Tests/TilesMath.Tests.csproj +++ b/test/TilesMath.Tests/TilesMath.Tests.csproj @@ -25,4 +25,5 @@ +