geocoder 0.9.10 → 0.9.11
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of geocoder might be problematic. Click here for more details.
- data/.gitignore +1 -0
- data/CHANGELOG.rdoc +10 -0
- 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 -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
|
+
|