timezone_finder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module TimezoneFinder
2
+ VERSION = '0.0.1' unless defined? TimezoneFinder::Version
3
+ # https://github.com/MrMinimal64/timezonefinder
4
+ BASED_SHA1_OF_PYTHON = '55861df0d1ae6c4727e7f48068f7cc51ea3891b3'
5
+ end
@@ -0,0 +1,357 @@
1
+ # rubocop:disable Metrics/ClassLength,Metrics/MethodLength,Metrics/LineLength
2
+ # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/ParameterLists
3
+ # rubocop:disable Style/PredicateName,Style/Next
4
+ # rubocop:disable Lint/Void
5
+ module TimezoneFinder
6
+ class Helpers
7
+ # tests if a point pX(x,y) is Left|On|Right of an infinite line from p1 to p2
8
+ # Return: 2 for pX left of the line from! p1 to! p2
9
+ # 1 for pX on the line
10
+ # 0 for pX right of the line
11
+ # this approach is only valid because we already know that y lies within ]y1;y2]
12
+ def self.position_to_line(x, y, x1, x2, y1, y2)
13
+ if x1 > x2
14
+ # p1 is further right than p2
15
+ if x > x1
16
+ # pX is further right than p1,
17
+ if y1 > y2
18
+ # so it has to be left of the line p1-p2
19
+ return 2
20
+ else
21
+ return 0
22
+ end
23
+ end
24
+
25
+ if x < x2
26
+ # pX is further left than p2,
27
+ if y1 > y2
28
+ # so it has to be right of the line p1-p2
29
+ return 0
30
+ else
31
+ return 2
32
+ end
33
+ end
34
+
35
+ # x1 greater than x2
36
+ x1gtx2 = True
37
+
38
+ elsif x1 == x2
39
+ # this is a vertical line, the position of pX is also determined by y1 and y2
40
+
41
+ if y1 > y2
42
+
43
+ return 2 if x > x1
44
+ if x == x1
45
+ return 1
46
+ else
47
+ return 0
48
+ end
49
+
50
+ else
51
+ return 0 if x > x1
52
+ if x == x1
53
+ return 1
54
+ else
55
+ return 2
56
+ end
57
+ end
58
+
59
+ else
60
+ # p2 is further right than p1
61
+
62
+ if x > x2
63
+ # pX is further right than p2,
64
+ if y1 > y2
65
+ return 2
66
+ else
67
+ return 0
68
+ end
69
+ end
70
+
71
+ if x < x1
72
+ # pX is further left than p1
73
+ if y1 > y2
74
+ # so it has to be right of the line p1-p2
75
+ return 0
76
+ else
77
+ return 2
78
+ end
79
+ end
80
+
81
+ x1gtx2 = False
82
+ end
83
+
84
+ # x is between [x1;x2]
85
+ # compute the x-intersection of the point with the line p1-p2
86
+ # delta_y = cannot be 0 here because of the condition 'y lies within ]y1;y2]'
87
+ # NOTE: bracket placement is important here (we are dealing with 64-bit ints!). first divide then multiply!
88
+ x_difference = ((y - y1) * ((x2 - x1).fdiv(y2 - y1))) + x1 - x
89
+
90
+ if x_difference > 0
91
+ if x1gtx2
92
+ if y1 > y2
93
+ return 0
94
+ else
95
+ return 2
96
+ end
97
+
98
+ else
99
+ if y1 > y2
100
+ return 0
101
+ else
102
+ return 2
103
+ end
104
+ end
105
+
106
+ elsif x_difference == 0
107
+ return 1
108
+
109
+ else
110
+ if x1gtx2
111
+ if y1 > y2
112
+ return 2
113
+ else
114
+ return 0
115
+ end
116
+
117
+ else
118
+ if y1 > y2
119
+ return 2
120
+ else
121
+ return 0
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def self.inside_polygon(x, y, coords)
128
+ wn = 0
129
+ i = 0
130
+ y1 = coords[1][0]
131
+ # TODO: why start with both y1=y2= y[0]?
132
+ coords[1].each do |y2|
133
+ if y1 < y
134
+ if y2 >= y
135
+ x1 = coords[0][i - 1]
136
+ x2 = coords[0][i]
137
+ # print(long2coord(x), long2coord(y), long2coord(x1), long2coord(x2), long2coord(y1), long2coord(y2),
138
+ # position_to_line(x, y, x1, x2, y1, y2))
139
+ if position_to_line(x, y, x1, x2, y1, y2) == 2
140
+ # point is left of line
141
+ # return true when its on the line?! this is very unlikely to happen!
142
+ # and would need to be checked every time!
143
+ wn += 1
144
+ end
145
+ end
146
+ else
147
+ if y2 < y
148
+ x1 = coords[0][i - 1]
149
+ x2 = coords[0][i]
150
+ if position_to_line(x, y, x1, x2, y1, y2) == 0
151
+ # point is right of line
152
+ wn -= 1
153
+ end
154
+ end
155
+ end
156
+
157
+ y1 = y2
158
+ i += 1
159
+ end
160
+
161
+ y1 = coords[1][-1]
162
+ y2 = coords[1][0]
163
+ if y1 < y
164
+ if y2 >= y
165
+ x1 = coords[0][-1]
166
+ x2 = coords[0][0]
167
+ if position_to_line(x, y, x1, x2, y1, y2) == 2
168
+ # point is left of line
169
+ wn += 1
170
+ end
171
+ end
172
+ else
173
+ if y2 < y
174
+ x1 = coords[0][-1]
175
+ x2 = coords[0][0]
176
+ if position_to_line(x, y, x1, x2, y1, y2) == 0
177
+ # point is right of line
178
+ wn -= 1
179
+ end
180
+ end
181
+ end
182
+ wn != 0
183
+ end
184
+
185
+ def self.cartesian2rad(x, y, z)
186
+ [Math.atan2(y, x), Math.asin(z)]
187
+ end
188
+
189
+ def self.radians(x)
190
+ x * Math::PI / 180.0
191
+ end
192
+
193
+ def self.degrees(x)
194
+ x * 180.0 / Math::PI
195
+ end
196
+
197
+ def self.cartesian2coords(x, y, z)
198
+ [degrees(Math.atan2(y, x)), degrees(Math.asin(z))]
199
+ end
200
+
201
+ def self.x_rotate(rad, point)
202
+ # Attention: this rotation uses radians!
203
+ # x stays the same
204
+ sin_rad = Math.sin(rad)
205
+ cos_rad = Math.cos(rad)
206
+ [point[0], point[1] * cos_rad + point[2] * sin_rad, point[2] * cos_rad - point[1] * sin_rad]
207
+ end
208
+
209
+ def self.y_rotate(degree, point)
210
+ # y stays the same
211
+ degree = radians(-degree)
212
+ sin_rad = Math.sin(degree)
213
+ cos_rad = Math.cos(degree)
214
+ [point[0] * cos_rad - point[2] * sin_rad, point[1], point[0] * sin_rad + point[2] * cos_rad]
215
+ end
216
+
217
+ def self.coords2cartesian(lng, lat)
218
+ lng = radians(lng)
219
+ lat = radians(lat)
220
+ [Math.cos(lng) * Math.cos(lat), Math.sin(lng) * Math.cos(lat), Math.sin(lat)]
221
+ end
222
+
223
+ # uses the simplified haversine formula for this special case
224
+ # :param lng_rad: the longitude of the point in radians
225
+ # :param lat_rad: the latitude of the point
226
+ # :param lng_rad_p1: the latitude of the point1 on the equator (lat=0)
227
+ # :return: distance between the point and p1 (lng_rad_p1,0) in radians
228
+ def self.distance_to_point_on_equator(lng_rad, lat_rad, lng_rad_p1)
229
+ 2 * Math.asin(Math.sqrt((Math.sin(lat_rad) / 2)**2 + Math.cos(lat_rad) * Math.sin((lng_rad - lng_rad_p1) / 2.0)**2))
230
+ end
231
+
232
+ # :param lng_p1: the longitude of point 1 in radians
233
+ # :param lat_p1: the latitude of point 1 in radians
234
+ # :param lng_p2: the longitude of point 1 in radians
235
+ # :param lat_p2: the latitude of point 1 in radians
236
+ # :return: distance between p1 and p2 in radians
237
+ def self.haversine(lng_p1, lat_p1, lng_p2, lat_p2)
238
+ 2 * Math.asin(Math.sqrt(Math.sin((lat_p1 - lat_p2) / 2.0)**2 + Math.cos(lat_p2) * Math.cos(lat_p1) * Math.sin((lng_p1 - lng_p2) / 2.0)**2))
239
+ end
240
+
241
+ # :param lng: lng of px in degree
242
+ # :param lat: lat of px in degree
243
+ # :param p0_lng: lng of p0 in degree
244
+ # :param p0_lat: lat of p0 in degree
245
+ # :param pm1_lng: lng of pm1 in degree
246
+ # :param pm1_lat: lat of pm1 in degree
247
+ # :param p1_lng: lng of p1 in degree
248
+ # :param p1_lat: lat of p1 in degree
249
+ # :return: shortest distance between pX and the polygon section (pm1---p0---p1) in radians
250
+ def self.compute_min_distance(lng, lat, p0_lng, p0_lat, pm1_lng, pm1_lat, p1_lng, p1_lat)
251
+ # rotate coordinate system (= all the points) so that p0 would have lat=lng=0 (=origin)
252
+ # z rotation is simply substracting the lng
253
+ # convert the points to the cartesian coorinate system
254
+ px_cartesian = coords2cartesian(lng - p0_lng, lat)
255
+ p1_cartesian = coords2cartesian(p1_lng - p0_lng, p1_lat)
256
+ pm1_cartesian = coords2cartesian(pm1_lng - p0_lng, pm1_lat)
257
+
258
+ px_cartesian = y_rotate(p0_lat, px_cartesian)
259
+ p1_cartesian = y_rotate(p0_lat, p1_cartesian)
260
+ pm1_cartesian = y_rotate(p0_lat, pm1_cartesian)
261
+
262
+ # for both p1 and pm1 separately do:
263
+
264
+ # rotate coordinate system so that this point also has lat=0 (p0 does not change!)
265
+ rotation_rad = Math.atan2(p1_cartesian[2], p1_cartesian[1])
266
+ p1_cartesian = x_rotate(rotation_rad, p1_cartesian)
267
+ lng_p1_rad = Math.atan2(p1_cartesian[1], p1_cartesian[0])
268
+ px_retrans_rad = cartesian2rad(*x_rotate(rotation_rad, px_cartesian))
269
+
270
+ # if lng of px is between 0 (<-point1) and lng of point 2:
271
+ # the distance between point x and the 'equator' is the shortest
272
+ # if the point is not between p0 and p1 the distance to the closest of the two points should be used
273
+ # so clamp/clip the lng of px to the interval of [0; lng p1] and compute the distance with it
274
+ temp_distance = distance_to_point_on_equator(px_retrans_rad[0], px_retrans_rad[1],
275
+ [[px_retrans_rad[0], lng_p1_rad].min, 0].max)
276
+
277
+ # ATTENTION: vars are being reused. p1 is actually pm1 here!
278
+ rotation_rad = Math.atan2(pm1_cartesian[2], pm1_cartesian[1])
279
+ p1_cartesian = x_rotate(rotation_rad, pm1_cartesian)
280
+ lng_p1_rad = Math.atan2(p1_cartesian[1], p1_cartesian[0])
281
+ px_retrans_rad = cartesian2rad(*x_rotate(rotation_rad, px_cartesian))
282
+
283
+ [
284
+ temp_distance,
285
+ distance_to_point_on_equator(px_retrans_rad[0], px_retrans_rad[1],
286
+ [[px_retrans_rad[0], lng_p1_rad].min, 0].max)
287
+ ].min
288
+ end
289
+
290
+ def self.int2coord(int32)
291
+ int32.fdiv(10**7)
292
+ end
293
+
294
+ def self.coord2int(double)
295
+ (double * 10**7).to_i
296
+ end
297
+
298
+ def self.distance_to_polygon(lng, lat, nr_points, points, trans_points)
299
+ # transform all points (long long) to coords
300
+ (0...nr_points).each do |i|
301
+ trans_points[0][i] = int2coord(points[0][i])
302
+ trans_points[1][i] = int2coord(points[1][i])
303
+ end
304
+
305
+ # check points -2, -1, 0 first
306
+ pm1_lng = trans_points[0][0]
307
+ pm1_lat = trans_points[1][0]
308
+
309
+ p1_lng = trans_points[0][-2]
310
+ p1_lat = trans_points[1][-2]
311
+ min_distance = compute_min_distance(lng, lat, trans_points[0][-1], trans_points[1][-1], pm1_lng, pm1_lat, p1_lng,
312
+ p1_lat)
313
+
314
+ index_p0 = 1
315
+ index_p1 = 2
316
+ (0...(((nr_points / 2.0) - 1).ceil.to_i)).each do |_i|
317
+ p1_lng = trans_points[0][index_p1]
318
+ p1_lat = trans_points[1][index_p1]
319
+
320
+ distance = compute_min_distance(lng, lat, trans_points[0][index_p0], trans_points[1][index_p0], pm1_lng,
321
+ pm1_lat, p1_lng, p1_lat)
322
+ min_distance = distance if distance < min_distance
323
+
324
+ index_p0 += 2
325
+ index_p1 += 2
326
+ pm1_lng = p1_lng
327
+ pm1_lat = p1_lat
328
+ end
329
+
330
+ min_distance
331
+ end
332
+
333
+ # Ruby original
334
+ # like numpy.fromfile
335
+ def self.fromfile(file, unsigned, byte_width, count)
336
+ if unsigned
337
+ case byte_width
338
+ when 2
339
+ unpack_format = 'S>*'
340
+ end
341
+ else
342
+ case byte_width
343
+ when 4
344
+ unpack_format = 'l>*'
345
+ when 8
346
+ unpack_format = 'q>*'
347
+ end
348
+ end
349
+
350
+ unless unpack_format
351
+ fail "#{unsigned ? 'unsigned' : 'signed'} #{byte_width}-byte width is not supported in fromfile"
352
+ end
353
+
354
+ file.read(count * byte_width).unpack(unpack_format)
355
+ end
356
+ end
357
+ end
@@ -0,0 +1,279 @@
1
+ # rubocop:disable Metrics/ClassLength,Metrics/MethodLength,Metrics/LineLength
2
+ # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/ParameterLists
3
+ # rubocop:disable Style/PredicateName,Style/Next,Style/AndOr
4
+ # rubocop:disable Lint/Void
5
+ require_relative 'helpers'
6
+ require_relative 'timezone_names'
7
+
8
+ module TimezoneFinder
9
+ # This class lets you quickly find the timezone of a point on earth.
10
+ # It keeps the binary file with the timezonefinder open in reading mode to enable fast consequent access.
11
+ # In the file currently used there are two shortcuts stored per degree of latitude and one per degree of longitude
12
+ # (tests evaluated this to be the fastest setup when being used with numba)
13
+ class TimezoneFinder
14
+ def initialize
15
+ # open the file in binary reading mode
16
+ @binary_file = open(File.join(File.dirname(__FILE__), 'timezone_data.bin'), 'rb')
17
+
18
+ # for more info on what is stored how in the .bin please read the comments in file_converter
19
+ # read the first 2byte int (= number of polygons stored in the .bin)
20
+ @nr_of_entries = @binary_file.read(2).unpack('S>')[0]
21
+
22
+ # set addresses
23
+ # the address where the shortcut section starts (after all the polygons) this is 34 433 054
24
+ @shortcuts_start = @binary_file.read(4).unpack('L>')[0]
25
+
26
+ @nr_val_start_address = 2 * @nr_of_entries + 6
27
+ @adr_start_address = 4 * @nr_of_entries + 6
28
+ @bound_start_address = 8 * @nr_of_entries + 6
29
+ # @poly_start_address = 40 * @nr_of_entries + 6
30
+ @poly_start_address = 24 * @nr_of_entries + 6
31
+ @first_shortcut_address = @shortcuts_start + 259_200
32
+
33
+ ObjectSpace.define_finalizer(self, self.class.__del__)
34
+ end
35
+
36
+ def self.__del__
37
+ proc do
38
+ @binary_file.close
39
+ end
40
+ end
41
+
42
+ def id_of(line = 0)
43
+ # ids start at address 6. per line one unsigned 2byte int is used
44
+ @binary_file.seek((6 + 2 * line))
45
+ @binary_file.read(2).unpack('S>')[0]
46
+ end
47
+
48
+ def ids_of(iterable)
49
+ id_array = [0] * iterable.length
50
+
51
+ i = 0
52
+ iterable.each do |line_nr|
53
+ @binary_file.seek((6 + 2 * line_nr))
54
+ id_array[i] = @binary_file.read(2).unpack('S>')[0]
55
+ i += 1
56
+ end
57
+
58
+ id_array
59
+ end
60
+
61
+ def shortcuts_of(lng = 0.0, lat = 0.0)
62
+ # convert coords into shortcut
63
+ x = (lng + 180).floor.to_i
64
+ y = ((90 - lat) * 2).floor.to_i
65
+
66
+ # get the address of the first entry in this shortcut
67
+ # offset: 180 * number of shortcuts per lat degree * 2bytes = entries per column of x shortcuts
68
+ # shortcuts are stored: (0,0) (0,1) (0,2)... (1,0)...
69
+ @binary_file.seek(@shortcuts_start + 720 * x + 2 * y)
70
+
71
+ nr_of_polygons = @binary_file.read(2).unpack('S>')[0]
72
+
73
+ @binary_file.seek(@first_shortcut_address + 1440 * x + 4 * y)
74
+ @binary_file.seek(@binary_file.read(4).unpack('L>')[0])
75
+ Helpers.fromfile(@binary_file, true, 2, nr_of_polygons)
76
+ end
77
+
78
+ def polygons_of_shortcut(x = 0, y = 0)
79
+ # get the address of the first entry in this shortcut
80
+ # offset: 180 * number of shortcuts per lat degree * 2bytes = entries per column of x shortcuts
81
+ # shortcuts are stored: (0,0) (0,1) (0,2)... (1,0)...
82
+ @binary_file.seek(@shortcuts_start + 720 * x + 2 * y)
83
+
84
+ nr_of_polygons = @binary_file.read(2).unpack('S>')[0]
85
+
86
+ @binary_file.seek(@first_shortcut_address + 1440 * x + 4 * y)
87
+ @binary_file.seek(@binary_file.read(4).unpack('L>')[0])
88
+ Helpers.fromfile(@binary_file, true, 2, nr_of_polygons)
89
+ end
90
+
91
+ def coords_of(line = 0)
92
+ @binary_file.seek((@nr_val_start_address + 2 * line))
93
+ nr_of_values = @binary_file.read(2).unpack('S>')[0]
94
+
95
+ @binary_file.seek(@adr_start_address + 4 * line)
96
+ @binary_file.seek(@binary_file.read(4).unpack('L>')[0])
97
+
98
+ # return [Helpers.fromfile(@binary_file, false, 8, nr_of_values),
99
+ # Helpers.fromfile(@binary_file, false, 8, nr_of_values)]
100
+
101
+ [Helpers.fromfile(@binary_file, false, 4, nr_of_values),
102
+ Helpers.fromfile(@binary_file, false, 4, nr_of_values)]
103
+ end
104
+
105
+ # @profile
106
+ # This function searches for the closest polygon in the surrounding shortcuts.
107
+ # Make sure that the point does not lie within a polygon (for that case the algorithm is simply wrong!)
108
+ # Note that the algorithm won't find the closest polygon when it's on the 'other end of earth'
109
+ # (it can't search beyond the 180 deg lng border yet)
110
+ # this checks all the polygons within [delta_degree] degree lng and lat
111
+ # Keep in mind that x degrees lat are not the same distance apart than x degree lng!
112
+ # :param lng: longitude of the point in degree
113
+ # :param lat: latitude in degree
114
+ # :param delta_degree: the 'search radius' in degree
115
+ # :return: the timezone name of the closest found polygon or None
116
+ def closest_timezone_at(lng, lat, delta_degree = 1)
117
+ if lng > 180.0 or lng < -180.0 or lat > 90.0 or lat < -90.0
118
+ fail "The coordinates are out ouf bounds: (#{lng}, #{lat})"
119
+ end
120
+
121
+ # the maximum possible distance is pi = 3.14...
122
+ min_distance = 4
123
+ # transform point X into cartesian coordinates
124
+ current_closest_id = nil
125
+ central_x_shortcut = (lng + 180).floor.to_i
126
+ central_y_shortcut = ((90 - lat) * 2).floor.to_i
127
+
128
+ polygon_nrs = []
129
+
130
+ # there are 2 shortcuts per 1 degree lat, so to cover 1 degree two shortcuts (rows) have to be checked
131
+ # the highest shortcut is 0
132
+ top = [central_y_shortcut - 2 * delta_degree, 0].max
133
+ # the lowest shortcut is 360 (= 2 shortcuts per 1 degree lat)
134
+ bottom = [central_y_shortcut + 2 * delta_degree, 360].min
135
+
136
+ # the most left shortcut is 0
137
+ left = [central_x_shortcut - delta_degree, 0].max
138
+ # the most right shortcut is 360 (= 1 shortcuts per 1 degree lng)
139
+ right = [central_x_shortcut + delta_degree, 360].min
140
+
141
+ # select all the polygons from the surrounding shortcuts
142
+ (left..right).each do |x|
143
+ (top..bottom).each do |y|
144
+ polygons_of_shortcut(x, y).each do |p|
145
+ polygon_nrs << p if polygon_nrs.index(p).nil?
146
+ end
147
+ end
148
+ end
149
+
150
+ polygons_in_list = polygon_nrs.length
151
+
152
+ return nil if polygons_in_list == 0
153
+
154
+ # initialize the list of ids
155
+ ids = polygon_nrs.map { |x| id_of(x) }
156
+
157
+ # if all the polygons in this shortcut belong to the same zone return it
158
+ first_entry = ids[0]
159
+ return TIMEZONE_NAMES[first_entry] if ids.count(first_entry) == polygons_in_list
160
+
161
+ # stores which polygons have been checked yet
162
+ already_checked = [false] * polygons_in_list
163
+
164
+ pointer = 0
165
+ polygons_checked = 0
166
+
167
+ while polygons_checked < polygons_in_list
168
+ # only check a polygon when its id is not the closest a the moment!
169
+ if already_checked[pointer] or ids[pointer] == current_closest_id
170
+ # go to the next polygon
171
+ polygons_checked += 1
172
+
173
+ else
174
+ # this polygon has to be checked
175
+ coords = coords_of(polygon_nrs[pointer])
176
+ nr_points = coords[0].length
177
+ empty_array = [[0.0] * nr_points, [0.0] * nr_points]
178
+ distance = Helpers.distance_to_polygon(lng, lat, nr_points, coords, empty_array)
179
+
180
+ already_checked[pointer] = true
181
+ if distance < min_distance
182
+ min_distance = distance
183
+ current_closest_id = ids[pointer]
184
+ # whole list has to be searched again!
185
+ polygons_checked = 1
186
+ end
187
+ end
188
+ pointer = (pointer + 1) % polygons_in_list
189
+ end
190
+
191
+ # the the whole list has been searched
192
+ TIMEZONE_NAMES[current_closest_id]
193
+ end
194
+
195
+ # this function looks up in which polygons the point could be included
196
+ # to speed things up there are shortcuts being used (stored in the binary file)
197
+ # especially for large polygons it is expensive to check if a point is really included,
198
+ # so certain simplifications are made and even when you get a hit the point might actually
199
+ # not be inside the polygon (for example when there is only one timezone nearby)
200
+ # if you want to make sure a point is really inside a timezone use 'certain_timezone_at'
201
+ # :param lng: longitude of the point in degree (-180 to 180)
202
+ # :param lat: latitude in degree (90 to -90)
203
+ # :return: the timezone name of the matching polygon or None
204
+ def timezone_at(lng = 0.0, lat = 0.0)
205
+ if lng > 180.0 or lng < -180.0 or lat > 90.0 or lat < -90.0
206
+ fail "The coordinates are out ouf bounds: (#{lng}, #{lat})"
207
+ end
208
+
209
+ possible_polygons = shortcuts_of(lng, lat)
210
+
211
+ # x = longitude y = latitude both converted to 8byte int
212
+ x = Helpers.coord2int(lng)
213
+ y = Helpers.coord2int(lat)
214
+
215
+ nr_possible_polygons = possible_polygons.length
216
+
217
+ return nil if nr_possible_polygons == 0
218
+
219
+ return TIMEZONE_NAMES[id_of(possible_polygons[0])] if nr_possible_polygons == 1
220
+
221
+ # initialize the list of ids
222
+ ids = possible_polygons.map { |p| id_of(p) }
223
+
224
+ # if all the polygons belong to the same zone return it
225
+ first_entry = ids[0]
226
+ if ids.count(first_entry) == nr_possible_polygons
227
+ return TIMEZONE_NAMES[first_entry]
228
+ end
229
+
230
+ # otherwise check if the point is included for all the possible polygons
231
+ (0...nr_possible_polygons).each do |i|
232
+ polygon_nr = possible_polygons[i]
233
+
234
+ # get the boundaries of the polygon = (lng_max, lng_min, lat_max, lat_min)
235
+ # self.binary_file.seek((@bound_start_address + 32 * polygon_nr), )
236
+ @binary_file.seek((@bound_start_address + 16 * polygon_nr))
237
+ boundaries = Helpers.fromfile(@binary_file, false, 4, 4)
238
+ # only run the algorithm if it the point is withing the boundaries
239
+ unless x > boundaries[0] or x < boundaries[1] or y > boundaries[2] or y < boundaries[3]
240
+
241
+ if Helpers.inside_polygon(x, y, coords_of(polygon_nr))
242
+ return TIMEZONE_NAMES[ids[i]]
243
+ end
244
+ end
245
+ end
246
+ nil
247
+ end
248
+
249
+ # this function looks up in which polygon the point certainly is included
250
+ # this is much slower than 'timezone_at'!
251
+ # :param lng: longitude of the point in degree
252
+ # :param lat: latitude in degree
253
+ # :return: the timezone name of the polygon the point is included in or None
254
+ def certain_timezone_at(lng = 0.0, lat = 0.0)
255
+ if lng > 180.0 or lng < -180.0 or lat > 90.0 or lat < -90.0
256
+ fail "The coordinates are out ouf bounds: (#{lng}, #{lat})"
257
+ end
258
+
259
+ possible_polygons = shortcuts_of(lng, lat)
260
+
261
+ # x = longitude y = latitude both converted to 8byte int
262
+ x = Helpers.coord2int(lng)
263
+ y = Helpers.coord2int(lat)
264
+
265
+ possible_polygons.each do |polygon_nr|
266
+ # get boundaries
267
+ @binary_file.seek((@bound_start_address + 16 * polygon_nr))
268
+ boundaries = Helpers.fromfile(@binary_file, false, 4, 4)
269
+ unless x > boundaries[0] or x < boundaries[1] or y > boundaries[2] or y < boundaries[3]
270
+ if Helpers.inside_polygon(x, y, coords_of(polygon_nr))
271
+ fail id_of(polygon_nr) if id_of(polygon_nr) >= 424
272
+ return TIMEZONE_NAMES[id_of(polygon_nr)]
273
+ end
274
+ end
275
+ end
276
+ nil
277
+ end
278
+ end
279
+ end