geokit 1.6.5 → 1.6.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +9 -0
  3. data/.travis.yml +13 -0
  4. data/CHANGELOG.md +92 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +25 -0
  7. data/Manifest.txt +21 -0
  8. data/README.markdown +179 -176
  9. data/Rakefile +8 -1
  10. data/ext/mkrf_conf.rb +15 -0
  11. data/geokit.gemspec +33 -0
  12. data/lib/geokit/geocoders.rb +7 -774
  13. data/lib/geokit/inflectors.rb +38 -0
  14. data/lib/geokit/mappable.rb +61 -3
  15. data/lib/geokit/multi_geocoder.rb +61 -0
  16. data/lib/geokit/services/ca_geocoder.rb +55 -0
  17. data/lib/geokit/services/fcc.rb +57 -0
  18. data/lib/geokit/services/geo_plugin.rb +31 -0
  19. data/lib/geokit/services/geonames.rb +53 -0
  20. data/lib/geokit/services/google.rb +158 -0
  21. data/lib/geokit/services/google3.rb +202 -0
  22. data/lib/geokit/services/ip.rb +103 -0
  23. data/lib/geokit/services/openstreetmap.rb +119 -0
  24. data/lib/geokit/services/us_geocoder.rb +50 -0
  25. data/lib/geokit/services/yahoo.rb +75 -0
  26. data/lib/geokit/version.rb +3 -0
  27. data/test/helper.rb +92 -0
  28. data/test/test_base_geocoder.rb +1 -15
  29. data/test/test_bounds.rb +1 -2
  30. data/test/test_ca_geocoder.rb +1 -1
  31. data/test/test_geoloc.rb +35 -5
  32. data/test/test_geoplugin_geocoder.rb +1 -2
  33. data/test/test_google_geocoder.rb +39 -2
  34. data/test/test_google_geocoder3.rb +55 -3
  35. data/test/test_google_reverse_geocoder.rb +1 -1
  36. data/test/test_inflector.rb +5 -3
  37. data/test/test_ipgeocoder.rb +25 -1
  38. data/test/test_latlng.rb +1 -3
  39. data/test/test_multi_geocoder.rb +1 -1
  40. data/test/test_multi_ip_geocoder.rb +1 -1
  41. data/test/test_openstreetmap_geocoder.rb +161 -0
  42. data/test/test_polygon_contains.rb +101 -0
  43. data/test/test_us_geocoder.rb +1 -1
  44. data/test/test_yahoo_geocoder.rb +18 -1
  45. 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
@@ -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 Errno::EDOM
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