timezone_finder 0.0.1 → 1.5.5

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 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