davetroy-geohash 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Manifest.txt ADDED
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ README.txt
3
+ Rakefile
4
+ extconf.rb
5
+ geohash_native.c
6
+ lib/geohash.rb
7
+ test/test_geohash.rb
data/README ADDED
@@ -0,0 +1,91 @@
1
+ = geohash
2
+
3
+ * http://rubyforge.org/projects/geohash
4
+
5
+ == DESCRIPTION:
6
+
7
+ GeoHash Gem for Ruby (c) 2008 David Troy
8
+ dave@roundhousetech.com
9
+
10
+ Geohash is a latitude/longitude encoding system invented by Gustavo Niemeyer when writing the
11
+ web service at geohash.org, and put into the public domain. Geohashes offer properties like
12
+ arbitrary precision, similar prefixes for nearby positions, and the possibility of gradually
13
+ removing characters from the end of the code to reduce its size (and gradually lose precision).
14
+
15
+ See these resources online:
16
+ http://geohash.org
17
+ http://en.wikipedia.org/wiki/Geohash
18
+ http://openlocation.org/geohash/geohash-js (javascript implementation and demo)
19
+
20
+ == FEATURES/PROBLEMS:
21
+
22
+ * Encode to Geohash Format to an arbitrary level of precision
23
+ * Decode from Geohash Format to an arbitrary level of precision
24
+ * C implementation is over 10 times faster than native Ruby code (we checked)
25
+ * No known problems
26
+
27
+ == SYNOPSIS:
28
+
29
+ GeoHash is very easy to use (and fast) because it's written in C with Ruby bindings.
30
+
31
+ require 'rubygems'
32
+ require 'geohash'
33
+
34
+ GeoHash.decode('f77')
35
+ => [63.98438, -73.82813]
36
+
37
+ GeoHash.encode(39.51, -76.24)
38
+ => "dr1bc0edrj"
39
+
40
+ # Decode a geohash to a bounding box
41
+ decode_bbox('dqcw4')
42
+ => [39.0234375, -76.552734375], [39.0673828125, -76.5087890625]]
43
+
44
+ You can encode or decode to an arbitrary level of precision:
45
+
46
+ # Encode latitude and longitude to a geohash with precision digits
47
+ encode(lat, lon, precision=10)
48
+
49
+ # Decode a geohash to a latitude and longitude with decimals digits
50
+ decode(geohash, decimals=5)
51
+
52
+ Have fun with this! GeoHash is the new black.
53
+
54
+ == REQUIREMENTS:
55
+
56
+ * GCC and a Gnu-ish build environment (for native extensions)
57
+
58
+ == INSTALLATION
59
+
60
+ 1) Enable gems from github, if you haven't already done so (rubygems >= 1.2):
61
+ > sudo gem sources -a http://gems.github.com
62
+
63
+ 2) Install gem
64
+ > sudo gem install davetroy-geohash
65
+
66
+ 3) Profit!
67
+
68
+ == LICENSE:
69
+
70
+ (The MIT License)
71
+
72
+ Copyright (c) 2008 David Troy, Roundhouse Technologies LLC
73
+
74
+ Permission is hereby granted, free of charge, to any person obtaining
75
+ a copy of this software and associated documentation files (the
76
+ 'Software'), to deal in the Software without restriction, including
77
+ without limitation the rights to use, copy, modify, merge, publish,
78
+ distribute, sublicense, and/or sell copies of the Software, and to
79
+ permit persons to whom the Software is furnished to do so, subject to
80
+ the following conditions:
81
+
82
+ The above copyright notice and this permission notice shall be
83
+ included in all copies or substantial portions of the Software.
84
+
85
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
88
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
89
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
90
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
91
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/ext/extconf.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'mkmf'
2
+ dir_config("geohash_native")
3
+ create_makefile("geohash_native")
@@ -0,0 +1,212 @@
1
+ // geohash-native.c
2
+ // (c) 2008 David Troy
3
+ // dave@roundhousetech.com
4
+ //
5
+ // (The MIT License)
6
+ //
7
+ // Copyright (c) 2008 David Troy, Roundhouse Technologies LLC
8
+ //
9
+ // Permission is hereby granted, free of charge, to any person obtaining
10
+ // a copy of this software and associated documentation files (the
11
+ // 'Software'), to deal in the Software without restriction, including
12
+ // without limitation the rights to use, copy, modify, merge, publish,
13
+ // distribute, sublicense, and/or sell copies of the Software, and to
14
+ // permit persons to whom the Software is furnished to do so, subject to
15
+ // the following conditions:
16
+ //
17
+ // The above copyright notice and this permission notice shall be
18
+ // included in all copies or substantial portions of the Software.
19
+ //
20
+ // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
21
+ // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23
+ // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24
+ // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
25
+ // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
26
+ // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+
28
+ #include "ruby.h"
29
+ #include <ctype.h>
30
+
31
+ static VALUE rb_cGeoHash;
32
+
33
+ #define BASE32 "0123456789bcdefghjkmnpqrstuvwxyz"
34
+
35
+ static void decode_geohash_bbox(char *geohash, double *lat, double *lon) {
36
+ int i, j, hashlen;
37
+ double lat_err, lon_err;
38
+ char c, cd, mask, is_even=1;
39
+ static char bits[] = {16,8,4,2,1};
40
+
41
+ lat[0] = -90.0; lat[1] = 90.0;
42
+ lon[0] = -180.0; lon[1] = 180.0;
43
+ lat_err = 90.0; lon_err = 180.0;
44
+ hashlen = strlen(geohash);
45
+
46
+ for (i=0; i<hashlen; i++) {
47
+ c = tolower(geohash[i]);
48
+ cd = strchr(BASE32, c)-BASE32;
49
+ for (j=0; j<5; j++) {
50
+ mask = bits[j];
51
+ if (is_even) {
52
+ lon_err /= 2;
53
+ lon[!(cd&mask)] = (lon[0] + lon[1])/2;
54
+ } else {
55
+ lat_err /= 2;
56
+ lat[!(cd&mask)] = (lat[0] + lat[1])/2;
57
+ }
58
+ is_even = !is_even;
59
+ }
60
+ }
61
+ }
62
+
63
+ static void decode_geohash(char *geohash, double *point) {
64
+ double lat[2], lon[2];
65
+
66
+ decode_geohash_bbox(geohash, lat, lon);
67
+
68
+ point[0] = (lat[0] + lat[1]) / 2;
69
+ point[1] = (lon[0] + lon[1]) / 2;
70
+ }
71
+
72
+ static void encode_geohash(double latitude, double longitude, int precision, char *geohash) {
73
+ int is_even=1, i=0;
74
+ double lat[2], lon[2], mid;
75
+ char bits[] = {16,8,4,2,1};
76
+ int bit=0, ch=0;
77
+
78
+ lat[0] = -90.0; lat[1] = 90.0;
79
+ lon[0] = -180.0; lon[1] = 180.0;
80
+
81
+ while (i < precision) {
82
+ if (is_even) {
83
+ mid = (lon[0] + lon[1]) / 2;
84
+ if (longitude > mid) {
85
+ ch |= bits[bit];
86
+ lon[0] = mid;
87
+ } else
88
+ lon[1] = mid;
89
+ } else {
90
+ mid = (lat[0] + lat[1]) / 2;
91
+ if (latitude > mid) {
92
+ ch |= bits[bit];
93
+ lat[0] = mid;
94
+ } else
95
+ lat[1] = mid;
96
+ }
97
+
98
+ is_even = !is_even;
99
+ if (bit < 4)
100
+ bit++;
101
+ else {
102
+ geohash[i++] = BASE32[ch];
103
+ bit = 0;
104
+ ch = 0;
105
+ }
106
+ }
107
+ geohash[i] = 0;
108
+ }
109
+
110
+ static VALUE encode(VALUE self, VALUE lat, VALUE lon, VALUE precision)
111
+ {
112
+ VALUE geohash;
113
+ char str[15];
114
+ int digits=10;
115
+
116
+ digits = NUM2INT(precision);
117
+
118
+ Check_Type(lat, T_FLOAT);
119
+ Check_Type(lon, T_FLOAT);
120
+ if (digits <3 || digits > 12)
121
+ digits = 12;
122
+
123
+ encode_geohash(RFLOAT(lat)->value, RFLOAT(lon)->value, digits, str);
124
+
125
+ geohash = rb_str_new2(str);
126
+ return geohash;
127
+ }
128
+
129
+ static VALUE decode_bbox(VALUE self, VALUE str)
130
+ {
131
+ VALUE ary, ret;
132
+ double lat[2], lon[2];
133
+ Check_Type(str, T_STRING);
134
+
135
+ decode_geohash_bbox(RSTRING(str)->ptr, lat, lon);
136
+
137
+ ret = rb_ary_new2(2); /* [[lat[0], lon[0]], [lat[1], lon[1]]] */
138
+
139
+ ary = rb_ary_new2(2); /* [lat[0], lon[0]] */
140
+ rb_ary_store(ary, 0, rb_float_new(lat[0]));
141
+ rb_ary_store(ary, 1, rb_float_new(lon[0]));
142
+ rb_ary_store(ret, 0, ary);
143
+
144
+ ary = rb_ary_new2(2); /* [lat[1], lon[1]] */
145
+ rb_ary_store(ary, 0, rb_float_new(lat[1]));
146
+ rb_ary_store(ary, 1, rb_float_new(lon[1]));
147
+ rb_ary_store(ret, 1, ary);
148
+
149
+ return ret;
150
+ }
151
+
152
+ static VALUE decode(VALUE self, VALUE str)
153
+ {
154
+ VALUE ary;
155
+ double point[2];
156
+ Check_Type(str, T_STRING);
157
+
158
+ decode_geohash(RSTRING(str)->ptr, point);
159
+
160
+ ary = rb_ary_new2(2);
161
+ rb_ary_store(ary, 0, rb_float_new(point[0]));
162
+ rb_ary_store(ary, 1, rb_float_new(point[1]));
163
+ return ary;
164
+ }
165
+
166
+ // Given a particular geohash string, a direction, and a final length
167
+ // Compute a neighbor using base32 lookups, recursively when necessary
168
+ void get_neighbor(char *str, int dir, int hashlen)
169
+ {
170
+ /* Right, Left, Top, Bottom */
171
+
172
+ static char *neighbors[] = { "bc01fg45238967deuvhjyznpkmstqrwx",
173
+ "238967debc01fg45kmstqrwxuvhjyznp",
174
+ "p0r21436x8zb9dcf5h7kjnmqesgutwvy",
175
+ "14365h7k9dcfesgujnmqp0r2twvyx8zb" };
176
+
177
+ static char *borders[] = { "bcfguvyz", "0145hjnp", "prxz", "028b" };
178
+
179
+ char last_chr, *border, *neighbor;
180
+ int index = ( 2 * (hashlen % 2) + dir) % 4;
181
+ neighbor = neighbors[index];
182
+ border = borders[index];
183
+ last_chr = str[hashlen-1];
184
+ if (strchr(border,last_chr))
185
+ get_neighbor(str, dir, hashlen-1);
186
+ str[hashlen-1] = BASE32[strchr(neighbor, last_chr)-neighbor];
187
+ }
188
+
189
+ // Acts as Ruby API wrapper to get_neighbor function, which is recursive and does nasty C things
190
+ static VALUE calculate_adjacent(VALUE self, VALUE geohash, VALUE dir)
191
+ {
192
+ char *str;
193
+ VALUE ret_val;
194
+ Check_Type(geohash, T_STRING);
195
+ Check_Type(dir, T_FIXNUM);
196
+ str = RSTRING(geohash)->ptr;
197
+ if (!strlen(str)) return Qnil;
198
+ ret_val = rb_str_new(str,strlen(str));
199
+ get_neighbor(RSTRING(ret_val)->ptr, NUM2INT(dir), strlen(str));
200
+ return ret_val;
201
+ }
202
+
203
+ void Init_geohash_native()
204
+ {
205
+ rb_cGeoHash = rb_define_class("GeoHash", rb_cObject);
206
+ rb_define_singleton_method(rb_cGeoHash, "decode_bbox", decode_bbox, 1);
207
+ rb_define_singleton_method(rb_cGeoHash, "decode_base", decode, 1);
208
+ rb_define_singleton_method(rb_cGeoHash, "encode_base", encode, 3);
209
+ rb_define_singleton_method(rb_cGeoHash, "calculate_adjacent", calculate_adjacent, 2);
210
+ }
211
+
212
+ // end
data/lib/geohash.rb ADDED
@@ -0,0 +1,62 @@
1
+ require 'geohash_native'
2
+
3
+ class Float
4
+ def decimals(places)
5
+ n = (self * (10 ** places)).round
6
+ n.to_f/(10**places)
7
+ end
8
+ end
9
+
10
+ class GeoHash
11
+
12
+ VERSION = '1.1.0'
13
+
14
+ NEIGHBOR_DIRECTIONS = [ [0, 1], [2, 3] ]
15
+
16
+ # Encode latitude and longitude to a geohash with precision digits
17
+ def self.encode(lat, lon, precision=10)
18
+ encode_base(lat, lon, precision)
19
+ end
20
+
21
+ # Decode a geohash to a latitude and longitude with decimals digits
22
+ def self.decode(geohash, decimals=5)
23
+ lat, lon = decode_base(geohash)
24
+ [lat.decimals(decimals), lon.decimals(decimals)]
25
+ end
26
+
27
+ # Create a new GeoHash object from a geohash or from a latlon
28
+ def initialize(*params)
29
+ if params.first.is_a?(Float)
30
+ @value = GeoHash.encode(*params)
31
+ @latitude, @longitude = params
32
+ else
33
+ @value = params.first
34
+ @latitude, @longitude = GeoHash.decode(@value)
35
+ end
36
+ @bounding_box = GeoHash.decode_bbox(@value)
37
+ end
38
+
39
+ def to_s
40
+ @value
41
+ end
42
+
43
+ def to_bbox
44
+ GeoHash.decode_bbox(@value)
45
+ end
46
+
47
+ def neighbor(dir)
48
+ GeoHash.calculate_adjacent(@value, dir)
49
+ end
50
+
51
+ def neighbors
52
+ immediate = NEIGHBOR_DIRECTIONS.flatten.map do |d|
53
+ neighbor(d)
54
+ end
55
+ diagonals = NEIGHBOR_DIRECTIONS.first.map do |y|
56
+ NEIGHBOR_DIRECTIONS.last.map do |x|
57
+ GeoHash.calculate_adjacent(GeoHash.calculate_adjacent(@value, x), y)
58
+ end
59
+ end.flatten
60
+ immediate + diagonals
61
+ end
62
+ end
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH << "#{File.dirname(__FILE__)}/../ext"
3
+ require "#{File.dirname(__FILE__)}/../lib/geohash"
4
+ require 'test/unit'
5
+
6
+ class GeoHashTest < Test::Unit::TestCase
7
+
8
+ def test_decoding
9
+ assert_equal [39.02474, -76.51100], GeoHash.decode("dqcw4bnrs6s7")
10
+ assert_equal [37.791562, -122.398541], GeoHash.decode("9q8yyz8pg3bb", 6)
11
+ assert_equal [37.791562, -122.398541], GeoHash.decode("9Q8YYZ8PG3BB", 6)
12
+ assert_equal [42.60498046875, -5.60302734375], GeoHash.decode("ezs42", 11)
13
+ assert_equal [-25.382708, -49.265506], GeoHash.decode("6gkzwgjzn820",6)
14
+ assert_equal [-25.383, -49.266], GeoHash.decode("6gkzwgjz", 3)
15
+ assert_equal [37.8565, -122.2554], GeoHash.decode("9q9p658642g7", 4)
16
+ end
17
+
18
+ def test_encoding
19
+ assert_equal "dqcw4bnrs6s7", GeoHash.encode(39.0247389581054, -76.5110040642321, 12)
20
+ assert_equal "dqcw4bnrs6", GeoHash.encode(39.0247389581054, -76.5110040642321, 10)
21
+ assert_equal "6gkzmg1u", GeoHash.encode(-25.427, -49.315, 8)
22
+ assert_equal "ezs42", GeoHash.encode(42.60498046875, -5.60302734375, 5)
23
+ end
24
+
25
+ def check_decoding(gh)
26
+ exact = GeoHash.decode(gh, 20)
27
+ bbox = GeoHash.decode_bbox(gh)
28
+
29
+ # check that the bbox is centered on the decoded point
30
+ bbox_center = [(bbox[0][0] + bbox[1][0]) / 2, (bbox[0][1] + bbox[1][1]) / 2]
31
+ assert_equal exact, bbox_center
32
+
33
+ # check that the bounding box is the expected size
34
+ bits = gh.size * 5
35
+ lon_bits = (bits.to_f/2).ceil
36
+ lat_bits = (bits.to_f/2).floor
37
+ correct_size = [180.0/2**lat_bits, 360.0/2**lon_bits]
38
+ bbox_size = [bbox[1][0] - bbox[0][0], bbox[1][1] - bbox[0][1]]
39
+ assert_equal bbox_size, correct_size
40
+ end
41
+
42
+ def test_decoding_bbox
43
+ s = "dqcw4bnrs6s7"
44
+ (s.length).downto(0) do |l|
45
+ check_decoding(s[0..l])
46
+ end
47
+ end
48
+
49
+ def test_specific_bbox
50
+ assert_equal [[39.0234375, -76.552734375], [39.0673828125, -76.5087890625]], GeoHash.decode_bbox('dqcw4')
51
+ end
52
+
53
+ def test_neighbors
54
+ assert_equal ["dqcjr1", "dqcjq9", "dqcjqf", "dqcjqb", "dqcjr4", "dqcjr0", "dqcjqd", "dqcjq8"], GeoHash.new("dqcjqc").neighbors
55
+
56
+ assert_equal "dqcw5", GeoHash.calculate_adjacent("dqcw4", 0) # right
57
+ assert_equal "dqcw1", GeoHash.calculate_adjacent("dqcw4", 1) # left
58
+
59
+ assert_equal "dqctc", GeoHash.calculate_adjacent("dqcw1", 3) # bottom
60
+
61
+ assert_equal "dqcwh", GeoHash.calculate_adjacent("dqcw5", 0) # right
62
+ assert_equal "dqcw4", GeoHash.calculate_adjacent("dqcw5", 1) # left
63
+ assert_equal "dqcw7", GeoHash.calculate_adjacent("dqcw5", 2) # top
64
+ assert_equal "dqctg", GeoHash.calculate_adjacent("dqcw5", 3) # bottom
65
+ assert_equal 8, (["dqcw7", "dqctg", "dqcw4", "dqcwh", "dqcw6", "dqcwk", "dqctf", "dqctu"] & GeoHash.new("dqcw5").neighbors).size
66
+ end
67
+
68
+ # require 'benchmark'
69
+ # def test_multiple
70
+ # Benchmark.bmbm(30) do |bm|
71
+ # bm.report("encoding") {30000.times { test_encoding }}
72
+ # bm.report("decoding") {30000.times { test_decoding }}
73
+ # #bm.report("neighbors") {30000.times { test_neighbors }}
74
+ # end
75
+ # end
76
+ end
77
+
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: davetroy-geohash
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Troy
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-27 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Geohash provides support for manipulating GeoHash strings in Ruby. See http://geohash.org.
17
+ email: dave@roundhousetech.com
18
+ executables: []
19
+
20
+ extensions:
21
+ - ext/extconf.rb
22
+ extra_rdoc_files:
23
+ - Manifest.txt
24
+ - README
25
+ files:
26
+ - ext/extconf.rb
27
+ - ext/geohash_native.c
28
+ - lib/geohash.rb
29
+ - Manifest.txt
30
+ - README
31
+ has_rdoc: true
32
+ homepage: http://github.com/davetroy/geohash
33
+ post_install_message:
34
+ rdoc_options:
35
+ - --main
36
+ - README
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements: []
52
+
53
+ rubyforge_project:
54
+ rubygems_version: 1.2.0
55
+ signing_key:
56
+ specification_version: 2
57
+ summary: GeoHash Library for Ruby, per http://geohash.org implementation.
58
+ test_files:
59
+ - test/test_geohash.rb