abuiles-geokit 1.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.bundle/config +2 -0
- data/.gitignore +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +19 -0
- data/History.txt +77 -0
- data/Manifest.txt +21 -0
- data/README.markdown +273 -0
- data/Rakefile +13 -0
- data/geokit.gemspec +24 -0
- data/lib/geokit.rb +55 -0
- data/lib/geokit/bounds.rb +95 -0
- data/lib/geokit/geo_loc.rb +115 -0
- data/lib/geokit/geocoders.rb +68 -0
- data/lib/geokit/geocoders/ca_geocoder.rb +54 -0
- data/lib/geokit/geocoders/geo_plugin_geocoder.rb +30 -0
- data/lib/geokit/geocoders/geocode_error.rb +7 -0
- data/lib/geokit/geocoders/geocoder.rb +75 -0
- data/lib/geokit/geocoders/geonames_geocoder.rb +53 -0
- data/lib/geokit/geocoders/google_geocoder.rb +145 -0
- data/lib/geokit/geocoders/google_premier_geocoder.rb +147 -0
- data/lib/geokit/geocoders/ip_geocoder.rb +76 -0
- data/lib/geokit/geocoders/multi_geocoder.rb +60 -0
- data/lib/geokit/geocoders/us_geocoder.rb +50 -0
- data/lib/geokit/geocoders/yahoo_geocoder.rb +49 -0
- data/lib/geokit/inflector.rb +39 -0
- data/lib/geokit/lat_lng.rb +112 -0
- data/lib/geokit/mappable.rb +210 -0
- data/lib/geokit/too_many_queries_error.rb +4 -0
- data/lib/geokit/version.rb +3 -0
- data/test/test_base_geocoder.rb +58 -0
- data/test/test_bounds.rb +97 -0
- data/test/test_ca_geocoder.rb +39 -0
- data/test/test_geoloc.rb +72 -0
- data/test/test_geoplugin_geocoder.rb +58 -0
- data/test/test_google_geocoder.rb +225 -0
- data/test/test_google_premier_geocoder.rb +88 -0
- data/test/test_google_reverse_geocoder.rb +47 -0
- data/test/test_inflector.rb +24 -0
- data/test/test_ipgeocoder.rb +109 -0
- data/test/test_latlng.rb +209 -0
- data/test/test_multi_geocoder.rb +91 -0
- data/test/test_multi_ip_geocoder.rb +36 -0
- data/test/test_us_geocoder.rb +54 -0
- data/test/test_yahoo_geocoder.rb +103 -0
- metadata +141 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
# Another geocoding web service
|
4
|
+
# http://www.geonames.org
|
5
|
+
class GeonamesGeocoder < Geocoder
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Template method which does the geocode lookup.
|
10
|
+
def self.do_geocode(address, options = {})
|
11
|
+
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
12
|
+
# geonames need a space seperated search string
|
13
|
+
address_str.gsub!(/,/, " ")
|
14
|
+
params = "/postalCodeSearch?placename=#{Geokit::Inflector::url_escape(address_str)}&maxRows=10"
|
15
|
+
|
16
|
+
if(Geokit::Geocoders::geonames)
|
17
|
+
url = "http://ws.geonames.net#{params}&username=#{Geokit::Geocoders::geonames}"
|
18
|
+
else
|
19
|
+
url = "http://ws.geonames.org#{params}"
|
20
|
+
end
|
21
|
+
|
22
|
+
res = self.call_geocoder_service(url)
|
23
|
+
|
24
|
+
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
25
|
+
|
26
|
+
xml=res.body
|
27
|
+
logger.debug "Geonames geocoding. Address: #{address}. Result: #{xml}"
|
28
|
+
doc=REXML::Document.new(xml)
|
29
|
+
|
30
|
+
if(doc.elements['//geonames/totalResultsCount'].text.to_i > 0)
|
31
|
+
res=GeoLoc.new
|
32
|
+
|
33
|
+
# only take the first result
|
34
|
+
res.lat=doc.elements['//code/lat'].text if doc.elements['//code/lat']
|
35
|
+
res.lng=doc.elements['//code/lng'].text if doc.elements['//code/lng']
|
36
|
+
res.country_code=doc.elements['//code/countryCode'].text if doc.elements['//code/countryCode']
|
37
|
+
res.provider='genomes'
|
38
|
+
res.city=doc.elements['//code/name'].text if doc.elements['//code/name']
|
39
|
+
res.state=doc.elements['//code/adminName1'].text if doc.elements['//code/adminName1']
|
40
|
+
res.zip=doc.elements['//code/postalcode'].text if doc.elements['//code/postalcode']
|
41
|
+
res.success=true
|
42
|
+
return res
|
43
|
+
else
|
44
|
+
logger.info "Geonames was unable to geocode address: "+address
|
45
|
+
return GeoLoc.new
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue
|
49
|
+
logger.error "Caught an error during Geonames geocoding call: "+$!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
# Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
|
4
|
+
# contain a Google API key. Conforms to the interface set by the Geocoder class.
|
5
|
+
class GoogleGeocoder < Geocoder
|
6
|
+
|
7
|
+
ENDPOINT = "http://maps.google.com/maps/geo"
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
# Template method which does the reverse-geocode lookup.
|
12
|
+
def self.do_reverse_geocode(latlng)
|
13
|
+
latlng = LatLng.normalize(latlng)
|
14
|
+
params = { :ll => latlng.ll, :output => "xml", :key => Geokit::Geocoders::google, :oe => "utf-8" }
|
15
|
+
url = "#{ENDPOINT}?" + params.to_query
|
16
|
+
res = self.call_geocoder_service(url)
|
17
|
+
return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
|
18
|
+
xml = res.body
|
19
|
+
logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
|
20
|
+
return self.xml2GeoLoc(xml)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Template method which does the geocode lookup.
|
24
|
+
#
|
25
|
+
# Supports viewport/country code biasing
|
26
|
+
#
|
27
|
+
# ==== OPTIONS
|
28
|
+
# * :bias - This option makes the Google Geocoder return results biased to a particular
|
29
|
+
# country or viewport. Country code biasing is achieved by passing the ccTLD
|
30
|
+
# ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
|
31
|
+
# look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
|
32
|
+
# will be biased to results within the US (ccTLD .com).
|
33
|
+
#
|
34
|
+
# If you'd like the Google Geocoder to prefer results within a given viewport,
|
35
|
+
# you can pass a Geokit::Bounds object as the :bias value.
|
36
|
+
#
|
37
|
+
# ==== EXAMPLES
|
38
|
+
# # By default, the geocoder will return Syracuse, NY
|
39
|
+
# Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
|
40
|
+
# # With country code biasing, it returns Syracuse in Sicily, Italy
|
41
|
+
# Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
|
42
|
+
#
|
43
|
+
# # By default, the geocoder will return Winnetka, IL
|
44
|
+
# Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
|
45
|
+
# # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
|
46
|
+
# bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
|
47
|
+
# Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
|
48
|
+
def self.do_geocode(address, options = {})
|
49
|
+
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
50
|
+
params = { :q => address_str, :output => "xml", :key => Geokit::Geocoders::google, :oe => "utf-8" }.merge(bias_options(options[:bias]))
|
51
|
+
url = "#{ENDPOINT}?" + params.to_query
|
52
|
+
res = self.call_geocoder_service(url)
|
53
|
+
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
54
|
+
xml = res.body.force_encoding('utf-8')
|
55
|
+
logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
|
56
|
+
return self.xml2GeoLoc(xml, address)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.bias_options(bias)
|
60
|
+
if bias.is_a?(String) or bias.is_a?(Symbol)
|
61
|
+
# country code biasing
|
62
|
+
{ :gl => bias.to_s.downcase }
|
63
|
+
elsif bias.is_a?(Bounds)
|
64
|
+
# viewport biasing
|
65
|
+
{ :ll => bias.center.ll, :spn => bias.to_span.ll }
|
66
|
+
else
|
67
|
+
{}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.xml2GeoLoc(xml, address="")
|
72
|
+
doc=REXML::Document.new(xml)
|
73
|
+
|
74
|
+
if doc.elements['//kml/Response/Status/code'].text == '200'
|
75
|
+
geoloc = nil
|
76
|
+
# Google can return multiple results as //Placemark elements.
|
77
|
+
# iterate through each and extract each placemark as a geoloc
|
78
|
+
doc.each_element('//Placemark') do |e|
|
79
|
+
extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc
|
80
|
+
if geoloc.nil?
|
81
|
+
# first time through, geoloc is still nil, so we make it the geoloc we just extracted
|
82
|
+
geoloc = extracted_geoloc
|
83
|
+
else
|
84
|
+
# second (and subsequent) iterations, we push additional
|
85
|
+
# geolocs onto "geoloc.all"
|
86
|
+
geoloc.all.push(extracted_geoloc)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
return geoloc
|
90
|
+
elsif doc.elements['//kml/Response/Status/code'].text == '620'
|
91
|
+
raise Geokit::TooManyQueriesError
|
92
|
+
else
|
93
|
+
logger.info "Google was unable to geocode address: "+address
|
94
|
+
return GeoLoc.new
|
95
|
+
end
|
96
|
+
|
97
|
+
rescue Geokit::TooManyQueriesError
|
98
|
+
# re-raise because of other rescue
|
99
|
+
raise Geokit::TooManyQueriesError, "Google returned a 620 status, too many queries. The given key has gone over the requests limit in the 24 hour period or has submitted too many requests in too short a period of time. If you're sending multiple requests in parallel or in a tight loop, use a timer or pause in your code to make sure you don't send the requests too quickly."
|
100
|
+
rescue
|
101
|
+
logger.error "Caught an error during Google geocoding call: "+$!
|
102
|
+
return GeoLoc.new
|
103
|
+
end
|
104
|
+
|
105
|
+
# extracts a single geoloc from a //placemark element in the google results xml
|
106
|
+
def self.extract_placemark(doc)
|
107
|
+
res = GeoLoc.new
|
108
|
+
coordinates=doc.elements['.//coordinates'].text.to_s.split(',')
|
109
|
+
|
110
|
+
#basics
|
111
|
+
res.lat=coordinates[1]
|
112
|
+
res.lng=coordinates[0]
|
113
|
+
res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode']
|
114
|
+
res.provider='google'
|
115
|
+
|
116
|
+
#extended -- false if not not available
|
117
|
+
res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName']
|
118
|
+
res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName']
|
119
|
+
res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName']
|
120
|
+
res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it
|
121
|
+
res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber']
|
122
|
+
res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName']
|
123
|
+
res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName']
|
124
|
+
res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName']
|
125
|
+
# Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
|
126
|
+
# For Google, 1=low accuracy, 8=high accuracy
|
127
|
+
address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
|
128
|
+
res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
|
129
|
+
res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
|
130
|
+
|
131
|
+
# google returns a set of suggested boundaries for the geocoded result
|
132
|
+
if suggested_bounds = doc.elements['//LatLonBox']
|
133
|
+
res.suggested_bounds = Bounds.normalize(
|
134
|
+
[suggested_bounds.attributes['south'], suggested_bounds.attributes['west']],
|
135
|
+
[suggested_bounds.attributes['north'], suggested_bounds.attributes['east']])
|
136
|
+
end
|
137
|
+
|
138
|
+
res.success=true
|
139
|
+
|
140
|
+
return res
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
class GooglePremierGeocoder < Geocoder
|
4
|
+
|
5
|
+
HOST = "maps.googleapis.com"
|
6
|
+
ENDPOINT = "/maps/api/geocode/json"
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def self.do_reverse_geocode(latlng)
|
11
|
+
latlng = LatLng.normalize(latlng)
|
12
|
+
params = { :latlng => latlng.ll, :sensor => false }
|
13
|
+
signed_url = signed_url_for_params(params)
|
14
|
+
res = self.call_geocoder_service(signed_url)
|
15
|
+
return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
|
16
|
+
json = JSON.parse(res.body)
|
17
|
+
logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{json}"
|
18
|
+
return self.json_to_geo_loc(json)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.do_geocode(address, options = {})
|
22
|
+
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
23
|
+
params = { :address => address_str, :sensor => false }
|
24
|
+
signed_url = signed_url_for_params(params)
|
25
|
+
res = self.call_geocoder_service(signed_url)
|
26
|
+
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
27
|
+
json = JSON.parse(res.body)
|
28
|
+
logger.debug "Google geocoding. Address: #{address}. Result: #{json}"
|
29
|
+
return self.json_to_geo_loc(json, address)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.json_to_geo_loc(json, address="")
|
33
|
+
if json["status"] == "OK"
|
34
|
+
geoloc = nil
|
35
|
+
|
36
|
+
# iterate through each and extract as a geoloc
|
37
|
+
json["results"].each do |result|
|
38
|
+
extracted_geoloc = extract_result(result) # g is now an instance of GeoLoc
|
39
|
+
if geoloc.nil?
|
40
|
+
# first time through, geoloc is still nil, so we make it the geoloc we just extracted
|
41
|
+
geoloc = extracted_geoloc
|
42
|
+
else
|
43
|
+
# second (and subsequent) iterations, we push additional
|
44
|
+
# geolocs onto "geoloc.all"
|
45
|
+
geoloc.all.push(extracted_geoloc)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
return geoloc
|
49
|
+
elsif json["status"] == "ZERO_RESULTS"
|
50
|
+
return GeoLoc.new
|
51
|
+
elsif json["status"] == "OVER_QUERY_LIMIT"
|
52
|
+
raise Geokit::TooManyQueriesError
|
53
|
+
elsif json["status"] == "REQUEST_DENIED"
|
54
|
+
logger.error "Google Premier request denied: "+$!
|
55
|
+
elsif json["status"] == "INVALID_REQUEST"
|
56
|
+
logger.error "Google Premier invalid request: "+$!
|
57
|
+
else
|
58
|
+
logger.info "Google was unable to geocode address: "+address
|
59
|
+
return GeoLoc.new
|
60
|
+
end
|
61
|
+
rescue Geokit::TooManyQueriesError
|
62
|
+
raise Geokit::TooManyQueriesError, "Google Premier returned a 620 status, too many queries. The given key has gone over the requests limit in the 24 hour period or has submitted too many requests in too short a period of time. If you're sending multiple requests in parallel or in a tight loop, use a timer or pause in your code to make sure you don't send the requests too quickly."
|
63
|
+
rescue
|
64
|
+
logger.error "Caught an error during Google Premier geocoding call: "+$!
|
65
|
+
return GeoLoc.new
|
66
|
+
end
|
67
|
+
|
68
|
+
ACCURACY_MAP = {
|
69
|
+
"ROOFTOP" => 8,
|
70
|
+
"RANGE_INTERPOLATED" => 7,
|
71
|
+
"GEOMETRIC_CENTER" => 6,
|
72
|
+
"APPROXIMATE" => 5,
|
73
|
+
}
|
74
|
+
|
75
|
+
# extracts a single geoloc from a result in the google results json
|
76
|
+
def self.extract_result(result)
|
77
|
+
res = GeoLoc.new
|
78
|
+
|
79
|
+
res.provider = "google_premier"
|
80
|
+
|
81
|
+
geometry = result["geometry"] if result
|
82
|
+
|
83
|
+
res.accuracy = ACCURACY_MAP[geometry["location_type"]] if geometry
|
84
|
+
res.precision = geometry["location_type"].underscore if geometry
|
85
|
+
|
86
|
+
location = geometry["location"] if geometry
|
87
|
+
res.lat = location["lat"] if location
|
88
|
+
res.lng = location["lng"] if location
|
89
|
+
|
90
|
+
res.full_address = result["formatted_address"]
|
91
|
+
res.street_address = result["formatted_address"]
|
92
|
+
|
93
|
+
address_components = result["address_components"] if result
|
94
|
+
address_components.each do |component|
|
95
|
+
res.zip = component["long_name"] if component["types"].include?("postal_code")
|
96
|
+
res.country = component["long_name"] if component["types"].include?("country")
|
97
|
+
res.country_code = component["short_name"] if component["types"].include?("country")
|
98
|
+
res.city = component["long_name"] if component["types"].include?("locality")
|
99
|
+
res.state = component["long_name"] if component["types"].include?("administrative_area_level_1")
|
100
|
+
res.province = component["long_name"] if component["types"].include?("administrative_area_level_2")
|
101
|
+
res.district = component["long_name"] if component["types"].include?("administrative_area_level_3")
|
102
|
+
end if address_components
|
103
|
+
|
104
|
+
bounds = geometry["bounds"]
|
105
|
+
|
106
|
+
res.suggested_bounds = Bounds.normalize(
|
107
|
+
[bounds["southwest"]["lat"], bounds["southwest"]["lng"]],
|
108
|
+
[bounds["northeast"]["lat"], bounds["northeast"]["lng"]]
|
109
|
+
) if bounds
|
110
|
+
|
111
|
+
res.success = true
|
112
|
+
|
113
|
+
res
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def self.url_for_params(params)
|
119
|
+
params_to_sign = params
|
120
|
+
"http://" + HOST + string_to_sign
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.signed_url_for_params(params)
|
124
|
+
params_to_sign = params.merge({ :client => Geokit::Geocoders.google_client, :channel => Geokit::Geocoders.google_channel }).reject{ |key, value| value.nil? }
|
125
|
+
string_to_sign = "#{ENDPOINT}?#{params_to_sign.to_query}"
|
126
|
+
signature = signature_for_string(string_to_sign)
|
127
|
+
"http://" + HOST + string_to_sign + "&signature=#{signature}"
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.signature_for_string(string)
|
131
|
+
raw_private_key = url_safe_base64_decode(Geokit::Geocoders.google)
|
132
|
+
digest = OpenSSL::Digest::Digest.new("sha1")
|
133
|
+
raw_signature = OpenSSL::HMAC.digest(digest, raw_private_key, string)
|
134
|
+
url_safe_base64_encode(raw_signature)
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.url_safe_base64_decode(base64_string)
|
138
|
+
Base64.decode64(base64_string.tr("-_","+/"))
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.url_safe_base64_encode(raw)
|
142
|
+
Base64.encode64(raw).tr("+/","-_").strip
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
# Provides geocoding based upon an IP address. The underlying web service is a hostip.info
|
4
|
+
# which sources their data through a combination of publicly available information as well
|
5
|
+
# as community contributions.
|
6
|
+
class IpGeocoder < Geocoder
|
7
|
+
|
8
|
+
# A number of non-routable IP ranges.
|
9
|
+
#
|
10
|
+
# --
|
11
|
+
# Sources for these:
|
12
|
+
# RFC 3330: Special-Use IPv4 Addresses
|
13
|
+
# The bogon list: http://www.cymru.com/Documents/bogon-list.html
|
14
|
+
|
15
|
+
NON_ROUTABLE_IP_RANGES = [
|
16
|
+
IPAddr.new('0.0.0.0/8'), # "This" Network
|
17
|
+
IPAddr.new('10.0.0.0/8'), # Private-Use Networks
|
18
|
+
IPAddr.new('14.0.0.0/8'), # Public-Data Networks
|
19
|
+
IPAddr.new('127.0.0.0/8'), # Loopback
|
20
|
+
IPAddr.new('169.254.0.0/16'), # Link local
|
21
|
+
IPAddr.new('172.16.0.0/12'), # Private-Use Networks
|
22
|
+
IPAddr.new('192.0.2.0/24'), # Test-Net
|
23
|
+
IPAddr.new('192.168.0.0/16'), # Private-Use Networks
|
24
|
+
IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
|
25
|
+
IPAddr.new('224.0.0.0/4'), # Multicast
|
26
|
+
IPAddr.new('240.0.0.0/4') # Reserved for future use
|
27
|
+
].freeze
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Given an IP address, returns a GeoLoc instance which contains latitude,
|
32
|
+
# longitude, city, and country code. Sets the success attribute to false if the ip
|
33
|
+
# parameter does not match an ip address.
|
34
|
+
def self.do_geocode(ip, options = {})
|
35
|
+
return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
|
36
|
+
return GeoLoc.new if self.private_ip_address?(ip)
|
37
|
+
url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
|
38
|
+
response = self.call_geocoder_service(url)
|
39
|
+
response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
|
40
|
+
rescue
|
41
|
+
logger.error "Caught an error during HostIp geocoding call: "+$!
|
42
|
+
return GeoLoc.new
|
43
|
+
end
|
44
|
+
|
45
|
+
# Converts the body to YAML since its in the form of:
|
46
|
+
#
|
47
|
+
# Country: UNITED STATES (US)
|
48
|
+
# City: Sugar Grove, IL
|
49
|
+
# Latitude: 41.7696
|
50
|
+
# Longitude: -88.4588
|
51
|
+
#
|
52
|
+
# then instantiates a GeoLoc instance to populate with location data.
|
53
|
+
def self.parse_body(body) # :nodoc:
|
54
|
+
yaml = YAML.load(body)
|
55
|
+
res = GeoLoc.new
|
56
|
+
res.provider = 'hostip'
|
57
|
+
res.city, res.state = yaml['City'].split(', ')
|
58
|
+
country, res.country_code = yaml['Country'].split(' (')
|
59
|
+
res.lat = yaml['Latitude']
|
60
|
+
res.lng = yaml['Longitude']
|
61
|
+
res.country_code.chop!
|
62
|
+
res.success = !(res.city =~ /\(.+\)/)
|
63
|
+
res
|
64
|
+
end
|
65
|
+
|
66
|
+
# Checks whether the IP address belongs to a private address range.
|
67
|
+
#
|
68
|
+
# This function is used to reduce the number of useless queries made to
|
69
|
+
# the geocoding service. Such queries can occur frequently during
|
70
|
+
# integration tests.
|
71
|
+
def self.private_ip_address?(ip)
|
72
|
+
return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
# -------------------------------------------------------------------------------------------
|
4
|
+
# The Multi Geocoder
|
5
|
+
# -------------------------------------------------------------------------------------------
|
6
|
+
|
7
|
+
# Provides methods to geocode with a variety of geocoding service providers, plus failover
|
8
|
+
# among providers in the order you configure. When 2nd parameter is set 'true', perform
|
9
|
+
# ip location lookup with 'address' as the ip address.
|
10
|
+
#
|
11
|
+
# Goal:
|
12
|
+
# - homogenize the results of multiple geocoders
|
13
|
+
#
|
14
|
+
# Limitations:
|
15
|
+
# - currently only provides the first result. Sometimes geocoders will return multiple results.
|
16
|
+
# - currently discards the "accuracy" component of the geocoding calls
|
17
|
+
class MultiGeocoder < Geocoder
|
18
|
+
|
19
|
+
private
|
20
|
+
# This method will call one or more geocoders in the order specified in the
|
21
|
+
# configuration until one of the geocoders work.
|
22
|
+
#
|
23
|
+
# The failover approach is crucial for production-grade apps, but is rarely used.
|
24
|
+
# 98% of your geocoding calls will be successful with the first call
|
25
|
+
def self.do_geocode(address, options = {})
|
26
|
+
geocode_ip = /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.match(address)
|
27
|
+
provider_order = geocode_ip ? Geokit::Geocoders::ip_provider_order : Geokit::Geocoders::provider_order
|
28
|
+
|
29
|
+
provider_order.each do |provider|
|
30
|
+
begin
|
31
|
+
klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
|
32
|
+
res = klass.send :geocode, address, options
|
33
|
+
return res if res.success?
|
34
|
+
rescue
|
35
|
+
logger.error("Something has gone very wrong during geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. Address: #{address}. Provider: #{provider}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
# If we get here, we failed completely.
|
39
|
+
GeoLoc.new
|
40
|
+
end
|
41
|
+
|
42
|
+
# This method will call one or more geocoders in the order specified in the
|
43
|
+
# configuration until one of the geocoders work, only this time it's going
|
44
|
+
# to try to reverse geocode a geographical point.
|
45
|
+
def self.do_reverse_geocode(latlng)
|
46
|
+
Geokit::Geocoders::provider_order.each do |provider|
|
47
|
+
begin
|
48
|
+
klass = Geokit::Geocoders.const_get "#{Geokit::Inflector::camelize(provider.to_s)}Geocoder"
|
49
|
+
res = klass.send :reverse_geocode, latlng
|
50
|
+
return res if res.success?
|
51
|
+
rescue
|
52
|
+
logger.error("Something has gone very wrong during reverse geocoding, OR you have configured an invalid class name in Geokit::Geocoders::provider_order. LatLng: #{latlng}. Provider: #{provider}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
# If we get here, we failed completely.
|
56
|
+
GeoLoc.new
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|