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.
- checksums.yaml +7 -0
- data/.editorconfig +9 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +2 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +46 -0
- data/README.md +122 -0
- data/Rakefile +10 -0
- data/lib/timezone_finder.rb +8 -0
- data/lib/timezone_finder/file_converter.rb +716 -0
- data/lib/timezone_finder/gem_version.rb +5 -0
- data/lib/timezone_finder/helpers.rb +357 -0
- data/lib/timezone_finder/timezone_data.bin +0 -0
- data/lib/timezone_finder/timezone_finder.rb +279 -0
- data/lib/timezone_finder/timezone_names.rb +420 -0
- data/timezone_finder.gemspec +30 -0
- metadata +132 -0
@@ -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
|
Binary file
|
@@ -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
|