timezone_finder 1.5.5 → 1.5.6

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