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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7655adbedfe635064333831aa217409072acbf95
4
+ data.tar.gz: f009e325c8ec9f02d7edadec013f3e5e5f14a152
5
+ SHA512:
6
+ metadata.gz: 0e0b6c8bf962b11f2ebbbd11fbc9b0aa6d48ed2a521630c3aae43f89cfe77cb48be89e78b5f6881fba74eeb6bc9b8608175012ff9c76ca20a45a976551c4e37c
7
+ data.tar.gz: fdbfd1f79f3a124fd6382f9b56d12f992db6d5cf464306b9db3b50f06ce7bbc199a5f8b0d8c6592d5a46b4e6eecca3315e016ee861db639ed097ee155c0eed6e
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*.rb]
4
+ charset = utf-8
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ indent_style = space
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
@@ -0,0 +1,20 @@
1
+ # Editor
2
+ *~
3
+ .*.sw[a-z]
4
+
5
+ # Windows
6
+ Thumbs.db
7
+
8
+ # Mac OS X
9
+ .DS_Store
10
+
11
+ # Ruby
12
+ Gemfile.lock
13
+ .ruby-version
14
+ .rbenv-gemsets
15
+ .bundle
16
+ coverage/
17
+ pkg/
18
+
19
+ # Data
20
+ lib/timezone_finder/tz_world.json
@@ -0,0 +1,2 @@
1
+ inherit_from:
2
+ - .rubocop_todo.yml
@@ -0,0 +1,2 @@
1
+ Documentation:
2
+ Enabled: false
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.1
5
+ - 2.2
6
+ before_install: gem install bundler
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in timezone_finder.gemspec
4
+ gemspec
@@ -0,0 +1,46 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Tasuku SUENAGA a.k.a. gunyarakun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ This software is transported from timezonefinder on Python, which is subject to MIT license.
24
+ Here is the original license for timezonefinder on Python.
25
+
26
+ The MIT License (MIT)
27
+
28
+ Copyright (c) 2016 MrMinimal64
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ of this software and associated documentation files (the "Software"), to deal
32
+ in the Software without restriction, including without limitation the rights
33
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ copies of the Software, and to permit persons to whom the Software is
35
+ furnished to do so, subject to the following conditions:
36
+
37
+ The above copyright notice and this permission notice shall be included in all
38
+ copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46
+ SOFTWARE.
@@ -0,0 +1,122 @@
1
+ # timezone\_finder
2
+
3
+ [![Build Status](https://travis-ci.org/gunyarakun/timezone_finder.svg?branch=master)](https://travis-ci.org/gunyarakun/timezone_finder)
4
+ [![Gem Version](https://badge.fury.io/rb/timezone_finder.svg)](https://badge.fury.io/rb/timezone_finder)
5
+
6
+ This is a fast and lightweight ruby project to lookup the corresponding
7
+ timezone for a given lat/lng on earth entirely offline.
8
+
9
+ This project is derived from
10
+ [timezonefinder](https://pypi.python.org/pypi/timezonefinder)
11
+ ([github](https://github.com/MrMinimal64/timezonefinder>)).
12
+
13
+ The underlying timezone data is based on work done by [Eric Muller](http://efele.net/maps/tz/world/).
14
+
15
+ Timezones at sea and Antarctica are not yet supported (because somewhat
16
+ special rules apply there).
17
+
18
+ ## Installation
19
+
20
+ in your terminal simply:
21
+
22
+ ```sh
23
+ gem install timezone_finder
24
+ ```
25
+
26
+ (you might need to run this command as administrator)
27
+
28
+ ## Usage
29
+
30
+ ### Basics:
31
+
32
+ ```ruby
33
+ require 'timezone_finder'
34
+ tf = TimezoneFinder.create
35
+ ```
36
+
37
+ #### fast algorithm:
38
+
39
+ ```ruby
40
+ # point = (longitude, latitude)
41
+ point = (13.358, 52.5061)
42
+ puts tf.timezone_at(*point)
43
+ # = Europe/Berlin
44
+ ```
45
+
46
+ #### To make sure a point is really inside a timezone (slower):
47
+
48
+ ```ruby
49
+ puts tf.certain_timezone_at(*point)
50
+ # = Europe/Berlin
51
+ ```
52
+
53
+ #### To find the closest timezone (slow):
54
+
55
+ ```ruby
56
+ # only use this when the point is not inside a polygon!
57
+ # this checks all the polygons within +-1 degree lng and +-1 degree lat
58
+ point = (12.773955, 55.578595)
59
+ puts tf.closest_timezone_at(*point)
60
+ # = Europe/Copenhagens
61
+ ```
62
+
63
+ #### To increase search radius even more (very slow):
64
+
65
+ ```ruby
66
+ # this checks all the polygons within +-3 degree lng and +-3 degree lat
67
+ # I recommend only slowly increasing the search radius
68
+ # keep in mind that x degrees lat are not the same distance apart than x degree lng!
69
+ puts tf.closest_timezone_at(*point, 3)
70
+ # = Europe/Copenhagens
71
+ ```
72
+
73
+ (to make sure you really got the closest timezone increase the search
74
+ radius until you get a result. then increase the radius once more and
75
+ take this result.)
76
+
77
+ ## Developer
78
+
79
+ ### Using the conversion tool:
80
+
81
+ Make sure you installed the GDAL framework (thats for converting .shp shapefiles into .json)
82
+ Change to the directory of the timezone\_finder package (location of ``file_converter.rb``) in your terminal and then:
83
+
84
+ ```sh
85
+ wget http://efele.net/maps/tz/world/tz_world.zip
86
+ # on mac: curl "http://efele.net/maps/tz/world/tz_world.zip" -o "tz_world.zip"
87
+ unzip tz_world
88
+ ogr2ogr -f GeoJSON -t_srs crs:84 tz_world.json ./world/tz_world.shp
89
+ rm ./world/ -r
90
+ rm tz_world.zip
91
+ ```
92
+
93
+ There has to be a tz\_world.json (of approx. 100MB) in the folder together with the ``file_converter.rb`` now.
94
+ Then you should run the converter by:
95
+
96
+ ```sh
97
+ ruby file_converter.rb
98
+ ```
99
+
100
+ this converts the .json into the needed .bin (overwriting the old version!) and updating the used timezone names.
101
+
102
+ ## Known Issues
103
+
104
+ The original author MrMinimal64 ran tests for approx. 5M points and this are the mistakes he found:
105
+
106
+ All points in **Lesotho** are counted to the 'Africa/Johannesburg' timezone instead of 'Africa/Maseru'.
107
+ I am pretty sure this is because it is completely surrounded by South Africa and in the data the area of Lesotho is not excluded from this timezone.
108
+
109
+ Same for the small **usbekish enclaves** in **Kirgisitan** and some points in the **Arizona Dessert** (some weird rules apply here).
110
+
111
+ Those are mistakes in the data not my algorithms and in order to fix this he would need check for and then separately handle these special cases.
112
+ This would not only slow down the algorithms, but also make them ugly.
113
+
114
+ ## Contact
115
+
116
+ If you notice that the tz data is outdated, encounter any bugs, have
117
+ suggestions, criticism, etc. feel free to **open an Issue**, **add a Pull Requests** on Git.
118
+
119
+ ## License
120
+
121
+ ``timezone_finder`` is distributed under the terms of the MIT license
122
+ (see LICENSE.txt).
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,8 @@
1
+ require_relative 'timezone_finder/timezone_finder'
2
+ require_relative 'timezone_finder/gem_version'
3
+
4
+ module TimezoneFinder
5
+ def self.create
6
+ TimezoneFinder.new
7
+ end
8
+ end
@@ -0,0 +1,716 @@
1
+ #!/usr/bin/env ruby
2
+ # rubocop:disable Metrics/ClassLength,Metrics/MethodLength,Metrics/LineLength
3
+ # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/ParameterLists
4
+ # rubocop:disable Style/PredicateName,Style/Next
5
+ # rubocop:disable Lint/Void
6
+ require 'set'
7
+ require_relative 'helpers'
8
+
9
+ module TimezoneFinder
10
+ class FileConverter
11
+ # Don't change this setup or timezonefinder wont work!
12
+ # different setups of shortcuts are not supported, because then addresses in the .bin
13
+ # would need to be calculated depending on how many shortcuts are being used.
14
+ # number of shortcuts per longitude
15
+ NR_SHORTCUTS_PER_LNG = 1
16
+ # shortcuts per latitude
17
+ NR_SHORTCUTS_PER_LAT = 2
18
+
19
+ def initialize
20
+ @all_tz_names = []
21
+ @ids = []
22
+ @boundaries = []
23
+ @all_coords = []
24
+ @all_lengths = []
25
+ end
26
+
27
+ # HELPERS:
28
+
29
+ # TODO
30
+ # :return:
31
+ def update_zone_names(path = 'timezone_names.rb')
32
+ puts('updating the zone names now')
33
+ unique_zones = []
34
+ @all_tz_names.each do |zone_name|
35
+ unique_zones << zone_name if unique_zones.index(zone_name).nil?
36
+ end
37
+
38
+ unique_zones.sort!
39
+
40
+ @all_tz_names.each do |zone_name|
41
+ # the ids of the polygons have to be set correctly
42
+ @ids << unique_zones.index(zone_name)
43
+ end
44
+
45
+ # write all unique zones into the file at path with the syntax of a ruby array
46
+
47
+ file = open(path, 'w')
48
+ file.write("module TimezoneFinder\n")
49
+ file.write(" TIMEZONE_NAMES = [\n")
50
+ unique_zones.each do |zone_name|
51
+ file.write(" '#{zone_name}',\n")
52
+ end
53
+
54
+ file.write(" ].freeze\n")
55
+ file.write("end\n")
56
+ puts("Done\n\n")
57
+ end
58
+
59
+ def inside_polygon(x, y, x_coords, y_coords)
60
+ def is_left_of(x, y, x1, x2, y1, y2)
61
+ (x2 - x1) * (y - y1) - (x - x1) * (y2 - y1)
62
+ end
63
+
64
+ n = y_coords.length - 1
65
+
66
+ wn = 0
67
+ (0...n).each do |i|
68
+ iplus = i + 1
69
+ if y_coords[i] <= y
70
+ # puts('Y1<=y')
71
+ if y_coords[iplus] > y
72
+ # puts('Y2>y')
73
+ if is_left_of(x, y, x_coords[i], x_coords[iplus], y_coords[i], y_coords[iplus]) > 0
74
+ wn += 1
75
+ # puts('wn is:')
76
+ # puts(wn)
77
+ end
78
+ end
79
+ else
80
+ # puts('Y1>y')
81
+ if y_coords[iplus] <= y
82
+ # puts('Y2<=y')
83
+ if is_left_of(x, y, x_coords[i], x_coords[iplus], y_coords[i], y_coords[iplus]) < 0
84
+ wn -= 1
85
+ # puts('wn is:')
86
+ # puts(wn)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ wn != 0
92
+ end
93
+
94
+ def parse_polygons_from_json(path = 'tz_world.json')
95
+ f = open(path, 'r')
96
+ puts 'Parsing data from .json'
97
+ n = 0
98
+ f.each_line do |row|
99
+ puts "line #{n}" if n % 1000 == 0
100
+ n += 1
101
+ # puts(row)
102
+ tz_name_match = /"TZID":\s\"(?<name>.*)"\s\}/.match(row)
103
+ # tz_name = /(TZID)/.match(row)
104
+ # puts tz_name
105
+ if tz_name_match
106
+ tz_name = tz_name_match['name'].gsub('\\', '')
107
+ @all_tz_names << tz_name
108
+ # puts tz_name
109
+
110
+ coordinates = row.scan(/[-]?\d+\.?\d+/)
111
+ # puts coordinates
112
+
113
+ # nr_floats = coordinates.length
114
+ x_coords = []
115
+ y_coords = []
116
+ i = 0
117
+ coordinates.each do |coord|
118
+ if i.even?
119
+ x_coords << coord.to_f
120
+ else
121
+ y_coords << coord.to_f
122
+ end
123
+ i += 1
124
+ end
125
+
126
+ fail "#{i} Floats in line #{n} found. Should be even (pairs or (x,y) )" if i.odd?
127
+
128
+ @all_coords << [x_coords, y_coords]
129
+ @all_lengths << x_coords.length
130
+ # puts x_coords
131
+ # puts y_coords
132
+ xmax = -180.0
133
+ xmin = 180.0
134
+ ymax = -90.0
135
+ ymin = 90.0
136
+
137
+ x_coords.each do |x|
138
+ xmax = x if x > xmax
139
+ xmin = x if x < xmin
140
+ end
141
+
142
+ y_coords.each do |y|
143
+ ymax = y if y > ymax
144
+ ymin = y if y < ymin
145
+ end
146
+
147
+ @boundaries << [xmax, xmin, ymax, ymin]
148
+ end
149
+ end
150
+
151
+ puts("Done\n\n")
152
+ end
153
+
154
+ def ints_of(line = 0)
155
+ x_coords, y_coords = @all_coords[line]
156
+ [x_coords.map { |x| Helpers.coord2int(x) }, y_coords.map { |y| Helpers.coord2int(y) }]
157
+ end
158
+
159
+ def compile_into_binary(path = 'tz_binary.bin')
160
+ nr_of_floats = 0
161
+ nr_of_lines = 0
162
+ zone_ids = []
163
+ shortcuts = {}
164
+
165
+ def x_shortcut(lng)
166
+ # puts lng if lng < -180 or lng >= 180
167
+ # raise 'longitude out of bounds'
168
+ ((lng + 180) * NR_SHORTCUTS_PER_LNG).floor
169
+ end
170
+
171
+ def y_shortcut(lat)
172
+ # puts lat if lat < -90 or lat >= 90
173
+ # raise 'this latitude is out of bounds'
174
+ ((90 - lat) * NR_SHORTCUTS_PER_LAT).floor
175
+ end
176
+
177
+ def big_zone(xmax, xmin, ymax, ymin)
178
+ # returns true if a zone with those boundaries could have more than 4 shortcuts
179
+ (xmax - xmin) > (2.0 / NR_SHORTCUTS_PER_LNG) && (ymax - ymin) > (2.0 / NR_SHORTCUTS_PER_LAT)
180
+ end
181
+
182
+ def included_shortcut_row_nrs(max_lat, min_lat)
183
+ (y_shortcut(max_lat)..y_shortcut(min_lat)).to_a
184
+ end
185
+
186
+ def included_shortcut_column_nrs(max_lng, min_lng)
187
+ (x_shortcut(min_lng)..x_shortcut(max_lng)).to_a
188
+ end
189
+
190
+ def longitudes_to_check(max_lng, min_lng)
191
+ output_list = []
192
+ step = 1.0 / NR_SHORTCUTS_PER_LNG
193
+ current = (min_lng * NR_SHORTCUTS_PER_LNG).ceil.fdiv(NR_SHORTCUTS_PER_LNG)
194
+ last = (max_lng * NR_SHORTCUTS_PER_LNG).floor.fdiv(NR_SHORTCUTS_PER_LNG)
195
+
196
+ while current < last
197
+ output_list << current
198
+ current += step
199
+ end
200
+
201
+ output_list << last
202
+ output_list
203
+ end
204
+
205
+ def latitudes_to_check(max_lat, min_lat)
206
+ output_list = []
207
+ step = 1.0 / NR_SHORTCUTS_PER_LAT
208
+ current = (min_lat * NR_SHORTCUTS_PER_LAT).ceil.fdiv(NR_SHORTCUTS_PER_LAT)
209
+ last = (max_lat * NR_SHORTCUTS_PER_LAT).floor.fdiv(NR_SHORTCUTS_PER_LAT)
210
+ while current < last
211
+ output_list << current
212
+ current += step
213
+ end
214
+
215
+ output_list << last
216
+ output_list
217
+ end
218
+
219
+ # returns the x intersection from a horizontal line in y with the line from x1,y1 to x1,y2
220
+ def compute_x_intersection(y, x1, x2, y1, y2)
221
+ delta_y = y2 - y1
222
+ return x1 if delta_y == 0
223
+ ((y - y1) * (x2 - x1)).fdiv(delta_y) + x1
224
+ end
225
+
226
+ # returns the y intersection from a vertical line in x with the line from x1,y1 to x1,y2
227
+ def compute_y_intersection(x, x1, x2, y1, y2)
228
+ delta_x = x2 - x1
229
+ return x1 if delta_x == 0
230
+ ((x - x1) * (y2 - y1)).fdiv(delta_x) + y1
231
+ end
232
+
233
+ def x_intersections(y, x_coords, y_coords)
234
+ # puts(x_coords.to_s)
235
+ # puts(y)
236
+ # puts(y_coords.to_s)
237
+
238
+ intersects = []
239
+ (0...(y_coords.length - 1)).each do |i|
240
+ iplus1 = i + 1
241
+ if y_coords[i] <= y
242
+ # puts('Y1<=y')
243
+ if y_coords[iplus1] > y
244
+ # this was a crossing. compute the intersect
245
+ # puts('Y2>y')
246
+ intersects << compute_x_intersection(y, x_coords[i], x_coords[iplus1], y_coords[i], y_coords[iplus1])
247
+ end
248
+ else
249
+ # puts('Y1>y')
250
+ if y_coords[iplus1] <= y
251
+ # this was a crossing. compute the intersect
252
+ # puts('Y2<=y')
253
+ intersects << compute_x_intersection(y, x_coords[i], x_coords[iplus1], y_coords[i], y_coords[iplus1])
254
+ end
255
+ end
256
+ end
257
+ intersects
258
+ end
259
+
260
+ def y_intersections(x, x_coords, y_coords)
261
+ intersects = []
262
+ (0...(y_coords.length - 1)).each do |i|
263
+ iplus1 = i + 1
264
+ if x_coords[i] <= x
265
+ if x_coords[iplus1] > x
266
+ # this was a crossing. compute the intersect
267
+ intersects << compute_y_intersection(x, x_coords[i], x_coords[iplus1], y_coords[i], y_coords[iplus1])
268
+ end
269
+ else
270
+ if x_coords[iplus1] <= x
271
+ # this was a crossing. compute the intersect
272
+ intersects << compute_y_intersection(x, x_coords[i], x_coords[iplus1], y_coords[i], y_coords[iplus1])
273
+ end
274
+ end
275
+ end
276
+ intersects
277
+ end
278
+
279
+ def compute_exact_shortcuts(xmax, xmin, ymax, ymin, line)
280
+ shortcuts_for_line = Set.new
281
+
282
+ # x_longs = binary_reader.x_coords_of(line)
283
+ longs = ints_of(line)
284
+ x_longs = longs[0]
285
+ y_longs = longs[1]
286
+
287
+ # y_longs = binary_reader.y_coords_of(line)
288
+ y_longs << y_longs[0]
289
+ x_longs << x_longs[0]
290
+
291
+ step = 1.0 / NR_SHORTCUTS_PER_LAT
292
+ # puts('checking the latitudes')
293
+ latitudes_to_check(ymax, ymin).each do |lat|
294
+ # puts(lat)
295
+ # puts(coordinate_to_longlong(lat))
296
+ # puts(y_longs)
297
+ # puts(x_intersections(coordinate_to_longlong(lat), x_longs, y_longs))
298
+ # raise
299
+ intersects = x_intersections(Helpers.coord2int(lat), x_longs, y_longs).map do |x|
300
+ Helpers.int2coord(x)
301
+ end.sort
302
+ # puts(intersects.to_s)
303
+
304
+ nr_of_intersects = intersects.length
305
+ if nr_of_intersects.odd?
306
+ fail 'an uneven number of intersections has been accounted'
307
+ end
308
+
309
+ (0...nr_of_intersects).step(2).each do |i|
310
+ possible_longitudes = []
311
+ # collect all the zones between two intersections [in,out,in,out,...]
312
+ iplus = i + 1
313
+ intersection_in = intersects[i]
314
+ intersection_out = intersects[iplus]
315
+ if intersection_in == intersection_out
316
+ # the polygon has a point exactly on the border of a shortcut zone here!
317
+ # only select the top shortcut if it is actually inside the polygon (point a little up is inside)
318
+ if inside_polygon(Helpers.coord2int(intersection_in), Helpers.coord2int(lat) + 1, x_longs,
319
+ y_longs)
320
+ shortcuts_for_line.add([x_shortcut(intersection_in), y_shortcut(lat) - 1])
321
+ end
322
+ # the bottom shortcut is always selected
323
+ shortcuts_for_line.add([x_shortcut(intersection_in), y_shortcut(lat)])
324
+
325
+ else
326
+ # add all the shortcuts for the whole found area of intersection
327
+ possible_y_shortcut = y_shortcut(lat)
328
+
329
+ # both shortcuts should only be selected when the polygon doesnt stays on the border
330
+ middle = intersection_in + (intersection_out - intersection_in) / 2
331
+ if inside_polygon(Helpers.coord2int(middle), Helpers.coord2int(lat) + 1, x_longs, y_longs)
332
+ while intersection_in < intersection_out
333
+ possible_longitudes << intersection_in
334
+ intersection_in += step
335
+ end
336
+
337
+ possible_longitudes << intersection_out
338
+
339
+ # the shortcut above and below of the intersection should be selected!
340
+ possible_y_shortcut_min1 = possible_y_shortcut - 1
341
+ possible_longitudes.each do |possible_x_coord|
342
+ shortcuts_for_line.add([x_shortcut(possible_x_coord), possible_y_shortcut])
343
+ shortcuts_for_line.add([x_shortcut(possible_x_coord), possible_y_shortcut_min1])
344
+ end
345
+ else
346
+ # polygon does not cross the border!
347
+ while intersection_in < intersection_out
348
+ possible_longitudes << intersection_in
349
+ intersection_in += step
350
+ end
351
+
352
+ possible_longitudes << intersection_out
353
+
354
+ # only the shortcut above of the intersection should be selected!
355
+ possible_longitudes.each do |possible_x_coord|
356
+ shortcuts_for_line.add([x_shortcut(possible_x_coord), possible_y_shortcut])
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
362
+
363
+ # puts('now all the longitudes to check')
364
+ # same procedure horizontally
365
+ step = 1.0 / NR_SHORTCUTS_PER_LAT
366
+ longitudes_to_check(xmax, xmin).each do |lng|
367
+ # puts(lng)
368
+ # puts(coordinate_to_longlong(lng))
369
+ # puts(x_longs)
370
+ # puts(x_intersections(coordinate_to_longlong(lng), x_longs, y_longs))
371
+ intersects = y_intersections(Helpers.coord2int(lng), x_longs, y_longs).map do |y|
372
+ Helpers.int2coord(y)
373
+ end.sort
374
+ # puts(intersects)
375
+
376
+ nr_of_intersects = intersects.length
377
+ if nr_of_intersects.odd?
378
+ fail 'an uneven number of intersections has been accounted'
379
+ end
380
+
381
+ possible_latitudes = []
382
+ (0...nr_of_intersects).step(2).each do |i|
383
+ # collect all the zones between two intersections [in,out,in,out,...]
384
+ iplus = i + 1
385
+ intersection_in = intersects[i]
386
+ intersection_out = intersects[iplus]
387
+ if intersection_in == intersection_out
388
+ # the polygon has a point exactly on the border of a shortcut here!
389
+ # only select the left shortcut if it is actually inside the polygon (point a little left is inside)
390
+ if inside_polygon(Helpers.coord2int(lng) - 1, Helpers.coord2int(intersection_in), x_longs,
391
+ y_longs)
392
+ shortcuts_for_line.add([x_shortcut(lng) - 1, y_shortcut(intersection_in)])
393
+ end
394
+ # the right shortcut is always selected
395
+ shortcuts_for_line.add([x_shortcut(lng), y_shortcut(intersection_in)])
396
+
397
+ else
398
+ # add all the shortcuts for the whole found area of intersection
399
+ possible_x_shortcut = x_shortcut(lng)
400
+
401
+ # both shortcuts should only be selected when the polygon doesnt stays on the border
402
+ middle = intersection_in + (intersection_out - intersection_in) / 2
403
+ if inside_polygon(Helpers.coord2int(lng) - 1, Helpers.coord2int(middle), x_longs,
404
+ y_longs)
405
+ while intersection_in < intersection_out
406
+ possible_latitudes << intersection_in
407
+ intersection_in += step
408
+ end
409
+
410
+ possible_latitudes << intersection_out
411
+
412
+ # both shortcuts right and left of the intersection should be selected!
413
+ possible_x_shortcut_min1 = possible_x_shortcut - 1
414
+ possible_latitudes.each do |possible_latitude|
415
+ shortcuts_for_line.add([possible_x_shortcut, y_shortcut(possible_latitude)])
416
+ shortcuts_for_line.add([possible_x_shortcut_min1, y_shortcut(possible_latitude)])
417
+ end
418
+
419
+ else
420
+ while intersection_in < intersection_out
421
+ possible_latitudes << intersection_in
422
+ intersection_in += step
423
+ end
424
+ # only the shortcut right of the intersection should be selected!
425
+ possible_latitudes << intersection_out
426
+
427
+ possible_latitudes.each do |possible_latitude|
428
+ shortcuts_for_line.add([possible_x_shortcut, y_shortcut(possible_latitude)])
429
+ end
430
+ end
431
+ end
432
+ end
433
+ end
434
+
435
+ shortcuts_for_line
436
+ end
437
+
438
+ def construct_shortcuts(shortcuts)
439
+ puts('building shortucts...')
440
+ puts('currently in line:')
441
+ line = 0
442
+ @boundaries.each do |xmax, xmin, ymax, ymin|
443
+ # xmax, xmin, ymax, ymin = boundaries_of(line=line)
444
+ if line % 1000 == 0
445
+ puts("line #{line}")
446
+ # puts([xmax, xmin, ymax, ymin])
447
+ end
448
+
449
+ column_nrs = included_shortcut_column_nrs(xmax, xmin)
450
+ row_nrs = included_shortcut_row_nrs(ymax, ymin)
451
+
452
+ if big_zone(xmax, xmin, ymax, ymin)
453
+
454
+ <<EOT
455
+ puts("line #{line}")
456
+ puts('This is a big zone! computing exact shortcuts')
457
+ puts('Nr of entries before')
458
+ puts(len(column_nrs) * row_nrs.length)
459
+
460
+ puts('columns and rows before optimisation:')
461
+
462
+ puts(column_nrs)
463
+ puts(row_nrs)
464
+ EOT
465
+
466
+ # This is a big zone! compute exact shortcuts with the whole polygon points
467
+ shortcuts_for_line = compute_exact_shortcuts(xmax, xmin, ymax, ymin, line)
468
+ # n += shortcuts_for_line.length
469
+
470
+ <<EOT
471
+ accurracy = 1000000000000
472
+ while len(shortcuts_for_line) < 3 and accurracy > 10000000000
473
+ shortcuts_for_line = compute_exact_shortcuts(line=i,accurracy)
474
+ accurracy = (accurracy/10).to_i
475
+ end
476
+ EOT
477
+ min_x_shortcut = column_nrs[0]
478
+ max_x_shortcut = column_nrs[-1]
479
+ min_y_shortcut = row_nrs[0]
480
+ max_y_shortcut = row_nrs[-1]
481
+ shortcuts_to_remove = []
482
+
483
+ # remove shortcuts from outside the possible/valid area
484
+ shortcuts_for_line.each do |x, y|
485
+ shortcuts_to_remove << [x, y] if x < min_x_shortcut
486
+ shortcuts_to_remove << [x, y] if x > max_x_shortcut
487
+ shortcuts_to_remove << [x, y] if y < min_y_shortcut
488
+ shortcuts_to_remove << [x, y] if y > max_y_shortcut
489
+ end
490
+
491
+ shortcuts_to_remove.each do |s|
492
+ shortcuts_for_line.delete(s)
493
+ end
494
+
495
+ <<EOT
496
+ puts('and after:')
497
+ puts(shortcuts_for_line.length)
498
+
499
+ column_nrs_after = Set.new
500
+ row_nrs_after = Set.new
501
+ shortcuts_for_line.each do |x, y|
502
+ column_nrs_after.add(x)
503
+ row_nrs_after.add(y)
504
+ end
505
+ puts(column_nrs_after)
506
+ puts(row_nrs_after)
507
+ puts(shortcuts_for_line)
508
+ EOT
509
+
510
+ if shortcuts_for_line.length > column_nrs.length * row_nrs.length
511
+ fail 'there are more shortcuts than before now. there is something wrong with the algorithm!'
512
+ end
513
+ if shortcuts_for_line.length < 3
514
+ fail 'algorithm not valid! less than 3 zones detected (should be at least 4)'
515
+ end
516
+
517
+ else
518
+
519
+ shortcuts_for_line = []
520
+ column_nrs.each do |column_nr|
521
+ row_nrs.each do |row_nr|
522
+ shortcuts_for_line << [column_nr, row_nr]
523
+
524
+ # puts(shortcuts_for_line)
525
+ end
526
+ end
527
+ end
528
+
529
+ shortcuts_for_line.each do |shortcut|
530
+ shortcuts[shortcut] = shortcuts.fetch(shortcut, []) + [line]
531
+ end
532
+
533
+ line += 1
534
+ # puts('collected entries:')
535
+ # puts(n)
536
+ end
537
+ end
538
+
539
+ puts('reading the converted .csv file')
540
+ @ids.each do |id|
541
+ nr_of_lines += 1
542
+ zone_ids << id
543
+ end
544
+
545
+ @all_lengths.each do |length|
546
+ nr_of_floats += 2 * length
547
+ end
548
+
549
+ start_time = Time.now
550
+ construct_shortcuts(shortcuts)
551
+ end_time = Time.now
552
+
553
+ puts("calculating the shortcuts took: #{end_time - start_time}")
554
+
555
+ # address where the actual polygon data starts. look in the description below to get more info
556
+ polygon_address = (24 * nr_of_lines + 6)
557
+
558
+ # for every original float now 4 bytes are needed (int32)
559
+ shortcut_start_address = polygon_address + 4 * nr_of_floats
560
+ puts("The number of polygons is: #{nr_of_lines}")
561
+ puts("The number of floats in all the polygons is (2 per point): #{nr_of_floats}")
562
+ puts("now writing file \"#{path}\"")
563
+ output_file = open(path, 'wb')
564
+ # write nr_of_lines
565
+ output_file.write([nr_of_lines].pack('S>'))
566
+ # write start address of shortcut_data:
567
+ output_file.write([shortcut_start_address].pack('L>'))
568
+ # write zone_ids
569
+ zone_ids.each do |zone_id|
570
+ output_file.write([zone_id].pack('S>'))
571
+ end
572
+ # write number of values
573
+ @all_lengths.each do |length|
574
+ output_file.write([length].pack('S>'))
575
+ end
576
+
577
+ # write polygon_addresses
578
+ @all_lengths.each do |length|
579
+ output_file.write([polygon_address].pack('L>'))
580
+ # data of the next polygon is at the address after all the space the points take
581
+ # nr of points stored * 2 ints per point * 4 bytes per int
582
+ polygon_address += 8 * length
583
+ end
584
+
585
+ if shortcut_start_address != polygon_address
586
+ # both should be the same!
587
+ fail 'shortcut_start_address and polygon_address should now be the same!'
588
+ end
589
+
590
+ # write boundary_data
591
+ @boundaries.each do |b|
592
+ output_file.write(b.map { |c| Helpers.coord2int(c) }.pack('l>l>l>l>'))
593
+ end
594
+
595
+ # write polygon_data
596
+ @all_coords.each do |x_coords, y_coords|
597
+ x_coords.each do |x|
598
+ output_file.write([Helpers.coord2int(x)].pack('l>'))
599
+ end
600
+ y_coords.each do |y|
601
+ output_file.write([Helpers.coord2int(y)].pack('l>'))
602
+ end
603
+ end
604
+
605
+ puts("position after writing all polygon data (=start of shortcut section): #{output_file.tell}")
606
+ # write number of entries in shortcut field (x,y)
607
+ nr_of_entries_in_shortcut = []
608
+ shortcut_entries = []
609
+ total_entries_in_shortcuts = 0
610
+
611
+ # count how many shortcut addresses will be written:
612
+ (0...(360 * NR_SHORTCUTS_PER_LNG)).each do |x|
613
+ (0...(180 * NR_SHORTCUTS_PER_LAT)).each do |y|
614
+ begin
615
+ this_lines_shortcuts = shortcuts.fetch([x, y])
616
+ shortcut_entries << this_lines_shortcuts
617
+ total_entries_in_shortcuts += 1
618
+ nr_of_entries_in_shortcut << this_lines_shortcuts.length
619
+ # puts("(#{x}, #{y}, #{this_lines_shortcuts})")
620
+ rescue KeyError
621
+ nr_of_entries_in_shortcut << 0
622
+ end
623
+ end
624
+ end
625
+
626
+ puts("The number of filled shortcut zones are: #{total_entries_in_shortcuts}")
627
+
628
+ if nr_of_entries_in_shortcut.length != 64_800 * NR_SHORTCUTS_PER_LNG * NR_SHORTCUTS_PER_LAT
629
+ puts(nr_of_entries_in_shortcut.length)
630
+ fail 'this number of shortcut zones is wrong'
631
+ end
632
+
633
+ # write all nr of entries
634
+ nr_of_entries_in_shortcut.each do |nr|
635
+ fail "There are too many polygons in this shortcuts: #{nr}" if nr > 300
636
+ output_file.write([nr].pack('S>'))
637
+ end
638
+
639
+ # write Address of first Polygon_nr in shortcut field (x,y)
640
+ # Attention: 0 is written when no entries are in this shortcut
641
+ shortcut_address = output_file.tell + 259_200 * NR_SHORTCUTS_PER_LNG * NR_SHORTCUTS_PER_LAT
642
+ nr_of_entries_in_shortcut.each do |nr|
643
+ if nr == 0
644
+ output_file.write([0].pack('L>'))
645
+ else
646
+ output_file.write([shortcut_address].pack('L>'))
647
+ # each polygon takes up 2 bytes of space
648
+ shortcut_address += 2 * nr
649
+ end
650
+ end
651
+
652
+ # write Line_Nrs for every shortcut
653
+ shortcut_entries.each do |entries|
654
+ entries.each do |entry|
655
+ fail entry if entry > nr_of_lines
656
+ output_file.write([entry].pack('S>'))
657
+ end
658
+ end
659
+
660
+ last_address = output_file.tell
661
+ shortcut_space = last_address - shortcut_start_address
662
+ polygon_space = nr_of_floats * 4
663
+
664
+ puts("the shortcuts make up #{((shortcut_space / last_address) * 100).round(2) }% of the file")
665
+ puts("the polygon data makes up #{((polygon_space / last_address) * 100)}.round(2)% of the file")
666
+
667
+ puts('Success!')
668
+ end
669
+
670
+ <<EOT
671
+ Data format in the .bin:
672
+ IMPORTANT: all coordinates (floats) are converted to int32 (multiplied by 10^7). This makes computations much faster
673
+ and it takes lot less space, without loosing too much accuracy (min accuracy is 1cm still at the equator)
674
+
675
+ no of rows (= no of polygons = no of boundaries)
676
+ approx. 28k -> use 2byte unsigned short (has range until 65k)
677
+ '!H' = n
678
+
679
+ I Address of Shortcut area (end of polygons+1) @ 2
680
+
681
+ '!H' n times [H unsigned short: zone number=ID in this line, @ 6 + 2* lineNr]
682
+
683
+ '!H' n times [H unsigned short: nr of values (coordinate PAIRS! x,y in long long) in this line, @ 6 + 2n + 2* lineNr]
684
+
685
+ '!I'n times [ I unsigned int: absolute address of the byte where the polygon-data of that line starts,
686
+ @ 6 + 4 * n + 4*lineNr]
687
+
688
+
689
+
690
+ n times 4 int32 (take up 4*4 per line): xmax, xmin, ymax, ymin @ 6 + 8n + 16* lineNr
691
+ '!iiii'
692
+
693
+
694
+ [starting @ 6+ 24*n = polygon data start address]
695
+ (for every line: x coords, y coords:) stored @ Address section (see above)
696
+ '!i' * amount of points
697
+
698
+ 360 * NR_SHORTCUTS_PER_LNG * 180 * NR_SHORTCUTS_PER_LAT:
699
+ [atm: 360* 1 * 180 * 2 = 129,600]
700
+ 129,600 times !H number of entries in shortcut field (x,y) @ Pointer see above
701
+
702
+
703
+ Address of first Polygon_nr in shortcut field (x,y) [0 if there is no entry] @ Pointer see above + 129,600
704
+ 129,600 times !I
705
+
706
+ [X = number of filled shortcuts]
707
+ X times !H * amount Polygon_Nr @ address stored in previous section
708
+
709
+ EOT
710
+ end
711
+ end
712
+
713
+ file_converter = TimezoneFinder::FileConverter.new
714
+ file_converter.parse_polygons_from_json('tz_world.json')
715
+ file_converter.update_zone_names('timezone_names.rb')
716
+ file_converter.compile_into_binary('timezone_data.bin')