PROWAREtech
Blazor: Reverse Geocode
Use latitude and longitude to reverse-geocode to country, US state/Canadian province and time zone in a Blazor WebAssembly (WASM) application; written in C#.
Being online is not required as it is with most reverse geocoding API, plus there is no need to pay a service.
To minimize memory usage, use only the service required. the time zone database is the largest and requires the most memory.
Note: this code was inspired by GeoTimeZone originally written by Matt Johnson-Pint and Simon Bartlett.
Download these files including the compiled assembly and geocode data files: REVERSEGEOCODE.zip.
This code has been tested with .NET 6 and .NET 8.
Example Blazor WASM usage:
@page "/geocode"
<PageTitle>Reverse Geocode</PageTitle>
<h1>Reverse Geocode</h1>
<pre>@result</pre>
<p><input type="number" @bind-value="lat" placeholder="latitude..." /></p>
<p><input type="number" @bind-value="lng" placeholder="longitude..." /></p>
<p><button class="btn btn-primary" @onclick="RevGeo">Reverse Geocode</button></p>
<pre>
49, -124 - Canada
48.287043, -124.581337 - United States of America, Washington
34.034145, 65.964733 - Islamic State of Afghanistan
50.449864, -105.077861 - Canada
30, -97 - United States of America, Texas
20, -100 - United Mexican States
10.243853, -61.148837 - Republic of Trinidad and Tobago
57.229295, -101.879814 - Canada
</pre>
@code {
private double? lat, lng;
private string result;
private void RevGeo()
{
if(lat == null || lng == null || lat < -90 || lat > 90 || lng < -180 || lng > 180)
{
result = "Enter a latitude (-90.0000° to 90.0000°) and longitude (-180.0000° to 180.0000°)";
}
else
{
var co = ReverseGeocode.CountryLookup.GetCountry(lat ?? 0, lng ?? 0);
if(co == "United States of America")
{
co += ", " + ReverseGeocode.UsStateLookup.GetUsState(lat ?? 0, lng ?? 0);
}
else if(co == "Canada")
{
co += ", " + ReverseGeocode.CaProvinceLookup.GetCaProvince(lat ?? 0, lng ?? 0);
}
result = (co ?? "not found") + "\r\n" + ReverseGeocode.TimeZoneLookup.GetTimeZone(lat ?? 0, lng ?? 0).Result;
}
}
}
Now, the class files for the assembly:
// CountryLookup.cs
using System.IO.Compression;
namespace ReverseGeocode;
/// <summary>
/// Provides the country lookup functionality.
/// </summary>
public static class CountryLookup
{
internal class Properties
{
public string? formal_en { get; set; }
public string? name_sort { get; set; }
}
internal class Geometry
{
public List<List<double[]>> coordinates { get; set; }
}
internal class Country
{
public Properties properties { get; set; }
public Geometry geometry { get; set; }
}
internal class Find
{
public string? name;
public double proximity;
}
private static readonly Lazy<List<Country>> LazyData = new(LoadData);
private static List<Country> LoadData()
{
var assembly = typeof(CountryLookup).Assembly;
using var compressedStream = assembly.GetManifestResourceStream("ReverseGeocode.COUNTRIES.json.gz");
using var stream = new GZipStream(compressedStream!, CompressionMode.Decompress);
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
return System.Text.Json.JsonSerializer.Deserialize<List<Country>>(System.Text.Encoding.UTF8.GetString(ms.ToArray())) ?? new List<Country>();
}
}
private static string? FormatCountryName(Country country)
{
if (country.properties.formal_en != null && country.properties.name_sort != null && country.properties.formal_en != country.properties.name_sort)
return country.properties.formal_en + '/' + country.properties.name_sort;
else
return country.properties.formal_en ?? country.properties.name_sort;
}
/// <summary>
/// Determines the country for given location coordinates.
/// </summary>
/// <param name="latitude">The latitude of the location.</param>
/// <param name="longitude">The longitude of the location.</param>
/// <returns>A <string?>, which contains the result of the operation or null if not found.</returns>
public static string? GetCountry(double latitude, double longitude)
{
var p = new double[2] { latitude, longitude };
foreach (var country in LazyData.Value)
{
foreach (var list in country.geometry.coordinates)
{
if (Common.IsCoordInsideArrayOfCoords(list, p))
return FormatCountryName(country);
}
}
return null;
}
/// <summary>
/// Determines the nearest country for given location coordinates.
/// </summary>
/// <param name="latitude">The latitude of the location.</param>
/// <param name="longitude">The longitude of the location.</param>
/// <returns>A <string>, which contains the result of the operation.</returns>
public static string GetNearestCountry(double latitude, double longitude)
{
var finds = new List<Find>();
foreach (var country in LazyData.Value)
{
foreach (var list in country.geometry.coordinates)
{
foreach (var coords in list)
finds.Add(new Find { name = FormatCountryName(country), proximity = (coords[0] - latitude) * (coords[0] - latitude) + (coords[1] - longitude) * (coords[1] - longitude) });
}
}
var find = finds.OrderBy(x => x.proximity).First();
return find.name ?? "n/a";
}
}
// UsStateLookup.cs
using System.IO.Compression;
namespace ReverseGeocode;
/// <summary>
/// Provides the US state lookup functionality.
/// </summary>
public static class UsStateLookup
{
internal class Geometry
{
public List<List<double[]>> coordinates { get; set; }
}
internal class UsState
{
public string name { get; set; }
public Geometry geometry { get; set; }
}
private static readonly Lazy<List<UsState>> LazyData = new(LoadData);
private static List<UsState> LoadData()
{
var assembly = typeof(UsStateLookup).Assembly;
using var compressedStream = assembly.GetManifestResourceStream("ReverseGeocode.USSTATES.json.gz");
using var stream = new GZipStream(compressedStream!, CompressionMode.Decompress);
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
return System.Text.Json.JsonSerializer.Deserialize<List<UsState>>(System.Text.Encoding.UTF8.GetString(ms.ToArray())) ?? new List<UsState>();
}
}
/// <summary>
/// Determines the US state for given location coordinates.
/// </summary>
/// <param name="latitude">The latitude of the location.</param>
/// <param name="longitude">The longitude of the location.</param>
/// <returns>A <string?>, which contains the result of the operation or null if not found.</returns>
public static string? GetUsState(double latitude, double longitude)
{
var p = new double[2] { latitude, longitude };
foreach (var state in LazyData.Value)
{
foreach (var list in state.geometry.coordinates)
{
if (Common.IsCoordInsideArrayOfCoords(list, p))
return state.name;
}
}
return null;
}
}
// CaProvinceLookup.cs
using System.IO.Compression;
namespace ReverseGeocode;
/// <summary>
/// Provides the Canadian province lookup functionality.
/// </summary>
public static class CaProvinceLookup
{
internal class Geometry
{
public List<List<double[]>> coordinates { get; set; }
}
internal class CaProvince
{
public string name_en { get; set; }
public string prov_type { get; set; }
public Geometry geometry { get; set; }
}
private static readonly Lazy<List<CaProvince>> LazyData = new(LoadData);
private static List<CaProvince> LoadData()
{
var assembly = typeof(CaProvinceLookup).Assembly;
using var compressedStream = assembly.GetManifestResourceStream("ReverseGeocode.CAPROVINCES.json.gz");
using var stream = new GZipStream(compressedStream!, CompressionMode.Decompress);
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
return System.Text.Json.JsonSerializer.Deserialize<List<CaProvince>>(System.Text.Encoding.UTF8.GetString(ms.ToArray())) ?? new List<CaProvince>();
}
}
/// <summary>
/// Determines the Canadian province/territory for given location coordinates.
/// </summary>
/// <param name="latitude">The latitude of the location.</param>
/// <param name="longitude">The longitude of the location.</param>
/// <returns>A <string?>, which contains the result of the operation or null if not found.</returns>
public static string? GetCaProvince(double latitude, double longitude)
{
var p = new double[2] { latitude, longitude };
foreach (var state in LazyData.Value)
{
foreach (var list in state.geometry.coordinates)
{
if (Common.IsCoordInsideArrayOfCoords(list, p))
return state.name_en;
}
}
return null;
}
}
// Geohash.cs
namespace ReverseGeocode;
internal static class Geohash
{
internal const int Precision = 5;
private const string Base32 = "0123456789bcdefghjkmnpqrstuvwxyz";
private static readonly int[] Bits = {16, 8, 4, 2, 1};
public static void Encode(double latitude, double longitude, Span<byte> geohash)
{
var even = true;
var bit = 0;
var ch = 0;
var length = 0;
Span<double> lat = stackalloc[] {-90.0, 90.0};
Span<double> lon = stackalloc[] {-180.0, 180.0};
while (length < Precision)
{
if (even)
{
var mid = (lon[0] + lon[1]) / 2;
if (longitude > mid)
{
ch |= Bits[bit];
lon[0] = mid;
}
else
{
lon[1] = mid;
}
}
else
{
var mid = (lat[0] + lat[1]) / 2;
if (latitude > mid)
{
ch |= Bits[bit];
lat[0] = mid;
}
else
{
lat[1] = mid;
}
}
even = !even;
if (bit < 4)
{
bit++;
}
else
{
geohash[length] = (byte) Base32[ch];
length++;
bit = 0;
ch = 0;
}
}
}
}
// TimezoneFileReader.cs
using System.IO.Compression;
namespace ReverseGeocode;
internal static class TimezoneFileReader
{
private const int LineLength = 8;
private const int LineEndLength = 1;
private static readonly Lazy<MemoryStream> LazyData = new(LoadData);
private static readonly Lazy<int> LazyCount = new(GetCount);
private static MemoryStream LoadData()
{
var assembly = typeof(TimezoneFileReader).Assembly;
using var compressedStream = assembly.GetManifestResourceStream("ReverseGeocode.TZ.txt.gz");
using var stream = new GZipStream(compressedStream!, CompressionMode.Decompress);
var ms = new MemoryStream();
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
return ms;
}
private static int GetCount() => (int) (LazyData.Value.Length / (LineLength + LineEndLength));
public static int Count => LazyCount.Value;
public static ReadOnlySpan<byte> GetGeohash(int line) => GetLine(line, 0, Geohash.Precision);
public static int GetLineNumber(int line)
{
var digits = GetLine(line, Geohash.Precision, LineLength - Geohash.Precision);
return GetDigit(digits[2]) + ((GetDigit(digits[1]) + (GetDigit(digits[0]) * 10)) * 10);
}
private static int GetDigit(byte b) => b - '0';
private static ReadOnlySpan<byte> GetLine(int line, int start, int count)
{
var index = ((LineLength + LineEndLength) * (line - 1)) + start;
var stream = LazyData.Value;
return new ReadOnlySpan<byte>(stream.GetBuffer(), index, count);
}
}
// TimeZoneLookup.cs
using System.IO.Compression;
namespace ReverseGeocode;
/// <summary>
/// Provides the time zone lookup functionality.
/// </summary>
public static class TimeZoneLookup
{
/// <summary>
/// Determines the IANA time zone for given location coordinates.
/// </summary>
/// <param name="latitude">The latitude of the location.</param>
/// <param name="longitude">The longitude of the location.</param>
/// <returns>A <see cref="TimeZoneResult"/> object, which contains the result(s) of the operation.</returns>
public static TimeZoneResult GetTimeZone(double latitude, double longitude)
{
Span<byte> geohash = stackalloc byte[Geohash.Precision];
Geohash.Encode(latitude, longitude, geohash);
var lineNumbers = GetTzDataLineNumbers(geohash);
if (lineNumbers.Length != 0)
{
var timeZones = GetTimeZonesFromData(lineNumbers);
return new TimeZoneResult(timeZones);
}
var offsetHours = CalculateOffsetHoursFromLongitude(longitude);
return new TimeZoneResult(GetTimeZoneId(offsetHours));
}
private static int[] GetTzDataLineNumbers(ReadOnlySpan<byte> geohash)
{
var seeked = SeekTimeZoneFile(geohash);
if (seeked == 0)
return Array.Empty<int>();
int min = seeked, max = seeked;
var seekedGeohash = TimezoneFileReader.GetGeohash(seeked);
while (true)
{
var prevGeohash = TimezoneFileReader.GetGeohash(min - 1);
if (GeohashEquals(seekedGeohash, prevGeohash))
min--;
else
break;
}
while (true)
{
var nextGeohash = TimezoneFileReader.GetGeohash(max + 1);
if (GeohashEquals(seekedGeohash, nextGeohash))
max++;
else
break;
}
var lineNumbers = new int[max - min + 1];
for (var i = 0; i < lineNumbers.Length; i++)
lineNumbers[i] = TimezoneFileReader.GetLineNumber(i + min);
return lineNumbers;
}
private static bool GeohashEquals (ReadOnlySpan<byte> a, ReadOnlySpan<byte> b)
{
var equals = true;
for (var i = Geohash.Precision - 1; i >= 0; i--)
equals &= a[i] == b[i];
return equals;
}
private static int SeekTimeZoneFile(ReadOnlySpan<byte> hash)
{
var min = 1;
var max = TimezoneFileReader.Count;
var converged = false;
while (true)
{
var mid = ((max - min) / 2) + min;
var midLine = TimezoneFileReader.GetGeohash(mid);
for (var i = 0; i < hash.Length; i++)
{
if (midLine[i] == '-')
return mid;
if (midLine[i] > hash[i])
{
max = mid == max ? min : mid;
break;
}
if (midLine[i] < hash[i])
{
min = mid == min ? max : mid;
break;
}
if (i == 4)
return mid;
if (min == mid)
{
min = max;
break;
}
}
if (min == max)
{
if (converged)
break;
converged = true;
}
}
return 0;
}
private static readonly Lazy<IList<string>> LookupData = new(LoadLookupData);
private static IList<string> LoadLookupData()
{
var assembly = typeof(TimeZoneLookup).Assembly;
using var compressedStream = assembly.GetManifestResourceStream("ReverseGeocode.TZL.txt.gz");
using var stream = new GZipStream(compressedStream!, CompressionMode.Decompress);
using var reader = new StreamReader(stream);
var list = new List<string>();
while (reader.ReadLine() is { } line)
list.Add(line);
return list;
}
private static List<string> GetTimeZonesFromData(int[] lineNumbers)
{
var lookupData = LookupData.Value;
var timezones = new List<string>(lineNumbers.Length);
Array.Sort(lineNumbers);
foreach (var lineNumber in lineNumbers)
timezones.Add(lookupData[lineNumber - 1]);
return timezones;
}
private static int CalculateOffsetHoursFromLongitude(double longitude)
{
var dir = longitude < 0 ? -1 : 1;
var posNo = Math.Abs(longitude);
if (posNo <= 7.5)
return 0;
posNo -= 7.5;
var offset = posNo / 15;
if (posNo % 15 > 0)
{
offset++;
}
return dir * (int) Math.Floor(offset);
}
private static string GetTimeZoneId(int offsetHours)
{
if (offsetHours == 0)
return "UTC";
var reversed = (offsetHours >= 0 ? "-" : "+") + Math.Abs(offsetHours);
return "Etc/GMT" + reversed;
}
}
// TimeZoneResult.cs
using System.Collections.ObjectModel;
namespace ReverseGeocode;
/// <summary>
/// Contains the result of a time zone lookup operation.
/// </summary>
public class TimeZoneResult
{
internal TimeZoneResult(List<string> timeZones)
{
Result = timeZones[0];
AlternativeResults = new ReadOnlyCollection<string>(timeZones.GetRange(1, timeZones.Count - 1));
}
internal TimeZoneResult(string timeZone)
{
Result = timeZone;
AlternativeResults = new ReadOnlyCollection<string>(new List<string>());
}
/// <summary>
/// Gets the primary result of the time zone lookup operation.
/// </summary>
public string Result { get; }
/// <summary>
/// Gets any alternative results of the time zone lookup operation.
/// This usually happens very close to borders between time zones.
/// </summary>
public ReadOnlyCollection<string> AlternativeResults { get; }
}
// Common.cs
namespace ReverseGeocode;
public class Common
{
public static bool IsCoordInsideArrayOfCoords(List<double[]> polygon, double[] p)
{
int counter = 0;
int i, N = polygon.Count;
double xinters;
double[] p1, p2;
const int x = 0, y = 1;
p1 = polygon[0];
for (i = 1; i <= N; i++)
{
p2 = polygon[i % N];
if (p[y] > Math.Min(p1[y], p2[y]))
{
if (p[y] <= Math.Max(p1[y], p2[y]))
{
if (p[x] <= Math.Max(p1[x], p2[x]))
{
if (p1[y] != p2[y])
{
xinters = (p[y] - p1[y]) * (p2[x] - p1[x]) / (p2[y] - p1[y]) + p1[x];
if (p1[x] == p2[x] || p[x] <= xinters)
counter++;
}
}
}
}
p1 = p2;
}
return (counter % 2 != 0);
}
}
Comment