Skip to content

Commit e183039

Browse files
committed
Duplicate sites to be explicitly marked as duplicate so any potential access exception actually explains this
1 parent 27387fb commit e183039

File tree

10 files changed

+102
-44
lines changed

10 files changed

+102
-44
lines changed

SharpVoronoiLib/Exceptions/VoronoiDoesntHaveSitesException.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,5 @@
22

33
namespace SharpVoronoiLib.Exceptions;
44

5-
public class VoronoiDoesntHaveSitesException : Exception
6-
{
7-
public VoronoiDoesntHaveSitesException()
8-
: base("This data is not ready yet, you must add sites to the plane first.")
9-
{
10-
11-
}
12-
}
5+
public class VoronoiDoesntHaveSitesException()
6+
: Exception("This data is not ready yet, you must add sites to the plane first.");

SharpVoronoiLib/Exceptions/VoronoiNotTessellatedException.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,5 @@
22

33
namespace SharpVoronoiLib.Exceptions;
44

5-
public class VoronoiNotTessellatedException : Exception
6-
{
7-
public VoronoiNotTessellatedException()
8-
: base("This data is not ready yet, you must tessellate the plane first.")
9-
{
10-
11-
}
12-
}
5+
public class VoronoiNotTessellatedException()
6+
: Exception("This data is not ready yet, you must tessellate the plane first.");
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
3+
namespace SharpVoronoiLib.Exceptions;
4+
5+
public class VoronoiSiteSkippedAsDuplicateException()
6+
: Exception("This site was skipped during tessellation because it duplicates another site. " +
7+
"If sites are not generated by the library and may contain duplicates, " +
8+
"you must handle duplicates yourself and either not generate duplicates or skip these sites afterwards. " +
9+
"Note that handling duplicate sites is not well-defined for tesselation algorithms in practice. " +
10+
"For the purpose of this library and from standard practice, duplicate sites can be considered collapsed/merged with their original.");
11+
12+
// Note that we can theoretically include the same cell data for a duplicate site as for the original site.
13+
// A purely mathematical result is possible - duplicate sites would have the same cell.
14+
// But this would require the implementation to actually handle duplicate cells, edges, points, etc.
15+
// In other words, if the implementation does not handle initial duplicates, then handling duplicates later is way more complicated.
16+
// So the default practice for libraries dealing with this is to simply skip/merge duplicates.

SharpVoronoiLib/Nearest Site Lookup/BruteForceNearestSiteLookup.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32

43
namespace SharpVoronoiLib;
54

