Skip to content

Instantly share code, notes, and snippets.

@corstian
Created February 22, 2019 20:41
Show Gist options
  • Save corstian/8ac817cc378c56de69b43aff8cf398f2 to your computer and use it in GitHub Desktop.
Save corstian/8ac817cc378c56de69b43aff8cf398f2 to your computer and use it in GitHub Desktop.
Representing coordinates in a human readable way
<Query Kind="Program">
<Reference>&lt;RuntimeDirectory&gt;\System.IO.Compression.dll</Reference>
<Reference>&lt;RuntimeDirectory&gt;\System.IO.Compression.FileSystem.dll</Reference>
<NuGetReference>Humanizer</NuGetReference>
<NuGetReference>NetTopologySuite</NuGetReference>
<Namespace>System.IO.Compression</Namespace>
<Namespace>System.Net</Namespace>
<Namespace>System.Threading.Tasks</Namespace>
<Namespace>NetTopologySuite.Index.KdTree</Namespace>
<Namespace>GeoAPI.Geometries</Namespace>
<Namespace>NetTopologySuite.Geometries</Namespace>
<Namespace>Humanizer</Namespace>
<Namespace>System.Globalization</Namespace>
</Query>
//Coordinate coordinate = new Coordinate(51.455230, 4.374745);
//Coordinate coordinate = new Coordinate(51.465941, 4.291208);
//Coordinate coordinate = new Coordinate(49.988079, 3.382322);
//Coordinate coordinate = new Coordinate(49.823420, 2.186107);
//Coordinate coordinate = new Coordinate(51.418730, 4.294168);
Coordinate coordinate = new Coordinate(51.489144, 4.153076);
//Coordinate coordinate = new Coordinate(51.528751, 4.428284);
async void Main()
{
// We're having trouble caching the whole tree. Rather have the string cached then.
var file = await Util.Cache(async () => await GeoNames.DownloadFile(), "cities5000.zip");
var tree = new KdTree<LocationEntry>();
GeoNames.ExtractData(file)
.ToList()
.ForEach((i) => tree.Insert(new Coordinate(i.Latitude, i.Longitude), i));
var landmark = tree.NearestNeighbor(coordinate);
// We get the bearing to the city (angle of the line between two points)
var bearing = Geo.DegreeBearing(landmark.Coordinate, coordinate);
var distance = (int)(landmark.Coordinate.DistanceTo(coordinate) / 1000);
$"{distance}km {bearing.ToHeading(HeadingStyle.Full)} of {landmark.Data.Name}".Dump();
}
// Define other methods and classes here
public static class GeoNames
{
public static async Task<string> DownloadFile()
{
using (var client = new WebClient())
{
var location = Path.GetTempFileName();
await client.DownloadFileTaskAsync("http://download.geonames.org/export/dump/cities5000.zip", location);
var entry = ZipFile.OpenRead(location).Entries[0].Open();
var reader = new StreamReader(entry);
var data = await reader.ReadToEndAsync();
return data;
}
}
public static IEnumerable<LocationEntry> ExtractData(string data)
{
foreach (var line in data.Split('\n', '\r'))
{
var tabs = line.Split('\t');
if (tabs.Length != 19) continue;
yield return new LocationEntry
{
Name = tabs[1],
Latitude = Convert.ToDouble(tabs[4], CultureInfo.InvariantCulture),
Longitude = Convert.ToDouble(tabs[5], CultureInfo.InvariantCulture)
};
}
}
}
/*
* So I have pasted my own geo helpers here because I'm too lazy to figure out if NTS has these functions, too.
*
* As I'm obviously not smart enough to come up with this stuff myself, and I have pulled these methods from all
* over the web, feel free to steal.
*/
public static class Geo
{
// See https://stackoverflow.com/a/2042883/1720761 for more information about these methods.
public static double DegreeBearing(
double lat1, double lon1,
double lat2, double lon2)
{
var dLon = ToRad(lon2 - lon1);
var dPhi = Math.Log(
Math.Tan(ToRad(lat2) / 2 + Math.PI / 4) / Math.Tan(ToRad(lat1) / 2 + Math.PI / 4));
if (Math.Abs(dLon) > Math.PI)
dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon);
return ToBearing(Math.Atan2(dLon, dPhi));
}
public static double DegreeBearing(
IPoint p1,
IPoint p2)
{
return DegreeBearing(p1.X, p1.Y, p2.X, p2.Y);
}
public static double DegreeBearing(
Coordinate c1,
Coordinate c2)
{
return DegreeBearing(c1.X, c1.Y, c2.X, c2.Y);
}
public static double ToRad(this double degrees)
{
return degrees * (Math.PI / 180);
}
public static double ToDegrees(this double radians)
{
return radians * 180 / Math.PI;
}
public static double ToBearing(this double radians)
{
// convert radians to degrees (as bearing: 0...360)
return (ToDegrees(radians) + 360) % 360;
}
// We're getting a rhumb line again
public static double DistanceTo(double lat1, double long1, double lat2, double long2)
{
if (double.IsNaN(lat1) || double.IsNaN(long1) || double.IsNaN(lat2) ||
double.IsNaN(long2))
{
throw new ArgumentException("Argument latitude or longitude is not a number");
}
var d1 = lat1 * (Math.PI / 180.0);
var num1 = long1 * (Math.PI / 180.0);
var d2 = lat2 * (Math.PI / 180.0);
var num2 = long2 * (Math.PI / 180.0) - num1;
var d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) +
Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0);
return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));
}
public static double DistanceTo(this IPoint from, IPoint to)
{
if (double.IsNaN(from.X) || double.IsNaN(from.Y) || double.IsNaN(to.X) ||
double.IsNaN(to.Y))
{
throw new ArgumentException("Argument latitude or longitude is not a number");
}
var d1 = from.X * (Math.PI / 180.0);
var num1 = from.Y * (Math.PI / 180.0);
var d2 = to.X * (Math.PI / 180.0);
var num2 = to.Y * (Math.PI / 180.0) - num1;
var d3 = Math.Pow(Math.Sin((d2 - d1) / 2.0), 2.0) +
Math.Cos(d1) * Math.Cos(d2) * Math.Pow(Math.Sin(num2 / 2.0), 2.0);
return 6376500.0 * (2.0 * Math.Atan2(Math.Sqrt(d3), Math.Sqrt(1.0 - d3)));
}
public static double DistanceTo(this Coordinate from, Coordinate to) {
return new Point(from.X, from.Y).DistanceTo(new Point(to.X, to.Y));
}
}
public class LocationEntry
{
public string Name { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment