proxihash 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ /*.gem
2
+ /Gemfile.lock
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ bundler_args: --without dev
3
+ script: testrb test
4
+ rvm:
5
+ - 1.9.2
6
+ - 1.9.3
7
+ - jruby-19mode
8
+ - rbx-19mode
9
+ - ruby-head
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2013-08-16
2
+
3
+ * Hi.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+ gemspec
3
+
4
+ group :dev do
5
+ gem 'debugger'
6
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) George Ogata
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,109 @@
1
+ ## Proxihash
2
+
3
+ A [GeoHash][geohash] implementation geared toward indexing data for performing
4
+ proximity searches at different radii.
5
+
6
+ The algorithm underlying Proxihash is the GeoHash algorithm, however the hashes
7
+ are returned offer the bit string as a numeric value, rather than a base-32
8
+ representation. This allows bit-precision, rather than 5-bit precision, which
9
+ comes in handy for variable-distance proximity searches.
10
+
11
+ Proxihash computes for you the tileset required for a given radius search from a
12
+ given point, within the specified bit precision range. This range lets you limit
13
+ your index to certain precisions, which reflects the search distances you need
14
+ to support.
15
+
16
+ [geohash]: http://en.wikipedia.org/wiki/Geohash
17
+
18
+ ### Usage
19
+
20
+ Create a proxihash with 31 bits of precision:
21
+
22
+ proxihash = Proxihash.encode(42.6, -5.6, 31)
23
+
24
+ Get the numeric value, for storage:
25
+
26
+ proxihash.value # 939008154
27
+ proxihash.num_bits # 31
28
+
29
+ Compute the tile (bounding box) represented by the proxihash:
30
+
31
+ proxihash.tile # [42.5994873046875, 42.60498046875, -5.60302734375, -5.5975341796875]
32
+
33
+ Find neighbors at the same precision:
34
+
35
+ proxihash.neighbor(-1, 1) # tile to the south-east
36
+ proxihash.neighbor( 1, -1) # tile to the north-west
37
+
38
+ # Alternatively:
39
+ proxihash.north # same as proxihash.neighbor( 1, 0)
40
+ proxihash.east # same as proxihash.neighbor( 0, 1)
41
+ proxihash.west # same as proxihash.neighbor( 0, -1)
42
+ proxihash.south # same as proxihash.neighbor(-1, 0)
43
+
44
+ Find the midpoint of the tile:
45
+
46
+ proxihash.decode # [42.60223388671875, -5.60028076171875]
47
+
48
+ Note that roundtripping through `encode` then `decode` may not give you the same
49
+ initial point, as decoding gives you the *center* of the tile. It will give you
50
+ a nearby point, however. Roundtripping through `decode` then `encode` will give
51
+ you back the same tile.
52
+
53
+ ## Indexing
54
+
55
+ The idea is to index your data with proxihashes at the precisions you care
56
+ about. These precisions depend on the search distances you need to support.
57
+
58
+ Consider that the radius of the earth is about 40,036 km, (24,873 miles). This
59
+ means 16 bits of longitude (16 + 15 = 31 bit proxihash) will give you
60
+ sub-kilometer precision. Sub-mile precision requires 29 bits. These fit nicely
61
+ into a 32-bit integer.
62
+
63
+ ## Searching
64
+
65
+ Once you've indexed all your data by their proxihash at the precisions you care
66
+ about, you probably want to answer a query like "find all users within 25 miles
67
+ of `(lat,lng)`". To do this, you have to find the tiles to search for:
68
+
69
+ Proxihash.radius = :earth_in_miles
70
+ Proxihash.search_tiles(lat, lng, 25, min_bits: 11, max_bits: 31)
71
+
72
+ The first line sets the units to use (default is kilometers).
73
+
74
+ The second returns up to 4 tiles (proxihashes): the tile that `(lat,lng)` falls
75
+ on, plus the 3 neighbors that need to be searched. The 3 neighbors depend on
76
+ which quadrant of the center tile `(lat,lng)` falls on. The tile size is the
77
+ smallest that will cover the 25 mile search circle.
78
+
79
+ The precision of the tiles (proxihashes) computed depends on two things: the
80
+ distance, and the latitude.
81
+
82
+ Larger tiles (shorter proxihashes) are required for larger distances and
83
+ latitudes nearer the poles; smaller tiles (longer proxihashes) for shorter
84
+ distances and latitudes nearer the equator. The latitude makes a difference
85
+ because tiles nearer the poles are smaller, and so a circle of a given radius
86
+ can cover more tiles of the same angular size near the poles than near the
87
+ equator.
88
+
89
+ There is one caveat that arises from this: Finding neighboring tiles degenerates
90
+ toward the poles. If the search circle overlaps a pole, or finding a neighboring
91
+ tile requires crossing a pole, a `PoleWrapException` is raised.
92
+
93
+ The `:max_bits` option prevents you from computing an arbitrarily precise
94
+ proxihash if you take the distance from user input. It will simply cap the
95
+ precision of the proxihashes returned. The `:min_bits` option makes
96
+ `search_tiles` return nil if a larger tile is required. `search_tiles` currently
97
+ does not return more than 4 tiles to accomodate a smaller tileset.
98
+
99
+ ## Contributing
100
+
101
+ * [Bug reports](https://github.com/howaboutwe/proxihash/issues)
102
+ * [Source](https://github.com/howaboutwe/proxihash)
103
+ * Patches: Fork on Github, send pull request.
104
+ * Include tests where practical.
105
+ * Leave the version alone, or bump it in a separate commit.
106
+
107
+ ## Copyright
108
+
109
+ Copyright (c) George Ogata. See LICENSE for details.
@@ -0,0 +1 @@
1
+ require 'ritual'
@@ -0,0 +1,232 @@
1
+ class Proxihash
2
+ autoload :VERSION, 'proxihash/version'
3
+
4
+ def initialize(value, num_bits)
5
+ @value = value
6
+ @num_bits = num_bits
7
+ num_bits.odd? or
8
+ raise ArgumentError, "bitlength must be odd"
9
+ value < 1 << num_bits or
10
+ raise ArgumentError, "value too large for #{num_bits} bits"
11
+ end
12
+
13
+ attr_reader :value, :num_bits
14
+
15
+ def id
16
+ value | 1 << num_bits
17
+ end
18
+
19
+ class << self
20
+ def radius=(radius)
21
+ @radius =
22
+ case radius
23
+ when :earth_in_miles
24
+ 3958.761
25
+ when :earth_in_kilometers
26
+ 6371.009
27
+ when :earth_in_meters
28
+ 6371009
29
+ else
30
+ radius
31
+ end
32
+ end
33
+
34
+ def angular_units=(units)
35
+ case units
36
+ when :radians
37
+ @min_lat = -Math::PI/2
38
+ @max_lat = Math::PI/2
39
+ @min_lng = -Math::PI
40
+ @max_lng = Math::PI
41
+ when :degrees
42
+ @min_lat = -90
43
+ @max_lat = 90
44
+ @min_lng = -180
45
+ @max_lng = 180
46
+ else
47
+ raise ArgumentError "angular units must be :radians or :degrees (#{units.inspect} given)"
48
+ end
49
+ @angular_units = units
50
+ end
51
+
52
+ attr_reader :radius, :angular_units
53
+ attr_reader :min_lat, :max_lat, :min_lng, :max_lng
54
+
55
+ def encode(lat, lng, num_bits=31)
56
+ lat = lat.to_f
57
+ lng = lng.to_f
58
+
59
+ value = 0
60
+
61
+ lat0 = min_lat
62
+ lat1 = max_lat
63
+ lng0 = min_lng
64
+ lng1 = max_lng
65
+
66
+ (num_bits - 1).downto(0) do |i|
67
+ if i.odd?
68
+ mid = 0.5 * (lat0 + lat1)
69
+ if lat > mid
70
+ value |= (1 << i)
71
+ lat0 = mid
72
+ else
73
+ lat1 = mid
74
+ end
75
+ else
76
+ mid = 0.5 * (lng0 + lng1)
77
+ if lng > mid
78
+ value |= (1 << i)
79
+ lng0 = mid
80
+ else
81
+ lng1 = mid
82
+ end
83
+ end
84
+ end
85
+
86
+ new(value, num_bits)
87
+ end
88
+
89
+ def search_tiles(lat, lng, distance, options={})
90
+ lat = lat.to_f
91
+ lng = lng.to_f
92
+ bits = 2*lng_bits(lat, distance.to_f) - 1
93
+
94
+ if (min_bits = options[:min_bits]) && bits < min_bits
95
+ return nil
96
+ elsif (max_bits = options[:max_bits]) && bits > max_bits
97
+ # TODO: avoid unnecessary neighboring tiles
98
+ bits = max_bits
99
+ end
100
+
101
+ # TODO: can use the next 2 bits to determine quadrant instead
102
+ center = encode(lat, lng, bits)
103
+ tile_lat, tile_lng = center.decode
104
+ dlat = lat < tile_lat ? -1 : 1
105
+ dlng = lng < tile_lng ? -1 : 1
106
+ [
107
+ center,
108
+ center.neighbor(0 , dlng),
109
+ center.neighbor(dlat, dlng),
110
+ center.neighbor(dlat, 0),
111
+ ]
112
+ rescue PoleWrapException
113
+ nil
114
+ end
115
+
116
+ private
117
+
118
+ def lng_bits(lat, distance)
119
+ lat = Math::PI / 180.0 * lat if angular_units == :degrees
120
+ distance >= Float::EPSILON or
121
+ raise ArgumentError, "distance too small"
122
+ distance <= Proxihash.radius*(0.5*Math::PI - lat.abs) or
123
+ raise PoleWrapException, "cannot search across pole"
124
+ dlng = Math.asin(Math.sin(0.5*distance/Proxihash.radius)/Math.cos(lat))
125
+ Math.log2(Math::PI/dlng.abs).ceil - 1
126
+ end
127
+ end
128
+
129
+ [:min_lat, :max_lat, :min_lng, :max_lng].each do |name|
130
+ class_eval "def #{name}; self.class.#{name}; end"
131
+ end
132
+
133
+ def decode
134
+ lat0, lat1, lng0, lng1 = tile
135
+ [0.5 * (lat0 + lat1), 0.5 * (lng0 + lng1)]
136
+ end
137
+
138
+ def tile
139
+ lat0 = min_lat
140
+ lat1 = max_lat
141
+ lng0 = min_lng
142
+ lng1 = max_lng
143
+
144
+ (num_bits - 1).downto(0) do |i|
145
+ if i.odd?
146
+ mid = 0.5 * (lat0 + lat1)
147
+ if value[i] == 1
148
+ lat0 = mid
149
+ else
150
+ lat1 = mid
151
+ end
152
+ else
153
+ mid = 0.5 * (lng0 + lng1)
154
+ if value[i] == 1
155
+ lng0 = mid
156
+ else
157
+ lng1 = mid
158
+ end
159
+ end
160
+ end
161
+
162
+ [lat0, lat1, lng0, lng1]
163
+ end
164
+
165
+ def neighbor(dlat, dlng)
166
+ value = self.value
167
+ value = bump(value, 1, dlat, false) unless dlat.zero?
168
+ value = bump(value, 0, dlng, true ) unless dlng.zero?
169
+ self.class.new(value, num_bits)
170
+ end
171
+
172
+ def north
173
+ neighbor(1, 0)
174
+ end
175
+
176
+ def south
177
+ neighbor(-1, 0)
178
+ end
179
+
180
+ def east
181
+ neighbor(0, 1)
182
+ end
183
+
184
+ def west
185
+ neighbor(0, -1)
186
+ end
187
+
188
+ def ==(other)
189
+ other.is_a?(Proxihash) && value == other.value && num_bits == other.num_bits
190
+ end
191
+
192
+ def hash
193
+ value.hash ^ num_bits
194
+ end
195
+ alias eql? ==
196
+
197
+ def inspect
198
+ "Proxihash[#{value.to_s(2).rjust(num_bits, '0')}]"
199
+ end
200
+
201
+ private
202
+
203
+ def bump(value, offset, direction, wrap)
204
+ bit = offset
205
+ if direction > 0
206
+ while bit < num_bits
207
+ if value[bit] == 0
208
+ return value | (1 << bit)
209
+ else
210
+ value &= ~(1 << bit)
211
+ end
212
+ bit += 2
213
+ end
214
+ return value if wrap
215
+ else
216
+ while bit < num_bits
217
+ if value[bit] == 0
218
+ value |= 1 << bit
219
+ else
220
+ return value & ~(1 << bit)
221
+ end
222
+ bit += 2
223
+ end
224
+ return value if wrap
225
+ end
226
+ raise PoleWrapException, "can't wrap around pole"
227
+ end
228
+
229
+ PoleWrapException = Class.new(ArgumentError)
230
+ self.radius = :earth_in_kilometers
231
+ self.angular_units = :degrees
232
+ end
@@ -0,0 +1,11 @@
1
+ class Proxihash
2
+ VERSION = [0, 0, 1]
3
+
4
+ class << VERSION
5
+ include Comparable
6
+
7
+ def to_s
8
+ join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.unshift File.expand_path('lib', File.dirname(__FILE__))
3
+ require 'proxihash/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = 'proxihash'
7
+ gem.version = Proxihash::VERSION
8
+ gem.authors = ['George Ogata']
9
+ gem.email = ['george.ogata@gmail.com']
10
+ gem.summary = "Hashes for geospatial proximity searches."
11
+ gem.homepage = 'http://github.com/howaboutwe/proxisearch'
12
+
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+
17
+ gem.add_development_dependency 'ritual', '~> 0.4.1'
18
+ end
@@ -0,0 +1,377 @@
1
+ $:.unshift File.expand_path('../lib', File.dirname(__FILE__))
2
+
3
+ require 'minitest/spec'
4
+ require 'set'
5
+ require 'proxihash'
6
+
7
+ describe Proxihash do
8
+ describe '.new' do
9
+ it "creates a Proxihash with the given value and number of bits" do
10
+ proxihash = Proxihash.new(0b011, 3)
11
+ proxihash.value.must_equal 0b011
12
+ proxihash.num_bits.must_equal 3
13
+ end
14
+
15
+ it "raises an ArgumentError if the value is too large for the number of bits" do
16
+ ->{ Proxihash.new(0b1000, 3) }.must_raise ArgumentError
17
+ end
18
+
19
+ it "raises an ArgumentError if the number of bits is even" do
20
+ ->{ Proxihash.new(0b100, 2) }.must_raise ArgumentError
21
+ end
22
+ end
23
+
24
+ describe '#id' do
25
+ it "returns the value prefixed with a 1-bit to uniquify it across precisions" do
26
+ proxihash = Proxihash.new(0b011, 3)
27
+ proxihash.id.must_equal 0b1011
28
+ end
29
+ end
30
+
31
+ describe '.encode' do
32
+ it "returns a proxihash for the given lat/lng at the given precision" do
33
+ Proxihash.encode(0, 0, 9).must_equal Proxihash.new(0b001111111, 9)
34
+ end
35
+
36
+ it "returns a sub-kilometer (31-bit) proxihash by default" do
37
+ Proxihash.encode(0, 0).must_equal Proxihash.new(0x1f_ff_ff_ff, 31)
38
+ end
39
+
40
+ it "computes the proxihash correctly" do
41
+ Proxihash.encode(-35.15625, 122.34375, 13).must_equal Proxihash.new(0b1_01_10_01_00_11_11, 13)
42
+ end
43
+ end
44
+
45
+ describe ".search_tiles" do
46
+ describe "when in the 1st quadrant of a tile" do
47
+ it "returns the center tile and the 3 neighbors to the top and right" do
48
+ center = Proxihash.new(0x6000, 15)
49
+ Proxihash.search_tiles(0.704, 0.704, 156).to_set.must_equal Set[
50
+ center, center.neighbor(1, 0), center.neighbor(1, 1), center.neighbor(0, 1)
51
+ ]
52
+ end
53
+ end
54
+
55
+ describe "when in the 2nd quadrant of a tile" do
56
+ it "returns the center tile and the 3 neighbors to the top and left" do
57
+ center = Proxihash.new(0x6000, 15)
58
+ Proxihash.search_tiles(0.704, 0.703, 156).to_set.must_equal Set[
59
+ center, center.neighbor(1, 0), center.neighbor(1, -1), center.neighbor(0, -1)
60
+ ]
61
+ end
62
+ end
63
+
64
+ describe "when in the 3rd quadrant of a tile" do
65
+ it "returns the center tile and the 3 neighbors to the bottom and left" do
66
+ center = Proxihash.new(0x6000, 15)
67
+ Proxihash.search_tiles(0.703, 0.703, 156).to_set.must_equal Set[
68
+ center, center.neighbor(0, -1), center.neighbor(-1, -1), center.neighbor(-1, 0)
69
+ ]
70
+ end
71
+ end
72
+
73
+ describe "when in the 4th quadrant of a tile" do
74
+ it "returns the center tile and the 3 neighbors to the bottom and right" do
75
+ center = Proxihash.new(0x6000, 15)
76
+ Proxihash.search_tiles(0.703, 0.704, 156).to_set.must_equal Set[
77
+ center, center.neighbor(-1, 0), center.neighbor(-1, 1), center.neighbor(0, 1)
78
+ ]
79
+ end
80
+ end
81
+
82
+ describe "larger-radius search" do
83
+ it "returns shorter proxihashes" do
84
+ center = Proxihash.new(0x1800, 13)
85
+ Proxihash.search_tiles(1.407, 1.407, 157).to_set.must_equal Set[
86
+ center, center.neighbor(1, 0), center.neighbor(1, 1), center.neighbor(0, 1)
87
+ ]
88
+ end
89
+ end
90
+
91
+ describe "lower-radius search" do
92
+ it "returns longer proxihashes" do
93
+ center = Proxihash.new(0x18000, 17)
94
+ Proxihash.search_tiles(0.352, 0.352, 78).to_set.must_equal Set[
95
+ center, center.neighbor(1, 0), center.neighbor(1, 1), center.neighbor(0, 1)
96
+ ]
97
+ end
98
+ end
99
+
100
+ describe "when :min_bits is given" do
101
+ it "returns nil if the hashes would be too short" do
102
+ Proxihash.search_tiles(0, 0, 20, min_bits: 21).must_be_nil
103
+ end
104
+
105
+ it "does not return nil if the hashes are ok" do
106
+ center = Proxihash.new(0x7ffff, 21)
107
+ Proxihash.search_tiles(0, 0, 19, max_bits: 21).to_set.must_equal Set[
108
+ center, center.neighbor(1, 0), center.neighbor(1, 1), center.neighbor(0, 1)
109
+ ]
110
+ end
111
+ end
112
+
113
+ describe "when :max_bits is given" do
114
+ it "caps the hash length if they would be too long" do
115
+ center = Proxihash.new(0x7ffff, 21)
116
+ Proxihash.search_tiles(0, 0, 0.01, max_bits: 21).to_set.must_equal Set[
117
+ center, center.neighbor(1, 0), center.neighbor(1, 1), center.neighbor(0, 1)
118
+ ]
119
+ end
120
+
121
+ it "does not cap the hash length if the hashes are ok" do
122
+ center = Proxihash.new(0x7ffff, 21)
123
+ Proxihash.search_tiles(0, 0, 19, max_bits: 21).to_set.must_equal Set[
124
+ center, center.neighbor(1, 0), center.neighbor(1, 1), center.neighbor(0, 1)
125
+ ]
126
+ end
127
+ end
128
+
129
+ describe "when the search circle overlaps a pole" do
130
+ it "returns nil" do
131
+ Proxihash.search_tiles(89.999, 0, 100).must_be_nil
132
+ end
133
+ end
134
+ end
135
+
136
+ describe '.lng_bits' do
137
+ Proxihash.singleton_class.send :public, :lng_bits
138
+
139
+ it "is higher toward the poles" do
140
+ Proxihash.lng_bits( 0, 78.18).must_equal 9
141
+ Proxihash.lng_bits( 60, 78.18).must_equal 8
142
+ Proxihash.lng_bits(-60, 78.18).must_equal 8
143
+ end
144
+
145
+ it "is lower for larger radii" do
146
+ Proxihash.lng_bits(0, 78.18).must_equal 9
147
+ Proxihash.lng_bits(0, 78.19).must_equal 8
148
+ end
149
+
150
+ it "raises a PoleWrapException if the search circle overlaps a pole" do
151
+ Proxihash.lng_bits( 60, 3335)
152
+ ->{ Proxihash.lng_bits( 60, 3336) }.must_raise Proxihash::PoleWrapException
153
+
154
+ Proxihash.lng_bits(-60, 3335)
155
+ ->{ Proxihash.lng_bits(-60, 3336) }.must_raise Proxihash::PoleWrapException
156
+ end
157
+
158
+ it "raises a PoleWrapException at either pole" do
159
+ ->{ Proxihash.lng_bits( 90, 0.1) }.must_raise Proxihash::PoleWrapException
160
+ ->{ Proxihash.lng_bits(-90, 0.1) }.must_raise Proxihash::PoleWrapException
161
+ end
162
+
163
+ it "raises an ArgumentError if the radius is zero" do
164
+ ->{ Proxihash.lng_bits(0, 0) }.must_raise ArgumentError
165
+ end
166
+ end
167
+
168
+ describe '#decode' do
169
+ it "returns the lat/lng for the center of the given proxihash's tile" do
170
+ Proxihash.new(0, 7).decode.must_equal [-78.75, -168.75]
171
+ Proxihash.new(0, 9).decode.must_equal [-84.375, -174.375]
172
+ end
173
+
174
+ it "computes the lat/lng correctly" do
175
+ Proxihash.new(0b0_01_10_01_00_11_11, 13).decode.must_equal [-35.15625, -57.65625]
176
+ end
177
+ end
178
+
179
+ describe "#tile" do
180
+ it "returns the lat/lng ranges of the given proxihash's tile" do
181
+ Proxihash.new(0b0_01_10_01_00_11_11, 13).tile.must_equal [-36.5625, -33.75, -59.0625, -56.25]
182
+ end
183
+ end
184
+
185
+ describe "#neighbor" do
186
+ describe "to the north" do
187
+ it "increments the latitude" do
188
+ Proxihash.new(0b0_00_00, 5).neighbor(1, 0).must_equal Proxihash.new(0b0_00_10, 5)
189
+ end
190
+
191
+ it "carries the 1 as necessary" do
192
+ Proxihash.new(0b0_00_00_10_10, 9).neighbor(1, 0).must_equal Proxihash.new(0b0_00_10_00_00, 9)
193
+ end
194
+
195
+ it "raises a PoleWrapException at the north pole" do
196
+ ->{ Proxihash.new(0b0_11_10_11_10, 9).neighbor(1, 0) }.must_raise Proxihash::PoleWrapException
197
+ end
198
+ end
199
+
200
+ describe "to the south" do
201
+ it "decrements the latitude" do
202
+ Proxihash.new(0b1_11_11, 5).neighbor(-1, 0).must_equal Proxihash.new(0b1_11_01, 5)
203
+ end
204
+
205
+ it "borrows a 1 as necessary" do
206
+ Proxihash.new(0b1_11_11_01_01, 9).neighbor(-1, 0).must_equal Proxihash.new(0b1_11_01_11_11, 9)
207
+ end
208
+
209
+ it "raises a PoleWrapException at the south pole" do
210
+ ->{ Proxihash.new(0b0_01_00_01_00, 9).neighbor(-1, 0) }.must_raise Proxihash::PoleWrapException
211
+ end
212
+ end
213
+
214
+ describe "to the east" do
215
+ it "increments the longitude" do
216
+ Proxihash.new(0b0_00_00, 5).neighbor(0, 1).must_equal Proxihash.new(0b00_01, 5)
217
+ end
218
+
219
+ it "carries the 1 as necessary" do
220
+ Proxihash.new(0b0_00_00_01_01, 9).neighbor(0, 1).must_equal Proxihash.new(0b00_01_00_00, 9)
221
+ end
222
+
223
+ it "wraps around at the prime meridian" do
224
+ Proxihash.new(0b1_11_01_11_01, 9).neighbor(0, 1).must_equal Proxihash.new(0b0_10_00_10_00, 9)
225
+ end
226
+ end
227
+
228
+ describe "to the west" do
229
+ it "decrements the longitude" do
230
+ Proxihash.new(0b0_11_11, 5).neighbor(0, -1).must_equal Proxihash.new(0b0_11_10, 5)
231
+ end
232
+
233
+ it "borrows a 1 as necessary" do
234
+ Proxihash.new(0b0_11_11_10_10, 9).neighbor(0, -1).must_equal Proxihash.new(0b0_11_10_11_11, 9)
235
+ end
236
+
237
+ it "wraps around at the prime meridian" do
238
+ Proxihash.new(0b0_10_00_10_00, 9).neighbor(0, -1).must_equal Proxihash.new(0b1_11_01_11_01, 9)
239
+ end
240
+ end
241
+
242
+ describe "to the north-east" do
243
+ it "increments both latitude and longitude" do
244
+ Proxihash.new(0b0_00_00, 5).neighbor(1, 1).must_equal Proxihash.new(0b0_00_11, 5)
245
+ end
246
+ end
247
+
248
+ describe "to the north-west" do
249
+ it "increments the latitude, decrements the longitude" do
250
+ Proxihash.new(0b0_00_01, 5).neighbor(1, -1).must_equal Proxihash.new(0b0_00_10, 5)
251
+ end
252
+ end
253
+
254
+ describe "to the south-east" do
255
+ it "decrements the latitude, increments the longitude" do
256
+ Proxihash.new(0b0_00_10, 5).neighbor(-1, 1).must_equal Proxihash.new(0b0_00_01, 5)
257
+ end
258
+ end
259
+
260
+ describe "to the south-west" do
261
+ it "decrements both latitude and longitude" do
262
+ Proxihash.new(0b0_11_11, 5).neighbor(-1, -1).must_equal Proxihash.new(0b0_11_00, 5)
263
+ end
264
+ end
265
+
266
+ describe "when both arguments are zero" do
267
+ it "returns itself" do
268
+ Proxihash.new(0b0_00_00, 5).neighbor(0, 0).must_equal Proxihash.new(0b0_00_00, 5)
269
+ end
270
+ end
271
+ end
272
+
273
+ describe "#north" do
274
+ it "returns the neighbor to the north" do
275
+ Proxihash.new(0b0_00_00, 5).north.must_equal Proxihash.new(0b0_00_10, 5)
276
+ end
277
+ end
278
+
279
+ describe "#south" do
280
+ it "returns the neighbor to the south" do
281
+ Proxihash.new(0b1_11_11, 5).south.must_equal Proxihash.new(0b1_11_01, 5)
282
+ end
283
+ end
284
+
285
+ describe "#east" do
286
+ it "returns the neighbor to the east" do
287
+ Proxihash.new(0b0_00_00, 5).east.must_equal Proxihash.new(0b00_01, 5)
288
+ end
289
+ end
290
+
291
+ describe "#west" do
292
+ it "returns the neighbor to the west" do
293
+ Proxihash.new(0b0_11_11, 5).west.must_equal Proxihash.new(0b0_11_10, 5)
294
+ end
295
+ end
296
+
297
+ describe "#inspect" do
298
+ it "shows the raw binary string to the correct precision" do
299
+ Proxihash.new(0b10011001100, 11).inspect.must_equal 'Proxihash[10011001100]'
300
+ end
301
+ end
302
+
303
+ describe "#hash and #eql?" do
304
+ it "allows proxihashes to be used as hash keys" do
305
+ hash = {}
306
+ hash[Proxihash.new(0b0001100, 7)] = 1
307
+ hash[Proxihash.new(0b0001100, 7)] = 2
308
+ hash[Proxihash.new(0b0001100, 7)].must_equal 2
309
+ end
310
+
311
+ it "does not treat Proxihashes with different precisions as the same hash key" do
312
+ hash = {}
313
+ hash[Proxihash.new(0b0001100, 7)] = 1
314
+ hash[Proxihash.new(0b0001100, 5)] = 2
315
+ hash[Proxihash.new(0b0001100, 7)].must_equal 1
316
+ end
317
+ end
318
+
319
+ describe "tile adjacency" do
320
+ it "returns tiles whose bounding boxes border the given tile" do
321
+ tile = Proxihash.new(0b1_10_00, 5)
322
+
323
+ tl = tile.neighbor( 1, -1).tile
324
+ tc = tile.neighbor( 1, 0).tile
325
+ tr = tile.neighbor( 1, 1).tile
326
+ ml = tile.neighbor( 0, -1).tile
327
+ mc = tile.neighbor( 0, 0).tile
328
+ mr = tile.neighbor( 0, 1).tile
329
+ bl = tile.neighbor(-1, -1).tile
330
+ bc = tile.neighbor(-1, 0).tile
331
+ br = tile.neighbor(-1, 1).tile
332
+
333
+ tl[0].must_be_close_to ml[1]
334
+ tc[0].must_be_close_to mc[1]
335
+ tr[0].must_be_close_to mr[1]
336
+
337
+ ml[0].must_be_close_to bl[1]
338
+ mc[0].must_be_close_to bc[1]
339
+ mr[0].must_be_close_to br[1]
340
+
341
+ tl[3].must_be_close_to tc[2]
342
+ ml[3].must_be_close_to mc[2]
343
+ bl[3].must_be_close_to bc[2]
344
+
345
+ tc[3].must_be_close_to tr[2]
346
+ mc[3].must_be_close_to mr[2]
347
+ bc[3].must_be_close_to br[2]
348
+
349
+ tl[0].must_be_close_to tc[0]
350
+ tl[1].must_be_close_to tc[1]
351
+ tl[0].must_be_close_to tr[0]
352
+ tl[1].must_be_close_to tr[1]
353
+
354
+ tl[2].must_be_close_to ml[2]
355
+ tl[3].must_be_close_to ml[3]
356
+ tl[2].must_be_close_to bl[2]
357
+ tl[3].must_be_close_to bl[3]
358
+
359
+ [tl,tc,tr,ml,mc,mr,bl,bc,br].each do |tile|
360
+ assert tile[0] < tile[1]
361
+ assert tile[2] < tile[3]
362
+ end
363
+ end
364
+ end
365
+
366
+ describe 'roundtripping' do
367
+ it "returns the original lat/lng if the lat/lng is in the center of a tile" do
368
+ proxihash = Proxihash.encode(-28.125, 61.875, 9)
369
+ proxihash.decode.must_equal [-28.125, 61.875]
370
+ end
371
+
372
+ it "returns the center of the tile of the given lat/lng" do
373
+ proxihash = Proxihash.encode(-28, 62, 9)
374
+ proxihash.decode.must_equal [-28.125, 61.875]
375
+ end
376
+ end
377
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: proxihash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - George Ogata
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ritual
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.4.1
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.4.1
30
+ description:
31
+ email:
32
+ - george.ogata@gmail.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - .travis.yml
39
+ - CHANGELOG
40
+ - Gemfile
41
+ - LICENSE
42
+ - README.markdown
43
+ - Rakefile
44
+ - lib/proxihash.rb
45
+ - lib/proxihash/version.rb
46
+ - proxihash.gemspec
47
+ - test/test_proxihash.rb
48
+ homepage: http://github.com/howaboutwe/proxisearch
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.25
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Hashes for geospatial proximity searches.
72
+ test_files:
73
+ - test/test_proxihash.rb
74
+ has_rdoc: