Skip to content

Commit

Permalink
Added a tile tree set.
Browse files Browse the repository at this point in the history
  • Loading branch information
xivk committed Oct 5, 2023
1 parent d1d796b commit 7a6ac46
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 5 deletions.
2 changes: 2 additions & 0 deletions TilesMath.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=decendant/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
146 changes: 146 additions & 0 deletions src/TilesMath/Collections/TileTreeSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System.Collections;

namespace TilesMath.Collections;

/// <summary>
/// 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.
/// </summary>
public class TileTreeSet : IEnumerable<Tile>
{
private readonly HashSet<Tile> _tiles = new();

/// <summary>
/// Adds the given tile.
/// </summary>
/// <param name="tile">The tile.</param>
/// <returns>True if the tile was added, false if it was already present.</returns>
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;
}
}
}

/// <summary>
/// Checks if a tile is covered by this tree.
/// </summary>
/// <param name="tile">The tile.</param>
/// <returns>True if the tile is covered, false otherwise.</returns>
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;
}
}

/// <summary>
/// Removes a tile from the set.
/// </summary>
/// <param name="tile">The tile to remove.</param>
/// <returns>True if the tile was removed, false if not.</returns>
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<Tile> 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;
}
}
}
}

/// <summary>
/// True if the set is empty.
/// </summary>
public bool IsEmpty => _tiles.Count == 0;

/// <summary>
/// Gets the inverted set.
/// </summary>
/// <returns></returns>
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<Tile> GetEnumerator()
{
return _tiles.GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
34 changes: 34 additions & 0 deletions src/TilesMath/Collections/TileTreeSetExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace TilesMath.Collections;

public static class TileTreeSetExtensions
{
/// <summary>
/// Enumerates all the tiles in the set at the given zoom level.
/// </summary>
/// <param name="set">The set.</param>
/// <param name="zoom">The zoom level.</param>
/// <returns>An enumerable with all tiles covered by the set in the given zoom level.</returns>
/// <exception cref="Exception">When tiles are found at a higher zoom level in the set it is not possible to enumerate.</exception>
public static IEnumerable<Tile> 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}");
}
}
}
}
17 changes: 13 additions & 4 deletions src/TilesMath/Tile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,30 +72,39 @@ public Tile? Parent
/// </summary>
public TileChildren Children => new TileChildren(this);

public IEnumerable<Tile> ChildrenAtZoom(int zoom)
/// <summary>
/// Enumerates the children at the given zoom level.
/// </summary>
/// <param name="zoom">The zoom to enumerate at.</param>
/// <param name="exclude">A callback to exclude children.</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public IEnumerable<Tile> ChildrenAtZoom(int zoom, Func<Tile, bool>? 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;
}

if (zoom - 1 == this.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;
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/TilesMath/TileExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ namespace TilesMath;
/// </summary>
public static class TileExtensions
{
/// <summary>
/// Checks if the tile is an ancestor of the given decendant.
/// </summary>
/// <param name="tile">The potential ancestor.</param>
/// <param name="decendant">The potential decendant.</param>
/// <returns>True if the given tile is a decendant, false otherwise.</returns>
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;
}

/// <summary>
/// Enumerates all tiles between the top left and bottom right tile.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/TilesMath/TilesMath.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageVersion>0.0.6</PackageVersion>
<PackageVersion>0.0.7</PackageVersion>
<Title>TilesMath</Title>
<Authors>ANYWAYS BV</Authors>
<Description>A tiny library for tiles math.</Description>
Expand All @@ -15,4 +15,5 @@
<PackageTags>tiles</PackageTags>
</PropertyGroup>


</Project>
104 changes: 104 additions & 0 deletions test/TilesMath.Tests/Collections/TileTreeSetTests.cs
Original file line number Diff line number Diff line change
@@ -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))

Check warning on line 98 in test/TilesMath.Tests/Collections/TileTreeSetTests.cs

View workflow job for this annotation

GitHub Actions / build

Nullable value type may be null.

Check warning on line 98 in test/TilesMath.Tests/Collections/TileTreeSetTests.cs

View workflow job for this annotation

GitHub Actions / build

Nullable value type may be null.
{
Assert.True(set.Contains(leaf));
}
Assert.False(set.Contains(removedTile));
}
}
18 changes: 18 additions & 0 deletions test/TilesMath.Tests/TileExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading

0 comments on commit 7a6ac46

Please sign in to comment.