Skip to content

Commit

Permalink
Use GeoJson for polygon testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Twinki14 committed Jan 10, 2024
1 parent 53ffb1b commit c2b7ce9
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 8 deletions.
1 change: 1 addition & 0 deletions data/10m_minor_islands.geojson

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions data/10m_minor_islands_label_points.geojson

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions src/PolyZone.Tests/Helpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CitizenFX.Core;
using GeoJSON.Text;
using GeoJSON.Text.Feature;
using GeoJSON.Text.Geometry;
using PolyZone.Shapes.Interfaces;
using Polygon = PolyZone.Shapes.Polygon;

namespace PolyZone.Tests;

public static class Helpers
{
public static string GetPolygonArrayString(List<IPolygon> polygons)
{
var resultBuilder = new StringBuilder();

for (var index = 0; index < polygons.Count; index++)
{
var polygon = polygons[index];

resultBuilder.AppendLine($"[{index}]");

foreach (var point in polygon.Points)
{
resultBuilder.AppendLine($" X: {point.X}, Y: {point.Y}");
}
}

return resultBuilder.ToString();
}

public static IEnumerable<Vector2> GetPointsFromGeoCollection(FeatureCollection featureCollection)
{
var points = new List<Vector2>();

foreach (var feature in featureCollection.Features)
{
switch (feature.Geometry.Type)
{
case GeoJSONObjectType.Point:
{
var point = feature.Geometry as Point;

points.Add(new Vector2 { X = (float) point.Coordinates.Longitude, Y = (float) point.Coordinates.Latitude });

break;
}
}
}

return points;
}

public static IEnumerable<Polygon> GetPolygonsFromGeoCollection(FeatureCollection featureCollection)
{
var polygons = new List<Polygon>();

foreach (var feature in featureCollection.Features)
{
switch (feature.Geometry.Type)
{
case GeoJSONObjectType.MultiPolygon:
{
var multiPolygon = feature.Geometry as MultiPolygon;
foreach (var polygon in multiPolygon!.Coordinates)
{
var points = new List<Vector2>();

foreach (var lines in polygon.Coordinates)
{
foreach (var line in lines.Coordinates)
{
points.Add(new Vector2 { X = (float) line.Longitude, Y = (float) line.Latitude });
}
}

polygons.Add(new Polygon(points));
}
break;
}
case GeoJSONObjectType.Polygon:
{
var polygon = feature.Geometry as GeoJSON.Text.Geometry.Polygon;

var points = new List<Vector2>();

foreach (var line in polygon!.Coordinates.First().Coordinates)
{
points.Add(new Vector2 { X = (float) line.Longitude, Y = (float) line.Latitude });
}

polygons.Add(new Polygon(points));

break;
}
}
}

return polygons;
}
}
26 changes: 23 additions & 3 deletions src/PolyZone.Tests/Internal/Vector2Assertions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using CitizenFX.Core;
using System.Collections.Generic;
using System.Linq;
using CitizenFX.Core;
using FluentAssertions;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
Expand All @@ -24,7 +26,25 @@ public AndConstraint<Vector2Assertions> BeInside(ISpatial2dShape shape, string b
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(shape.Contains(_instance))
.FailWith($"Expected point to be inside polygon, point was { _instance.X }, { _instance.Y }");
.FailWith($"Expected point to be inside shape, point was { _instance.X }, { _instance.Y }");

return new AndConstraint<Vector2Assertions>(this);
}

public AndConstraint<Vector2Assertions> BeInsideOnlyOneOf(IEnumerable<IPolygon> polygons, string because = "", params object[] becauseArgs)
{
var polygonsString = "";
var insidePolygons = polygons.Where(p => p.Contains(_instance)).ToList();

if (insidePolygons.Count > 1)
{
polygonsString = Helpers.GetPolygonArrayString(insidePolygons);
}

Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(insidePolygons.Count == 1)
.FailWith($"Expected point {_instance.X} {_instance.Y} to be inside only one of these polygons, but was inside {insidePolygons.Count}\n{polygonsString}");

return new AndConstraint<Vector2Assertions>(this);
}
Expand All @@ -34,7 +54,7 @@ public AndConstraint<Vector2Assertions> BeOutside(ISpatial2dShape shape, string
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(shape.Contains(_instance) is false)
.FailWith($"Expected point to be outside polygon, point was { _instance.X }, { _instance.Y }");
.FailWith($"Expected point to be outside shape, point was { _instance.X }, { _instance.Y }");

return new AndConstraint<Vector2Assertions>(this);
}
Expand Down
10 changes: 9 additions & 1 deletion src/PolyZone.Tests/PolyZone.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="GeoJSON.Text" Version="1.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
Expand All @@ -28,7 +29,14 @@
</ItemGroup>

