timezone_finder 1.5.5 → 1.5.6

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: 13d427ec4275e9a9cf091179165cdaee2c427f08
4
- data.tar.gz: bd258529875fec7b818f7839c2a116ff6a29e434
3
+ metadata.gz: e74c3bfc9784467f66b1c270bc2473168e8b8838
4
+ data.tar.gz: c5bce138f2d33d467b46a3cdf8bfd214e3d01971
5
5
  SHA512:
6
- metadata.gz: a7e3da84f42cc2b1a55c06cfd0ceb35119be8dce2c549a1872fd7f32f3373685582891d34800d03456111e3b89c3c55bda86902a8ff62aa3699077e3e72c525b
7
- data.tar.gz: 48374abaab46b420242964dd2373899b4d602f10605964f7497b7580f24200af8d62e99b6f551ea32b79c446dfc2b6cf266d41cfbdad58328f0722c4e9b2097c
6
+ metadata.gz: 205cb31ffe251b8c2104fbe97f4535462d94a1a71625f7e090ecdb05937fd9c79861e8c912858d344ff6cf6b8aa8e00ab720c5d48f62ac2fa82010b0b3e1d4b9
7
+ data.tar.gz: c183ace8223052a82150e8627e91779647be2ca83ee83b33ca456c0033c56eecb2e40e315d70734794232cca6f6db355db4e015b4fdf7ebbcd4e283e5c39dad3
data/ChangeLog CHANGED
@@ -1,3 +1,11 @@
1
+ == 2016-06-19 version 1.5.6
2
+
3
+ * using little endian encoding now
4
+ * introduced test for checking the proper functionality of the helper functions
5
+ * wrote tests for proximity algorithms
6
+ * improved proximity algorithms: introduced exact_computation, return_distances and force_evaluation functionality (s. Readme or documentation for more info)
7
+
8
+
1
9
  == 2016-06-03 version 1.5.5
2
10
 
3
11
  * using the newest version (2016d, May 2016) of the tz_world data from http://efele.net/maps/tz/world/
data/README.md CHANGED
@@ -34,10 +34,13 @@ require 'timezone_finder'
34
34
  tf = TimezoneFinder.create
35
35
  ```
36
36
 
37
- #### fast algorithm:
37
+ #### timezone\_at():
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).
39
+ This is the default function to check which timezone a point lies in.
40
+ If no timezone has been found, `nil` is being returned.
41
+ **NOTE:** This approach is optimized for speed and the common case to only query points actually within a timezone.
42
+ This might not be what you are looking for however: When there is only one possible timezone in proximity, this timezone would be returned
43
+ (without checking if the point is included first).
41
44
 
42
45
  ```ruby
43
46
  # point = (longitude, latitude)
@@ -46,36 +49,53 @@ puts tf.timezone_at(*point)
46
49
  # = Europe/Berlin
47
50
  ```
48
51
 
49
- #### To make sure a point is really inside a timezone (slower):
52
+ #### certain\_timezone\_at()
53
+
54
+ This function is for making sure a point is really inside a timezone. It is slower, because all polygons (with shortcuts in that area)
55
+ are checked until one polygon is matched.
50
56
 
51
57
  ```ruby
52
58
  puts tf.certain_timezone_at(*point)
53
59
  # = Europe/Berlin
54
60
  ```
55
61
 
56
- #### To find the closest timezone (slow):
62
+ #### Proximity algorithm
63
+
64
+ Only use this when the point is not inside a polygon, because the approach otherwise makes no sense.
65
+ This returns the closest timezone of all polygons within +-1 degree lng and +-1 degree lat (or None).
57
66
 
58
67
  ```ruby
59
- # only use this when the point is not inside a polygon!
60
- # this checks all the polygons within +-1 degree lng and +-1 degree lat
61
68
  point = (12.773955, 55.578595)
62
69
  puts tf.closest_timezone_at(*point)
63
70
  # = Europe/Copenhagens
64
71
  ```
65
72
 
66
- #### To increase search radius even more (very slow):
73
+ #### Other options:
74
+
75
+ To increase search radius even more, use the `delta_degree`-option:
67
76
 
68
77
  ```ruby
69
- # this checks all the polygons within +-3 degree lng and +-3 degree lat
70
- # I recommend only slowly increasing the search radius
71
- # keep in mind that x degrees lat are not the same distance apart than x degree lng!
72
78
  puts tf.closest_timezone_at(*point, 3)
73
79
  # = Europe/Copenhagens