SharpVoronoiLib/Tessellation/Fortune/FortunesTessellation.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ public List<VoronoiEdge> Run(List<VoronoiSite> sites, double minX, double minY,
1515
foreach (VoronoiSite site in sites)
1616
{
1717
if (!siteCache.Add(site))
18+
{
19+
site.MarkSkippedAsDuplicate();
1820
continue;
21+
}
1922

2023
if (site == null) throw new ArgumentNullException(nameof(sites));
2124

SharpVoronoiLib/Tessellation/Fortune/MinHeap.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Diagnostics;
32

43
namespace SharpVoronoiLib;
54

SharpVoronoiLib/Voronoi/VoronoiSite.cs

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ public class VoronoiSite
2222

2323
/// <summary>
2424
/// The state of this site.
25-
/// If may be untesselated, if the algorith hasn't been run yet.
26-
/// Or it may be skipped if it's a duplicate to another site.
25+
/// It may be untesselated, if the algorithm hasn't been run yet.
26+
/// Or it may be skipped if it's a duplicate to another site, in which case <see cref="SkippedAsDuplicate"/> will be true.
2727
/// </summary>
2828
[PublicAPI]
2929
public bool Tesselated => _tessellated;
3030

31+
/// <summary>
32+
/// When not <see cref="Tesselated"/>, but the main <see cref="VoronoiPlane"/> is,
33+
/// this may be true if tessellation skipped this site because it duplicates another site.
34+
/// </summary>
35+
[PublicAPI]
36+
public bool SkippedAsDuplicate => _skippedAsDuplicate;
37+
3138
/// <summary>
3239
/// The edges that make up this cell.
3340
/// The vertices of these edges are the <see cref="Points"/>.
@@ -38,8 +45,7 @@ public IEnumerable<VoronoiEdge> Cell
3845
{
3946
get
4047
{
41-
if (!_tessellated)
42-
throw new VoronoiNotTessellatedException();
48+
ThrowIfUnavailable();
4349

4450
return cell;
4551
}
@@ -54,8 +60,7 @@ public IEnumerable<VoronoiEdge> ClockwiseCell
5460
{
5561
get
5662
{
57-
if (!_tessellated)
58-
throw new VoronoiNotTessellatedException();
63+
ThrowIfUnavailable();
5964

6065
if (_clockwiseCell == null)
6166
{
@@ -75,8 +80,7 @@ public IEnumerable<VoronoiSite> Neighbours
7580
{
7681
get
7782
{
78-
if (!_tessellated)
79-
throw new VoronoiNotTessellatedException();
83+
ThrowIfUnavailable();
8084

8185
return neighbours;
8286
}
@@ -90,8 +94,7 @@ public IEnumerable<VoronoiPoint> Points
9094
{
9195
get
9296
{
93-
if (!_tessellated)
94-
throw new VoronoiNotTessellatedException();
97+
ThrowIfUnavailable();
9598

9699
if (_points == null)
97100
{
@@ -128,8 +131,7 @@ public IEnumerable<VoronoiPoint> ClockwisePoints
128131
{
129132
get
130133
{
131-
if (!_tessellated)
132-
throw new VoronoiNotTessellatedException();
134+
ThrowIfUnavailable();
133135

134136
if (_clockwisePoints == null)
135137
{
@@ -151,8 +153,7 @@ public VoronoiEdge? LiesOnEdge
151153
{
152154
get
153155
{
154-
if (!_tessellated)
155-
throw new VoronoiNotTessellatedException();
156+
ThrowIfUnavailable();
156157

157158
return _liesOnEdge;
158159
}
@@ -167,8 +168,7 @@ public VoronoiPoint? LiesOnCorner
167168
{
168169
get
169170
{
170-
if (!_tessellated)
171-
throw new VoronoiNotTessellatedException();
171+
ThrowIfUnavailable();
172172

173173
return _liesOnCorner;
174174
}
@@ -184,8 +184,7 @@ public VoronoiPoint Centroid
184184
{
185185
get
186186
{
187-
if (!_tessellated)
188-
throw new VoronoiNotTessellatedException();
187+
ThrowIfUnavailable();
189188

190189
if (_centroid != null)
191190
return _centroid;
@@ -202,6 +201,7 @@ public VoronoiPoint Centroid
202201

203202

204203
private bool _tessellated;
204+
private bool _skippedAsDuplicate;
205205

206206
private List<VoronoiPoint>? _points;
207207
private List<VoronoiPoint>? _clockwisePoints;
@@ -231,8 +231,7 @@ public VoronoiSite(double x, double y)
231231
[PublicAPI]
232232
public bool Contains(double x, double y)
233233
{
234-
if (!_tessellated)
235-
throw new VoronoiNotTessellatedException();
234+
ThrowIfUnavailable();
236235

237236
// If we don't have points generated yet, do so now (by calling the property that does so when read)
238237
if (_clockwisePoints == null)
@@ -313,6 +312,9 @@ internal void Relocate(double newX, double newY)
313312

314313
// We are no longer part of voronoi
315314
_tessellated = false;
315+
316+
// We are not skipped as duplicate anymore as we have not been tessellated yet
317+
_skippedAsDuplicate = false;
316318

317319
// Clear all the values we used before
318320

@@ -326,6 +328,10 @@ internal void Relocate(double newX, double newY)
326328
_centroid = null;
327329
}
328330

331+
internal void MarkSkippedAsDuplicate()
332+
{
333+
_skippedAsDuplicate = true;
334+
}
329335

330336
[Pure]
331337
private static int SortPointsClockwise(VoronoiPoint point1, VoronoiPoint point2, double x, double y)
@@ -557,11 +563,24 @@ internal void Invalidated()
557563
InvalidateComputedValues();
558564
}
559565

566+
567+
private void ThrowIfUnavailable()
568+
{
569+
if (!_tessellated)
570+
{
571+
if (_skippedAsDuplicate)
572+
throw new VoronoiSiteSkippedAsDuplicateException();
573+
574+
throw new VoronoiNotTessellatedException();
575+
}
576+
}
577+
560578

561579
#if DEBUG
562580
public override string ToString()
563581
{
564582
return "(" + X.ToString("F3") + "," + Y.ToString("F3") + ")";
565583
}
566584
#endif
567-
}
585+
}
586+

UnitTests/BugReproTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System;
21
using NUnit.Framework;
32
using System.Collections.Generic;
43

UnitTests/ExceptionTest.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public void AccessPlaneEdgesBeforeTesselate()
2727

2828
VoronoiPlane plane = new VoronoiPlane(0, 0, 600, 600);
2929

30-
plane.SetSites(new List<VoronoiSite>());
30+
plane.SetSites([ ]);
3131

3232
// Act - Assert
3333

@@ -41,7 +41,7 @@ public void AccessSiteDataBeforeTesselate()
4141

4242
VoronoiPlane plane = new VoronoiPlane(0, 0, 600, 600);
4343

44-
List<VoronoiSite> sites = new List<VoronoiSite>() { new VoronoiSite(100, 100) };
44+
List<VoronoiSite> sites = [ new VoronoiSite(100, 100) ];
4545

4646
plane.SetSites(sites);
4747

@@ -57,4 +57,40 @@ public void AccessSiteDataBeforeTesselate()
5757
Assert.Throws<VoronoiNotTessellatedException>(() => _ = sites[0].Centroid);
5858
Assert.Throws<VoronoiNotTessellatedException>(() => _ = sites[0].Contains(100, 100));
5959
}
60+
61+
[Test]
62+
public void DuplicateSiteAccess()
63+
{
64+
// Arrange
65+
66+
VoronoiPlane plane = new VoronoiPlane(0, 0, 600, 600);
67+
List<VoronoiSite> sites =
68+
[
69+
new VoronoiSite(100, 100),
70+
new VoronoiSite(100, 100)
71+
];
72+
plane.SetSites(sites);
73+
74+
// Act
75+
76+
plane.Tessellate();
77+
78+
// Assert
79+
80+
Assert.That(sites[0].Tesselated, Is.True);
81+
Assert.That(sites[0].SkippedAsDuplicate, Is.False);
82+
83+
Assert.That(sites[1].Tesselated, Is.False);
84+
Assert.That(sites[1].SkippedAsDuplicate, Is.True);
85+
86+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].Cell);
87+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].ClockwiseCell);
88+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].Neighbours);
89+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].Points);
90+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].ClockwisePoints);
91+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].LiesOnEdge);
92+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].LiesOnCorner);
93+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].Centroid);
94+
Assert.Throws<VoronoiSiteSkippedAsDuplicateException>(() => _ = sites[1].Contains(100, 100));
95+
}
6096
}

UnitTests/LloydsAlgorithmTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Collections.Generic;
2-
using System.Linq;
32
using NUnit.Framework;
43

54
namespace SharpVoronoiLib.UnitTests;

0 commit comments

Comments
 (0)