<ItemGroup>
<Folder Include="Reference\Polygon\" />
<Content Include="..\..\data\10m_minor_islands.geojson">
<Link>Data\10m_minor_islands.geojson</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="..\..\data\10m_minor_islands_label_points.geojson">
<Link>Data\10m_minor_islands_label_points.geojson</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
25 changes: 24 additions & 1 deletion src/PolyZone.Tests/Shapes/PolygonTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
using CitizenFX.Core;
using System.IO;
using System.Linq;
using System.Text.Json;
using CitizenFX.Core;
using FluentAssertions;
using GeoJSON.Text.Feature;
using PolyZone.Shapes;
using PolyZone.Tests.Internal;

namespace PolyZone.Tests.Shapes;

public class PolygonTests
{
[Fact]
public void Polygon_EveryIslandShouldHaveOnePoint()
{
var islandsGeoJson = File.ReadAllText("./Data/10m_minor_islands.geojson");
var pointsGeoJson = File.ReadAllText("./Data/10m_minor_islands_label_points.geojson");

var islandCollection = JsonSerializer.Deserialize<FeatureCollection>(islandsGeoJson);
var pointCollection = JsonSerializer.Deserialize<FeatureCollection>(pointsGeoJson);

var islands = Helpers.GetPolygonsFromGeoCollection(islandCollection!).ToList();
var points = Helpers.GetPointsFromGeoCollection(pointCollection!).ToList();

foreach (var point in points)
{
point.Should().BeInsideOnlyOneOf(islands);
}
}


[Fact]
public void Polygon_A_IsInside_ShouldPassTest()
{
Expand Down
42 changes: 42 additions & 0 deletions src/PolyZone/Shapes/Box.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using CitizenFX.Core;
using PolyZone.Shapes.Interfaces;

namespace PolyZone.Shapes;

public class Box(float x, float y, float z, float length, float width, float height) : IBox
{
private readonly float _x = x;
private readonly float _y = y;
private readonly float _z = z;
private readonly float _length = length;
private readonly float _width = width;
private readonly float _height = height;

/// <summary>
/// Calculated corners of the <see cref="Box"/>, starts with the Upper Left
/// </summary>
public Vector3[] Corners { get; } =
[
// Upper face
new Vector3 { X = x, Y = y, Z = z }, // Upper Left (Front)
new Vector3 { X = x + length, Y = y, Z = z }, // Upper Right (Front)
new Vector3 { X = x + length, Y = y + width, Z = z }, // Lower Right (Front)
new Vector3 { X = x, Y = y + width, Z = z }, // Lower Left (Front)

// Lower face
new Vector3 { X = x, Y = y, Z = z + height }, // Upper Left (Back)
new Vector3 { X = x + length, Y = y, Z = z + height }, // Upper Right (Back)
new Vector3 { X = x + length, Y = y + width, Z = z + height }, // Lower Right (Back)
new Vector3 { X = x, Y = y + width, Z = z + height } // Lower Left (Back)
];

public Box(in Vector3 upperLeft, float length, float width, float height) : this(upperLeft.X, upperLeft.Y, upperLeft.Z, length, width, height)
{

}

public bool Contains(in Vector3 point)
{
throw new NotImplementedException();
}
}
6 changes: 6 additions & 0 deletions src/PolyZone/Shapes/Interfaces/IBox.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace PolyZone.Shapes.Interfaces;

public interface IBox : ISpatial3dShape
{

}
9 changes: 7 additions & 2 deletions src/PolyZone/Shapes/Interfaces/IPolygon.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
namespace PolyZone.Shapes.Interfaces;
using CitizenFX.Core;

namespace PolyZone.Shapes.Interfaces;

/// <summary>
/// A 2d polygon
/// </summary>
public interface IPolygon : ISpatial2dShape;
public interface IPolygon : ISpatial2dShape
{
public IReadOnlyList<Vector2> Points { get; }
}
13 changes: 13 additions & 0 deletions src/PolyZone/Shapes/Interfaces/ISpatial3dShape.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using CitizenFX.Core;

namespace PolyZone.Shapes.Interfaces;

public interface ISpatial3dShape
{
/// <summary>
/// Spatially tests a given <see cref="Vector2"/> to determine if it lies within the shape
/// </summary>
/// <param name="point"><see cref="Vector2"/>, otherwise known as a 2d position</param>
/// <returns>True if the <see cref="Vector2"/> is inside the shape</returns>
bool Contains(in Vector3 point);
}
2 changes: 1 addition & 1 deletion src/PolyZone/Shapes/Polygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace PolyZone.Shapes;
/// <param name="points">A list of <see cref="Vector2"/> in sequential order, to make-up a polygonal shape</param>
public class Polygon(IReadOnlyList<Vector2> points) : IPolygon
{
public readonly IReadOnlyList<Vector2> Points = points;
public IReadOnlyList<Vector2> Points { get; } = points;

/// <inheritdoc cref="ISpatial2dShape.Contains"/>
public bool Contains(in Vector2 point) => Contains(point, Points);
Expand Down

0 comments on commit c2b7ce9

Please sign in to comment.