PROWAREtech

articles » current » blazor » wasm » reverse-geocode

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);
	}
}

This site uses cookies. Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site. Read the privacy policy.
CLOSE