74
80
  ```
75
81
 
76
- (to make sure you really got the closest timezone increase the search
77
- radius until you get a result. then increase the radius once more and
78
- take this result.)
82
+ This checks all the polygons within +-3 degree lng and +-3 degree lat.
83
+ I recommend only slowly increasing the search radius, since computation time increases quite quickly
84
+ (with the amount of polygons which need to be evaluated) and there might be many polygons within a couple degrees.
85
+
86
+ Also keep in mind that x degrees lat are not the same distance apart than x degree lng (earth is a sphere)!
87
+ So to really make sure you got the closest timezone increase the search radius until you get a result,
88
+ then increase the radius once more and take this result. (this should only make a difference in really rare cases)
89
+
90
+ With `exact_computation=true` the distance to every polygon edge is computed (way more complicated)
91
+ , instead of just evaluating the distances to all the vertices. This only makes a real difference when polygons are very close.
92
+
93
+ With `return_distances=true` the output looks like this:
94
+
95
+ [ 'tz_name_of_the_closest_polygon',[ distances to every polygon in km], [tz_names of every polygon]]
96
+
97
+ Note that some polygons might not be tested (for example when a zone is found to be the closest already).
98
+ To prevent this use `force_evaluation=true`.
79
99
 
80
100
  ## Developer
81
101
 
@@ -102,6 +102,8 @@ module TimezoneFinder
102
102
  def parse_polygons_from_json(path = 'tz_world.json')
103
103
  f = open(path, 'r')
104
104
  puts 'Parsing data from .json'
105
+ puts 'encountered holes at: '
106
+
105
107
  # file_line is the current line in the .json file being parsed. This is not the id of the Polygon!
106
108
  file_line = 0
107
109
  f.each_line do |row|
@@ -671,7 +673,7 @@ EOT
671
673
 
672
674
  puts("number of filled shortcut zones are: #{amount_filled_shortcuts} (=#{(amount_filled_shortcuts.fdiv(amount_of_shortcuts) * 100).round(2)}% of all shortcuts)")
673
675
 
674
- # for every shortcut S> and L> is written (nr of entries and address)
676
+ # for every shortcut S< and L< is written (nr of entries and address)
675
677
  shortcut_space = 360 * NR_SHORTCUTS_PER_LNG * 180 * NR_SHORTCUTS_PER_LAT * 6
676
678
  nr_of_entries_in_shortcut.each do |nr|
677
679
  # every line in every shortcut takes up 2bytes
@@ -685,28 +687,28 @@ EOT
685
687
  puts("now writing file \"#{path}\"")
686
688
  output_file = open(path, 'wb')
687
689
  # write nr_of_lines
688
- output_file.write([@nr_of_lines].pack('S>'))
690
+ output_file.write([@nr_of_lines].pack('S<'))
689
691
  # write start address of shortcut_data:
690
- output_file.write([shortcut_start_address].pack('L>'))
692
+ output_file.write([shortcut_start_address].pack('L<'))
691
693
 
692
- # S> amount of holes
693
- output_file.write([@amount_of_holes].pack('S>'))
694
+ # S< amount of holes
695
+ output_file.write([@amount_of_holes].pack('S<'))
694
696
 
695
- # L> Address of Hole area (end of shortcut area +1) @ 8
696
- output_file.write([hole_start_address].pack('L>'))
697
+ # L< Address of Hole area (end of shortcut area +1) @ 8
698
+ output_file.write([hole_start_address].pack('L<'))
697
699
 
698
700
  # write zone_ids
699
701
  zone_ids.each do |zone_id|
700
- output_file.write([zone_id].pack('S>'))
702
+ output_file.write([zone_id].pack('S<'))
701
703
  end
702
704
  # write number of values
703
705
  @all_lengths.each do |length|
704
- output_file.write([length].pack('S>'))
706
+ output_file.write([length].pack('S<'))
705
707
  end
706
708
 
707
709
  # write polygon_addresses
708
710
  @all_lengths.each do |length|
709
- output_file.write([polygon_address].pack('L>'))
711
+ output_file.write([polygon_address].pack('L<'))
710
712
  # data of the next polygon is at the address after all the space the points take
711
713
  # nr of points stored * 2 ints per point * 4 bytes per int
712
714
  polygon_address += 8 * length
@@ -719,16 +721,16 @@ EOT
719
721
 
720
722
  # write boundary_data
721
723
  @boundaries.each do |b|
722
- output_file.write(b.map { |c| Helpers.coord2int(c) }.pack('l>l>l>l>'))
724
+ output_file.write(b.map { |c| Helpers.coord2int(c) }.pack('l<l<l<l<'))
723
725
  end
724
726
 
725
727
  # write polygon_data
726
728
  @all_coords.each do |x_coords, y_coords|
727
729
  x_coords.each do |x|
728
- output_file.write([Helpers.coord2int(x)].pack('l>'))
730
+ output_file.write([Helpers.coord2int(x)].pack('l<'))
729
731
  end
730
732
  y_coords.each do |y|
731
- output_file.write([Helpers.coord2int(y)].pack('l>'))
733
+ output_file.write([Helpers.coord2int(y)].pack('l<'))
732
734
  end
733
735
  end
734
736
 
@@ -738,7 +740,7 @@ EOT
738
740
  # write all nr of entries
739
741
  nr_of_entries_in_shortcut.each do |nr|
740
742
  fail "There are too many polygons in this shortcuts: #{nr}" if nr > 300
741
- output_file.write([nr].pack('S>'))
743
+ output_file.write([nr].pack('S<'))
742
744
  end
743
745
 
744
746
  # write Address of first Polygon_nr in shortcut field (x,y)
@@ -746,9 +748,9 @@ EOT
746
748
  shortcut_address = output_file.tell + 259_200 * NR_SHORTCUTS_PER_LNG * NR_SHORTCUTS_PER_LAT
747
749
  nr_of_entries_in_shortcut.each do |nr|
748
750
  if nr == 0
749
- output_file.write([0].pack('L>'))
751
+ output_file.write([0].pack('L<'))
750
752
  else
751
- output_file.write([shortcut_address].pack('L>'))
753
+ output_file.write([shortcut_address].pack('L<'))
752
754
  # each line_nr takes up 2 bytes of space
753
755
  shortcut_address += 2 * nr
754
756
  end
@@ -758,17 +760,17 @@ EOT
758
760
  shortcut_entries.each do |entries|
759
761
  entries.each do |entry|
760
762
  fail entry if entry > @nr_of_lines
761
- output_file.write([entry].pack('S>'))
763
+ output_file.write([entry].pack('S<'))
762
764
  end
763
765
  end
764
766
 
765
767
  # [HOLE AREA, Y = number of holes (very few: around 22)]
766
768
 
767
- # '!H' for every hole store the related line
769
+ # 'S<' for every hole store the related line
768
770
  i = 0
769
771
  @related_line.each do |line|
770
772
  fail ArgumentError, line if line > @nr_of_lines
771
- output_file.write([line].pack('S>'))
773
+ output_file.write([line].pack('S<'))
772
774
  i += 1
773
775
  end
774
776
 
@@ -776,15 +778,15 @@ EOT
776
778
  fail ArgumentError, 'There are more related lines than holes.'
777
779
  end
778
780
 
779
- # 'S>' Y times [H unsigned short: nr of values (coordinate PAIRS! x,y in int32 int32) in this hole]
781
+ # 'S<' Y times [H unsigned short: nr of values (coordinate PAIRS! x,y in int32 int32) in this hole]
780
782
  @all_hole_lengths.each do |length|
781
- output_file.write([length].pack('S>'))
783
+ output_file.write([length].pack('S<'))
782
784
  end
783
785
 
784
- # '!I' Y times [ I unsigned int: absolute address of the byte where the data of that hole starts]
786
+ # 'L<' Y times [ I unsigned int: absolute address of the byte where the data of that hole starts]
785
787
  hole_address = output_file.tell + @amount_of_holes * 4
786
788
  @all_hole_lengths.each do |length|
787
- output_file.write([hole_address].pack('L>'))
789
+ output_file.write([hole_address].pack('L<'))
788
790
  # each pair of points takes up 8 bytes of space
789
791
  hole_address += 8 * length
790
792
  end
@@ -793,10 +795,10 @@ EOT
793
795
  # write hole polygon_data
794
796
  @all_holes.each do |x_coords, y_coords|
795
797
  x_coords.each do |x|
796
- output_file.write([Helpers.coord2int(x)].pack('l>'))
798
+ output_file.write([Helpers.coord2int(x)].pack('l<'))
797
799
  end
798
800
  y_coords.each do |y|
799
- output_file.write([Helpers.coord2int(y)].pack('l>'))
801
+ output_file.write([Helpers.coord2int(y)].pack('l<'))
800
802
  end
801
803
  end
802
804
 
@@ -821,56 +823,56 @@ and it takes lot less space, without loosing too much accuracy (min accuracy is
821
823
 
822
824
  no of rows (= no of polygons = no of boundaries)
823
825
  approx. 28k -> use 2byte unsigned short (has range until 65k)
824
- 'S>' = n
826
+ 'S<' = n
825
827
 
826
- L> Address of Shortcut area (end of polygons+1) @ 2
828
+ L< Address of Shortcut area (end of polygons+1) @ 2
827
829
 
828
- S> amount of holes @6
830
+ S< amount of holes @6
829
831
 
830
- L> Address of Hole area (end of shortcut area +1) @ 8
832
+ L< Address of Hole area (end of shortcut area +1) @ 8
831
833
 
832
- 'S>' n times [H unsigned short: zone number=ID in this line, @ 12 + 2* lineNr]
834
+ 'S<' n times [H unsigned short: zone number=ID in this line, @ 12 + 2* lineNr]
833
835
 
834
- 'S>' n times [H unsigned short: nr of values (coordinate PAIRS! x,y in long long) in this line, @ 12 + 2n + 2* lineNr]
836
+ 'S<' n times [H unsigned short: nr of values (coordinate PAIRS! x,y in long long) in this line, @ 12 + 2n + 2* lineNr]
835
837
 
836
- 'L>'n times [ I unsigned int: absolute address of the byte where the polygon-data of that line starts,
838
+ 'L<'n times [ I unsigned int: absolute address of the byte where the polygon-data of that line starts,
837
839
  @ 12 + 4 * n + 4*lineNr]
838
840
 
839
841
 
840
842
 
841
843
  n times 4 int32 (take up 4*4 per line): xmax, xmin, ymax, ymin @ 12 + 8n + 16* lineNr
842
- 'l>l>l>l>'
844
+ 'l<l<l<l<'
843
845
 
844
846
 
845
847
  [starting @ 12+ 24*n = polygon data start address]
846
848
  (for every line: x coords, y coords:) stored @ Address section (see above)
847
- 'l>' * amount of points
849
+ 'l<' * amount of points
848
850
 
849
851
  360 * NR_SHORTCUTS_PER_LNG * 180 * NR_SHORTCUTS_PER_LAT:
850
852
  [atm: 360* 1 * 180 * 2 = 129,600]
851
- 129,600 times S> number of entries in shortcut field (x,y) @ Pointer see above
853
+ 129,600 times S< number of entries in shortcut field (x,y) @ Pointer see above
852
854
 
853
855
 
854
856
  [SHORTCUT AREA]
855
857
  360 * NR_SHORTCUTS_PER_LNG * 180 * NR_SHORTCUTS_PER_LAT:
856
858
  [atm: 360* 1 * 180 * 2 = 129,600]
857
- 129,600 times S> number of entries in shortcut field (x,y) @ Pointer see above
859
+ 129,600 times S< number of entries in shortcut field (x,y) @ Pointer see above
858
860
 
859
861
 
860
862
  Address of first Polygon_nr in shortcut field (x,y) [0 if there is no entry] @ Pointer see above + 129,600
861
- 129,600 times L>
863
+ 129,600 times L<
862
864
 
863
865
  [X = number of filled shortcuts]
864
- X times S> * amount Polygon_Nr @ address stored in previous section
866
+ X times S< * amount Polygon_Nr @ address stored in previous section
865
867
 
866
868
 
867
869
  [HOLE AREA, Y = number of holes (very few: around 22)]
868
870
 
869
- 'S>' for every hole store the related line
871
+ 'S<' for every hole store the related line
870
872
 
871
- 'S>' Y times [S unsigned short: nr of values (coordinate PAIRS! x,y in int32 int32) in this hole]
873
+ 'S<' Y times [S unsigned short: nr of values (coordinate PAIRS! x,y in int32 int32) in this hole]
872
874
 
873
- 'L>' Y times [ L unsigned int: absolute address of the byte where the data of that hole starts]
875
+ 'L<' Y times [ L unsigned int: absolute address of the byte where the data of that hole starts]
874
876
 
875
877
  Y times [ 2x i signed ints for every hole: x coords, y coords ]
876
878
 
@@ -1,5 +1,5 @@
1
1
  module TimezoneFinder
2
- VERSION = '1.5.5' unless defined? TimezoneFinder::Version
2
+ VERSION = '1.5.6' unless defined? TimezoneFinder::Version
3
3
  # https://github.com/MrMinimal64/timezonefinder
4
- BASED_SHA1_OF_PYTHON = 'f291dad017a839f25c472f4976d2a187de104ca8'
4
+ BASED_SHA1_OF_PYTHON = 'c948967a2f9c8bd03535b181fa6faa13168955ca'
5
5
  end
@@ -5,9 +5,9 @@
5
5
  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
- # Return: 2 for pX left of the line from! p1 to! p2
9
- # 1 for pX on the line [is not needed]
10
- # 0 for pX right of the line
8
+ # Return: -1 for pX left of the line from! p1 to! p2
9
+ # 0 for pX on the line [is not needed]
10
+ # 1 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
13
  if x1 < x2
@@ -15,9 +15,9 @@ module TimezoneFinder
15
15
  if x > x2
16
16
  # pX is further right than p2,
17
17
  if y1 > y2
18
- return 2
18
+ return -1
19
19
  else
20
- return 0
20
+ return 1
21
21
  end
22
22
  end
23
23
 
@@ -25,9 +25,9 @@ module TimezoneFinder
25
25
  # pX is further left than p1
26
26
  if y1 > y2
27
27
  # so it has to be right of the line p1-p2
28
- return 0
28
+ return 1
29
29
  else
30
- return 2
30
+ return -1
31
31
  end
32
32
  end
33
33
 
@@ -39,9 +39,9 @@ module TimezoneFinder
39
39
  # pX is further right than p1,
40
40
  if y1 > y2
41
41
  # so it has to be left of the line p1-p2
42
- return 2
42
+ return -1
43
43
  else
44
- return 0
44
+ return 1
45
45
  end
46
46
  end
47
47
 
@@ -49,16 +49,16 @@ module TimezoneFinder
49
49
  # pX is further left than p2,
50
50
  if y1 > y2
51
51
  # so it has to be right of the line p1-p2
52
- return 0
52
+ return 1
53
53
  else
54
- return 2
54
+ return -1
55
55
  end
56
56
  end
57
57
 
58
58
  # TODO: is not return also accepted
59
59
  if x1 == x2 && x == x1
60
60
  # could also be equal
61
- return 1
61
+ return 0
62
62
  end
63
63
 
64
64
  # x1 greater than x2
@@ -74,35 +74,35 @@ module TimezoneFinder
74
74
  if delta_x > 0
75
75
  if x1gtx2
76
76
  if y1 > y2
77
- return 0
77
+ return 1
78
78
  else
79
- return 2
79
+ return -1
80
80
  end
81
81
 
82
82
  else
83
83
  if y1 > y2
84
- return 0
84
+ return 1
85
85
  else
86
- return 2
86
+ return -1
87
87
  end
88
88
  end
89
89
 
90
90
  elsif delta_x == 0
91
- return 1
91
+ return 0
92
92
 
93
93
  else
94
94
  if x1gtx2
95
95
  if y1 > y2
96
- return 2
96
+ return -1
97
97
  else
98
- return 0
98
+ return 1
99
99
  end
100
100
 
101
101
  else
102
102
  if y1 > y2
103
- return 2
103
+ return -1
104
104
  else
105
- return 0
105
+ return 1
106
106
  end
107
107
  end
108
108
  end
@@ -120,7 +120,7 @@ module TimezoneFinder
120
120
  x2 = coords[0][i]
121
121
  # print(long2coord(x), long2coord(y), long2coord(x1), long2coord(x2), long2coord(y1), long2coord(y2),
122
122
  # position_to_line(x, y, x1, x2, y1, y2))
123
- if position_to_line(x, y, x1, x2, y1, y2) == 2
123
+ if position_to_line(x, y, x1, x2, y1, y2) == -1
124
124
  # point is left of line
125
125
  # return true when its on the line?! this is very unlikely to happen!
126
126
  # and would need to be checked every time!
@@ -131,7 +131,7 @@ module TimezoneFinder
131
131
  if y2 < y
132
132
  x1 = coords[0][i - 1]
133
133
  x2 = coords[0][i]
134
- if position_to_line(x, y, x1, x2, y1, y2) == 0
134
+ if position_to_line(x, y, x1, x2, y1, y2) == 1
135
135
  # point is right of line
136
136
  wn -= 1
137
137
  end
@@ -148,7 +148,7 @@ module TimezoneFinder
148
148
  if y2 >= y
149
149
  x1 = coords[0][-1]
150
150
  x2 = coords[0][0]
151
- if position_to_line(x, y, x1, x2, y1, y2) == 2
151
+ if position_to_line(x, y, x1, x2, y1, y2) == -1
152
152
  # point is left of line
153
153
  wn += 1
154
154
  end
@@ -157,7 +157,7 @@ module TimezoneFinder
157
157
  if y2 < y
158
158
  x1 = coords[0][-1]
159
159
  x2 = coords[0][0]
160
- if position_to_line(x, y, x1, x2, y1, y2) == 0
160
+ if position_to_line(x, y, x1, x2, y1, y2) == 1
161
161
  # point is right of line
162
162
  wn -= 1
163
163
  end
@@ -205,52 +205,54 @@ module TimezoneFinder
205
205
  [point[0], point[1] * cos_rad + point[2] * sin_rad, point[2] * cos_rad - point[1] * sin_rad]
206
206
  end
207
207
 
208
- def self.y_rotate(degree, point)
208
+ def self.y_rotate(rad, point)
209
209
  # y stays the same
210
- degree = radians(-degree)
211
- sin_rad = Math.sin(degree)
212
- cos_rad = Math.cos(degree)
213
- [point[0] * cos_rad - point[2] * sin_rad, point[1], point[0] * sin_rad + point[2] * cos_rad]
210
+ # this is actually a rotation with -rad (use symmetry of sin/cos)
211
+ sin_rad = Math.sin(rad)
212
+ cos_rad = Math.cos(rad)
213
+ [point[0] * cos_rad - point[2] * sin_rad, point[1], point[2] * cos_rad - point[0] * sin_rad]
214
214
  end
215
215
 
216
- def self.coords2cartesian(lng, lat)
217
- lng = radians(lng)
218
- lat = radians(lat)
219
- [Math.cos(lng) * Math.cos(lat), Math.sin(lng) * Math.cos(lat), Math.sin(lat)]
216
+ def self.coords2cartesian(lng_rad, lat_rad)
217
+ [Math.cos(lng_rad) * Math.cos(lat_rad), Math.sin(lng_rad) * Math.cos(lat_rad), Math.sin(lat_rad)]
220
218
  end
221
219
 
222
- # uses the simplified haversine formula for this special case
220
+ # uses the simplified haversine formula for this special case (lat_p1 = 0)
223
221
  # :param lng_rad: the longitude of the point in radians
224
222
  # :param lat_rad: the latitude of the point
225
223
  # :param lng_rad_p1: the latitude of the point1 on the equator (lat=0)
226
- # :return: distance between the point and p1 (lng_rad_p1,0) in radians
224
+ # :return: distance between the point and p1 (lng_rad_p1,0) in km
225
+ # this is only an approximation since the earth is not a real sphere
227
226
  def self.distance_to_point_on_equator(lng_rad, lat_rad, lng_rad_p1)
228
- 2 * Math.asin(Math.sqrt((Math.sin(lat_rad) / 2)**2 + Math.cos(lat_rad) * Math.sin((lng_rad - lng_rad_p1) / 2.0)**2))
227
+ # 2* for the distance in rad and * 12742 (mean diameter of earth) for the distance in km
228
+ 12742 * Math.asin(Math.sqrt((Math.sin(lat_rad / 2.0)) ** 2 + Math.cos(lat_rad) * Math.sin((lng_rad - lng_rad_p1) / 2.0) ** 2))
229
229
  end
230
230
 
231
231
  # :param lng_p1: the longitude of point 1 in radians
232
232
  # :param lat_p1: the latitude of point 1 in radians
233
233
  # :param lng_p2: the longitude of point 1 in radians
234
234
  # :param lat_p2: the latitude of point 1 in radians
235
- # :return: distance between p1 and p2 in radians
235
+ # :return: distance between p1 and p2 in km
236
+ # this is only an approximation since the earth is not a real sphere
236
237
  def self.haversine(lng_p1, lat_p1, lng_p2, lat_p2)
237
- 2 * Math.asin(Math.sqrt(Math.sin((lat_p1 - lat_p2) / 2.0)**2 + Math.cos(lat_p2) * Math.cos(lat_p1) * Math.sin((lng_p1 - lng_p2) / 2.0)**2))
238
+ # 2* for the distance in rad and * 12742(mean diameter of earth) for the distance in km
239
+ 12742 * Math.asin(Math.sqrt(Math.sin((lat_p1 - lat_p2) / 2.0) ** 2 + Math.cos(lat_p2) * Math.cos(lat_p1) * Math.sin((lng_p1 - lng_p2) / 2.0) ** 2))
238
240
  end
239
241
 
240
- # :param lng: lng of px in degree
241
- # :param lat: lat of px in degree
242
- # :param p0_lng: lng of p0 in degree
243
- # :param p0_lat: lat of p0 in degree
244
- # :param pm1_lng: lng of pm1 in degree
245
- # :param pm1_lat: lat of pm1 in degree
246
- # :param p1_lng: lng of p1 in degree
247
- # :param p1_lat: lat of p1 in degree
242
+ # :param lng_rad: lng of px in radians
243
+ # :param lat_rad: lat of px in radians
244
+ # :param p0_lng: lng of p0 in radians
245
+ # :param p0_lat: lat of p0 in radians
246
+ # :param pm1_lng: lng of pm1 in radians
247
+ # :param pm1_lat: lat of pm1 in radians
248
+ # :param p1_lng: lng of p1 in radians
249
+ # :param p1_lat: lat of p1 in radians
248
250
  # :return: shortest distance between pX and the polygon section (pm1---p0---p1) in radians
249
- def self.compute_min_distance(lng, lat, p0_lng, p0_lat, pm1_lng, pm1_lat, p1_lng, p1_lat)
250
- # rotate coordinate system (= all the points) so that p0 would have lat=lng=0 (=origin)
251
- # z rotation is simply substracting the lng
251
+ def self.compute_min_distance(lng_rad, lat_rad, p0_lng, p0_lat, pm1_lng, pm1_lat, p1_lng, p1_lat)
252
+ # rotate coordinate system (= all the points) so that p0 would have lat_rad=lng_rad=0 (=origin)
253
+ # z rotation is simply substracting the lng_rad
252
254
  # convert the points to the cartesian coorinate system
253
- px_cartesian = coords2cartesian(lng - p0_lng, lat)
255
+ px_cartesian = coords2cartesian(lng_rad - p0_lng, lat_rad)
254
256
  p1_cartesian = coords2cartesian(p1_lng - p0_lng, p1_lat)
255
257
  pm1_cartesian = coords2cartesian(pm1_lng - p0_lng, pm1_lat)
256
258
 
@@ -260,16 +262,16 @@ module TimezoneFinder
260
262
 
261
263
  # for both p1 and pm1 separately do:
262
264
 
263
- # rotate coordinate system so that this point also has lat=0 (p0 does not change!)
265
+ # rotate coordinate system so that this point also has lat_p1_rad=0 and lng_p1_rad>0 (p0 does not change!)
264
266
  rotation_rad = Math.atan2(p1_cartesian[2], p1_cartesian[1])
265
267
  p1_cartesian = x_rotate(rotation_rad, p1_cartesian)
266
268
  lng_p1_rad = Math.atan2(p1_cartesian[1], p1_cartesian[0])
267
269
  px_retrans_rad = cartesian2rad(*x_rotate(rotation_rad, px_cartesian))
268
270
 
269
- # if lng of px is between 0 (<-point1) and lng of point 2:
271
+ # if lng_rad of px is between 0 (<-point1) and lng_rad of point 2:
270
272
  # the distance between point x and the 'equator' is the shortest
271
273
  # if the point is not between p0 and p1 the distance to the closest of the two points should be used
272
- # so clamp/clip the lng of px to the interval of [0; lng p1] and compute the distance with it
274
+ # so clamp/clip the lng_rad of px to the interval of [0; lng_rad p1] and compute the distance with it
273
275
  temp_distance = distance_to_point_on_equator(px_retrans_rad[0], px_retrans_rad[1],
274
276
  [[px_retrans_rad[0], lng_p1_rad].min, 0].max)
275
277
 
@@ -294,11 +296,11 @@ module TimezoneFinder
294
296
  (double * 10**7).to_i
295
297
  end
296
298
 
297
- def self.distance_to_polygon(lng, lat, nr_points, points, trans_points)
299
+ def self.distance_to_polygon_exact(lng_rad, lat_rad, nr_points, points, trans_points)
298
300
  # transform all points (long long) to coords
299
301
  (0...nr_points).each do |i|
300
- trans_points[0][i] = int2coord(points[0][i])
301
- trans_points[1][i] = int2coord(points[1][i])
302
+ trans_points[0][i] = radians(int2coord(points[0][i]))
303
+ trans_points[1][i] = radians(int2coord(points[1][i]))
302
304
  end
303
305
 
304
306
  # check points -2, -1, 0 first
@@ -307,8 +309,8 @@ module TimezoneFinder
307
309
 
308
310
  p1_lng = trans_points[0][-2]
309
311
  p1_lat = trans_points[1][-2]
310
- min_distance = compute_min_distance(lng, lat, trans_points[0][-1], trans_points[1][-1], pm1_lng, pm1_lat, p1_lng,
311
- p1_lat)
312
+ min_distance = compute_min_distance(lng_rad, lat_rad, trans_points[0][-1], trans_points[1][-1], pm1_lng, pm1_lat,
313
+ p1_lng, p1_lat)
312
314
 
313
315
  index_p0 = 1
314
316
  index_p1 = 2
@@ -316,9 +318,9 @@ module TimezoneFinder
316
318
  p1_lng = trans_points[0][index_p1]
317
319
  p1_lat = trans_points[1][index_p1]
318
320
 
319
- distance = compute_min_distance(lng, lat, trans_points[0][index_p0], trans_points[1][index_p0], pm1_lng,
320
- pm1_lat, p1_lng, p1_lat)
321
- min_distance = distance if distance < min_distance
321
+ min_distance = [min_distance,
322
+ compute_min_distance(lng_rad, lat_rad, trans_points[0][index_p0], trans_points[1][index_p0],
323
+ pm1_lng, pm1_lat, p1_lng, p1_lat)].min
322
324
 
323
325
  index_p0 += 2
324
326
  index_p1 += 2
@@ -329,20 +331,31 @@ module TimezoneFinder
329
331
  min_distance
330
332
  end
331
333
 
334
+ def self.distance_to_polygon(lng_rad, lat_rad, nr_points, points)
335
+ min_distance = 40100000
336
+
337
+ (0...nr_points).each do |i|
338
+ min_distance = [min_distance, haversine(lng_rad, lat_rad, radians(int2coord(points[0][i])),
339
+ radians(int2coord(points[1][i])))].min
340
+ end
341
+
342
+ min_distance
343
+ end
344
+
332
345
  # Ruby original
333
346
  # like numpy.fromfile
334
347
  def self.fromfile(file, unsigned, byte_width, count)
335
348
  if unsigned
336
349
  case byte_width
337
350
  when 2
338
- unpack_format = 'S>*'
351
+ unpack_format = 'S<*'
339
352
  end
340
353
  else
341
354
  case byte_width
342
355
  when 4
343
- unpack_format = 'l>*'
356
+ unpack_format = 'l<*'
344
357
  when 8
345
- unpack_format = 'q>*'
358
+ unpack_format = 'q<*'
346
359
  end
347
360
  end
348
361
 
@@ -17,15 +17,15 @@ module TimezoneFinder
17
17
 
18
18
  # for more info on what is stored how in the .bin please read the comments in file_converter
19
19
  # read the first 2byte int (= number of polygons stored in the .bin)
20
- @nr_of_entries = @binary_file.read(2).unpack('S>')[0]
20
+ @nr_of_entries = @binary_file.read(2).unpack('S<')[0]
21
21
 
22
22
  # set addresses
23
23
  # the address where the shortcut section starts (after all the polygons) this is 34 433 054
24
- @shortcuts_start = @binary_file.read(4).unpack('L>')[0]
24
+ @shortcuts_start = @binary_file.read(4).unpack('L<')[0]
25
25
 
26
- @amount_of_holes = @binary_file.read(2).unpack('S>')[0]
26
+ @amount_of_holes = @binary_file.read(2).unpack('S<')[0]
27
27
 
28
- @hole_area_start = @binary_file.read(4).unpack('L>')[0]
28
+ @hole_area_start = @binary_file.read(4).unpack('L<')[0]
29
29
 
30
30
  @nr_val_start_address = 2 * @nr_of_entries + 12
31
31
  @adr_start_address = 4 * @nr_of_entries + 12
@@ -44,7 +44,7 @@ module TimezoneFinder
44
44
  amount_of_holes = 0
45
45
  @binary_file.seek(@hole_area_start)
46
46
  (0...@amount_of_holes).each do |i|
47
- related_line = @binary_file.read(2).unpack('S>')[0]
47
+ related_line = @binary_file.read(2).unpack('S<')[0]
48
48
  # puts(related_line)
49
49
  if related_line == last_encountered_line_nr
50
50
  amount_of_holes += 1
@@ -74,7 +74,7 @@ module TimezoneFinder
74
74
  def id_of(line = 0)
75
75
  # ids start at address 6. per line one unsigned 2byte int is used
76
76
  @binary_file.seek((12 + 2 * line))
77
- @binary_file.read(2).unpack('S>')[0]
77
+ @binary_file.read(2).unpack('S<')[0]
78
78
  end
79
79
 
80
80
  def ids_of(iterable)
@@ -83,7 +83,7 @@ module TimezoneFinder
83
83
  i = 0
84
84
  iterable.each do |line_nr|
85
85
  @binary_file.seek((12 + 2 * line_nr))
86
- id_array[i] = @binary_file.read(2).unpack('S>')[0]
86
+ id_array[i] = @binary_file.read(2).unpack('S<')[0]
87
87
  i += 1
88
88
  end
89
89
 
@@ -100,10 +100,10 @@ module TimezoneFinder
100
100
  # shortcuts are stored: (0,0) (0,1) (0,2)... (1,0)...
101
101
  @binary_file.seek(@shortcuts_start + 720 * x + 2 * y)
102
102
 
103
- nr_of_polygons = @binary_file.read(2).unpack('S>')[0]
103
+ nr_of_polygons = @binary_file.read(2).unpack('S<')[0]
104
104
 
105
105
  @binary_file.seek(@first_shortcut_address + 1440 * x + 4 * y)
106
- @binary_file.seek(@binary_file.read(4).unpack('L>')[0])
106
+ @binary_file.seek(@binary_file.read(4).unpack('L<')[0])
107
107
  Helpers.fromfile(@binary_file, true, 2, nr_of_polygons)
108
108
  end
109
109
 
@@ -113,19 +113,19 @@ module TimezoneFinder
113
113
  # shortcuts are stored: (0,0) (0,1) (0,2)... (1,0)...
114
114
  @binary_file.seek(@shortcuts_start + 720 * x + 2 * y)
115
115
 
116
- nr_of_polygons = @binary_file.read(2).unpack('S>')[0]
116
+ nr_of_polygons = @binary_file.read(2).unpack('S<')[0]
117
117
 
118
118
  @binary_file.seek(@first_shortcut_address + 1440 * x + 4 * y)
119
- @binary_file.seek(@binary_file.read(4).unpack('L>')[0])
119
+ @binary_file.seek(@binary_file.read(4).unpack('L<')[0])
120
120
  Helpers.fromfile(@binary_file, true, 2, nr_of_polygons)
121
121
  end
122
122
 
123
123
  def coords_of(line = 0)
124
124
  @binary_file.seek((@nr_val_start_address + 2 * line))
125
- nr_of_values = @binary_file.read(2).unpack('S>')[0]
125
+ nr_of_values = @binary_file.read(2).unpack('S<')[0]
126
126
 
127
127
  @binary_file.seek(@adr_start_address + 4 * line)
128
- @binary_file.seek(@binary_file.read(4).unpack('L>')[0])
128
+ @binary_file.seek(@binary_file.read(4).unpack('L<')[0])
129
129
 
130
130
  # return [Helpers.fromfile(@binary_file, false, 8, nr_of_values),
131
131
  # Helpers.fromfile(@binary_file, false, 8, nr_of_values)]
@@ -139,10 +139,10 @@ module TimezoneFinder
139
139
 
140
140
  (0...amount_of_holes).each do |_i|
141
141
  @binary_file.seek(@nr_val_hole_address + 2 * hole_id)
142
- nr_of_values = @binary_file.read(2).unpack('S>')[0]
142
+ nr_of_values = @binary_file.read(2).unpack('S<')[0]
143
143
 
144
144
  @binary_file.seek(@adr_hole_address + 4 * hole_id)
145
- @binary_file.seek(@binary_file.read(4).unpack('L>')[0])
145
+ @binary_file.seek(@binary_file.read(4).unpack('L<')[0])
146
146
 
147
147
  yield [Helpers.fromfile(@binary_file, false, 4, nr_of_values),
148
148
  Helpers.fromfile(@binary_file, false, 4, nr_of_values)]
@@ -158,22 +158,54 @@ module TimezoneFinder
158
158
  # (it can't search beyond the 180 deg lng border yet)
159
159
  # this checks all the polygons within [delta_degree] degree lng and lat
160
160
  # Keep in mind that x degrees lat are not the same distance apart than x degree lng!
161
+ # This is also the reason why there could still be a closer polygon even though you got a result already.
162
+ # order to make sure to get the closest polygon, you should increase the search radius
163
+ # until you get a result and then increase it once more (and take that result).
164
+ # This should only make a difference in really rare cases however.
161
165
  # :param lng: longitude of the point in degree
162
166
  # :param lat: latitude in degree
163
167
  # :param delta_degree: the 'search radius' in degree
164
- # :return: the timezone name of the closest found polygon or None
165
- def closest_timezone_at(lng, lat, delta_degree = 1)
168
+ # :param exact_computation: when enabled the distance to every polygon edge is computed (way more complicated),
169
+ # instead of only evaluating the distances to all the vertices (=default).
170
+ # This only makes a real difference when polygons are very close.
171
+ # :param return_distances: when enabled the output looks like this:
172
+ # ( 'tz_name_of_the_closest_polygon',[ distances to all polygons in km], [tz_names of all polygons])
173
+ # :param force_evaluation:
174
+ # :return: the timezone name of the closest found polygon, the list of distances or None
175
+ def closest_timezone_at(lng, lat, delta_degree = 1, exact_computation = false, return_distances = false, force_evaluation = false)
176
+ exact_routine = lambda do |polygon_nr|
177
+ coords = coords_of(polygon_nr)
178
+ nr_points = coords[0].length
179
+ empty_array = [[0.0] * nr_points, [0.0] * nr_points]
180
+ Helpers.distance_to_polygon_exact(lng, lat, nr_points, coords, empty_array)
181
+ end
182
+
183
+ normal_routine = lambda do |polygon_nr|
184
+ coords = coords_of(polygon_nr)
185
+ nr_points = coords[0].length
186
+ Helpers.distance_to_polygon(lng, lat, nr_points, coords)
187
+ end
188
+
166
189
  if lng > 180.0 or lng < -180.0 or lat > 90.0 or lat < -90.0
167
190
  fail "The coordinates are out ouf bounds: (#{lng}, #{lat})"
168
191
  end
169
192
 
170
- # the maximum possible distance is pi = 3.14...
171
- min_distance = 4
193
+ if exact_computation
194
+ routine = exact_routine
195
+ else
196
+ routine = normal_routine
197
+ end
198
+
199
+ # the maximum possible distance is half the perimeter of earth pi * 12743km = 40,054.xxx km
200
+ min_distance = 40100
172
201
  # transform point X into cartesian coordinates
173
202
  current_closest_id = nil
174
203
  central_x_shortcut = (lng + 180).floor.to_i
175
204
  central_y_shortcut = ((90 - lat) * 2).floor.to_i
176
205
 
206
+ lng = Helpers.radians(lng)
207
+ lat = Helpers.radians(lat)
208
+
177
209
  polygon_nrs = []
178
210
 
179
211
  # there are 2 shortcuts per 1 degree lat, so to cover 1 degree two shortcuts (rows) have to be checked
@@ -205,39 +237,57 @@ module TimezoneFinder
205
237
 
206
238
  # if all the polygons in this shortcut belong to the same zone return it
207
239
  first_entry = ids[0]
208
- return TIMEZONE_NAMES[first_entry] if ids.count(first_entry) == polygons_in_list
209
-
210
- # stores which polygons have been checked yet
211
- already_checked = [false] * polygons_in_list
240
+ if ids.count(first_entry) == polygons_in_list
241
+ unless return_distances || force_evaluation
242
+ return TIMEZONE_NAMES[first_entry]
243
+ # TODO: sort from least to most occurrences
244
+ end
245
+ end
212
246
 
247
+ distances = [nil] * polygons_in_list
213
248
  pointer = 0
214
- polygons_checked = 0
215
-
216
- while polygons_checked < polygons_in_list
217
- # only check a polygon when its id is not the closest a the moment!
218
- if already_checked[pointer] or ids[pointer] == current_closest_id
219
- # go to the next polygon
220
- polygons_checked += 1
221
-
222
- else
223
- # this polygon has to be checked
224
- coords = coords_of(polygon_nrs[pointer])
225
- nr_points = coords[0].length
226
- empty_array = [[0.0] * nr_points, [0.0] * nr_points]
227
- distance = Helpers.distance_to_polygon(lng, lat, nr_points, coords, empty_array)
228
-
229
- already_checked[pointer] = true
249
+ if force_evaluation
250
+ polygon_nrs.each do |polygon_nr|
251
+ distance = routine.call(polygon_nr)
252
+ distances[pointer] = distance
230
253
  if distance < min_distance
231
254
  min_distance = distance
232
255
  current_closest_id = ids[pointer]
233
- # whole list has to be searched again!
234
- polygons_checked = 1
235
256
  end
257
+ pointer += 1
236
258
  end
237
- pointer = (pointer + 1) % polygons_in_list
259
+ else
260
+ # stores which polygons have been checked yet
261
+ already_checked = [false] * polygons_in_list
262
+ polygons_checked = 0
263
+
264
+ while polygons_checked < polygons_in_list
265
+ # only check a polygon when its id is not the closest a the moment!
266
+ if already_checked[pointer] or ids[pointer] == current_closest_id
267
+ # go to the next polygon
268
+ polygons_checked += 1
269
+
270
+ else
271
+ # this polygon has to be checked
272
+ distance = routine.call(polygon_nrs[pointer])
273
+ distances[pointer] = distance
274
+
275
+ already_checked[pointer] = true
276
+ if distance < min_distance
277
+ min_distance = distance
278
+ current_closest_id = ids[pointer]
279
+ # whole list has to be searched again!
280
+ polygons_checked = 1
281
+ end
282
+ end
283
+ pointer = (pointer + 1) % polygons_in_list
284
+ end
285
+ end
286
+
287
+ if return_distances
288
+ return TIMEZONE_NAMES[current_closest_id], distances, ids.map { |x| TIMEZONE_NAMES[x] }
238
289
  end
239
290
 
240
- # the the whole list has been searched
241
291
  TIMEZONE_NAMES[current_closest_id]
242
292
  end
243
293
 
@@ -279,7 +329,6 @@ module TimezoneFinder
279
329
  return TIMEZONE_NAMES[same_element] if same_element != -1
280
330
 
281
331
  # get the boundaries of the polygon = (lng_max, lng_min, lat_max, lat_min)
282
- # self.binary_file.seek((@bound_start_address + 32 * polygon_nr), )
283
332
  @binary_file.seek((@bound_start_address + 16 * polygon_nr))
284
333
  boundaries = Helpers.fromfile(@binary_file, false, 4, 4)
285
334
  # only run the algorithm if it the point is withing the boundaries
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: 1.5.5
4
+ version: 1.5.6
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-06-04 00:00:00.000000000 Z
11
+ date: 2016-06-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler