geocoder 1.1.6 → 1.1.7

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of geocoder might be problematic. Click here for more details.

Files changed (48) hide show
  1. data/CHANGELOG.md +15 -1
  2. data/README.md +82 -2
  3. data/examples/autoexpire_cache_dalli.rb +62 -0
  4. data/examples/{autoexpire_cache.rb → autoexpire_cache_redis.rb} +4 -4
  5. data/lib/generators/geocoder/config/templates/initializer.rb +1 -1
  6. data/lib/geocoder.rb +1 -1
  7. data/lib/geocoder/cache.rb +4 -0
  8. data/lib/geocoder/calculations.rb +33 -1
  9. data/lib/geocoder/lookup.rb +2 -0
  10. data/lib/geocoder/lookups/base.rb +10 -10
  11. data/lib/geocoder/lookups/esri.rb +52 -0
  12. data/lib/geocoder/lookups/mapquest.rb +1 -1
  13. data/lib/geocoder/lookups/maxmind.rb +19 -3
  14. data/lib/geocoder/lookups/ovi.rb +52 -0
  15. data/lib/geocoder/lookups/test.rb +6 -0
  16. data/lib/geocoder/lookups/yahoo.rb +4 -2
  17. data/lib/geocoder/query.rb +4 -4
  18. data/lib/geocoder/request.rb +1 -1
  19. data/lib/geocoder/results/esri.rb +51 -0
  20. data/lib/geocoder/results/google.rb +28 -0
  21. data/lib/geocoder/results/mapquest.rb +1 -1
  22. data/lib/geocoder/results/maxmind.rb +4 -5
  23. data/lib/geocoder/results/ovi.rb +62 -0
  24. data/lib/geocoder/results/test.rb +2 -1
  25. data/lib/geocoder/results/yandex.rb +12 -2
  26. data/lib/geocoder/stores/active_record.rb +38 -16
  27. data/lib/geocoder/stores/mongo_base.rb +2 -1
  28. data/lib/geocoder/version.rb +1 -1
  29. data/test/calculations_test.rb +6 -0
  30. data/test/fixtures/esri_madison_square_garden +59 -0
  31. data/test/fixtures/esri_no_results +8 -0
  32. data/test/fixtures/esri_reverse +21 -0
  33. data/test/fixtures/maxmind_24_24_24_21 +1 -1
  34. data/test/fixtures/maxmind_24_24_24_22 +1 -1
  35. data/test/fixtures/maxmind_24_24_24_23 +1 -1
  36. data/test/fixtures/maxmind_24_24_24_24 +1 -1
  37. data/test/fixtures/ovi_madison_square_garden +72 -0
  38. data/test/fixtures/ovi_no_results +8 -0
  39. data/test/fixtures/yandex_no_city_and_town +112 -0
  40. data/test/lookup_test.rb +1 -0
  41. data/test/near_test.rb +32 -0
  42. data/test/query_test.rb +5 -0
  43. data/test/request_test.rb +29 -0
  44. data/test/result_test.rb +9 -0
  45. data/test/services_test.rb +59 -5
  46. data/test/test_helper.rb +1 -1
  47. data/test/test_mode_test.rb +32 -23
  48. metadata +15 -3
@@ -15,9 +15,12 @@ module Geocoder::Lookup
15
15
 
16
16
  private # ---------------------------------------------------------------
17
17
 
18
- def service_code
18
+ ##
19
+ # Return the name of the configured service, or raise an exception.
20
+ #
21
+ def configured_service!
19
22
  if s = configuration[:service] and services.keys.include?(s)
20
- services[s]
23
+ return s
21
24
  else
