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,202 @@
1
+ module Geokit
2
+ module Geocoders
3
+ class GoogleGeocoder3 < 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
+ submit_url = submit_url("/maps/api/geocode/json?sensor=false&latlng=#{Geokit::Inflector::url_escape(latlng.ll)}")
10
+ res = self.call_geocoder_service(submit_url)
11
+ return GeoLoc.new unless (res.is_a?(Net::HTTPSuccess) || res.is_a?(Net::HTTPOK))
12
+ json = res.body
13
+ logger.debug "Google reverse-geocoding. LL: #{latlng}. Result: #{CGI.escape(json)}"
14
+ return self.json2GeoLoc(json)
15
+ end
16
+
17
+ # Template method which does the geocode lookup.
18
+ #
19
+ # Supports viewport/country code biasing
20
+ #
21
+ # ==== OPTIONS
22
+ # * :bias - This option makes the Google Geocoder return results biased to a particular
23
+ # country or viewport. Country code biasing is achieved by passing the ccTLD
24
+ # ('uk' for .co.uk, for example) as a :bias value. For a list of ccTLD's,
25
+ # look here: http://en.wikipedia.org/wiki/CcTLD. By default, the geocoder
26
+ # will be biased to results within the US (ccTLD .com).
27
+ #
28
+ # If you'd like the Google Geocoder to prefer results within a given viewport,
29
+ # you can pass a Geokit::Bounds object as the :bias value.
30
+ #
31
+ # ==== EXAMPLES
32
+ # # By default, the geocoder will return Syracuse, NY
33
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse').country_code # => 'US'
34
+ # # With country code biasing, it returns Syracuse in Sicily, Italy
35
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Syracuse', :bias => :it).country_code # => 'IT'
36
+ #
37
+ # # By default, the geocoder will return Winnetka, IL
38
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka').state # => 'IL'
39
+ # # When biased to an bounding box around California, it will now return the Winnetka neighbourhood, CA
40
+ # bounds = Geokit::Bounds.normalize([34.074081, -118.694401], [34.321129, -118.399487])
41
+ # Geokit::Geocoders::GoogleGeocoder.geocode('Winnetka', :bias => bounds).state # => 'CA'
42
+ def self.do_geocode(address, options = {})
43
+ bias_str = options[:bias] ? construct_bias_string_from_options(options[:bias]) : ''
44
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
45
+ submit_url = submit_url("/maps/api/geocode/json?sensor=false&address=#{Geokit::Inflector::url_escape(address_str)}#{bias_str}")
46
+
47
+ res = self.call_geocoder_service(submit_url)
48
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
49
+
50
+ json = res.body
51
+ logger.debug "Google geocoding. Address: #{address}. Result: #{CGI.escape(json)}"
52
+
53
+ return self.json2GeoLoc(json, address)
54
+ end
55
+
56
+ # This code comes from Googles Examples
57
+ # http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/urlsigner.rb
58
+ def self.sign_gmap_bus_api_url(urlToSign, google_cryptographic_key)
59
+ require 'base64'
60
+ require 'openssl'
61
+ # Decode the private key
62
+ rawKey = Base64.decode64(google_cryptographic_key.tr('-_','+/'))
63
+ # create a signature using the private key and the URL
64
+ rawSignature = OpenSSL::HMAC.digest('sha1', rawKey, urlToSign)
65
+ # encode the signature into base64 for url use form.
66
+ return Base64.encode64(rawSignature).tr('+/','-_').gsub(/\n/, '')
67
+ end
68
+
69
+
70
+ def self.submit_url(query_string)
71
+ if !Geokit::Geocoders::google_client_id.nil? and !Geokit::Geocoders::google_cryptographic_key.nil?
72
+ urlToSign = query_string + "&client=#{Geokit::Geocoders::google_client_id}" + "#{(!Geokit::Geocoders::google_channel.nil? ? ("&channel="+ Geokit::Geocoders::google_channel) : "")}"
73
+ signature = sign_gmap_bus_api_url(urlToSign, Geokit::Geocoders::google_cryptographic_key)
74
+ "http://maps.googleapis.com" + urlToSign + "&signature=#{signature}"
75
+ else
76
+ "http://maps.google.com" + query_string
77
+ end
78
+ end
79
+
80
+
81
+ def self.construct_bias_string_from_options(bias)
82
+ case bias
83
+ when String, Symbol
84
+ # country code biasing
85
+ "&region=#{bias.to_s.downcase}"
86
+ when Bounds
87
+ # viewport biasing
88
+ url_escaped_string = Geokit::Inflector::url_escape("#{bias.sw.to_s}|#{bias.ne.to_s}")
89
+ "&bounds=#{url_escaped_string}"
90
+ end
91
+ end
92
+
93
+ def self.json2GeoLoc(json, address="")
94
+ results = MultiJson.load(json)
95
+
96
+ case results['status']
97
+ when 'OVER_QUERY_LIMIT' then raise Geokit::TooManyQueriesError
98
+ when 'ZERO_RESULTS' then return GeoLoc.new
99
+ end
100
+ # this should probably be smarter.
101
+ if results['status'] != 'OK'
102
+ raise Geokit::Geocoders::GeocodeError
103
+ end
104
+
105
+ unsorted = results['results'].map do |addr|
106
+ single_json_to_geoloc(addr)
107
+ end
108
+
109
+ all = unsorted.sort_by(&:accuracy).reverse
110
+ encoded = all.first
111
+ encoded.all = all
112
+ encoded
113
+ end
114
+
115
+
116
+ # location_type stores additional data about the specified location.
117
+ # The following values are currently supported:
118
+ # "ROOFTOP" indicates that the returned result is a precise geocode
119
+ # for which we have location information accurate down to street
120
+ # address precision.
121
+ # "RANGE_INTERPOLATED" indicates that the returned result reflects an
122
+ # approximation (usually on a road) interpolated between two precise
123
+ # points (such as intersections). Interpolated results are generally
124
+ # returned when rooftop geocodes are unavailable for a street address.
125
+ # "GEOMETRIC_CENTER" indicates that the returned result is the
126
+ # geometric center of a result such as a polyline (for example, a
127
+ # street) or polygon (region).
128
+ # "APPROXIMATE" indicates that the returned result is approximate
129
+
130
+ # these do not map well. Perhaps we should guess better based on size
131
+ # of bounding box where it exists? Does it really matter?
132
+ ACCURACY = {
133
+ "ROOFTOP" => 9,
134
+ "RANGE_INTERPOLATED" => 8,
135
+ "GEOMETRIC_CENTER" => 5,
136
+ "APPROXIMATE" => 4
137
+ }
138
+
139
+ def self.single_json_to_geoloc(addr)
140
+ res = GeoLoc.new
141
+ res.provider = 'google3'
142
+ res.success = true
143
+ res.full_address = addr['formatted_address']
144
+
145
+ addr['address_components'].each do |comp|
146
+ case
147
+ when comp['types'].include?("subpremise")
148
+ res.sub_premise = comp['short_name']
149
+ when comp['types'].include?("street_number")
150
+ res.street_number = comp['short_name']
151
+ when comp['types'].include?("route")
152
+ res.street_name = comp['long_name']
153
+ when comp['types'].include?("locality")
154
+ res.city = comp['long_name']
155
+ when comp['types'].include?("administrative_area_level_1")
156
+ res.state = comp['short_name']
157
+ res.province = comp['short_name']
158
+ when comp['types'].include?("postal_code")
159
+ res.zip = comp['long_name']
160
+ when comp['types'].include?("country")
161
+ res.country_code = comp['short_name']
162
+ res.country = comp['long_name']
163
+ when comp['types'].include?("administrative_area_level_2")
164
+ res.district = comp['long_name']
165
+ when comp['types'].include?('neighborhood')
166
+ res.neighborhood = comp['short_name']
167
+ end
168
+ end
169
+ if res.street_name
170
+ res.street_address=[res.street_number,res.street_name].join(' ').strip
171
+ end
172
+ res.accuracy = ACCURACY[addr['geometry']['location_type']]
173
+ res.precision=%w{unknown country state state city zip zip+4 street address building}[res.accuracy]
174
+ # try a few overrides where we can
175
+ if res.sub_premise
176
+ res.accuracy = 9
177
+ res.precision = 'building'
178
+ end
179
+ if res.street_name && res.precision=='city'
180
+ res.precision = 'street'
181
+ res.accuracy = 7
182
+ end
183
+
184
+ res.lat=addr['geometry']['location']['lat'].to_f
185
+ res.lng=addr['geometry']['location']['lng'].to_f
186
+
187
+ ne=Geokit::LatLng.new(
188
+ addr['geometry']['viewport']['northeast']['lat'].to_f,
189
+ addr['geometry']['viewport']['northeast']['lng'].to_f
190
+ )
191
+ sw=Geokit::LatLng.new(
192
+ addr['geometry']['viewport']['southwest']['lat'].to_f,
193
+ addr['geometry']['viewport']['southwest']['lng'].to_f
194
+ )
195
+ res.suggested_bounds = Geokit::Bounds.new(sw,ne)
196
+
197
+ res
198
+ end
199
+ end
200
+ Google3Geocoder = GoogleGeocoder3
201
+ end
202
+ end
@@ -0,0 +1,103 @@
1
+ module Geokit
2
+ module Geocoders
3
+ # Provides geocoding based upon an IP address. The underlying web service is a hostip.info
4
+ # which sources their data through a combination of publicly available information as well
5
+ # as community contributions.
6
+ class IpGeocoder < Geocoder
7
+
8
+ # A number of non-routable IP ranges.
9
+ #
10
+ # --
11
+ # Sources for these:
12
+ # RFC 3330: Special-Use IPv4 Addresses
13
+ # The bogon list: http://www.cymru.com/Documents/bogon-list.html
14
+
15
+ NON_ROUTABLE_IP_RANGES = [
16
+ IPAddr.new('0.0.0.0/8'), # "This" Network
17
+ IPAddr.new('10.0.0.0/8'), # Private-Use Networks
18
+ IPAddr.new('14.0.0.0/8'), # Public-Data Networks
19
+ IPAddr.new('127.0.0.0/8'), # Loopback
20
+ IPAddr.new('169.254.0.0/16'), # Link local
21
+ IPAddr.new('172.16.0.0/12'), # Private-Use Networks
22
+ IPAddr.new('192.0.2.0/24'), # Test-Net
23
+ IPAddr.new('192.168.0.0/16'), # Private-Use Networks
24
+ IPAddr.new('198.18.0.0/15'), # Network Interconnect Device Benchmark Testing
25
+ IPAddr.new('224.0.0.0/4'), # Multicast
26
+ IPAddr.new('240.0.0.0/4') # Reserved for future use
27
+ ].freeze
28
+
29
+ private
30
+
31
+ # Given an IP address, returns a GeoLoc instance which contains latitude,
32
+ # longitude, city, and country code. Sets the success attribute to false if the ip
33
+ # parameter does not match an ip address.
34
+ def self.do_geocode(ip, options = {})
35
+ return GeoLoc.new unless /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(ip)
36
+ return GeoLoc.new if self.private_ip_address?(ip)
37
+ url = "http://api.hostip.info/get_html.php?ip=#{ip}&position=true"
38
+ response = self.call_geocoder_service(url)
39
+ ensure_utf8_encoding(response)
40
+ response.is_a?(Net::HTTPSuccess) ? parse_body(response.body) : GeoLoc.new
41
+ rescue
42
+ logger.error "Caught an error during HostIp geocoding call: " + $!.to_s
43
+ return GeoLoc.new
44
+ end
45
+
46
+ # Converts the body to YAML since its in the form of:
47
+ #
48
+ # Country: UNITED STATES (US)
49
+ # City: Sugar Grove, IL
50
+ # Latitude: 41.7696
51
+ # Longitude: -88.4588
52
+ #
53
+ # then instantiates a GeoLoc instance to populate with location data.
54
+ def self.parse_body(body) # :nodoc:
55
+ body = body.encode('UTF-8') if body.respond_to? :encode
56
+ yaml = YAML.load(body)
57
+ res = GeoLoc.new
58
+ res.provider = 'hostip'
59
+ res.city, res.state = yaml['City'].split(', ')
60
+ res.country, res.country_code = yaml['Country'].split(' (')
61
+ res.lat = yaml['Latitude']
62
+ res.lng = yaml['Longitude']
63
+ res.country_code.chop!
64
+ res.success = !(res.city =~ /\(.+\)/)
65
+ res
66
+ end
67
+
68
+ # Forces UTF-8 encoding on the body
69
+ # Rails expects string input to be UTF-8
70
+ # hostip.info specifies the charset encoding in the headers
71
+ # thus extract encoding from headers and tell Rails about it by forcing it
72
+ def self.ensure_utf8_encoding(response)
73
+ if (enc_string = extract_charset(response))
74
+ if defined?(Encoding) && Encoding.aliases.values.include?(enc_string.upcase)
75
+ response.body.force_encoding(enc_string.upcase) if response.body.respond_to?(:force_encoding)
76
+ response.body.encode("UTF-8")
77
+ else
78
+ require 'iconv'
79
+ response.body.replace Iconv.conv("UTF8", "iso88591", response.body)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Extracts charset out of the response headers
85
+ def self.extract_charset(response)
86
+ if (content_type = response['content-type'])
87
+ capture = content_type.match(/charset=(.+)/)
88
+ capture && capture[1]
89
+ end
90
+ end
91
+
92
+ # Checks whether the IP address belongs to a private address range.
93
+ #
94
+ # This function is used to reduce the number of useless queries made to
95
+ # the geocoding service. Such queries can occur frequently during
96
+ # integration tests.
97
+ def self.private_ip_address?(ip)
98
+ return NON_ROUTABLE_IP_RANGES.any? { |range| range.include?(ip) }
99
+ end
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,119 @@
1
+ module Geokit
2
+ module Geocoders
3
+ # Open Street Map geocoder implementation.
4
+ class OSMGeocoder < Geocoder
5
+
6
+ private
7
+
8
+ # Template method which does the geocode lookup.
9
+ def self.do_geocode(address, options = {})
10
+ options_str = generate_bool_param_for_option(:polygon, options)
11
+ options_str << generate_param_for_option(:json_callback, options)
12
+ options_str << generate_param_for_option(:countrycodes, options)
13
+ options_str << generate_param_for_option(:viewbox, options)
14
+
15
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
16
+
17
+ #url="http://where.yahooapis.com/geocode?flags=J&appid=#{Geokit::Geocoders::yahoo}&q=#{Geokit::Inflector::url_escape(address_str)}"
18
+ url="http://nominatim.openstreetmap.org/search?format=json#{options_str}&addressdetails=1&q=#{Geokit::Inflector::url_escape(address_str)}"
19
+ res = self.call_geocoder_service(url)
20
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
21
+ json = res.body
22
+ logger.debug "OSM geocoding. Address: #{address}. Result: #{json}"
23
+ return self.json2GeoLoc(json, address)
24
+ end
25
+
26
+ def self.do_reverse_geocode(latlng, options = {})
27
+ latlng = LatLng.normalize(latlng)
28
+ options_str = generate_param_for(:lat, latlng.lat)
29
+ options_str << generate_param_for(:lon, latlng.lng)
30
+ options_str << generate_param_for_option(:zoom, options)
31
+ options_str << generate_param_for_option(:osm_type, options)
32
+ options_str << generate_param_for_option(:osm_id, options)
33
+ options_str << generate_param_for_option(:json_callback, options)
34
+ url = "http://nominatim.openstreetmap.org/reverse?format=json&addressdetails=1#{options_str}"
35
+ res = self.call_geocoder_service(url)
36
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
37
+ json = res.body
38
+ logger.debug "OSM reverse geocoding: Lat: #{latlng.lat}, Lng: #{latlng.lng}. Result: #{json}"
39
+ return self.json2GeoLoc(json, latlng)
40
+ end
41
+
42
+ def self.generate_param_for(param, value)
43
+ "&#{param}=#{Geokit::Inflector::url_escape(value.to_s)}"
44
+ end
45
+
46
+ def self.generate_param_for_option(param, options)
47
+ options[param] ? "&#{param}=#{Geokit::Inflector::url_escape(options[param])}" : ''
48
+ end
49
+
50
+ def self.generate_bool_param_for_option(param, options)
51
+ options[param] ? "&#{param}=1" : "&#{param}=0"
52
+ end
53
+
54
+ def self.json2GeoLoc(json, obj)
55
+ results = MultiJson.load(json)
56
+ if results.is_a?(Hash)
57
+ return GeoLoc.new if results['error']
58
+ results = [results]
59
+ end
60
+ unless results.empty?
61
+ geoloc = nil
62
+ results.each do |result|
63
+ extract_geoloc = extract_geoloc(result)
64
+ if geoloc.nil?
65
+ geoloc = extract_geoloc
66
+ else
67
+ geoloc.all.push(extract_geoloc)
68
+ end
69
+ end
70
+ return geoloc
71
+ else
72
+ logger.info "OSM was unable to geocode #{obj}"
73
+ return GeoLoc.new
74
+ end
75
+ end
76
+
77
+ def self.extract_geoloc(result_json)
78
+ geoloc = GeoLoc.new
79
+
80
+ # basic
81
+ geoloc.lat = result_json['lat']
82
+ geoloc.lng = result_json['lon']
83
+
84
+ geoloc.provider = 'osm'
85
+ geoloc.precision = result_json['class']
86
+ geoloc.accuracy = result_json['type']
87
+
88
+ # Todo accuracy does not work as Yahoo and Google maps on OSM
89
+ #geoloc.accuracy = %w{unknown amenity building highway historic landuse leisure natural place railway shop tourism waterway man_made}.index(geoloc.precision)
90
+ #geoloc.full_address = result_json['display_name']
91
+ if result_json['address']
92
+ address_data = result_json['address']
93
+
94
+ geoloc.country = address_data['country']
95
+ geoloc.country_code = address_data['country_code'].upcase if address_data['country_code']
96
+ geoloc.state = address_data['state']
97
+ geoloc.city = address_data['city']
98
+ geoloc.city = address_data['county'] if geoloc.city.nil? && address_data['county']
99
+ geoloc.zip = address_data['postcode']
100
+ geoloc.district = address_data['city_district']
101
+ geoloc.district = address_data['state_district'] if geoloc.district.nil? && address_data['state_district']
102
+ geoloc.street_address = "#{address_data['road']} #{address_data['house_number']}".strip if address_data['road']
103
+ geoloc.street_name = address_data['road']
104
+ geoloc.street_number = address_data['house_number']
105
+ end
106
+
107
+ if result_json['boundingbox']
108
+ geoloc.suggested_bounds = Bounds.normalize(
109
+ [result_json['boundingbox'][0], result_json['boundingbox'][1]],
110
+ [result_json['boundingbox'][2], result_json['boundingbox'][3]])
111
+ end
112
+
113
+ geoloc.success = true
114
+
115
+ return geoloc
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,50 @@
1
+ # Geocoder Us geocoder implementation. Requires the Geokit::Geocoders::GEOCODER_US variable to
2
+ # contain true or false based upon whether authentication is to occur. Conforms to the
3
+ # interface set by the Geocoder class.
4
+ module Geokit
5
+ module Geocoders
6
+ class UsGeocoder < Geocoder
7
+
8
+ private
9
+ def self.do_geocode(address, options = {})
10
+ address_str = address.is_a?(GeoLoc) ? address.to_geocodeable_s : address
11
+
12
+ query = (address_str =~ /^\d{5}(?:-\d{4})?$/ ? "zip" : "address") + "=#{Geokit::Inflector::url_escape(address_str)}"
13
+ url = if GeoKit::Geocoders::geocoder_us
14
+ "http://#{GeoKit::Geocoders::geocoder_us}@geocoder.us/member/service/csv/geocode"
15
+ else
16
+ "http://geocoder.us/service/csv/geocode"
17
+ end
18
+
19
+ url = "#{url}?#{query}"
20
+ res = self.call_geocoder_service(url)
21
+
22
+ return GeoLoc.new if !res.is_a?(Net::HTTPSuccess)
23
+ data = res.body
24
+ logger.debug "Geocoder.us geocoding. Address: #{address}. Result: #{data}"
25
+ array = data.chomp.split(',')
26
+
27
+ if array.length == 5
28
+ res=GeoLoc.new
29
+ res.lat,res.lng,res.city,res.state,res.zip=array
30
+ res.country_code='US'
31
+ res.success=true
32
+ return res
33
+ elsif array.length == 6
34
+ res=GeoLoc.new
35
+ res.lat,res.lng,res.street_address,res.city,res.state,res.zip=array
36
+ res.country_code='US'
37
+ res.success=true
38
+ return res
39
+ else
40
+ logger.info "geocoder.us was unable to geocode address: "+address
41
+ return GeoLoc.new
42
+ end
43
+ rescue
44
+ logger.error "Caught an error during geocoder.us geocoding call: "+$!
45
+ return GeoLoc.new
46
+
47
+ end
48
+ end
49
+ end
50
+ end