timezone_finder 0.0.1 → 1.5.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7655adbedfe635064333831aa217409072acbf95
4
- data.tar.gz: f009e325c8ec9f02d7edadec013f3e5e5f14a152
3
+ metadata.gz: 13d427ec4275e9a9cf091179165cdaee2c427f08
4
+ data.tar.gz: bd258529875fec7b818f7839c2a116ff6a29e434
5
5
  SHA512:
6
- metadata.gz: 0e0b6c8bf962b11f2ebbbd11fbc9b0aa6d48ed2a521630c3aae43f89cfe77cb48be89e78b5f6881fba74eeb6bc9b8608175012ff9c76ca20a45a976551c4e37c
7
- data.tar.gz: fdbfd1f79f3a124fd6382f9b56d12f992db6d5cf464306b9db3b50f06ce7bbc199a5f8b0d8c6592d5a46b4e6eecca3315e016ee861db639ed097ee155c0eed6e
6
+ metadata.gz: a7e3da84f42cc2b1a55c06cfd0ceb35119be8dce2c549a1872fd7f32f3373685582891d34800d03456111e3b89c3c55bda86902a8ff62aa3699077e3e72c525b
7
+ data.tar.gz: 48374abaab46b420242964dd2373899b4d602f10605964f7497b7580f24200af8d62e99b6f551ea32b79c446dfc2b6cf266d41cfbdad58328f0722c4e9b2097c
@@ -0,0 +1,14 @@
1
+ == 2016-06-03 version 1.5.5
2
+
3
+ * using the newest version (2016d, May 2016) of the tz_world data from http://efele.net/maps/tz/world/
4
+ * holes in the polygons which are stored in the tz_world data are now correctly stored and handled
5
+ * rewrote the file_converter for storing the holes at the end of the
6
+ timezone_data.bin
7
+ * added specific test cases for hole handling
8
+ * made some optimizations in the algorithms
9
+
10
+
11
+ == 2016-05-26 version 0.0.1
12
+
13
+ * First release
14
+
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Build Status](https://travis-ci.org/gunyarakun/timezone_finder.svg?branch=master)](https://travis-ci.org/gunyarakun/timezone_finder)
4
4
  [![Gem Version](https://badge.fury.io/rb/timezone_finder.svg)](https://badge.fury.io/rb/timezone_finder)
5
5
 
6
- This is a fast and lightweight ruby project to lookup the corresponding
6
+ This is a fast and lightweight ruby project for looking up the corresponding
7
7
  timezone for a given lat/lng on earth entirely offline.
8
8
 
9
9
  This project is derived from
@@ -36,6 +36,9 @@ tf = TimezoneFinder.create
36
36
 
37
37
  #### fast algorithm:
38
38
 
39
+ This approach is fast, but might not be what you are looking for:
40
+ For example when there is only one possible timezone in proximity, this timezone would be returned (without checking if the point is included first).
41
+
39
42
  ```ruby
40
43
  # point = (longitude, latitude)
41
44
  point = (13.358, 52.5061)
@@ -101,21 +104,17 @@ this converts the .json into the needed .bin (overwriting the old version!) and
101
104
 
102
105
  ## Known Issues
103
106
 
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.
107
+ The original author MrMinimal64 ran tests for approx. 5M points and these are no mistakes he found.
113
108
 
114
109
  ## Contact
115
110
 
116
111
  If you notice that the tz data is outdated, encounter any bugs, have
117
112
  suggestions, criticism, etc. feel free to **open an Issue**, **add a Pull Requests** on Git.
118
113
 
114
+ ## Credits
115
+
116
+ Thanks to [MrMinimal64](https://github.com/MrMinimal64) for developing the original version and giving me some advices.
117
+
119
118
  ## License
120
119
 
121
120
  ``timezone_finder`` is distributed under the terms of the MIT license
@@ -17,11 +17,19 @@ module TimezoneFinder
17
17
  NR_SHORTCUTS_PER_LAT = 2
18
18
 
19
19
  def initialize
20
+ @nr_of_lines = -1
21
+
20
22
  @all_tz_names = []
21
23
  @ids = []
22
24
  @boundaries = []
23
25
  @all_coords = []
24
26
  @all_lengths = []
27
+ @amount_of_holes = 0
28
+ @first_hole_id_in_line = []
29
+ @related_line = []
30
+
31
+ @all_holes = []
32
+ @all_hole_lengths = []
25
33
  end
26
34
 
27
35
  # HELPERS:
@@ -94,10 +102,9 @@ module TimezoneFinder
94
102
  def parse_polygons_from_json(path = 'tz_world.json')
95
103
  f = open(path, 'r')
96
104
  puts 'Parsing data from .json'
97
- n = 0
105
+ # file_line is the current line in the .json file being parsed. This is not the id of the Polygon!
106
+ file_line = 0
98
107
  f.each_line do |row|
99
- puts "line #{n}" if n % 1000 == 0
100
- n += 1
101
108
  # puts(row)
102
109
  tz_name_match = /"TZID":\s\"(?<name>.*)"\s\}/.match(row)
103
110
  # tz_name = /(TZID)/.match(row)
@@ -105,49 +112,124 @@ module TimezoneFinder
105
112
  if tz_name_match
106
113
  tz_name = tz_name_match['name'].gsub('\\', '')
107
114
  @all_tz_names << tz_name
115
+ @nr_of_lines += 1
108
116
  # puts tz_name
109
117
 
118
+ actual_depth = 0
119
+ counted_coordinate_pairs = 0
120
+ encountered_nr_of_coordinates = []
121
+ row.each_char do |char|
122
+ if char == '['
123
+ actual_depth += 1
124
+ elsif char == ']'
125
+ actual_depth -= 1
126
+ if actual_depth == 2
127
+ counted_coordinate_pairs += 1
128
+ elsif actual_depth == 1
129
+ encountered_nr_of_coordinates << counted_coordinate_pairs
130
+ counted_coordinate_pairs = 0
131
+ end
132
+ end
133
+ end
134
+
135
+ if actual_depth != 0
136
+ fail ArgumentError, "uneven number of brackets detected. Something is wrong in line #{file_line}"
137
+ end
138
+
110
139
  coordinates = row.scan(/[-]?\d+\.?\d+/)
140
+
141
+ sum = encountered_nr_of_coordinates.inject(0) { |a, e| a + e }
142
+ if coordinates.length != sum * 2
143
+ fail ArgumentError, "There number of coordinates is counten wrong: #{coordinates.length} #{sum * 2}"
144
+ end
145
+ # TODO: detect and store all the holes in the bin
111
146
  # puts coordinates
112
147
 
113
148
  # nr_floats = coordinates.length
114
149
  x_coords = []
115
150
  y_coords = []
116
- i = 0
117
- coordinates.each do |coord|
118
- if i.even?
119
- x_coords << coord.to_f
151
+ xmax = -180.0
152
+ xmin = 180.0
153
+ ymax = -90.0
154
+ ymin = 90.0
155
+
156
+ pointer = 0
157
+ # the coordiate pairs within the first brackets [ [x,y], ..., [xn, yn] ] are the polygon coordinates
158
+ # The last coordinate pair should be left out (is equal to the first one)
159
+ (0...(2 * (encountered_nr_of_coordinates[0] - 1))).each do |n|
160
+ if n.even?
161
+ x = coordinates[pointer].to_f
162
+ x_coords << x
163
+ xmax = x if x > xmax
164
+ xmin = x if x < xmin
120
165
  else
121
- y_coords << coord.to_f
166
+ y = coordinates[pointer].to_f
167
+ y_coords << y
168
+ ymax = y if y > ymax
169
+ ymin = y if y < ymin
122
170
  end
123
- i += 1
124
- end
125
171
 
126
- fail "#{i} Floats in line #{n} found. Should be even (pairs or (x,y) )" if i.odd?
172
+ pointer += 1
173
+ end
127
174
 
128
175
  @all_coords << [x_coords, y_coords]
129
176
  @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
177
+ # puts(x_coords)
178
+ # puts(y_coords)
136
179
 
137
- x_coords.each do |x|
138
- xmax = x if x > xmax
139
- xmin = x if x < xmin
140
- end
180
+ @boundaries << [xmax, xmin, ymax, ymin]
141
181
 
142
- y_coords.each do |y|
143
- ymax = y if y > ymax
144
- ymin = y if y < ymin
182
+ amount_holes_this_line = encountered_nr_of_coordinates.length - 1
183
+ if amount_holes_this_line > 0
184
+ # store how many holes there are in this line
185
+ # store what the number of the first hole for this line is (for calculating the address to jump)
186
+ @first_hole_id_in_line << @amount_of_holes
187
+ # keep track of how many holes there are
188
+ @amount_of_holes += amount_holes_this_line
189
+ puts(tz_name)
190
+
191
+ (0...amount_holes_this_line).each do |_i|
192
+ @related_line << @nr_of_lines
193
+ puts(@nr_of_lines)
194
+
195
+ # puts(amount_holes_this_line)
196
+ end
145
197
  end
146
198
 
147
- @boundaries << [xmax, xmin, ymax, ymin]
199
+ # for every encountered hole
200
+ (1...(amount_holes_this_line + 1)).each do |i|
201
+ x_coords = []
202
+ y_coords = []
203
+
204
+ # since the last coordinate was being left out,
205
+ # we have to move the pointer 2 floats further to be in the hole data again
206
+ pointer += 2
207
+
208
+ # The last coordinate pair should be left out (is equal to the first one)
209
+ (0...(2 * (encountered_nr_of_coordinates[i] - 1))).each do |n|
210
+ if n.even?
211
+ x_coords << coordinates[pointer].to_f
212
+ else
213
+ y_coords << coordinates[pointer].to_f
214
+ end
215
+
216
+ pointer += 1
217
+ end
218
+
219
+ @all_holes << [x_coords, y_coords]
220
+ @all_hole_lengths << x_coords.length
221
+ end
148
222
  end
223
+
224
+ file_line += 1
149
225
  end
150
226
 
227
+ # so far the nr_of_lines was used to point to the current polygon but there is actually 1 more polygons in total
228
+ @nr_of_lines += 1
229
+
230
+ puts("amount_of_holes: #{@amount_of_holes}")
231
+ puts("amount of timezones: #{@nr_of_lines}")
232
+
151
233
  puts("Done\n\n")
152
234
  end
153
235
 
@@ -158,7 +240,6 @@ module TimezoneFinder
158
240
 
159
241
  def compile_into_binary(path = 'tz_binary.bin')
160
242
  nr_of_floats = 0
161
- nr_of_lines = 0
162
243
  zone_ids = []
163
244
  shortcuts = {}
164
245
 
@@ -436,7 +517,7 @@ module TimezoneFinder
436
517
  end
437
518
 
438
519
  def construct_shortcuts(shortcuts)
439
- puts('building shortucts...')
520
+ puts('building shortcuts...')
440
521
  puts('currently in line:')
441
522
  line = 0
442
523
  @boundaries.each do |xmax, xmin, ymax, ymin|
@@ -536,12 +617,16 @@ EOT
536
617
  end
537
618
  end
538
619
 
539
- puts('reading the converted .csv file')
620
+ # test_length = 0
540
621
  @ids.each do |id|
541
- nr_of_lines += 1
622
+ # test_length += 1
542
623
  zone_ids << id
543
624
  end
544
625
 
626
+ # if test_length != @nr_of_lines
627
+ # raise ArgumentError, "#{test_length} #{@nr_of_lines} #{@ids.length}"
628
+ # end
629
+
545
630
  @all_lengths.each do |length|
546
631
  nr_of_floats += 2 * length
547
632
  end
@@ -553,18 +638,63 @@ EOT
553
638
  puts("calculating the shortcuts took: #{end_time - start_time}")
554
639
 
555
640
  # address where the actual polygon data starts. look in the description below to get more info
556
- polygon_address = (24 * nr_of_lines + 6)
641
+ polygon_address = (24 * @nr_of_lines + 12)
557
642
 
558
643
  # for every original float now 4 bytes are needed (int32)
559
644
  shortcut_start_address = polygon_address + 4 * nr_of_floats
560
- puts("The number of polygons is: #{nr_of_lines}")
645
+
646
+ # write number of entries in shortcut field (x,y)
647
+ nr_of_entries_in_shortcut = []
648
+ shortcut_entries = []
649
+ amount_filled_shortcuts = 0
650
+
651
+ # count how many shortcut addresses will be written:
652
+ (0...(360 * NR_SHORTCUTS_PER_LNG)).each do |x|
653
+ (0...(180 * NR_SHORTCUTS_PER_LAT)).each do |y|
654
+ begin
655
+ this_lines_shortcuts = shortcuts.fetch([x, y])
656
+ shortcut_entries << this_lines_shortcuts
657
+ amount_filled_shortcuts += 1
658
+ nr_of_entries_in_shortcut << this_lines_shortcuts.length
659
+ # puts "(#{x}, #{y}, #{this_lines_shortcuts})"
660
+ rescue KeyError
661
+ nr_of_entries_in_shortcut << 0
662
+ end
663
+ end
664
+ end
665
+
666
+ amount_of_shortcuts = nr_of_entries_in_shortcut.length
667
+ if amount_of_shortcuts != 64_800 * NR_SHORTCUTS_PER_LNG * NR_SHORTCUTS_PER_LAT
668
+ puts(amount_of_shortcuts)
669
+ fail ArgumentError, 'this number of shortcut zones is wrong'
670
+ end
671
+
672
+ puts("number of filled shortcut zones are: #{amount_filled_shortcuts} (=#{(amount_filled_shortcuts.fdiv(amount_of_shortcuts) * 100).round(2)}% of all shortcuts)")
673
+
674
+ # for every shortcut S> and L> is written (nr of entries and address)
675
+ shortcut_space = 360 * NR_SHORTCUTS_PER_LNG * 180 * NR_SHORTCUTS_PER_LAT * 6
676
+ nr_of_entries_in_shortcut.each do |nr|
677
+ # every line in every shortcut takes up 2bytes
678
+ shortcut_space += 2 * nr
679
+ end
680
+
681
+ hole_start_address = shortcut_start_address + shortcut_space
682
+
683
+ puts("The number of polygons is: #{@nr_of_lines}")
561
684
  puts("The number of floats in all the polygons is (2 per point): #{nr_of_floats}")
562
685
  puts("now writing file \"#{path}\"")
563
686
  output_file = open(path, 'wb')
564
687
  # write nr_of_lines
565
- output_file.write([nr_of_lines].pack('S>'))
688
+ output_file.write([@nr_of_lines].pack('S>'))
566
689
  # write start address of shortcut_data:
567
690
  output_file.write([shortcut_start_address].pack('L>'))
691
+
692
+ # S> amount of holes
693
+ output_file.write([@amount_of_holes].pack('S>'))
694
+
695
+ # L> Address of Hole area (end of shortcut area +1) @ 8
696
+ output_file.write([hole_start_address].pack('L>'))
697
+
568
698
  # write zone_ids
569
699
  zone_ids.each do |zone_id|
570
700
  output_file.write([zone_id].pack('S>'))
@@ -603,33 +733,8 @@ EOT
603
733
  end
604
734
 
605
735
  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
736
 
737
+ # [SHORTCUT AREA]
633
738
  # write all nr of entries
634
739
  nr_of_entries_in_shortcut.each do |nr|
635
740
  fail "There are too many polygons in this shortcuts: #{nr}" if nr > 300
@@ -644,7 +749,7 @@ EOT
644
749
  output_file.write([0].pack('L>'))
645
750
  else
646
751
  output_file.write([shortcut_address].pack('L>'))
647
- # each polygon takes up 2 bytes of space
752
+ # each line_nr takes up 2 bytes of space
648
753
  shortcut_address += 2 * nr
649
754
  end
650
755
  end
@@ -652,17 +757,59 @@ EOT
652
757
  # write Line_Nrs for every shortcut
653
758
  shortcut_entries.each do |entries|
654
759
  entries.each do |entry|
655
- fail entry if entry > nr_of_lines
760
+ fail entry if entry > @nr_of_lines
656
761
  output_file.write([entry].pack('S>'))
657
762
  end
658
763
  end
659
764
 
765
+ # [HOLE AREA, Y = number of holes (very few: around 22)]
766
+
767
+ # '!H' for every hole store the related line
768
+ i = 0
769
+ @related_line.each do |line|
770
+ fail ArgumentError, line if line > @nr_of_lines
771
+ output_file.write([line].pack('S>'))
772
+ i += 1
773
+ end
774
+
775
+ if i > @amount_of_holes
776
+ fail ArgumentError, 'There are more related lines than holes.'
777
+ end
778
+
779
+ # 'S>' Y times [H unsigned short: nr of values (coordinate PAIRS! x,y in int32 int32) in this hole]
780
+ @all_hole_lengths.each do |length|
781
+ output_file.write([length].pack('S>'))
782
+ end
783
+
784
+ # '!I' Y times [ I unsigned int: absolute address of the byte where the data of that hole starts]
785
+ hole_address = output_file.tell + @amount_of_holes * 4
786
+ @all_hole_lengths.each do |length|
787
+ output_file.write([hole_address].pack('L>'))
788
+ # each pair of points takes up 8 bytes of space
789
+ hole_address += 8 * length
790
+ end
791
+
792
+ # Y times [ 2x i signed ints for every hole: x coords, y coords ]
793
+ # write hole polygon_data
794
+ @all_holes.each do |x_coords, y_coords|
795
+ x_coords.each do |x|
796
+ output_file.write([Helpers.coord2int(x)].pack('l>'))
797
+ end
798
+ y_coords.each do |y|
799
+ output_file.write([Helpers.coord2int(y)].pack('l>'))
800
+ end
801
+ end
802
+
660
803
  last_address = output_file.tell
661
- shortcut_space = last_address - shortcut_start_address
804
+ hole_space = last_address - hole_start_address
805
+ if shortcut_space != last_address - shortcut_start_address - hole_space
806
+ fail ArgumentError, 'shortcut space is computed wrong'
807
+ end
662
808
  polygon_space = nr_of_floats * 4
663
809
 
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")
810
+ puts("the polygon data makes up #{(polygon_space.fdiv(last_address) * 100).round(2)}% of the file")
811
+ puts("the shortcuts make up #{(shortcut_space.fdiv(last_address) * 100).round(2) }% of the file")
812
+ puts("the holes make up #{(hole_space.fdiv(last_address) * 100).round(2) }% of the file")
666
813
 
667
814
  puts('Success!')
668
815
  end
@@ -674,37 +821,58 @@ and it takes lot less space, without loosing too much accuracy (min accuracy is
674
821
 
675
822
  no of rows (= no of polygons = no of boundaries)
676
823
  approx. 28k -> use 2byte unsigned short (has range until 65k)
677
- '!H' = n
824
+ 'S>' = n
825
+
826
+ L> Address of Shortcut area (end of polygons+1) @ 2
827
+
828
+ S> amount of holes @6
678
829
 
679
- I Address of Shortcut area (end of polygons+1) @ 2
830
+ L> Address of Hole area (end of shortcut area +1) @ 8
680
831
 
681
- '!H' n times [H unsigned short: zone number=ID in this line, @ 6 + 2* lineNr]
832
+ 'S>' n times [H unsigned short: zone number=ID in this line, @ 12 + 2* lineNr]
682
833
 
683
- '!H' n times [H unsigned short: nr of values (coordinate PAIRS! x,y in long long) in this line, @ 6 + 2n + 2* lineNr]
834
+ 'S>' n times [H unsigned short: nr of values (coordinate PAIRS! x,y in long long) in this line, @ 12 + 2n + 2* lineNr]
684
835
 
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]
836
+ 'L>'n times [ I unsigned int: absolute address of the byte where the polygon-data of that line starts,
837
+ @ 12 + 4 * n + 4*lineNr]
687
838
 
688
839
 
689
840
 
690
- n times 4 int32 (take up 4*4 per line): xmax, xmin, ymax, ymin @ 6 + 8n + 16* lineNr
691
- '!iiii'
841
+ n times 4 int32 (take up 4*4 per line): xmax, xmin, ymax, ymin @ 12 + 8n + 16* lineNr
842
+ 'l>l>l>l>'
692
843
 
693
844
 
694
- [starting @ 6+ 24*n = polygon data start address]
845
+ [starting @ 12+ 24*n = polygon data start address]
695
846
  (for every line: x coords, y coords:) stored @ Address section (see above)
696
- '!i' * amount of points
847
+ 'l>' * amount of points
848
+
849
+ 360 * NR_SHORTCUTS_PER_LNG * 180 * NR_SHORTCUTS_PER_LAT:
850
+ [atm: 360* 1 * 180 * 2 = 129,600]
851
+ 129,600 times S> number of entries in shortcut field (x,y) @ Pointer see above
697
852
 
853
+
854
+ [SHORTCUT AREA]
698
855
  360 * NR_SHORTCUTS_PER_LNG * 180 * NR_SHORTCUTS_PER_LAT:
699
856
  [atm: 360* 1 * 180 * 2 = 129,600]
700
- 129,600 times !H number of entries in shortcut field (x,y) @ Pointer see above
857
+ 129,600 times S> number of entries in shortcut field (x,y) @ Pointer see above
701
858
 
702
859
 
703
860
  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
861
+ 129,600 times L>
705
862
 
706
863
  [X = number of filled shortcuts]
707
- X times !H * amount Polygon_Nr @ address stored in previous section
864
+ X times S> * amount Polygon_Nr @ address stored in previous section
865
+
866
+
867
+ [HOLE AREA, Y = number of holes (very few: around 22)]
868
+
869
+ 'S>' for every hole store the related line
870
+
871
+ 'S>' Y times [S unsigned short: nr of values (coordinate PAIRS! x,y in int32 int32) in this hole]
872
+
873
+ 'L>' Y times [ L unsigned int: absolute address of the byte where the data of that hole starts]
874
+
875
+ Y times [ 2x i signed ints for every hole: x coords, y coords ]
708
876
 
709
877
  EOT
710
878
  end
@@ -1,5 +1,5 @@
1
1
  module TimezoneFinder
2
- VERSION = '0.0.1' unless defined? TimezoneFinder::Version
2
+ VERSION = '1.5.5' unless defined? TimezoneFinder::Version
3
3
  # https://github.com/MrMinimal64/timezonefinder
4
- BASED_SHA1_OF_PYTHON = '55861df0d1ae6c4727e7f48068f7cc51ea3891b3'
4
+ BASED_SHA1_OF_PYTHON = 'f291dad017a839f25c472f4976d2a187de104ca8'
5
5
  end
@@ -6,24 +6,23 @@ module TimezoneFinder
6
6
  class Helpers
7
7
  # tests if a point pX(x,y) is Left|On|Right of an infinite line from p1 to p2
8
8
  # Return: 2 for pX left of the line from! p1 to! p2
9
- # 1 for pX on the line
9
+ # 1 for pX on the line [is not needed]
10
10
  # 0 for pX right of the line
11
11
  # this approach is only valid because we already know that y lies within ]y1;y2]
12
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,
13
+ if x1 < x2
14
+ # p2 is further right than p1
15
+ if x > x2
16
+ # pX is further right than p2,
17
17
  if y1 > y2
18
- # so it has to be left of the line p1-p2
19
18
  return 2
20
19
  else
21
20
  return 0
22
21
  end
23
22
  end
24
23
 
25
- if x < x2
26
- # pX is further left than p2,
24
+ if x < x1
25
+ # pX is further left than p1
27
26
  if y1 > y2
28
27
  # so it has to be right of the line p1-p2
29
28
  return 0
@@ -32,44 +31,22 @@ module TimezoneFinder
32
31
  end
33
32
  end
34
33
 
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
34
+ x1gtx2 = false
58
35
 
59
36
  else
60
- # p2 is further right than p1
61
-
62
- if x > x2
63
- # pX is further right than p2,
37
+ # p1 is further right than p2
38
+ if x > x1
39
+ # pX is further right than p1,
64
40
  if y1 > y2
41
+ # so it has to be left of the line p1-p2
65
42
  return 2
66
43
  else
67
44
  return 0
68
45
  end
69
46
  end
70
47
 
71
- if x < x1
72
- # pX is further left than p1
48
+ if x < x2
49
+ # pX is further left than p2,
73
50
  if y1 > y2
74
51
  # so it has to be right of the line p1-p2
75
52
  return 0
@@ -78,16 +55,23 @@ module TimezoneFinder
78
55
  end
79
56
  end
80
57
 
81
- x1gtx2 = False
58
+ # TODO: is not return also accepted
59
+ if x1 == x2 && x == x1
60
+ # could also be equal
61
+ return 1
62
+ end
63
+
64
+ # x1 greater than x2
65
+ x1gtx2 = true
82
66
  end
83
67
 
84
68
  # x is between [x1;x2]
85
69
  # 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]'
70
+ # delta_y cannot be 0 here because of the condition 'y lies within ]y1;y2]'
87
71
  # 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
72
+ delta_x = ((y - y1) * ((x2 - x1).fdiv(y2 - y1))) + x1 - x
89
73
 
90
- if x_difference > 0
74
+ if delta_x > 0
91
75
  if x1gtx2
92
76
  if y1 > y2
93
77
  return 0
@@ -103,7 +87,7 @@ module TimezoneFinder
103
87
  end
104
88
  end
105
89
 
106
- elsif x_difference == 0
90
+ elsif delta_x == 0
107
91
  return 1
108
92
 
109
93
  else
@@ -182,6 +166,21 @@ module TimezoneFinder
182
166
  wn != 0
183
167
  end
184
168
 
169
+ def self.all_the_same(pointer, length, id_list)
170
+ # List mustn't be empty or Null
171
+ # There is at least one
172
+
173
+ element = id_list[pointer]
174
+ pointer += 1
175
+
176
+ while pointer < length
177
+ return -1 if element != id_list[pointer]
178
+ pointer += 1
179
+ end
180
+
181
+ element
182
+ end
183
+
185
184
  def self.cartesian2rad(x, y, z)
186
185
  [Math.atan2(y, x), Math.asin(z)]
187
186
  end
@@ -1,7 +1,7 @@
1
1
  # rubocop:disable Metrics/ClassLength,Metrics/MethodLength,Metrics/LineLength
2
2
  # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/ParameterLists
3
3
  # rubocop:disable Style/PredicateName,Style/Next,Style/AndOr
4
- # rubocop:disable Lint/Void
4
+ # rubocop:disable Lint/Void,Lint/HandleExceptions
5
5
  require_relative 'helpers'
6
6
  require_relative 'timezone_names'
7
7
 
@@ -23,13 +23,45 @@ module TimezoneFinder
23
23
  # the address where the shortcut section starts (after all the polygons) this is 34 433 054
24
24
  @shortcuts_start = @binary_file.read(4).unpack('L>')[0]
25
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
26
+ @amount_of_holes = @binary_file.read(2).unpack('S>')[0]
27
+
28
+ @hole_area_start = @binary_file.read(4).unpack('L>')[0]
29
+
30
+ @nr_val_start_address = 2 * @nr_of_entries + 12
31
+ @adr_start_address = 4 * @nr_of_entries + 12
32
+ @bound_start_address = 8 * @nr_of_entries + 12
33
+ # @poly_start_address = 40 * @nr_of_entries + 12
31
34
  @first_shortcut_address = @shortcuts_start + 259_200
32
35
 
36
+ @nr_val_hole_address = @hole_area_start + @amount_of_holes * 2
37
+ @adr_hole_address = @hole_area_start + @amount_of_holes * 4
38
+ # @hole_data_start = @hole_area_start + @amount_of_holes * 8
39
+
40
+ # for store for which polygons (how many) holes exits and the id of the first of those holes
41
+ @hole_registry = {}
42
+ last_encountered_line_nr = 0
43
+ first_hole_id = 0
44
+ amount_of_holes = 0
45
+ @binary_file.seek(@hole_area_start)
46
+ (0...@amount_of_holes).each do |i|
47
+ related_line = @binary_file.read(2).unpack('S>')[0]
48
+ # puts(related_line)
49
+ if related_line == last_encountered_line_nr
50
+ amount_of_holes += 1
51
+ else
52
+ if i != 0
53
+ @hole_registry.update(last_encountered_line_nr => [amount_of_holes, first_hole_id])
54
+ end
55
+
56
+ last_encountered_line_nr = related_line
57
+ first_hole_id = i
58
+ amount_of_holes = 1
59
+ end
60
+ end
61
+
62
+ # write the entry for the last hole(s) in the registry
63
+ @hole_registry.update(last_encountered_line_nr => [amount_of_holes, first_hole_id])
64
+
33
65
  ObjectSpace.define_finalizer(self, self.class.__del__)
34
66
  end
35
67
 
@@ -41,7 +73,7 @@ module TimezoneFinder
41
73
 
42
74
  def id_of(line = 0)
43
75
  # ids start at address 6. per line one unsigned 2byte int is used
44
- @binary_file.seek((6 + 2 * line))
76
+ @binary_file.seek((12 + 2 * line))
45
77
  @binary_file.read(2).unpack('S>')[0]
46
78
  end
47
79
 
@@ -50,7 +82,7 @@ module TimezoneFinder
50
82
 
51
83
  i = 0
52
84
  iterable.each do |line_nr|
53
- @binary_file.seek((6 + 2 * line_nr))
85
+ @binary_file.seek((12 + 2 * line_nr))
54
86
  id_array[i] = @binary_file.read(2).unpack('S>')[0]
55
87
  i += 1
56
88
  end
@@ -102,7 +134,24 @@ module TimezoneFinder
102
134
  Helpers.fromfile(@binary_file, false, 4, nr_of_values)]
103
135
  end
104
136
 
105
- # @profile
137
+ def _holes_of_line(line = 0)
138
+ amount_of_holes, hole_id = @hole_registry.fetch(line)
139
+
140
+ (0...amount_of_holes).each do |_i|
141
+ @binary_file.seek(@nr_val_hole_address + 2 * hole_id)
142
+ nr_of_values = @binary_file.read(2).unpack('S>')[0]
143
+
144
+ @binary_file.seek(@adr_hole_address + 4 * hole_id)
145
+ @binary_file.seek(@binary_file.read(4).unpack('L>')[0])
146
+
147
+ yield [Helpers.fromfile(@binary_file, false, 4, nr_of_values),
148
+ Helpers.fromfile(@binary_file, false, 4, nr_of_values)]
149
+
150
+ hole_id += 1
151
+ end
152
+ rescue KeyError
153
+ end
154
+
106
155
  # This function searches for the closest polygon in the surrounding shortcuts.
107
156
  # Make sure that the point does not lie within a polygon (for that case the algorithm is simply wrong!)
108
157
  # Note that the algorithm won't find the closest polygon when it's on the 'other end of earth'
@@ -219,18 +268,16 @@ module TimezoneFinder
219
268
  return TIMEZONE_NAMES[id_of(possible_polygons[0])] if nr_possible_polygons == 1
220
269
 
221
270
  # initialize the list of ids
271
+ # TODO: sort from least to most occurrences
222
272
  ids = possible_polygons.map { |p| id_of(p) }
223
273
 
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
274
  # otherwise check if the point is included for all the possible polygons
231
275
  (0...nr_possible_polygons).each do |i|
232
276
  polygon_nr = possible_polygons[i]
233
277
 
278
+ same_element = Helpers.all_the_same(i, nr_possible_polygons, ids)
279
+ return TIMEZONE_NAMES[same_element] if same_element != -1
280
+
234
281
  # get the boundaries of the polygon = (lng_max, lng_min, lat_max, lat_min)
235
282
  # self.binary_file.seek((@bound_start_address + 32 * polygon_nr), )
236
283
  @binary_file.seek((@bound_start_address + 16 * polygon_nr))
@@ -238,8 +285,19 @@ module TimezoneFinder
238
285
  # only run the algorithm if it the point is withing the boundaries
239
286
  unless x > boundaries[0] or x < boundaries[1] or y > boundaries[2] or y < boundaries[3]
240
287
 
241
- if Helpers.inside_polygon(x, y, coords_of(polygon_nr))
242
- return TIMEZONE_NAMES[ids[i]]
288
+ outside_all_holes = true
289
+ # when the point is within a hole of the polygon this timezone doesn't need to be checked
290
+ _holes_of_line(polygon_nr) do |hole_coordinates|
291
+ if Helpers.inside_polygon(x, y, hole_coordinates)
292
+ outside_all_holes = false
293
+ break
294
+ end
295
+ end
296
+
297
+ if outside_all_holes
298
+ if Helpers.inside_polygon(x, y, coords_of(polygon_nr))
299
+ return TIMEZONE_NAMES[ids[i]]
300
+ end
243
301
  end
244
302
  end
245
303
  end
@@ -267,9 +325,20 @@ module TimezoneFinder
267
325
  @binary_file.seek((@bound_start_address + 16 * polygon_nr))
268
326
  boundaries = Helpers.fromfile(@binary_file, false, 4, 4)
269
327
  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)]
328
+
329
+ outside_all_holes = true
330
+ # when the point is within a hole of the polygon this timezone doesn't need to be checked
331
+ _holes_of_line(polygon_nr) do |hole_coordinates|
332
+ if Helpers.inside_polygon(x, y, hole_coordinates)
333
+ outside_all_holes = false
334
+ break
335
+ end
336
+ end
337
+
338
+ if outside_all_holes
339
+ if Helpers.inside_polygon(x, y, coords_of(polygon_nr))
340
+ return TIMEZONE_NAMES[id_of(polygon_nr)]
341
+ end
273
342
  end
274
343
  end
275
344
  end
@@ -276,6 +276,7 @@ module TimezoneFinder
276
276
  'Asia/Tehran',
277
277
  'Asia/Thimphu',
278
278
  'Asia/Tokyo',
279
+ 'Asia/Tomsk',
279
280
  'Asia/Ulaanbaatar',
280
281
  'Asia/Urumqi',
281
282
  'Asia/Ust-Nera',
@@ -328,6 +329,7 @@ module TimezoneFinder
328
329
  'Europe/Jersey',
329
330
  'Europe/Kaliningrad',
330
331
  'Europe/Kiev',
332
+ 'Europe/Kirov',
331
333
  'Europe/Lisbon',
332
334
  'Europe/Ljubljana',
333
335
  'Europe/London',
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = 'Look up timezone from lat / long offline.'
13
13
  spec.description = %(
14
- Python library to look up timezone from lat / long offline.
14
+ A pure Ruby library to look up timezone from latitude / longitude offline.
15
15
  Ported version of 'timezonefinder' on PyPI.
16
16
  ).strip.gsub(/\s+/, ' ')
17
17
  spec.homepage = 'https://github.com/gunyarakun/timezone_finder'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timezone_finder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tasuku SUENAGA a.k.a. gunyarakun
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-26 00:00:00.000000000 Z
11
+ date: 2016-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -80,8 +80,8 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.40'
83
- description: Python library to look up timezone from lat / long offline. Ported version
84
- of 'timezonefinder' on PyPI.
83
+ description: A pure Ruby library to look up timezone from latitude / longitude offline.
84
+ Ported version of 'timezonefinder' on PyPI.
85
85
  email:
86
86
  - tasuku-s-github@titech.ac
87
87
  executables: []
@@ -93,6 +93,7 @@ files:
93
93
  - ".rubocop.yml"
94
94
  - ".rubocop_todo.yml"
95
95
  - ".travis.yml"
96
+ - ChangeLog
96
97
  - Gemfile
97
98
  - LICENSE.txt
98
99
  - README.md