timezone_finder 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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