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

PROWAREtech

Hello there! How can I help you today?
Ask any question

PROWAREtech

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