geo.tcl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. # Helper functions to simulate search-in-radius in the Tcl side in order to
  2. # verify the Redis implementation with a fuzzy test.
  3. proc geo_degrad deg {expr {$deg*atan(1)*8/360}}
  4. proc geo_distance {lon1d lat1d lon2d lat2d} {
  5. set lon1r [geo_degrad $lon1d]
  6. set lat1r [geo_degrad $lat1d]
  7. set lon2r [geo_degrad $lon2d]
  8. set lat2r [geo_degrad $lat2d]
  9. set v [expr {sin(($lon2r - $lon1r) / 2)}]
  10. set u [expr {sin(($lat2r - $lat1r) / 2)}]
  11. expr {2.0 * 6372797.560856 * \
  12. asin(sqrt($u * $u + cos($lat1r) * cos($lat2r) * $v * $v))}
  13. }
  14. proc geo_random_point {lonvar latvar} {
  15. upvar 1 $lonvar lon
  16. upvar 1 $latvar lat
  17. # Note that the actual latitude limit should be -85 to +85, we restrict
  18. # the test to -70 to +70 since in this range the algorithm is more precise
  19. # while outside this range occasionally some element may be missing.
  20. set lon [expr {-180 + rand()*360}]
  21. set lat [expr {-70 + rand()*140}]
  22. }
  23. # Return elements non common to both the lists.
  24. # This code is from http://wiki.tcl.tk/15489
  25. proc compare_lists {List1 List2} {
  26. set DiffList {}
  27. foreach Item $List1 {
  28. if {[lsearch -exact $List2 $Item] == -1} {
  29. lappend DiffList $Item
  30. }
  31. }
  32. foreach Item $List2 {
  33. if {[lsearch -exact $List1 $Item] == -1} {
  34. if {[lsearch -exact $DiffList $Item] == -1} {
  35. lappend DiffList $Item
  36. }
  37. }
  38. }
  39. return $DiffList
  40. }
  41. # The following list represents sets of random seed, search position
  42. # and radius that caused bugs in the past. It is used by the randomized
  43. # test later as a starting point. When the regression vectors are scanned
  44. # the code reverts to using random data.
  45. #
  46. # The format is: seed km lon lat
  47. set regression_vectors {
  48. {1482225976969 7083 81.634948934258375 30.561509253718668}
  49. {1482340074151 5416 -70.863281847379767 -46.347003465679947}
  50. {1499014685896 6064 -89.818768962202014 -40.463868561416803}
  51. {1412 156 149.29737817929004 15.95807862745508}
  52. {441574 143 59.235461856813856 66.269555127373678}
  53. {160645 187 -101.88575239939883 49.061997951502917}
  54. {750269 154 -90.187939661642517 66.615930412251487}
  55. {342880 145 163.03472387745728 64.012747720821181}
  56. {729955 143 137.86663517256579 63.986745399416776}
  57. {939895 151 59.149620271823181 65.204186651485145}
  58. {1412 156 149.29737817929004 15.95807862745508}
  59. {564862 149 84.062063109158544 -65.685403922426232}
  60. {1546032440391 16751 -1.8175081637769495 20.665668878082954}
  61. }
  62. set rv_idx 0
  63. start_server {tags {"geo"}} {
  64. test {GEOADD create} {
  65. r geoadd nyc -73.9454966 40.747533 "lic market"
  66. } {1}
  67. test {GEOADD update} {
  68. r geoadd nyc -73.9454966 40.747533 "lic market"
  69. } {0}
  70. test {GEOADD invalid coordinates} {
  71. catch {
  72. r geoadd nyc -73.9454966 40.747533 "lic market" \
  73. foo bar "luck market"
  74. } err
  75. set err
  76. } {*valid*}
  77. test {GEOADD multi add} {
  78. r geoadd nyc -73.9733487 40.7648057 "central park n/q/r" -73.9903085 40.7362513 "union square" -74.0131604 40.7126674 "wtc one" -73.7858139 40.6428986 "jfk" -73.9375699 40.7498929 "q4" -73.9564142 40.7480973 4545
  79. } {6}
  80. test {Check geoset values} {
  81. r zrange nyc 0 -1 withscores
  82. } {{wtc one} 1791873972053020 {union square} 1791875485187452 {central park n/q/r} 1791875761332224 4545 1791875796750882 {lic market} 1791875804419201 q4 1791875830079666 jfk 1791895905559723}
  83. test {GEORADIUS simple (sorted)} {
  84. r georadius nyc -73.9798091 40.7598464 3 km asc
  85. } {{central park n/q/r} 4545 {union square}}
  86. test {GEORADIUS withdist (sorted)} {
  87. r georadius nyc -73.9798091 40.7598464 3 km withdist asc
  88. } {{{central park n/q/r} 0.7750} {4545 2.3651} {{union square} 2.7697}}
  89. test {GEORADIUS with COUNT} {
  90. r georadius nyc -73.9798091 40.7598464 10 km COUNT 3
  91. } {{central park n/q/r} 4545 {union square}}
  92. test {GEORADIUS with COUNT but missing integer argument} {
  93. catch {r georadius nyc -73.9798091 40.7598464 10 km COUNT} e
  94. set e
  95. } {ERR*syntax*}
  96. test {GEORADIUS with COUNT DESC} {
  97. r georadius nyc -73.9798091 40.7598464 10 km COUNT 2 DESC
  98. } {{wtc one} q4}
  99. test {GEORADIUS HUGE, issue #2767} {
  100. r geoadd users -47.271613776683807 -54.534504198047678 user_000000
  101. llength [r GEORADIUS users 0 0 50000 km WITHCOORD]
  102. } {1}
  103. test {GEORADIUSBYMEMBER simple (sorted)} {
  104. r georadiusbymember nyc "wtc one" 7 km
  105. } {{wtc one} {union square} {central park n/q/r} 4545 {lic market}}
  106. test {GEORADIUSBYMEMBER withdist (sorted)} {
  107. r georadiusbymember nyc "wtc one" 7 km withdist
  108. } {{{wtc one} 0.0000} {{union square} 3.2544} {{central park n/q/r} 6.7000} {4545 6.1975} {{lic market} 6.8969}}
  109. test {GEOHASH is able to return geohash strings} {
  110. # Example from Wikipedia.
  111. r del points
  112. r geoadd points -5.6 42.6 test
  113. lindex [r geohash points test] 0
  114. } {ezs42e44yx0}
  115. test {GEOPOS simple} {
  116. r del points
  117. r geoadd points 10 20 a 30 40 b
  118. lassign [lindex [r geopos points a b] 0] x1 y1
  119. lassign [lindex [r geopos points a b] 1] x2 y2
  120. assert {abs($x1 - 10) < 0.001}
  121. assert {abs($y1 - 20) < 0.001}
  122. assert {abs($x2 - 30) < 0.001}
  123. assert {abs($y2 - 40) < 0.001}
  124. }
  125. test {GEOPOS missing element} {
  126. r del points
  127. r geoadd points 10 20 a 30 40 b
  128. lindex [r geopos points a x b] 1
  129. } {}
  130. test {GEODIST simple & unit} {
  131. r del points
  132. r geoadd points 13.361389 38.115556 "Palermo" \
  133. 15.087269 37.502669 "Catania"
  134. set m [r geodist points Palermo Catania]
  135. assert {$m > 166274 && $m < 166275}
  136. set km [r geodist points Palermo Catania km]
  137. assert {$km > 166.2 && $km < 166.3}
  138. }
  139. test {GEODIST missing elements} {
  140. r del points
  141. r geoadd points 13.361389 38.115556 "Palermo" \
  142. 15.087269 37.502669 "Catania"
  143. set m [r geodist points Palermo Agrigento]
  144. assert {$m eq {}}
  145. set m [r geodist points Ragusa Agrigento]
  146. assert {$m eq {}}
  147. set m [r geodist empty_key Palermo Catania]
  148. assert {$m eq {}}
  149. }
  150. test {GEORADIUS STORE option: syntax error} {
  151. r del points
  152. r geoadd points 13.361389 38.115556 "Palermo" \
  153. 15.087269 37.502669 "Catania"
  154. catch {r georadius points 13.361389 38.115556 50 km store} e
  155. set e
  156. } {*ERR*syntax*}
  157. test {GEORANGE STORE option: incompatible options} {
  158. r del points
  159. r geoadd points 13.361389 38.115556 "Palermo" \
  160. 15.087269 37.502669 "Catania"
  161. catch {r georadius points 13.361389 38.115556 50 km store points2 withdist} e
  162. assert_match {*ERR*} $e
  163. catch {r georadius points 13.361389 38.115556 50 km store points2 withhash} e
  164. assert_match {*ERR*} $e
  165. catch {r georadius points 13.361389 38.115556 50 km store points2 withcoords} e
  166. assert_match {*ERR*} $e
  167. }
  168. test {GEORANGE STORE option: plain usage} {
  169. r del points
  170. r geoadd points 13.361389 38.115556 "Palermo" \
  171. 15.087269 37.502669 "Catania"
  172. r georadius points 13.361389 38.115556 500 km store points2
  173. assert_equal [r zrange points 0 -1] [r zrange points2 0 -1]
  174. }
  175. test {GEORANGE STOREDIST option: plain usage} {
  176. r del points
  177. r geoadd points 13.361389 38.115556 "Palermo" \
  178. 15.087269 37.502669 "Catania"
  179. r georadius points 13.361389 38.115556 500 km storedist points2
  180. set res [r zrange points2 0 -1 withscores]
  181. assert {[lindex $res 1] < 1}
  182. assert {[lindex $res 3] > 166}
  183. assert {[lindex $res 3] < 167}
  184. }
  185. test {GEORANGE STOREDIST option: COUNT ASC and DESC} {
  186. r del points
  187. r geoadd points 13.361389 38.115556 "Palermo" \
  188. 15.087269 37.502669 "Catania"
  189. r georadius points 13.361389 38.115556 500 km storedist points2 asc count 1
  190. assert {[r zcard points2] == 1}
  191. set res [r zrange points2 0 -1 withscores]
  192. assert {[lindex $res 0] eq "Palermo"}
  193. r georadius points 13.361389 38.115556 500 km storedist points2 desc count 1
  194. assert {[r zcard points2] == 1}
  195. set res [r zrange points2 0 -1 withscores]
  196. assert {[lindex $res 0] eq "Catania"}
  197. }
  198. test {GEOADD + GEORANGE randomized test} {
  199. set attempt 30
  200. while {[incr attempt -1]} {
  201. set rv [lindex $regression_vectors $rv_idx]
  202. incr rv_idx
  203. unset -nocomplain debuginfo
  204. set srand_seed [clock milliseconds]
  205. if {$rv ne {}} {set srand_seed [lindex $rv 0]}
  206. lappend debuginfo "srand_seed is $srand_seed"
  207. expr {srand($srand_seed)} ; # If you need a reproducible run
  208. r del mypoints
  209. if {[randomInt 10] == 0} {
  210. # From time to time use very big radiuses
  211. set radius_km [expr {[randomInt 50000]+10}]
  212. } else {
  213. # Normally use a few - ~200km radiuses to stress
  214. # test the code the most in edge cases.
  215. set radius_km [expr {[randomInt 200]+10}]
  216. }
  217. if {$rv ne {}} {set radius_km [lindex $rv 1]}
  218. set radius_m [expr {$radius_km*1000}]
  219. geo_random_point search_lon search_lat
  220. if {$rv ne {}} {
  221. set search_lon [lindex $rv 2]
  222. set search_lat [lindex $rv 3]
  223. }
  224. lappend debuginfo "Search area: $search_lon,$search_lat $radius_km km"
  225. set tcl_result {}
  226. set argv {}
  227. for {set j 0} {$j < 20000} {incr j} {
  228. geo_random_point lon lat
  229. lappend argv $lon $lat "place:$j"
  230. set distance [geo_distance $lon $lat $search_lon $search_lat]
  231. if {$distance < $radius_m} {
  232. lappend tcl_result "place:$j"
  233. }
  234. lappend debuginfo "place:$j $lon $lat [expr {$distance/1000}] km"
  235. }
  236. r geoadd mypoints {*}$argv
  237. set res [lsort [r georadius mypoints $search_lon $search_lat $radius_km km]]
  238. set res2 [lsort $tcl_result]
  239. set test_result OK
  240. if {$res != $res2} {
  241. set rounding_errors 0
  242. set diff [compare_lists $res $res2]
  243. foreach place $diff {
  244. set mydist [geo_distance $lon $lat $search_lon $search_lat]
  245. set mydist [expr $mydist/1000]
  246. if {($mydist / $radius_km) > 0.999} {
  247. incr rounding_errors
  248. continue
  249. }
  250. if {$mydist < $radius_m} {
  251. # This is a false positive for redis since given the
  252. # same points the higher precision calculation provided
  253. # by TCL shows the point within range
  254. incr rounding_errors
  255. continue
  256. }
  257. }
  258. # Make sure this is a real error and not a rounidng issue.
  259. if {[llength $diff] == $rounding_errors} {
  260. set res $res2; # Error silenced
  261. }
  262. }
  263. if {$res != $res2} {
  264. set diff [compare_lists $res $res2]
  265. puts "*** Possible problem in GEO radius query ***"
  266. puts "Redis: $res"
  267. puts "Tcl : $res2"
  268. puts "Diff : $diff"
  269. puts [join $debuginfo "\n"]
  270. foreach place $diff {
  271. if {[lsearch -exact $res2 $place] != -1} {
  272. set where "(only in Tcl)"
  273. } else {
  274. set where "(only in Redis)"
  275. }
  276. lassign [lindex [r geopos mypoints $place] 0] lon lat
  277. set mydist [geo_distance $lon $lat $search_lon $search_lat]
  278. set mydist [expr $mydist/1000]
  279. puts "$place -> [r geopos mypoints $place] $mydist $where"
  280. if {($mydist / $radius_km) > 0.999} {incr rounding_errors}
  281. }
  282. set test_result FAIL
  283. }
  284. unset -nocomplain debuginfo
  285. if {$test_result ne {OK}} break
  286. }
  287. set test_result
  288. } {OK}
  289. }