Skip to content

Commit e84c9b9

Browse files
authored
Polygon - Generate Geo polygon around a specific point (#53)
* Add Polygon class with geospatial methods and tests Implemented a new static `Polygon` class in the `GeoUK` namespace for generating polygons around geographic points. The class includes input validation and methods for degree-radian conversion. Also added a suite of unit tests in `PolygonTests.cs` using Xunit to cover various scenarios, ensuring the correctness and performance of the `GeneratePolygonAroundPoint` method. * Fix longitude normalization in Polygon.cs Updated longitude normalization to the range of [-180, 180] instead of [-180, 180). Adjusted logic to correctly handle values exceeding the bounds by adding or subtracting 360.0 as necessary, ensuring proper wrapping of longitude values. * Add GeoUK.OSTN.XUnit project and update README.md - Included a new project `GeoUK.OSTN.XUnit` in the solution. - Added `pr.yml` to the Solution Items section. - Enhanced `README.md` with a new section on generating polygons around points, including code examples and a link to GeoJson.io for testing.
1 parent 573ee64 commit e84c9b9

File tree

4 files changed

+470
-0
lines changed

4 files changed

+470
-0
lines changed

GeoUK.OSTN.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ EndProject
1818
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
1919
ProjectSection(SolutionItems) = preProject
2020
.github\workflows\pr.yml = .github\workflows\pr.yml
21+
README.md = README.md
2122
.github\workflows\release.yml = .github\workflows\release.yml
2223
EndProjectSection
2324
EndProject

GeoUK/Polygon.cs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace GeoUK
5+
{
6+
public static class Polygon
7+
{
8+
/// <summary>
9+
/// WGS84 equatorial radius in meters
10+
/// </summary>
11+
const double wgs84_equatorial_radius = 6378137.0;
12+
13+
/// <summary>
14+
/// Maximum radius to prevent numerical instability (quarter of Earth's circumference)
15+
/// </summary>
16+
const double max_radius_meters = Math.PI * wgs84_equatorial_radius / 2.0;
17+
18+
/// <summary>
19+
/// Generates a polygon around a given point.
20+
/// Each point is 'radius' from the center
21+
/// </summary>
22+
/// <param name="longitude">Longitude of the center point</param>
23+
/// <param name="latitude">Latitude of the center point</param>
24+
/// <param name="radius">Radius of the polygon in meters</param>
25+
/// <param name="numberOfPoints">Number of points to make up the polygon (minimum 3)</param>
26+
/// <returns>List of [longitude, latitude] coordinate arrays forming the polygon</returns>
27+
public static List<double[]> GeneratePolygonAroundPoint(double longitude, double latitude, double radius, int numberOfPoints)
28+
{
29+
if(latitude < -90 || latitude > 90)
30+
{
31+
throw new ArgumentOutOfRangeException(nameof(latitude), "Latitude must be between -90 and 90 degrees");
32+
}
33+
34+
if(longitude < -180 || longitude > 180)
35+
{
36+
throw new ArgumentOutOfRangeException(nameof(longitude), "Longitude must be between -180 and 180 degrees");
37+
}
38+
39+
if(radius <= 0)
40+
{
41+
throw new ArgumentOutOfRangeException(nameof(radius), "Radius must be positive");
42+
}
43+
44+
if(radius > max_radius_meters)
45+
{
46+
throw new ArgumentOutOfRangeException(nameof(radius), $"Radius must not exceed {max_radius_meters:F0} meters");
47+
}
48+
49+
// Handle polar regions - near poles, create a simple circle in projected coordinates
50+
if(Math.Abs(latitude) > 89.0)
51+
{
52+
throw new ArgumentOutOfRangeException(nameof(latitude), "Geodesic calculations are unstable near the poles (latitude > ±89°)");
53+
}
54+
55+
if(numberOfPoints < 3)
56+
{
57+
numberOfPoints = 3;
58+
}
59+
60+
// Pre-allocate with exact capacity (including closing point)
61+
List<double[]> coordinates = new List<double[]>(numberOfPoints + 1);
62+
63+
// Pre-calculate common values to avoid repeated calculations
64+
double angleIncrement = 2.0 * Math.PI / numberOfPoints;
65+
double latRad = DegreesToRadians(latitude);
66+
double lonRad = DegreesToRadians(longitude);
67+
double angularDistance = radius / wgs84_equatorial_radius;
68+
69+
// Pre-calculate trigonometric values for the center point
70+
double sinLatRad = Math.Sin(latRad);
71+
double cosLatRad = Math.Cos(latRad);
72+
double sinAngularDistance = Math.Sin(angularDistance);
73+
double cosAngularDistance = Math.Cos(angularDistance);
74+
75+
for(int i = 0; i < numberOfPoints; i++)
76+
{
77+
double angle = angleIncrement * i;
78+
79+
// Calculate destination latitude
80+
double asinArg = sinLatRad * cosAngularDistance + cosLatRad * sinAngularDistance * Math.Cos(angle);
81+
asinArg = Math.Max(-1.0, Math.Min(1.0, asinArg));
82+
double lat2Rad = Math.Asin(asinArg);
83+
84+
// Calculate destination longitude
85+
double lon2Rad = lonRad + Math.Atan2(
86+
Math.Sin(angle) * sinAngularDistance * cosLatRad,
87+
cosAngularDistance - sinLatRad * Math.Sin(lat2Rad)
88+
);
89+
90+
// Convert to degrees and normalize longitude properly
91+
double lat2 = RadiansToDegrees(lat2Rad);
92+
double lon2 = RadiansToDegrees(lon2Rad);
93+
94+
// Normalize longitude to [-180, 180)
95+
if(lon2 > 180.0)
96+
{
97+
lon2 -= 360.0;
98+
}
99+
else if(lon2 <= -180.0)
100+
{
101+
lon2 += 360.0;
102+
}
103+
104+
coordinates.Add(new double[] { lon2, lat2 });
105+
}
106+
107+
// Close the polygon by repeating the first point
108+
coordinates.Add(new double[] { coordinates[0][0], coordinates[0][1] });
109+
110+
return coordinates;
111+
}
112+
113+
/// <summary>
114+
/// Converts degrees to radians.
115+
/// </summary>
116+
static double DegreesToRadians(double degrees) => degrees * Math.PI / 180.0;
117+
118+
/// <summary>
119+
/// Converts radians to degrees.
120+
/// </summary>
121+
static double RadiansToDegrees(double radians) => radians * 180.0 / Math.PI;
122+
}
123+
}

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,30 @@ Cartesian bngCartesian = Transform.Etrs89ToOsgb36(cartesian);
5151
EastingNorthing bngEN = Convert.ToEastingNorthing(new Airy1830(), new BritishNationalGrid(), bngCartesian);
5252
```
5353

54+
## Generate Polygon Around Point
55+
The `Polygon.GeneratePolygonAroundPoint` method creates a geodesically accurate polygon around a given latitude/longitude point.
56+
57+
```csharp
58+
// Generate an 8-sided polygon with 1km radius around London
59+
var polygon = Polygon.GeneratePolygonAroundPoint(
60+
longitude: -0.1276, // London longitude
61+
latitude: 51.5074, // London latitude
62+
radius: 1000, // 1000 meters (1km)
63+
numberOfPoints: 8 // 8-sided polygon
64+
);
65+
66+
// Result is List<double[]> where each array is [longitude, latitude]
67+
// The polygon is automatically closed (first point repeated at end)
68+
foreach (var coordinate in polygon)
69+
{
70+
double longitude = coordinate[0];
71+
double latitude = coordinate[1];
72+
Console.WriteLine($"Point: {longitude:F6}, {latitude:F6}");
73+
}
74+
```
75+
> GeoJson is a useful website to test the generated polygon - [GeoJson.io](http://geojson.io/)
76+
77+
5478
## Get OS Map reference
5579
The map references (Easting/Northing) used in Ordnance Survey maps are divided into 500km squares which are sub-divided into 100km squares. These squares are given a two letter code. The first letter represents the 500km square and the second represents the 100km square within it. A six digit map reference would look something like TL123456 where the first two characters represents the 100km square as indicated on the map with the first three digits of the six representing the easting and the last three digits representing the northing. Using this system means that a map reference is quoted as an easting/northing (in metres) from the square's origin. An EastingNorthing coordinate object, as returned from the transformation described above, can be converted to an OS map reference by using the Osgb36 class as follows:
5680
```csharp

0 commit comments

Comments
 (0)