timezone_finder 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|