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.
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