22
25
  raise(
23
26
  Geocoder::ConfigurationError,
@@ -28,6 +31,19 @@ module Geocoder::Lookup
28
31
  end
29
32
  end
30
33
 
34
+ def service_code
35
+ services[configured_service!]
36
+ end
37
+
38
+ def service_response_fields_count
39
+ Geocoder::Result::Maxmind.field_names[configured_service!].size
40
+ end
41
+
42
+ def data_contains_error?(parsed_data)
43
+ # if all fields given then there is an error
44
+ parsed_data.size == service_response_fields_count and !parsed_data.last.nil?
45
+ end
46
+
31
47
  ##
32
48
  # Service names mapped to code used in URL.
33
49
  #
@@ -45,7 +61,7 @@ module Geocoder::Lookup
45
61
  return [reserved_result] if query.loopback_ip_address?
46
62
  doc = fetch_data(query)
47
63
  if doc and doc.is_a?(Array)
48
- if doc.last.nil?
64
+ if !data_contains_error?(doc)
49
65
  return [doc]
50
66
  elsif doc.last == "INVALID_LICENSE_KEY"
51
67
  raise_error(Geocoder::InvalidApiKey) || warn("Invalid MaxMind API key.")
@@ -0,0 +1,52 @@
1
+ require 'geocoder/lookups/base'
2
+ require 'geocoder/results/ovi'
3
+
4
+ module Geocoder::Lookup
5
+ class Ovi < Base
6
+
7
+ def name
8
+ "Ovi"
9
+ end
10
+
11
+ def required_api_key_parts
12
+ []
13
+ end
14
+
15
+ def query_url(query)
16
+ "#{protocol}://lbs.ovi.com/search/6.2/geocode.json?" + url_query_string(query)
17
+ end
18
+
19
+ private # ---------------------------------------------------------------
20
+
21
+ def results(query)
22
+ return [] unless doc = fetch_data(query)
23
+ return [] unless doc['Response'] && doc['Response']['View']
24
+ if r=doc['Response']['View']
25
+ return [] if r.nil? || !r.is_a?(Array) || r.empty?
26
+ return r.first['Result']
27
+ end
28
+ []
29
+ end
30
+
31
+ def query_url_params(query)
32
+ super.merge(
33
+ :searchtext=>query.sanitized_text,
34
+ :gen=>1,
35
+ :app_id=>api_key,
36
+ :app_code=>api_code
37
+ )
38
+ end
39
+
40
+ def api_key
41
+ if a=configuration.api_key
42
+ return a.first if a.is_a?(Array)
43
+ end
44
+ end
45
+
46
+ def api_code
47
+ if a=configuration.api_key
48
+ return a.last if a.is_a?(Array)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -13,8 +13,13 @@ module Geocoder
13
13
  stubs[query_text] = results
14
14
  end
15
15
 
16
+ def self.set_default_stub(results)
17
+ @default_stub = results
18
+ end
19
+
16
20
  def self.read_stub(query_text)
17
21
  stubs.fetch(query_text) {
22
+ return @default_stub unless @default_stub.nil?
18
23
  raise ArgumentError, "unknown stub request #{query_text}"
19
24
  }
20
25
  end
@@ -25,6 +30,7 @@ module Geocoder
25
30
 
26
31
  def self.reset
27
32
  @stubs = {}
33
+ @default_stub = nil
28
34
  end
29
35
 
30
36
  private
@@ -46,13 +46,15 @@ module Geocoder::Lookup
46
46
  # Yahoo returns errors as XML even when JSON format is specified.
47
47
  # Handle that here, without parsing the XML
48
48
  # (which would add unnecessary complexity).
49
+ # Yahoo auth errors can also be cryptic, so add raw error desc
50
+ # to warning message.
49
51
  #
50
52
  def parse_raw_data(raw_data)
51
53
  if raw_data.match /^<\?xml/
52
54
  if raw_data.include?("Rate Limit Exceeded")
53
55
  raise_error(Geocoder::OverQueryLimitError) || warn("Over API query limit.")
54
- elsif raw_data.include?("Please provide valid credentials")
55
- raise_error(Geocoder::InvalidApiKey) || warn("Invalid API key.")
56
+ elsif raw_data =~ /<yahoo:description>(Please provide valid credentials.*)<\/yahoo:description>/i
57
+ raise_error(Geocoder::InvalidApiKey) || warn("Invalid API key. Error response: #{$1}")
56
58
  end
57
59
  else
58
60
  super(raw_data)
@@ -48,7 +48,7 @@ module Geocoder
48
48
  def blank?
49
49
  !params_given? and (
50
50
  (text.is_a?(Array) and text.compact.size < 2) or
51
- text.to_s.match(/^\s*$/)
51
+ text.to_s.match(/\A\s*\z/)
52
52
  )
53
53
  end
54
54
 
@@ -59,14 +59,14 @@ module Geocoder
59
59
  # dot-delimited numbers.
60
60
  #
61
61
  def ip_address?
62
- !!text.to_s.match(/^(::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
62
+ !!text.to_s.match(/\A(::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/)
63
63
  end
64
64
 
65
65
  ##
66
66
  # Is the Query text a loopback IP address?
67
67
  #
68
68
  def loopback_ip_address?
69
- !!(text == "0.0.0.0" or text.to_s.match(/^127/))
69
+ !!(self.ip_address? and (text == "0.0.0.0" or text.to_s.match(/\A127/)))
70
70
  end
71
71
 
72
72
  ##
@@ -75,7 +75,7 @@ module Geocoder
75
75
  def coordinates?
76
76
  text.is_a?(Array) or (
77
77
  text.is_a?(String) and
78
- !!text.to_s.match(/^-?[0-9\.]+, *-?[0-9\.]+$/)
78
+ !!text.to_s.match(/\A-?[0-9\.]+, *-?[0-9\.]+\z/)
79
79
  )
80
80
  end
81
81
 
@@ -8,7 +8,7 @@ module Geocoder
8
8
  if env.has_key?('HTTP_X_REAL_IP')
9
9
  @location = Geocoder.search(env['HTTP_X_REAL_IP']).first
10
10
  elsif env.has_key?('HTTP_X_FORWARDED_FOR')
11
- @location = Geocoder.search(env['HTTP_X_FORWARDED_FOR']).first
11
+ @location = Geocoder.search(env['HTTP_X_FORWARDED_FOR'].split(/\s*,\s*/)[0]).first
12
12
  else
13
13
  @location = Geocoder.search(ip).first
14
14
  end
@@ -0,0 +1,51 @@
1
+ require 'geocoder/results/base'
2
+
3
+ module Geocoder::Result
4
+ class Esri < Base
5
+
6
+ def address
7
+ address = reverse_geocode? ? 'Address' : 'Match_addr'
8
+ attributes[address]
9
+ end
10
+
11
+ def city
12
+ attributes['City']
13
+ end
14
+
15
+ def state_code
16
+ attributes['Region']
17
+ end
18
+
19
+ alias_method :state, :state_code
20
+
21
+ def country
22
+ country = reverse_geocode? ? "CountryCode" : "Country"
23
+ attributes[country]
24
+ end
25
+
26
+ alias_method :country_code, :country
27
+
28
+ def postal_code
29
+ attributes['Postal']
30
+ end
31
+
32
+ def coordinates
33
+ [geometry["y"], geometry["x"]]
34
+ end
35
+
36
+ private
37
+
38
+ def attributes
39
+ reverse_geocode? ? @data['address'] : @data['locations'].first['feature']['attributes']
40
+ end
41
+
42
+ def geometry
43
+ reverse_geocode? ? @data["location"] : @data['locations'].first['feature']["geometry"]
44
+ end
45
+
46
+ def reverse_geocode?
47
+ @data['locations'].nil?
48
+ end
49
+
50
+ end
51
+ end
@@ -11,6 +11,12 @@ module Geocoder::Result
11
11
  formatted_address
12
12
  end
13
13
 
14
+ def neighborhood
15
+ if neighborhood = address_components_of_type(:neighborhood).first
16
+ neighborhood['long_name']
17
+ end
18
+ end
19
+
14
20
  def city
15
21
  fields = [:locality, :sublocality,
16
22
  :administrative_area_level_3,
@@ -35,6 +41,18 @@ module Geocoder::Result
35
41
  end
36
42
  end
37
43
 
44
+ def sub_state
45
+ if state = address_components_of_type(:administrative_area_level_2).first
46
+ state['long_name']
47
+ end
48
+ end
49
+
50
+ def sub_state_code
51
+ if state = address_components_of_type(:administrative_area_level_2).first
52
+ state['short_name']
53
+ end
54
+ end
55
+
38
56
  def country
39
57
  if country = address_components_of_type(:country).first
40
58
  country['long_name']
@@ -59,6 +77,16 @@ module Geocoder::Result
59
77
  end
60
78
  end
61
79
 
80
+ def street_number
81
+ if street_number = address_components_of_type(:street_number).first
82
+ street_number['long_name']
83
+ end
84
+ end
85
+
86
+ def street_address
87
+ [street_number, route].compact.join(' ')
88
+ end
89
+
62
90
  def types
63
91
  @data['types']
64
92
  end
@@ -29,7 +29,7 @@ module Geocoder::Result
29
29
  alias_method :state_code, :state
30
30
 
31
31
  #FIXME: these might not be right, unclear with MQ documentation
32
- alias_method :provinice, :state
32
+ alias_method :province, :state
33
33
  alias_method :province_code, :state
34
34
 
35
35
  def postal_code
@@ -67,13 +67,12 @@ module Geocoder::Result
67
67
 
68
68
  ##
69
69
  # Name of the MaxMind service being used.
70
- # Inferred from format of @data.
71
70
  #
72
71
  def service_name
73
- self.class.field_names.to_a.each do |n,f|
74
- return n if f.size == @data.size
75
- end
76
- nil
72
+ # it would be much better to infer this from the length of the @data
73
+ # array, but MaxMind seems to send inconsistent and wide-ranging response
74
+ # lengths (see https://github.com/alexreisner/geocoder/issues/396)
75
+ Geocoder.config.maxmind[:service]
77
76
  end
78
77
 
79
78
  def field_names
@@ -0,0 +1,62 @@
1
+ require 'geocoder/results/base'
2
+
3
+ module Geocoder::Result
4
+ class Ovi < Base
5
+
6
+ ##
7
+ # A string in the given format.
8
+ #
9
+ def address(format = :full)
10
+ address_data['Label']
11
+ end
12
+
13
+ ##
14
+ # A two-element array: [lat, lon].
15
+ #
16
+ def coordinates
17
+ fail unless d = @data['Location']['DisplayPosition']
18
+ [d['Latitude'].to_f, d['Longitude'].to_f]
19
+ end
20
+
21
+ def state
22
+ address_data['County']
23
+ end
24
+
25
+ def province
26
+ address_data['County']
27
+ end
28
+
29
+ def postal_code
30
+ address_data['PostalCode']
31
+ end
32
+
33
+ def city
34
+ address_data['City']
35
+ end
36
+
37
+ def state_code
38
+ address_data['State']
39
+ end
40
+
41
+ def province_code
42
+ address_data['State']
43
+ end
44
+
45
+ def country
46
+ fail unless d = address_data['AdditionalData']
47
+ if v = d.find{|ad| ad['key']=='CountryName'}
48
+ return v['value']
49
+ end
50
+ end
51
+
52
+ def country_code
53
+ address_data['Country']
54
+ end
55
+
56
+ private # ----------------------------------------------------------------
57
+
58
+ def address_data
59
+ @data['Location']['Address'] || fail
60
+ end
61
+ end
62
+ end
@@ -5,7 +5,8 @@ module Geocoder
5
5
  class Test < Base
6
6
 
7
7
  %w[latitude longitude city state state_code province
8
- province_code postal_code country country_code address].each do |attr|
8
+ province_code postal_code country country_code address
9
+ street_address street_number route].each do |attr|
9
10
  define_method(attr) do
10
11
  @data[attr.to_s] || @data[attr.to_sym]
11
12
  end
@@ -16,8 +16,10 @@ module Geocoder::Result
16
16
  address_details['Locality']['LocalityName']
17
17
  elsif sub_state.empty?
18
18
  address_details['AdministrativeArea']['Locality']['LocalityName']
19
- else
20
- address_details['AdministrativeArea']['SubAdministrativeArea']['Locality']['LocalityName']
19
+ elsif not sub_state_city.empty?
20
+ sub_state_city
21
+ else
22
+ ""
21
23
  end
22
24
  end
23
25
 
@@ -66,5 +68,13 @@ module Geocoder::Result
66
68
  def address_details
67
69
  @data['GeoObject']['metaDataProperty']['GeocoderMetaData']['AddressDetails']['Country']
68
70
  end
71
+
72
+ def sub_state_city
73
+ if sub_state && sub_state["Locality"]
74
+ sub_state['Locality']['LocalityName']
75
+ else
76
+ ""
77
+ end
78
+ end
69
79
  end
70
80
  end
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  require 'geocoder/sql'
2
3
  require 'geocoder/stores/base'
3
4
 
@@ -17,13 +18,15 @@ module Geocoder::Store
17
18
 
18
19
  # scope: geocoded objects
19
20
  scope :geocoded, lambda {
20
- {:conditions => "#{geocoder_options[:latitude]} IS NOT NULL " +
21
- "AND #{geocoder_options[:longitude]} IS NOT NULL"}}
21
+ where("#{geocoder_options[:latitude]} IS NOT NULL " +
22
+ "AND #{geocoder_options[:longitude]} IS NOT NULL")
23
+ }
22
24
 
23
25
  # scope: not-geocoded objects
24
26
  scope :not_geocoded, lambda {
25
- {:conditions => "#{geocoder_options[:latitude]} IS NULL " +
26
- "OR #{geocoder_options[:longitude]} IS NULL"}}
27
+ where("#{geocoder_options[:latitude]} IS NULL " +
28
+ "OR #{geocoder_options[:longitude]} IS NULL")
29
+ }
27
30
 
28
31
  ##
29
32
  # Find all objects within a radius of the given location.
@@ -35,7 +38,9 @@ module Geocoder::Store
35
38
  scope :near, lambda{ |location, *args|
36
39
  latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
37
40
  if Geocoder::Calculations.coordinates_present?(latitude, longitude)
38
- near_scope_options(latitude, longitude, *args)
41
+ options = near_scope_options(latitude, longitude, *args)
42
+ select(options[:select]).where(options[:conditions]).
43
+ order(options[:order])
39
44
  else
40
45
  # If no lat/lon given we don't want any results, but we still
41
46
  # need distance and bearing columns so you can add, for example:
@@ -53,11 +58,11 @@ module Geocoder::Store
53
58
  scope :within_bounding_box, lambda{ |bounds|
54
59
  sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds
55
60
  if sw_lat && sw_lng && ne_lat && ne_lng
56
- {:conditions => Geocoder::Sql.within_bounding_box(
61
+ where(Geocoder::Sql.within_bounding_box(
57
62
  sw_lat, sw_lng, ne_lat, ne_lng,
58
63
  full_column_name(geocoder_options[:latitude]),
59
64
  full_column_name(geocoder_options[:longitude])
60
- )}
65
+ ))
61
66
  else
62
67
  select(select_clause(nil, "NULL", "NULL")).where(false_condition)
63
68
  end
@@ -93,13 +98,22 @@ module Geocoder::Store
93
98
  # between the given point and each found nearby point;
94
99
  # set to false for no bearing calculation. Use
95
100
  # Geocoder.configure[:distances] to configure default calculation method.
96
- # * +:select+ - string with the SELECT SQL fragment (e.g. “id, name”)
97
- # * +:order+ - column(s) for ORDER BY SQL clause; default is distance;
98
- # set to false or nil to omit the ORDER BY clause
99
- # * +:exclude+ - an object to exclude (used by the +nearbys+ method)
101
+ # * +:select+ - string with the SELECT SQL fragment (e.g. “id, name”)
102
+ # * +:select_distance+ - whether to include the distance alias in the
103
+ # SELECT SQL fragment (e.g. <formula> AS distance)
104
+ # * +:select_bearing+ - like +:select_distance+ but for bearing.
105
+ # * +:order+ - column(s) for ORDER BY SQL clause; default is distance;
106
+ # set to false or nil to omit the ORDER BY clause
107
+ # * +:exclude+ - an object to exclude (used by the +nearbys+ method)
100
108
  #
101
109
  def near_scope_options(latitude, longitude, radius = 20, options = {})
110
+ if options[:units]
111
+ options[:units] = options[:units].to_sym
112
+ end
102
113
  options[:units] ||= (geocoder_options[:units] || Geocoder.config.units)
114
+ select_distance = options.fetch(:select_distance, true)
115
+ options[:order] = "" if !select_distance && !options.include?(:order)
116
+ select_bearing = options.fetch(:select_bearing, true)
103
117
  bearing = bearing_sql(latitude, longitude, options)
104
118
  distance = distance_sql(latitude, longitude, options)
105
119
 
@@ -116,7 +130,9 @@ module Geocoder::Store
116
130
  conditions = [bounding_box_conditions + " AND #{distance} <= ?", radius]
117
131
  end
118
132
  {
119
- :select => select_clause(options[:select], distance, bearing),
133
+ :select => select_clause(options[:select],
134
+ select_distance ? distance : nil,
135
+ select_bearing ? bearing : nil),
120
136
  :conditions => add_exclude_condition(conditions, options[:exclude]),
121
137
  :order => options.include?(:order) ? options[:order] : "distance ASC"
122
138
  }
@@ -166,10 +182,17 @@ module Geocoder::Store
166
182
  elsif columns == :geo_only
167
183
  clause = ""
168
184
  else
169
- clause = (columns || full_column_name("*")) + ", "
185
+ clause = (columns || full_column_name("*"))
170
186
  end
171
- clause + "#{distance} AS distance" +
172
- (bearing ? ", #{bearing} AS bearing" : "")
187
+ if distance
188
+ clause += ", " unless clause.empty?
189
+ clause += "#{distance} AS distance"
190
+ end
191
+ if bearing
192
+ clause += ", " unless clause.empty?
193
+ clause += "#{bearing} AS bearing"
194
+ end
195
+ clause
173
196
  end
174
197
 
175
198
  ##
@@ -241,4 +264,3 @@ module Geocoder::Store
241
264
  alias_method :fetch_address, :reverse_geocode
242
265
  end
243
266
  end
244
-