geokit 1.6.5 → 1.6.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +9 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +92 -0
- data/Gemfile +4 -0
- data/LICENSE +25 -0
- data/Manifest.txt +21 -0
- data/README.markdown +179 -176
- data/Rakefile +8 -1
- data/ext/mkrf_conf.rb +15 -0
- data/geokit.gemspec +33 -0
- data/lib/geokit/geocoders.rb +7 -774
- data/lib/geokit/inflectors.rb +38 -0
- data/lib/geokit/mappable.rb +61 -3
- data/lib/geokit/multi_geocoder.rb +61 -0
- data/lib/geokit/services/ca_geocoder.rb +55 -0
- data/lib/geokit/services/fcc.rb +57 -0
- data/lib/geokit/services/geo_plugin.rb +31 -0
- data/lib/geokit/services/geonames.rb +53 -0
- data/lib/geokit/services/google.rb +158 -0
- data/lib/geokit/services/google3.rb +202 -0
- data/lib/geokit/services/ip.rb +103 -0
- data/lib/geokit/services/openstreetmap.rb +119 -0
- data/lib/geokit/services/us_geocoder.rb +50 -0
- data/lib/geokit/services/yahoo.rb +75 -0
- data/lib/geokit/version.rb +3 -0
- data/test/helper.rb +92 -0
- data/test/test_base_geocoder.rb +1 -15
- data/test/test_bounds.rb +1 -2
- data/test/test_ca_geocoder.rb +1 -1
- data/test/test_geoloc.rb +35 -5
- data/test/test_geoplugin_geocoder.rb +1 -2
- data/test/test_google_geocoder.rb +39 -2
- data/test/test_google_geocoder3.rb +55 -3
- data/test/test_google_reverse_geocoder.rb +1 -1
- data/test/test_inflector.rb +5 -3
- data/test/test_ipgeocoder.rb +25 -1
- data/test/test_latlng.rb +1 -3
- data/test/test_multi_geocoder.rb +1 -1
- data/test/test_multi_ip_geocoder.rb +1 -1
- data/test/test_openstreetmap_geocoder.rb +161 -0
- data/test/test_polygon_contains.rb +101 -0
- data/test/test_us_geocoder.rb +1 -1
- data/test/test_yahoo_geocoder.rb +18 -1
- metadata +164 -83
@@ -0,0 +1,38 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Inflector
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def titleize(word)
|
8
|
+
humanize(underscore(word)).gsub(/\b([a-z])/u) { $1.capitalize }
|
9
|
+
end
|
10
|
+
|
11
|
+
def underscore(camel_cased_word)
|
12
|
+
camel_cased_word.to_s.gsub(/::/, '/').
|
13
|
+
gsub(/([A-Z]+)([A-Z][a-z])/u,'\1_\2').
|
14
|
+
gsub(/([a-z\d])([A-Z])/u,'\1_\2').
|
15
|
+
tr("-", "_").
|
16
|
+
downcase
|
17
|
+
end
|
18
|
+
|
19
|
+
def humanize(lower_case_and_underscored_word)
|
20
|
+
lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
|
21
|
+
end
|
22
|
+
|
23
|
+
def snake_case(s)
|
24
|
+
return s.downcase if s =~ /^[A-Z]+$/u
|
25
|
+
s.gsub(/([A-Z]+)(?=[A-Z][a-z]?)|\B[A-Z]/u, '_\&') =~ /_*(.*)/
|
26
|
+
return $+.downcase
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def url_escape(s)
|
31
|
+
CGI.escape(s)
|
32
|
+
end
|
33
|
+
|
34
|
+
def camelize(str)
|
35
|
+
str.split('_').map {|w| w.capitalize}.join
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/geokit/mappable.rb
CHANGED
@@ -41,12 +41,18 @@ module Geokit
|
|
41
41
|
formula = options[:formula] || Geokit::default_formula
|
42
42
|
case formula
|
43
43
|
when :sphere
|
44
|
+
error_classes = [Errno::EDOM]
|
45
|
+
|
46
|
+
# Ruby 1.9 raises {Math::DomainError}, but it is not defined in Ruby
|
47
|
+
# 1.8. Backwards-compatibly rescue both errors.
|
48
|
+
error_classes << Math::DomainError if defined?(Math::DomainError)
|
49
|
+
|
44
50
|
begin
|
45
51
|
units_sphere_multiplier(units) *
|
46
52
|
Math.acos( Math.sin(deg2rad(from.lat)) * Math.sin(deg2rad(to.lat)) +
|
47
53
|
Math.cos(deg2rad(from.lat)) * Math.cos(deg2rad(to.lat)) *
|
48
54
|
Math.cos(deg2rad(to.lng) - deg2rad(from.lng)))
|
49
|
-
rescue
|
55
|
+
rescue *error_classes
|
50
56
|
0.0
|
51
57
|
end
|
52
58
|
when :flat
|
@@ -351,7 +357,7 @@ module Geokit
|
|
351
357
|
# 100 Spear St, San Francisco, CA, 94101, US
|
352
358
|
# Street number and street name are extracted from the street address attribute if they don't exist
|
353
359
|
attr_accessor :street_number, :street_name, :street_address, :city, :state, :zip, :country_code, :country
|
354
|
-
attr_accessor :full_address, :all, :district, :province, :sub_premise
|
360
|
+
attr_accessor :full_address, :all, :district, :province, :sub_premise, :neighborhood
|
355
361
|
# Attributes set upon return from geocoding. Success will be true for successful
|
356
362
|
# geocode lookups. The provider will be set to the name of the providing geocoder.
|
357
363
|
# Finally, precision is an indicator of the accuracy of the geocoding.
|
@@ -443,7 +449,13 @@ module Geokit
|
|
443
449
|
end
|
444
450
|
|
445
451
|
def to_yaml_properties
|
446
|
-
(instance_variables - ['@all']).sort
|
452
|
+
(instance_variables - ['@all', :@all]).sort
|
453
|
+
end
|
454
|
+
|
455
|
+
def encode_with(coder)
|
456
|
+
to_yaml_properties.each do |name|
|
457
|
+
coder[name[1..-1].to_s] = instance_variable_get(name.to_s)
|
458
|
+
end
|
447
459
|
end
|
448
460
|
|
449
461
|
# Returns a string representation of the instance.
|
@@ -545,4 +557,50 @@ module Geokit
|
|
545
557
|
end
|
546
558
|
end
|
547
559
|
end
|
560
|
+
|
561
|
+
# A complex polygon made of multiple points. End point must equal start point to close the poly.
|
562
|
+
class Polygon
|
563
|
+
|
564
|
+
attr_accessor :poly_y, :poly_x
|
565
|
+
|
566
|
+
def initialize(points)
|
567
|
+
# Pass in an array of Geokit::LatLng
|
568
|
+
@poly_x = []
|
569
|
+
@poly_y = []
|
570
|
+
|
571
|
+
points.each do |point|
|
572
|
+
@poly_x << point.lng
|
573
|
+
@poly_y << point.lat
|
574
|
+
end
|
575
|
+
|
576
|
+
# A Polygon must be 'closed', the last point equal to the first point
|
577
|
+
if not @poly_x[0] == @poly_x[-1] or not @poly_y[0] == @poly_y[-1]
|
578
|
+
# Append the first point to the array to close the polygon
|
579
|
+
@poly_x << @poly_x[0]
|
580
|
+
@poly_y << @poly_y[0]
|
581
|
+
end
|
582
|
+
|
583
|
+
end
|
584
|
+
|
585
|
+
def contains?(point)
|
586
|
+
j = @poly_x.length - 1
|
587
|
+
oddNodes = false
|
588
|
+
x = point.lng
|
589
|
+
y = point.lat
|
590
|
+
|
591
|
+
for i in (0..j)
|
592
|
+
if (@poly_y[i] < y && @poly_y[j] >= y ||
|
593
|
+
@poly_y[j] < y && @poly_y[i] >= y)
|
594
|
+
if (@poly_x[i] + (y - @poly_y[i]) / (@poly_y[j] - @poly_y[i]) * (@poly_x[j] - @poly_x[i]) < x)
|
595
|
+
oddNodes = !oddNodes
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
j=i
|
600
|
+
end
|
601
|
+
|
602
|
+
oddNodes
|
603
|
+
end # contains?
|
604
|
+
end # class Polygon
|
605
|
+
|
548
606
|
end
|
@@ -0,0 +1,61 @@
|
|
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 => e
|
35
|
+
logger.error("An error has occurred during geocoding: #{e}\nAddress: #{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 => e
|
52
|
+
logger.error("An error has occurred during geocoding: #{e}\nLatlng: #{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
|
61
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
# Geocoder CA geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_CA variable to
|
3
|
+
# contain true or false based upon whether authentication is to occur. Conforms to the
|
4
|
+
# interface set by the Geocoder class.
|
5
|
+
#
|
6
|
+
# Returns a response like:
|
7
|
+
# <?xml version="1.0" encoding="UTF-8" ?>
|
8
|
+
# <geodata>
|
9
|
+
# <latt>49.243086</latt>
|
10
|
+
# <longt>-123.153684</longt>
|
11
|
+
# </geodata>
|
12
|
+
module Geokit
|
13
|
+
module Geocoders
|
14
|
+
class CaGeocoder < Geocoder
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Template method which does the geocode lookup.
|
19
|
+
def self.do_geocode(address, options = {})
|
20
|
+
raise ArgumentError('Geocoder.ca requires a GeoLoc argument') unless address.is_a?(GeoLoc)
|
21
|
+
url = construct_request(address)
|
22
|
+
res = self.call_geocoder_service(url)
|
23
|
+
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
24
|
+
xml = res.body
|
25
|
+
logger.debug "Geocoder.ca geocoding. Address: #{address}. Result: #{xml}"
|
26
|
+
# Parse the document.
|
27
|
+
doc = REXML::Document.new(xml)
|
28
|
+
address.lat = doc.elements['//latt'].text
|
29
|
+
address.lng = doc.elements['//longt'].text
|
30
|
+
address.success = true
|
31
|
+
return address
|
32
|
+
rescue
|
33
|
+
logger.error "Caught an error during Geocoder.ca geocoding call: "+$!
|
34
|
+
return GeoLoc.new
|
35
|
+
end
|
36
|
+
|
37
|
+
# Formats the request in the format acceptable by the CA geocoder.
|
38
|
+
def self.construct_request(location)
|
39
|
+
url = ""
|
40
|
+
url += add_ampersand(url) + "stno=#{location.street_number}" if location.street_address
|
41
|
+
url += add_ampersand(url) + "addresst=#{Geokit::Inflector::url_escape(location.street_name)}" if location.street_address
|
42
|
+
url += add_ampersand(url) + "city=#{Geokit::Inflector::url_escape(location.city)}" if location.city
|
43
|
+
url += add_ampersand(url) + "prov=#{location.state}" if location.state
|
44
|
+
url += add_ampersand(url) + "postal=#{location.zip}" if location.zip
|
45
|
+
url += add_ampersand(url) + "auth=#{Geokit::Geocoders::geocoder_ca}" if Geokit::Geocoders::geocoder_ca
|
46
|
+
url += add_ampersand(url) + "geoit=xml"
|
47
|
+
'http://geocoder.ca/?' + url
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.add_ampersand(url)
|
51
|
+
url && url.length > 0 ? "&" : ""
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
class FCCGeocoder < Geocoder
|
4
|
+
|
5
|
+
private
|
6
|
+
# Template method which does the reverse-geocode lookup.
|
7
|
+
def self.do_reverse_geocode(latlng)
|
8
|
+
latlng=LatLng.normalize(latlng)
|
9
|
+
res = self.call_geocoder_service("http://data.fcc.gov/api/block/find?format=json&latitude=#{Geokit::Inflector::url_escape(latlng.lat.to_s)}&longitude=#{Geokit::Inflector::url_escape(latlng.lng.to_s)}")
|
10
|
+
return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
|
11
|
+
json = res.body
|
12
|
+
logger.debug "FCC reverse-geocoding. LL: #{latlng}. Result: #{json}"
|
13
|
+
return self.json2GeoLoc(json)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Template method which does the geocode lookup.
|
17
|
+
#
|
18
|
+
# ==== EXAMPLES
|
19
|
+
# ll=GeoKit::LatLng.new(40, -85)
|
20
|
+
# Geokit::Geocoders::FCCGeocoder.geocode(ll) #
|
21
|
+
|
22
|
+
# JSON result looks like this
|
23
|
+
# => {"County"=>{"name"=>"Wayne", "FIPS"=>"18177"},
|
24
|
+
# "Block"=>{"FIPS"=>"181770103002004"},
|
25
|
+
# "executionTime"=>"0.099",
|
26
|
+
# "State"=>{"name"=>"Indiana", "code"=>"IN", "FIPS"=>"18"},
|
27
|
+
# "status"=>"OK"}
|
28
|
+
|
29
|
+
def self.json2GeoLoc(json, address="")
|
30
|
+
ret = nil
|
31
|
+
results = MultiJson.load(json)
|
32
|
+
|
33
|
+
if results.has_key?('Err') and results['Err']["msg"] == 'There are no results for this location'
|
34
|
+
return GeoLoc.new
|
35
|
+
end
|
36
|
+
# this should probably be smarter.
|
37
|
+
if !results['status'] == 'OK'
|
38
|
+
raise Geokit::Geocoders::GeocodeError
|
39
|
+
end
|
40
|
+
|
41
|
+
res = GeoLoc.new
|
42
|
+
res.provider = 'fcc'
|
43
|
+
res.success = true
|
44
|
+
res.precision = 'block'
|
45
|
+
res.country_code = 'US'
|
46
|
+
res.district = results['County']['name']
|
47
|
+
res.district_fips = results['County']['FIPS']
|
48
|
+
res.state = results['State']['code']
|
49
|
+
res.state_fips = results['State']['FIPS']
|
50
|
+
res.block_fips = results['Block']['FIPS']
|
51
|
+
|
52
|
+
res
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
# Provides geocoding based upon an IP address. The underlying web service is geoplugin.net
|
4
|
+
class GeoPluginGeocoder < Geocoder
|
5
|
+
private
|
6
|
+
|
7
|
+
def self.do_geocode(ip, options = {})
|
8
|
+
return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
|
9
|
+
response = self.call_geocoder_service("http://www.geoplugin.net/xml.gp?ip=#{ip}")
|
10
|
+
return response.is_a?(Net::HTTPSuccess) ? parse_xml(response.body) : GeoLoc.new
|
11
|
+
rescue
|
12
|
+
logger.error "Caught an error during GeoPluginGeocoder geocoding call: "+$!
|
13
|
+
return GeoLoc.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.parse_xml(xml)
|
17
|
+
xml = REXML::Document.new(xml)
|
18
|
+
geo = GeoLoc.new
|
19
|
+
geo.provider='geoPlugin'
|
20
|
+
geo.city = xml.elements['//geoplugin_city'].text
|
21
|
+
geo.state = xml.elements['//geoplugin_region'].text
|
22
|
+
geo.country_code = xml.elements['//geoplugin_countryCode'].text
|
23
|
+
geo.lat = xml.elements['//geoplugin_latitude'].text.to_f
|
24
|
+
geo.lng = xml.elements['//geoplugin_longitude'].text.to_f
|
25
|
+
geo.success = !!geo.city && !geo.city.empty?
|
26
|
+
return geo
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -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,158 @@
|
|
1
|
+
module Geokit
|
2
|
+
module Geocoders
|
3
|
+
# -------------------------------------------------------------------------------------------
|
4
|
+
# Address geocoders that also provide reverse geocoding
|
5
|
+
# -------------------------------------------------------------------------------------------
|
6
|
+
|
7
|
+
# Google geocoder implementation. Requires the Geokit::Geocoders::GOOGLE variable to
|
8
|
+
# contain a Google API key. Conforms to the interface set by the Geocoder class.
|
9
|
+
class GoogleGeocoder < Geocoder
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# Template method which does the reverse-geocode lookup.
|
14
|
+
def self.do_reverse_geocode(latlng)
|
15
|
+
latlng=LatLng.normalize(latlng)
|
16
|
+
res = self.call_geocoder_service("http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(latlng.ll)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8")
|
17
|
+
# res = Net::HTTP.get_response(URI.parse("http://maps.google.com/maps/geo?ll=#{Geokit::Inflector::url_escape(address_str)}&output=xml&key=#{Geokit::Geocoders::google}&oe=utf-8"))
|
18
|
+
return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
|
19
|
+
xml = self.transcode_to_utf8(res.body)
|
20
|
+
logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{xml}"
|
21
|
+
return self.xml2GeoLoc(xml)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Template method which does the geocode lookup.
|
25
|
+
#
|
26
|
+
# Supports viewport/country code biasing
|
27
|
+
#
|
28
|
+
# ==== OPTIONS
|
29
|
+
# * :bias - This option makes the Google Geocoder return results biased to a particular
|
30
|
+
# country or viewport. Country code biasing is achieved by passing the ccTLD
|
31
|
+
# ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
|
32
|
+
# look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
|
33
|
+
# will be biased to results within the US (ccTLD .com).
|
34
|
+
#
|
35
|
+
# If you'd like the Google Geocoder to prefer results within a given viewport,
|
36
|
+
# you can pass a Geokit::Bounds object as the :bias value.
|
37
|
+
#
|
38
|
+
# ==== EXAMPLES
|
39
|
+
# # By default, the geocoder will return Syracuse, NY
|
40
|
+
# Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
|
41
|
+
# # With country code biasing, it returns Syracuse in Sicily, Italy
|
42
|
+
# Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
|
43
|
+
#
|
44
|
+
# # By default, the geocoder will return Winnetka, IL
|
45
|
+
# Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
|
46
|
+
# # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
|
47
|
+
# bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
|
48
|
+
# Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
|
49
|
+
def self.do_geocode(address, options = {})
|
50
|
+
bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
|
51
|
+
address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
|
52
|
+
res = self.call_geocoder_service("http://maps.google.com/maps/geo?q=#{Geokit::Inflector::url_escape(address_str)}&output=xml#{bias_str}&key=#{Geokit::Geocoders::google}&oe=utf-8")
|
53
|
+
return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
|
54
|
+
xml = self.transcode_to_utf8(res.body)
|
55
|
+
logger.debug "Google geocoding. Address: #{address}. Result: #{xml}"
|
56
|
+
return self.xml2GeoLoc(xml, address)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.construct_bias_string_from_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=#{precise_ll(bias.center)}&spn=#{precise_ll(bias.to_span)}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Precision to 6 decimal places as per:
|
70
|
+
# https://developers.google.com/maps/documentation/staticmaps/?hl=en#Latlons
|
71
|
+
def self.precise_ll(loc)
|
72
|
+
"#{"%.6f" % loc.lat},#{"%.6f" % loc.lng}"
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.xml2GeoLoc(xml, address="")
|
76
|
+
doc=REXML::Document.new(xml)
|
77
|
+
|
78
|
+
if doc.elements['//kml/Response/Status/code'].text == '200'
|
79
|
+
geoloc = nil
|
80
|
+
# Google can return multiple results as //Placemark elements.
|
81
|
+
# iterate through each and extract each placemark as a geoloc
|
82
|
+
doc.each_element('//Placemark') do |e|
|
83
|
+
extracted_geoloc = extract_placemark(e) # g is now an instance of GeoLoc
|
84
|
+
if geoloc.nil?
|
85
|
+
# first time through, geoloc is still nil, so we make it the geoloc we just extracted
|
86
|
+
geoloc = extracted_geoloc
|
87
|
+
else
|
88
|
+
# second (and subsequent) iterations, we push additional
|
89
|
+
# geolocs onto "geoloc.all"
|
90
|
+
geoloc.all.push(extracted_geoloc)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
return geoloc
|
94
|
+
elsif doc.elements['//kml/Response/Status/code'].text == '620'
|
95
|
+
raise Geokit::TooManyQueriesError
|
96
|
+
else
|
97
|
+
logger.info "Google was unable to geocode address: "+address
|
98
|
+
return GeoLoc.new
|
99
|
+
end
|
100
|
+
|
101
|
+
rescue Geokit::TooManyQueriesError
|
102
|
+
# re-raise because of other rescue
|
103
|
+
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."
|
104
|
+
rescue
|
105
|
+
logger.error "Caught an error during Google geocoding call: "+$!
|
106
|
+
return GeoLoc.new
|
107
|
+
end
|
108
|
+
|
109
|
+
# extracts a single geoloc from a //placemark element in the google results xml
|
110
|
+
def self.extract_placemark(doc)
|
111
|
+
res = GeoLoc.new
|
112
|
+
coordinates=doc.elements['.//coordinates'].text.to_s.split(',')
|
113
|
+
|
114
|
+
#basics
|
115
|
+
res.lat=coordinates[1]
|
116
|
+
res.lng=coordinates[0]
|
117
|
+
res.country_code=doc.elements['.//CountryNameCode'].text if doc.elements['.//CountryNameCode']
|
118
|
+
res.provider='google'
|
119
|
+
|
120
|
+
#extended -- false if not not available
|
121
|
+
res.city = doc.elements['.//LocalityName'].text if doc.elements['.//LocalityName']
|
122
|
+
res.state = doc.elements['.//AdministrativeAreaName'].text if doc.elements['.//AdministrativeAreaName']
|
123
|
+
res.province = doc.elements['.//SubAdministrativeAreaName'].text if doc.elements['.//SubAdministrativeAreaName']
|
124
|
+
res.full_address = doc.elements['.//address'].text if doc.elements['.//address'] # google provides it
|
125
|
+
res.zip = doc.elements['.//PostalCodeNumber'].text if doc.elements['.//PostalCodeNumber']
|
126
|
+
res.street_address = doc.elements['.//ThoroughfareName'].text if doc.elements['.//ThoroughfareName']
|
127
|
+
res.country = doc.elements['.//CountryName'].text if doc.elements['.//CountryName']
|
128
|
+
res.district = doc.elements['.//DependentLocalityName'].text if doc.elements['.//DependentLocalityName']
|
129
|
+
# Translate accuracy into Yahoo-style token address, street, zip, zip+4, city, state, country
|
130
|
+
# For Google, 1=low accuracy, 8=high accuracy
|
131
|
+
address_details=doc.elements['.//*[local-name() = "AddressDetails"]']
|
132
|
+
res.accuracy = address_details ? address_details.attributes['Accuracy'].to_i : 0
|
133
|
+
res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
|
134
|
+
|
135
|
+
# google returns a set of suggested boundaries for the geocoded result
|
136
|
+
if suggested_bounds = doc.elements['//LatLonBox']
|
137
|
+
res.suggested_bounds = Bounds.normalize(
|
138
|
+
[suggested_bounds.attributes['south'], suggested_bounds.attributes['west']],
|
139
|
+
[suggested_bounds.attributes['north'], suggested_bounds.attributes['east']])
|
140
|
+
end
|
141
|
+
|
142
|
+
res.success=true
|
143
|
+
|
144
|
+
return res
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.transcode_to_utf8(body)
|
148
|
+
require 'iconv' unless String.method_defined?(:encode)
|
149
|
+
if String.method_defined?(:encode)
|
150
|
+
body.encode!('UTF-8', 'UTF-8', :invalid => :replace)
|
151
|
+
else
|
152
|
+
ic = Iconv.new('UTF-8', 'UTF-8//IGNORE')
|
153
|
+
body = ic.iconv(body)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|