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
checksums.yaml
ADDED
@@ -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
|
data/.editorconfig
ADDED
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# timezone\_finder
|
2
|
+
|
3
|
+
[](https://travis-ci.org/gunyarakun/timezone_finder)
|
4
|
+
[](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).
|
data/Rakefile
ADDED
@@ -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')
|