Monday, March 3, 2014

Finding TimeZone by Latitude / Longitude using a TimeZone Shapefile with .NET

Updated to version: 1.0.0.2


A Time Zone is a region that has a uniform standard time for legal, commercial, and social purposes. It is convenient for areas in close commercial or other communication to keep the same time, so time zones tend to follow the boundaries of countries and their subdivisions.

The code snippet proposed here can be used to finding TimeZone by geographical coordinate Latitude / Longitude using a TimeZone Shapefile database on .NET framework

The TimeZone shapefile used for conversion is:
All the library used are open source, also you can find License for those library inside the sample project attached to this post:
At first we need to load all the Geometries contained in the shapefile, and the TimeZone associated, we build a structure used as the container for all data:

private List<GetTimeZones> m_geotimezones = new List<GetTimeZones>();

private class GetTimeZones
{
  public string timezone { get; set; }
  public GeoAPI.Geometries.IGeometry[] geometry { get; set; }
}

Then we load all the data to this container:

public FindTimeZoneByLocation(string shapeFileName)
{
  try
  {
    List<NetTopologySuite.Features.Feature> featureCollection = new List<NetTopologySuite.Features.Feature>();

    //built feature collection from shapefile
    NetTopologySuite.IO.ShapefileDataReader dataReader = new NetTopologySuite.IO.ShapefileDataReader(shapeFileName, new NetTopologySuite.Geometries.GeometryFactory());
    while (dataReader.Read())
    {
      //read current feature
      NetTopologySuite.Features.Feature feature = new NetTopologySuite.Features.Feature();
      feature.Geometry = dataReader.Geometry;
      //get feature keys
      int length = dataReader.DbaseHeader.NumFields;
      string[] keys = new string[length];
      for (int i = 0; i < length; i++)
        keys[i] = dataReader.DbaseHeader.Fields[i].Name;
      //get features attributes
      feature.Attributes = new NetTopologySuite.Features.AttributesTable();
      for (int i = 0; i < length; i++)
      {
        object val = dataReader.GetValue(i);
        feature.Attributes.AddAttribute(keys[i], val);
      }
      //add feature to collection
      featureCollection.Add(feature);
    }
    
    GeoAPI.Geometries.IGeometryFactory gf = GeoAPI.GeometryServiceProvider.Instance.CreateGeometryFactory();
    //built timezones array
    foreach (NetTopologySuite.Features.Feature feature in featureCollection)
    {
      try
      {
        //get timezone
        NetTopologySuite.Features.AttributesTable table = (NetTopologySuite.Features.AttributesTable)feature.Attributes;
        string timezone = table["TZID"].ToString();

        //set getometry
        GeoAPI.Geometries.IGeometry geometry = feature.Geometry;

        bool addnew = true;
        foreach (GetTimeZones geotimezone in m_geotimezones)
        {
          //add geometry to existing timezone
          if (geotimezone.timezone.CompareTo(timezone) == 0)
          {
            addnew = false;
            geotimezone.geometry = geotimezone.geometry.Concat<GeoAPI.Geometries.IGeometry>(new[] { geometry }).ToArray();
          }
        }
        //add a new timezone and his first geometry
        if (addnew)
        {
          GetTimeZones addgeotimezone = new GetTimeZones();
          addgeotimezone.timezone = timezone;
          addgeotimezone.geometry = new[] { geometry };
          m_geotimezones.Add(addgeotimezone);
        }
      }
      catch { }
    }            
  }
  catch { }
}

Then the function that finds if a point, given by latitude / longitude, it also search for the points at a given distance it the exact point is not found, this is usefull for location next to the sea.

public string GetIanaTZName(double latitude, double longitude)
{
  string ret = "";
  
  //make search point
  GeoAPI.Geometries.Coordinate point = new GeoAPI.Geometries.Coordinate(longitude, latitude);

  //loop through zones and search for a point insize the zone
  foreach (GetTimeZones geotimezone in m_geotimezones)
  {
    foreach (GeoAPI.Geometries.IGeometry geometry in geotimezone.geometry)
    {
      try
      {
        if(NetTopologySuite.Algorithm.Locate.SimplePointInAreaLocator.Locate(point, geometry).Equals(GeoAPI.Geometries.Location.Interior))
        {
          ret = geotimezone.timezone;
          return ret;
        }
      }
      catch { }
    }
  }
  
  double distance = 0;
  //set search distance for unfound point
  distance = 0.01;
  if (ret == "")
  {
    foreach (GetTimeZones geotimezone in m_geotimezones)
    {
      foreach (GeoAPI.Geometries.IGeometry geometry in geotimezone.geometry)
      {
        try
        {
          NetTopologySuite.Algorithm.Distance.PointPairDistance ptdist = new NetTopologySuite.Algorithm.Distance.PointPairDistance();
          NetTopologySuite.Algorithm.Distance.DistanceToPoint.ComputeDistance(geometry, point, ptdist);
          if (ptdist.Distance < distance)
          {
            ret = geotimezone.timezone;
            return ret;
          }
        }
        catch { }
      }
    }
  }

  return ret;
}

Attacched a code sample that test the code over more than 5000 points (airports).

Contain also a Iana to Windows TimezoneId conversion based on the http://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml conversion file.


Changelog

  • 1.0.0.2: added the IanaToWindows Timezone Id converter
  • 1.0.0.1: first release

Code

Notes
  • read risk disclaimer
  • excuse my bad english

2 comments:

  1. Beautiful, concise code!

    The solution compiled and run without issue - sadly not always the case these days!

    I wondered - is there a way of finding the time offset for a zone? I'm writing a program that produces a sunrise/sunset chart for the year at a given lat/long, but the calculations (aCos out of range) go squify for absolute longitudes > 100. So I've been looking for a solution that provides the time offset. (-11 to +11 hours)

    Could you spare a few minutes to help me out?

    ReplyDelete
    Replies
    1. Hello, you can find an update versione (1.0.0.2), which contains also a Iana to Windows timezone Id. Once you have converted the iana to windows timezone using the Find method from IanaToWindowsTimeZoneMapper class, you can get offset by calling TimeZoneInfo timezone = TimeZoneInfo.FindSystemTimeZoneById(yourwindowsidstring);
      var offset = timezone.BaseUtcOffset;

      Delete