module WhereTZ

WhereTZ is quick and simple time zone lookup by geographic point.

Usage:

“`ruby WhereTZ.lookup(50.004444, 36.231389) # => 'Europe/Kiev'

WhereTZ.get(50.004444, 36.231389) # => #<TZInfo::DataTimezone: Europe/Kiev> “`

Constants

FILES

@private

Public Instance Methods

get(lat, lng) click to toggle source

`TZInfo::DataTimezone` object by coordinates.

Note that you should add `tzinfo` to your Gemfile to use this method. `wheretz` doesn't depend on `tzinfo` by itself.

@param lat Latitude (floating point number) @param lng Longitude (floating point number)

@return [TZInfo::DataTimezone, nil, Array<TZInfo::DataTimezone>] timezone object or `nil` if no

timezone corresponds to (lat, lng); in rare (yet existing) cases of ambiguous timezones may
return an array of timezones
# File lib/wheretz.rb, line 57
def get(lat, lng)
  begin
    require 'tzinfo'
  rescue LoadError
    raise LoadError, 'Please install tzinfo for using #get'
  end

  name = lookup(lat, lng)
  case name
  when String
    TZInfo::Timezone.get(name)
  when Array
    name.map(&TZInfo::Timezone.method(:get))
  end
end
lookup(lat, lng) click to toggle source

Time zone name by coordinates.

@param lat Latitude (floating point number) @param lng Longitude (floating point number)

@return [String, nil, Array<String>] time zone name, or `nil` if no time zone corresponds

to (lat, lng); in rare (yet existing) cases of ambiguous timezones may return an array of names
# File lib/wheretz.rb, line 35
def lookup(lat, lng)
  candidates = FILES.select { |_f, _z, xr, yr| xr.cover?(lng) && yr.cover?(lat) }

  case candidates.size
  when 0 then nil
  when 1 then candidates.first[1]
  else
    lookup_geo(lat, lng, candidates)
  end
end

Private Instance Methods

contains_point?(polygon, (x, y)) click to toggle source
# File lib/wheretz.rb, line 113
def contains_point?(polygon, (x, y))
  # Taken from GeoRuby's Polygon#contains_point?, which just delegates to LinearRing#contains_point?
  # Polygon's geometry is just an array of linear ring; linear ring is array of points.
  polygon.any? { |points|
    [*points, points.first]
      .each_cons(2)
      .select { |(xa, ya), (xb, yb)|
        (yb > y != ya > y) && (x < (xa - xb) * (y - yb) / (ya - yb) + xb)
      }.size.odd?
  }
end
geom_from_file(fname) click to toggle source
# File lib/wheretz.rb, line 89
def geom_from_file(fname)
  JSON.parse(File.read(fname)).dig('features', 0, 'geometry')
end
guess_outside(point, geometries) click to toggle source

Last resort: pretty slow check for the cases when the point is slightly outside polygons. See github.com/zverok/wheretz/issues/4 NB: Not used currently, since switching to timezone-boundary-builder _with oceans_, there are no empty spaces anymore. Left here in case we'll switch to timezones-without-oceans dataset at some point.

# File lib/wheretz.rb, line 131
def guess_outside(point, geometries)
  # create pairs [timezone, distance to closest point of its polygon]
  distances = geometries.map { |zone, multipolygon|
    [
      zone,
      polygons(multipolygon).map(&:rings).flatten
                            .map { |p| p.ellipsoidal_distance(point) }.min
    ]
  }

  # FIXME: maybe need some tolerance range for maximal reasonable distance?

  distances.min_by(&:last).first
end
inside_multipolygon?(multipolygon, point) click to toggle source
# File lib/wheretz.rb, line 93
def inside_multipolygon?(multipolygon, point)
  polygons(multipolygon).any? { |polygon| contains_point?(polygon, point) }
end
lookup_geo(lat, lng, candidate_files) click to toggle source
# File lib/wheretz.rb, line 75
def lookup_geo(lat, lng, candidate_files)
  point = [lng, lat]

  polygons = candidate_files.map { |fname, zone, *| [zone, geom_from_file(fname)] }
  candidates = polygons.select { |_, multipolygon| inside_multipolygon?(multipolygon, point) }

  case candidates.size
  # Since switching to tz-boundary-builder, there should be no "empty" spaces anymore
  when 0 then raise ArgumentError, 'Point outside any known timezone'
  when 1 then candidates.first.first
  else candidates.map(&:first)
  end
end
polygons(geometry) click to toggle source

Previously each timezones geojson always contained multypolygon, now it can be just a simple polygon. Make it polymorphic.

If it is polygon, ['coordinates'] contains its geometry (array of linear rings, each is array of points) If it is multypolygon, ['coordinates'] is an array of such geometries

# File lib/wheretz.rb, line 102
def polygons(geometry)
  case geometry['type']
  when 'Polygon'
    [geometry['coordinates']]
  when 'MultiPolygon'
    geometry['coordinates']
  else
    raise ArgumentError, "Unsupported geometry type: #{geometry['type']}"
  end
end