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