proxihash 0.0.1

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