geokit 1.6.5 → 1.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|