rails-geocoder 0.9.10 → 0.9.11
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGELOG.rdoc +15 -4
- data/LICENSE +1 -1
- data/README.rdoc +118 -31
- data/VERSION +1 -1
- data/lib/geocoder.rb +56 -14
- data/lib/geocoder/cache.rb +70 -0
- data/lib/geocoder/calculations.rb +162 -22
- data/lib/geocoder/configuration.rb +46 -9
- data/lib/geocoder/lookups/base.rb +40 -9
- data/lib/geocoder/lookups/freegeoip.rb +4 -6
- data/lib/geocoder/lookups/geocoder_ca.rb +44 -0
- data/lib/geocoder/lookups/google.rb +8 -5
- data/lib/geocoder/lookups/yahoo.rb +6 -4
- data/lib/geocoder/orms/active_record.rb +85 -39
- data/lib/geocoder/orms/active_record_legacy.rb +8 -4
- data/lib/geocoder/orms/base.rb +24 -21
- data/lib/geocoder/request.rb +1 -1
- data/lib/geocoder/results/base.rb +0 -14
- data/lib/geocoder/results/geocoder_ca.rb +58 -0
- data/lib/geocoder/results/google.rb +16 -4
- data/test/fixtures/geocoder_ca_madison_square_garden.json +1 -0
- data/test/fixtures/geocoder_ca_no_results.json +1 -0
- data/test/fixtures/geocoder_ca_reverse.json +34 -0
- data/test/fixtures/google_no_locality.json +51 -0
- data/test/geocoder_test.rb +220 -64
- data/test/test_helper.rb +48 -9
- metadata +15 -15
- data/rails-geocoder.gemspec +0 -14
@@ -2,34 +2,113 @@ module Geocoder
|
|
2
2
|
module Calculations
|
3
3
|
extend self
|
4
4
|
|
5
|
+
##
|
6
|
+
# Compass point names, listed clockwise starting at North.
|
7
|
+
#
|
8
|
+
# If you want bearings named using more, fewer, or different points
|
9
|
+
# override Geocoder::Calculations.COMPASS_POINTS with your own array.
|
10
|
+
#
|
11
|
+
COMPASS_POINTS = %w[N NE E SE S SW W NW]
|
12
|
+
|
13
|
+
##
|
14
|
+
# Radius of the Earth, in kilometers.
|
15
|
+
# Value taken from: http://en.wikipedia.org/wiki/Earth_radius
|
16
|
+
#
|
17
|
+
EARTH_RADIUS = 6371.0
|
18
|
+
|
19
|
+
##
|
20
|
+
# Conversion factor: multiply by kilometers to get miles.
|
21
|
+
#
|
22
|
+
KM_IN_MI = 0.621371192
|
23
|
+
|
24
|
+
##
|
25
|
+
# Calculate the distance spanned by one
|
26
|
+
# degree of latitude in the given units.
|
27
|
+
#
|
28
|
+
def latitude_degree_distance(units = :mi)
|
29
|
+
2 * Math::PI * earth_radius(units) / 360
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Calculate the distance spanned by one degree of longitude
|
34
|
+
# at the given latitude. This ranges from around 69 miles at
|
35
|
+
# the equator to zero at the poles.
|
36
|
+
#
|
37
|
+
def longitude_degree_distance(latitude, units = :mi)
|
38
|
+
latitude_degree_distance(units) * Math.cos(to_radians(latitude))
|
39
|
+
end
|
40
|
+
|
5
41
|
##
|
6
42
|
# Calculate the distance between two points on Earth (Haversine formula).
|
7
43
|
# Takes two sets of coordinates and an options hash:
|
8
44
|
#
|
9
|
-
# <tt>:units</tt>
|
45
|
+
# * <tt>:units</tt> - <tt>:mi</tt> (default) or <tt>:km</tt>
|
10
46
|
#
|
11
47
|
def distance_between(lat1, lon1, lat2, lon2, options = {})
|
12
48
|
|
13
49
|
# set default options
|
14
50
|
options[:units] ||= :mi
|
15
51
|
|
16
|
-
# define conversion factors
|
17
|
-
conversions = { :mi => 3956, :km => 6371 }
|
18
|
-
|
19
52
|
# convert degrees to radians
|
20
|
-
lat1 = to_radians(lat1)
|
21
|
-
lon1 = to_radians(lon1)
|
22
|
-
lat2 = to_radians(lat2)
|
23
|
-
lon2 = to_radians(lon2)
|
53
|
+
lat1, lon1, lat2, lon2 = to_radians(lat1, lon1, lat2, lon2)
|
24
54
|
|
25
|
-
# compute
|
26
|
-
dlat =
|
27
|
-
dlon =
|
55
|
+
# compute deltas
|
56
|
+
dlat = lat2 - lat1
|
57
|
+
dlon = lon2 - lon1
|
28
58
|
|
29
59
|
a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
|
30
60
|
(Math.sin(dlon / 2))**2 * Math.cos(lat2)
|
31
61
|
c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
|
32
|
-
c *
|
62
|
+
c * earth_radius(options[:units])
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Calculate bearing between two sets of coordinates.
|
67
|
+
# Returns a number of degrees from due north (clockwise).
|
68
|
+
#
|
69
|
+
# Also accepts an options hash:
|
70
|
+
#
|
71
|
+
# * <tt>:method</tt> - <tt>:linear</tt> (default) or <tt>:spherical</tt>;
|
72
|
+
# the spherical method is "correct" in that it returns the shortest path
|
73
|
+
# (one along a great circle) but the linear method is the default as it
|
74
|
+
# is less confusing (returns due east or west when given two points with
|
75
|
+
# the same latitude)
|
76
|
+
#
|
77
|
+
# Based on: http://www.movable-type.co.uk/scripts/latlong.html
|
78
|
+
#
|
79
|
+
def bearing_between(lat1, lon1, lat2, lon2, options = {})
|
80
|
+
options[:method] = :linear unless options[:method] == :spherical
|
81
|
+
|
82
|
+
# convert degrees to radians
|
83
|
+
lat1, lon1, lat2, lon2 = to_radians(lat1, lon1, lat2, lon2)
|
84
|
+
|
85
|
+
# compute deltas
|
86
|
+
dlat = lat2 - lat1
|
87
|
+
dlon = lon2 - lon1
|
88
|
+
|
89
|
+
case options[:method]
|
90
|
+
when :linear
|
91
|
+
y = dlon
|
92
|
+
x = dlat
|
93
|
+
|
94
|
+
when :spherical
|
95
|
+
y = Math.sin(dlon) * Math.cos(lat2)
|
96
|
+
x = Math.cos(lat1) * Math.sin(lat2) -
|
97
|
+
Math.sin(lat1) * Math.cos(lat2) * Math.cos(dlon)
|
98
|
+
end
|
99
|
+
|
100
|
+
bearing = Math.atan2(x,y)
|
101
|
+
# Answer is in radians counterclockwise from due east.
|
102
|
+
# Convert to degrees clockwise from due north:
|
103
|
+
(90 - to_degrees(bearing) + 360) % 360
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Translate a bearing (float) into a compass direction (string, eg "North").
|
108
|
+
#
|
109
|
+
def compass_point(bearing, points = COMPASS_POINTS)
|
110
|
+
seg_size = 360 / points.size
|
111
|
+
points[((bearing + (seg_size / 2)) % 360) / seg_size]
|
33
112
|
end
|
34
113
|
|
35
114
|
##
|
@@ -41,12 +120,10 @@ module Geocoder
|
|
41
120
|
def geographic_center(points)
|
42
121
|
|
43
122
|
# convert objects to [lat,lon] arrays and remove nils
|
44
|
-
points
|
45
|
-
p.is_a?(Array) ? p : (p.geocoded?? p.read_coordinates : nil)
|
46
|
-
}.compact
|
123
|
+
points.map!{ |p| p.is_a?(Array) ? p : p.to_coordinates }.compact
|
47
124
|
|
48
125
|
# convert degrees to radians
|
49
|
-
points.map!{ |p|
|
126
|
+
points.map!{ |p| to_radians(p) }
|
50
127
|
|
51
128
|
# convert to Cartesian coordinates
|
52
129
|
x = []; y = []; z = []
|
@@ -67,28 +144,91 @@ module Geocoder
|
|
67
144
|
lat = Math.atan2(za, hyp)
|
68
145
|
|
69
146
|
# return answer in degrees
|
70
|
-
[
|
147
|
+
to_degrees [lat, lon]
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# Returns coordinates of the lower-left and upper-right corners of a box
|
152
|
+
# with the given point at its center. The radius is the shortest distance
|
153
|
+
# from the center point to any side of the box (the length of each side
|
154
|
+
# is twice the radius).
|
155
|
+
#
|
156
|
+
# This is useful for finding corner points of a map viewport, or for
|
157
|
+
# roughly limiting the possible solutions in a geo-spatial search
|
158
|
+
# (ActiveRecord queries use it thusly).
|
159
|
+
#
|
160
|
+
def bounding_box(latitude, longitude, radius, options = {})
|
161
|
+
units = options[:units] || :mi
|
162
|
+
radius = radius.to_f
|
163
|
+
[
|
164
|
+
latitude - (radius / latitude_degree_distance(units)),
|
165
|
+
longitude - (radius / longitude_degree_distance(latitude, units)),
|
166
|
+
latitude + (radius / latitude_degree_distance(units)),
|
167
|
+
longitude + (radius / longitude_degree_distance(latitude, units))
|
168
|
+
]
|
71
169
|
end
|
72
170
|
|
73
171
|
##
|
74
172
|
# Convert degrees to radians.
|
173
|
+
# If an array (or multiple arguments) is passed,
|
174
|
+
# converts each value and returns array.
|
75
175
|
#
|
76
|
-
def to_radians(
|
77
|
-
|
176
|
+
def to_radians(*args)
|
177
|
+
args = args.first if args.first.is_a?(Array)
|
178
|
+
if args.size == 1
|
179
|
+
args.first * (Math::PI / 180)
|
180
|
+
else
|
181
|
+
args.map{ |i| to_radians(i) }
|
182
|
+
end
|
78
183
|
end
|
79
184
|
|
80
185
|
##
|
81
186
|
# Convert radians to degrees.
|
187
|
+
# If an array (or multiple arguments) is passed,
|
188
|
+
# converts each value and returns array.
|
189
|
+
#
|
190
|
+
def to_degrees(*args)
|
191
|
+
args = args.first if args.first.is_a?(Array)
|
192
|
+
if args.size == 1
|
193
|
+
(args.first * 180.0) / Math::PI
|
194
|
+
else
|
195
|
+
args.map{ |i| to_degrees(i) }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
##
|
200
|
+
# Convert miles to kilometers.
|
201
|
+
#
|
202
|
+
def to_kilometers(mi)
|
203
|
+
mi * mi_in_km
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# Convert kilometers to miles.
|
82
208
|
#
|
83
|
-
def
|
84
|
-
|
209
|
+
def to_miles(km)
|
210
|
+
km * km_in_mi
|
211
|
+
end
|
212
|
+
|
213
|
+
##
|
214
|
+
# Radius of the Earth in the given units (:mi or :km). Default is :mi.
|
215
|
+
#
|
216
|
+
def earth_radius(units = :mi)
|
217
|
+
units == :km ? EARTH_RADIUS : to_miles(EARTH_RADIUS)
|
85
218
|
end
|
86
219
|
|
87
220
|
##
|
88
221
|
# Conversion factor: km to mi.
|
89
222
|
#
|
90
223
|
def km_in_mi
|
91
|
-
|
224
|
+
KM_IN_MI
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Conversion factor: mi to km.
|
229
|
+
#
|
230
|
+
def mi_in_km
|
231
|
+
1.0 / KM_IN_MI
|
92
232
|
end
|
93
233
|
end
|
94
234
|
end
|
@@ -1,16 +1,53 @@
|
|
1
1
|
module Geocoder
|
2
2
|
class Configuration
|
3
|
-
def self.timeout; @@timeout; end
|
4
|
-
def self.timeout=(obj); @@timeout = obj; end
|
5
3
|
|
6
|
-
def self.
|
7
|
-
|
4
|
+
def self.options_and_defaults
|
5
|
+
[
|
6
|
+
# geocoding service timeout (secs)
|
7
|
+
[:timeout, 3],
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
# name of geocoding service (symbol)
|
10
|
+
[:lookup, :google],
|
11
|
+
|
12
|
+
# ISO-639 language code
|
13
|
+
[:language, :en],
|
14
|
+
|
15
|
+
# use HTTPS for lookup requests? (if supported)
|
16
|
+
[:use_https, false],
|
17
|
+
|
18
|
+
# API key for geocoding service
|
19
|
+
[:api_key, nil],
|
20
|
+
|
21
|
+
# cache object (must respond to #[], #[]=, and #keys)
|
22
|
+
[:cache, nil],
|
23
|
+
|
24
|
+
# prefix (string) to use for all cache keys
|
25
|
+
[:cache_prefix, "geocoder:"]
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
# define getters and setters for all configuration settings
|
30
|
+
self.options_and_defaults.each do |o,d|
|
31
|
+
eval("def self.#{o}; @@#{o}; end")
|
32
|
+
eval("def self.#{o}=(obj); @@#{o} = obj; end")
|
33
|
+
end
|
34
|
+
|
35
|
+
# legacy support
|
36
|
+
def self.yahoo_app_id=(value)
|
37
|
+
warn "DEPRECATION WARNING: Geocoder's 'yahoo_app_id' setting has been replaced by 'api_key'. " +
|
38
|
+
"This method will be removed in Geocoder v1.0."
|
39
|
+
@@api_key = value
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Set all values to default.
|
44
|
+
#
|
45
|
+
def self.set_defaults
|
46
|
+
self.options_and_defaults.each do |o,d|
|
47
|
+
self.send("#{o}=", d)
|
48
|
+
end
|
49
|
+
end
|
11
50
|
end
|
12
51
|
end
|
13
52
|
|
14
|
-
Geocoder::Configuration.
|
15
|
-
Geocoder::Configuration.lookup = :google
|
16
|
-
Geocoder::Configuration.yahoo_appid = ""
|
53
|
+
Geocoder::Configuration.set_defaults
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
unless defined?(ActiveSupport::JSON)
|
3
3
|
begin
|
4
|
+
require 'rubygems' # for Ruby 1.8
|
4
5
|
require 'json'
|
5
6
|
rescue LoadError
|
6
7
|
raise LoadError, "Please install the 'json' or 'json_pure' gem to parse geocoder results."
|
@@ -17,12 +18,11 @@ module Geocoder
|
|
17
18
|
#
|
18
19
|
# Takes a search string (eg: "Mississippi Coast Coliseumf, Biloxi, MS",
|
19
20
|
# "205.128.54.202") for geocoding, or coordinates (latitude, longitude)
|
20
|
-
# for reverse geocoding.
|
21
|
+
# for reverse geocoding. Returns an array of <tt>Geocoder::Result</tt>s.
|
21
22
|
#
|
22
23
|
def search(*args)
|
23
|
-
|
24
|
-
|
25
|
-
end
|
24
|
+
reverse = (args.size == 2) || coordinates?(args.first)
|
25
|
+
results(args.join(","), reverse).map{ |r| result_class.new(r) }
|
26
26
|
end
|
27
27
|
|
28
28
|
|
@@ -31,7 +31,7 @@ module Geocoder
|
|
31
31
|
##
|
32
32
|
# Geocoder::Result object or nil on timeout or other error.
|
33
33
|
#
|
34
|
-
def
|
34
|
+
def results(query, reverse = false)
|
35
35
|
fail
|
36
36
|
end
|
37
37
|
|
@@ -60,7 +60,7 @@ module Geocoder
|
|
60
60
|
rescue TimeoutError
|
61
61
|
warn "Geocoding API not responding fast enough " +
|
62
62
|
"(see Geocoder::Configuration.timeout to set limit)."
|
63
|
-
|
63
|
+
end
|
64
64
|
end
|
65
65
|
|
66
66
|
##
|
@@ -78,16 +78,37 @@ module Geocoder
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
+
##
|
82
|
+
# Protocol to use for communication with geocoding services.
|
83
|
+
# Set in configuration but not available for every service.
|
84
|
+
#
|
85
|
+
def protocol
|
86
|
+
"http" + (Geocoder::Configuration.use_https ? "s" : "")
|
87
|
+
end
|
88
|
+
|
81
89
|
##
|
82
90
|
# Fetches a raw search result (JSON string).
|
83
91
|
#
|
84
92
|
def fetch_raw_data(query, reverse = false)
|
85
|
-
url = query_url(query, reverse)
|
86
93
|
timeout(Geocoder::Configuration.timeout) do
|
87
|
-
|
94
|
+
url = query_url(query, reverse)
|
95
|
+
unless cache and response = cache[url]
|
96
|
+
response = Net::HTTP.get_response(URI.parse(url)).body
|
97
|
+
if cache
|
98
|
+
cache[url] = response
|
99
|
+
end
|
100
|
+
end
|
101
|
+
response
|
88
102
|
end
|
89
103
|
end
|
90
104
|
|
105
|
+
##
|
106
|
+
# The working Cache object.
|
107
|
+
#
|
108
|
+
def cache
|
109
|
+
Geocoder.cache
|
110
|
+
end
|
111
|
+
|
91
112
|
##
|
92
113
|
# Is the given string a loopback IP address?
|
93
114
|
#
|
@@ -95,12 +116,22 @@ module Geocoder
|
|
95
116
|
!!(ip == "0.0.0.0" or ip.match(/^127/))
|
96
117
|
end
|
97
118
|
|
119
|
+
##
|
120
|
+
# Does the given string look like latitude/longitude coordinates?
|
121
|
+
#
|
122
|
+
def coordinates?(value)
|
123
|
+
!!value.to_s.match(/^[0-9\.\-]+, ?[0-9\.\-]+$/)
|
124
|
+
end
|
125
|
+
|
98
126
|
##
|
99
127
|
# Simulate ActiveSupport's Object#to_query.
|
128
|
+
# Removes any keys with nil value.
|
100
129
|
#
|
101
130
|
def hash_to_query(hash)
|
102
131
|
require 'cgi' unless defined?(CGI) && defined?(CGI.escape)
|
103
|
-
hash.collect{ |p|
|
132
|
+
hash.collect{ |p|
|
133
|
+
p[1].nil? ? nil : p.map{ |i| CGI.escape i.to_s } * '='
|
134
|
+
}.compact.sort * '&'
|
104
135
|
end
|
105
136
|
end
|
106
137
|
end
|
@@ -6,15 +6,13 @@ module Geocoder::Lookup
|
|
6
6
|
|
7
7
|
private # ---------------------------------------------------------------
|
8
8
|
|
9
|
-
def
|
9
|
+
def results(query, reverse = false)
|
10
10
|
# don't look up a loopback address, just return the stored result
|
11
|
-
return reserved_result(query) if loopback_address?(query)
|
11
|
+
return [reserved_result(query)] if loopback_address?(query)
|
12
12
|
begin
|
13
|
-
|
14
|
-
doc
|
15
|
-
end
|
13
|
+
return [fetch_data(query, reverse)]
|
16
14
|
rescue StandardError # Freegeoip.net returns HTML on bad request
|
17
|
-
|
15
|
+
return []
|
18
16
|
end
|
19
17
|
end
|
20
18
|
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'geocoder/lookups/base'
|
2
|
+
require "geocoder/results/geocoder_ca"
|
3
|
+
|
4
|
+
module Geocoder::Lookup
|
5
|
+
class GeocoderCa < Base
|
6
|
+
|
7
|
+
private # ---------------------------------------------------------------
|
8
|
+
|
9
|
+
def results(query, reverse = false)
|
10
|
+
return [] unless doc = fetch_data(query, reverse)
|
11
|
+
if doc['error'].nil?
|
12
|
+
return [doc]
|
13
|
+
elsif doc['error']['code'] == "005"
|
14
|
+
# "Postal Code is not in the proper Format" => no results, just shut up
|
15
|
+
else
|
16
|
+
warn "Geocoder.ca service error: #{doc['error']['code']} (#{doc['error']['description']})."
|
17
|
+
end
|
18
|
+
return []
|
19
|
+
end
|
20
|
+
|
21
|
+
def query_url(query, reverse = false)
|
22
|
+
params = {
|
23
|
+
:geoit => "xml",
|
24
|
+
:jsonp => 1,
|
25
|
+
:callback => "test"
|
26
|
+
}
|
27
|
+
if reverse
|
28
|
+
lat,lon = query.split(',')
|
29
|
+
params[:latt] = lat
|
30
|
+
params[:longt] = lon
|
31
|
+
params[:corner] = 1
|
32
|
+
params[:reverse] = 1
|
33
|
+
else
|
34
|
+
params[:locate] = query
|
35
|
+
end
|
36
|
+
"http://geocoder.ca/?" + hash_to_query(params)
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_raw_data(raw_data)
|
40
|
+
super raw_data[/^test\((.*)\)\;\s*$/, 1]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|