geocoder 1.1.3 → 1.1.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 (98) hide show
  1. data/.travis.yml +4 -0
  2. data/{CHANGELOG.rdoc → CHANGELOG.md} +109 -35
  3. data/README.md +645 -0
  4. data/examples/autoexpire_cache.rb +28 -0
  5. data/lib/generators/geocoder/config/templates/initializer.rb +18 -22
  6. data/lib/geocoder/cache.rb +2 -1
  7. data/lib/geocoder/calculations.rb +13 -13
  8. data/lib/geocoder/cli.rb +17 -19
  9. data/lib/geocoder/configuration.rb +68 -42
  10. data/lib/geocoder/configuration_hash.rb +11 -0
  11. data/lib/geocoder/exceptions.rb +3 -0
  12. data/lib/geocoder/lookup.rb +81 -0
  13. data/lib/geocoder/lookups/base.rb +106 -56
  14. data/lib/geocoder/lookups/bing.rb +25 -11
  15. data/lib/geocoder/lookups/freegeoip.rb +11 -7
  16. data/lib/geocoder/lookups/geocoder_ca.rb +17 -9
  17. data/lib/geocoder/lookups/google.rb +29 -8
  18. data/lib/geocoder/lookups/google_premier.rb +20 -11
  19. data/lib/geocoder/lookups/mapquest.rb +29 -19
  20. data/lib/geocoder/lookups/maxmind.rb +72 -0
  21. data/lib/geocoder/lookups/nominatim.rb +21 -13
  22. data/lib/geocoder/lookups/test.rb +12 -6
  23. data/lib/geocoder/lookups/yahoo.rb +63 -13
  24. data/lib/geocoder/lookups/yandex.rb +26 -11
  25. data/lib/geocoder/models/mongoid.rb +1 -2
  26. data/lib/geocoder/query.rb +103 -0
  27. data/lib/geocoder/request.rb +7 -1
  28. data/lib/geocoder/results/base.rb +8 -2
  29. data/lib/geocoder/results/google.rb +6 -0
  30. data/lib/geocoder/results/mapquest.rb +19 -34
  31. data/lib/geocoder/results/maxmind.rb +136 -0
  32. data/lib/geocoder/results/nominatim.rb +14 -3
  33. data/lib/geocoder/results/yahoo.rb +9 -2
  34. data/lib/geocoder/sql.rb +106 -0
  35. data/lib/geocoder/stores/active_record.rb +75 -142
  36. data/lib/geocoder/stores/base.rb +3 -2
  37. data/lib/geocoder/stores/mongo_base.rb +2 -2
  38. data/lib/geocoder/version.rb +1 -1
  39. data/lib/geocoder.rb +11 -89
  40. data/lib/hash_recursive_merge.rb +74 -0
  41. data/lib/oauth_util.rb +112 -0
  42. data/test/active_record_test.rb +15 -0
  43. data/test/cache_test.rb +19 -0
  44. data/test/calculations_test.rb +11 -4
  45. data/test/configuration_test.rb +26 -51
  46. data/test/error_handling_test.rb +15 -11
  47. data/test/fixtures/bing_invalid_key +1 -0
  48. data/test/fixtures/mapquest_madison_square_garden +52 -0
  49. data/test/fixtures/mapquest_no_results +7 -0
  50. data/test/fixtures/maxmind_24_24_24_21 +1 -0
  51. data/test/fixtures/maxmind_24_24_24_22 +1 -0
  52. data/test/fixtures/maxmind_24_24_24_23 +1 -0
  53. data/test/fixtures/maxmind_24_24_24_24 +1 -0
  54. data/test/fixtures/maxmind_74_200_247_59 +1 -0
  55. data/test/fixtures/maxmind_invalid_key +1 -0
  56. data/test/fixtures/maxmind_no_results +1 -0
  57. data/test/fixtures/yahoo_error +1 -0
  58. data/test/fixtures/yahoo_invalid_key +2 -0
  59. data/test/fixtures/yahoo_madison_square_garden +52 -0
  60. data/test/fixtures/yahoo_no_results +10 -0
  61. data/test/fixtures/yahoo_over_limit +2 -0
  62. data/test/https_test.rb +3 -3
  63. data/test/integration/smoke_test.rb +8 -6
  64. data/test/lookup_test.rb +92 -13
  65. data/test/near_test.rb +11 -0
  66. data/test/oauth_util_test.rb +30 -0
  67. data/test/proxy_test.rb +2 -2
  68. data/test/query_test.rb +42 -0
  69. data/test/result_test.rb +4 -3
  70. data/test/services_test.rb +164 -26
  71. data/test/test_helper.rb +93 -112
  72. data/test/test_mode_test.rb +3 -4
  73. metadata +52 -35
  74. data/README.rdoc +0 -552
  75. data/test/fixtures/mapquest_madison_square_garden.json +0 -27
  76. data/test/fixtures/mapquest_no_results.json +0 -1
  77. data/test/fixtures/yahoo_garbage.json +0 -50
  78. data/test/fixtures/yahoo_madison_square_garden.json +0 -46
  79. data/test/fixtures/yahoo_no_results.json +0 -10
  80. data/test/input_handling_test.rb +0 -43
  81. /data/test/fixtures/{bing_madison_square_garden.json → bing_madison_square_garden} +0 -0
  82. /data/test/fixtures/{bing_no_results.json → bing_no_results} +0 -0
  83. /data/test/fixtures/{bing_reverse.json → bing_reverse} +0 -0
  84. /data/test/fixtures/{freegeoip_74_200_247_59.json → freegeoip_74_200_247_59} +0 -0
  85. /data/test/fixtures/{freegeoip_no_results.json → freegeoip_no_results} +0 -0
  86. /data/test/fixtures/{geocoder_ca_madison_square_garden.json → geocoder_ca_madison_square_garden} +0 -0
  87. /data/test/fixtures/{geocoder_ca_no_results.json → geocoder_ca_no_results} +0 -0
  88. /data/test/fixtures/{geocoder_ca_reverse.json → geocoder_ca_reverse} +0 -0
  89. /data/test/fixtures/{google_garbage.json → google_garbage} +0 -0
  90. /data/test/fixtures/{google_madison_square_garden.json → google_madison_square_garden} +0 -0
  91. /data/test/fixtures/{google_no_city_data.json → google_no_city_data} +0 -0
  92. /data/test/fixtures/{google_no_locality.json → google_no_locality} +0 -0
  93. /data/test/fixtures/{google_no_results.json → google_no_results} +0 -0
  94. /data/test/fixtures/{nominatim_madison_square_garden.json → nominatim_madison_square_garden} +0 -0
  95. /data/test/fixtures/{nominatim_no_results.json → nominatim_no_results} +0 -0
  96. /data/test/fixtures/{yandex_invalid_key.json → yandex_invalid_key} +0 -0
  97. /data/test/fixtures/{yandex_kremlin.json → yandex_kremlin} +0 -0
  98. /data/test/fixtures/{yandex_no_results.json → yandex_no_results} +0 -0
@@ -0,0 +1,106 @@
1
+ module Geocoder
2
+ module Sql
3
+ extend self
4
+
5
+ ##
6
+ # Distance calculation for use with a database that supports POWER(),
7
+ # SQRT(), PI(), and trigonometric functions SIN(), COS(), ASIN(),
8
+ # ATAN2(), DEGREES(), and RADIANS().
9
+ #
10
+ # Based on the excellent tutorial at:
11
+ # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
12
+ #
13
+ def full_distance(latitude, longitude, lat_attr, lon_attr, options = {})
14
+ units = options[:units] || Geocoder.config.units
15
+ earth = Geocoder::Calculations.earth_radius(units)
16
+
17
+ "#{earth} * 2 * ASIN(SQRT(" +
18
+ "POWER(SIN((#{latitude.to_f} - #{lat_attr}) * PI() / 180 / 2), 2) + " +
19
+ "COS(#{latitude.to_f} * PI() / 180) * COS(#{lat_attr} * PI() / 180) * " +
20
+ "POWER(SIN((#{longitude.to_f} - #{lon_attr}) * PI() / 180 / 2), 2)" +
21
+ "))"
22
+ end
23
+
24
+ ##
25
+ # Distance calculation for use with a database without trigonometric
26
+ # functions, like SQLite. Approach is to find objects within a square
27
+ # rather than a circle, so results are very approximate (will include
28
+ # objects outside the given radius).
29
+ #
30
+ # Distance and bearing calculations are *extremely inaccurate*. To be
31
+ # clear: this only exists to provide interface consistency. Results
32
+ # are not intended for use in production!
33
+ #
34
+ def approx_distance(latitude, longitude, lat_attr, lon_attr, options = {})
35
+ units = options[:units] || Geocoder.config.units
36
+ dx = Geocoder::Calculations.longitude_degree_distance(30, units)
37
+ dy = Geocoder::Calculations.latitude_degree_distance(units)
38
+
39
+ # sin of 45 degrees = average x or y component of vector
40
+ factor = Math.sin(Math::PI / 4)
41
+
42
+ "(#{dy} * ABS(#{lat_attr} - #{latitude.to_f}) * #{factor}) + " +
43
+ "(#{dx} * ABS(#{lon_attr} - #{longitude.to_f}) * #{factor})"
44
+ end
45
+
46
+ def within_bounding_box(sw_lat, sw_lng, ne_lat, ne_lng, lat_attr, lon_attr)
47
+ spans = "#{lat_attr} BETWEEN #{sw_lat} AND #{ne_lat} AND "
48
+ # handle box that spans 180 longitude
49
+ if sw_lng.to_f > ne_lng.to_f
50
+ spans + "#{lon_attr} BETWEEN #{sw_lng} AND 180 OR " +
51
+ "#{lon_attr} BETWEEN -180 AND #{ne_lng}"
52
+ else
53
+ spans + "#{lon_attr} BETWEEN #{sw_lng} AND #{ne_lng}"
54
+ end
55
+ end
56
+
57
+ ##
58
+ # Fairly accurate bearing calculation. Takes a latitude, longitude,
59
+ # and an options hash which must include a :bearing value
60
+ # (:linear or :spherical).
61
+ #
62
+ # Based on:
63
+ # http://www.beginningspatial.com/calculating_bearing_one_point_another
64
+ #
65
+ def full_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
66
+ case options[:bearing] || Geocoder.config.distances
67
+ when :linear
68
+ "CAST(" +
69
+ "DEGREES(ATAN2( " +
70
+ "RADIANS(#{lon_attr} - #{longitude.to_f}), " +
71
+ "RADIANS(#{lat_attr} - #{latitude.to_f})" +
72
+ ")) + 360 " +
73
+ "AS decimal) % 360"
74
+ when :spherical
75
+ "CAST(" +
76
+ "DEGREES(ATAN2( " +
77
+ "SIN(RADIANS(#{lon_attr} - #{longitude.to_f})) * " +
78
+ "COS(RADIANS(#{lat_attr})), (" +
79
+ "COS(RADIANS(#{latitude.to_f})) * SIN(RADIANS(#{lat_attr}))" +
80
+ ") - (" +
81
+ "SIN(RADIANS(#{latitude.to_f})) * COS(RADIANS(#{lat_attr})) * " +
82
+ "COS(RADIANS(#{lon_attr} - #{longitude.to_f}))" +
83
+ ")" +
84
+ ")) + 360 " +
85
+ "AS decimal) % 360"
86
+ end
87
+ end
88
+
89
+ ##
90
+ # Totally lame bearing calculation. Basically useless except that it
91
+ # returns *something* in databases without trig functions.
92
+ #
93
+ def approx_bearing(latitude, longitude, lat_attr, lon_attr, options = {})
94
+ "CASE " +
95
+ "WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
96
+ "#{lon_attr} >= #{longitude.to_f}) THEN 45.0 " +
97
+ "WHEN (#{lat_attr} < #{latitude.to_f} AND " +
98
+ "#{lon_attr} >= #{longitude.to_f}) THEN 135.0 " +
99
+ "WHEN (#{lat_attr} < #{latitude.to_f} AND " +
100
+ "#{lon_attr} < #{longitude.to_f}) THEN 225.0 " +
101
+ "WHEN (#{lat_attr} >= #{latitude.to_f} AND " +
102
+ "#{lon_attr} < #{longitude.to_f}) THEN 315.0 " +
103
+ "END"
104
+ end
105
+ end
106
+ end
@@ -1,3 +1,4 @@
1
+ require 'geocoder/sql'
1
2
  require 'geocoder/stores/base'
2
3
 
3
4
  ##
@@ -36,11 +37,13 @@ module Geocoder::Store
36
37
  if Geocoder::Calculations.coordinates_present?(latitude, longitude)
37
38
  near_scope_options(latitude, longitude, *args)
38
39
  else
39
- where(false_condition) # no results if no lat/lon given
40
+ # If no lat/lon given we don't want any results, but we still
41
+ # need distance and bearing columns so you can add, for example:
42
+ # .order("distance")
43
+ select(select_clause(nil, "NULL", "NULL")).where(false_condition)
40
44
  end
41
45
  }
42
46
 
43
-
44
47
  ##
45
48
  # Find all objects within the area of a given bounding box.
46
49
  # Bounds must be an array of locations specifying the southwest
@@ -49,14 +52,15 @@ module Geocoder::Store
49
52
  #
50
53
  scope :within_bounding_box, lambda{ |bounds|
51
54
  sw_lat, sw_lng, ne_lat, ne_lng = bounds.flatten if bounds
52
- return where(false_condition) unless sw_lat && sw_lng && ne_lat && ne_lng
53
- spans = "#{geocoder_options[:latitude]} BETWEEN #{sw_lat} AND #{ne_lat} AND "
54
- spans << if sw_lng > ne_lng # Handle a box that spans 180
55
- "#{geocoder_options[:longitude]} BETWEEN #{sw_lng} AND 180 OR #{geocoder_options[:longitude]} BETWEEN -180 AND #{ne_lng}"
55
+ if sw_lat && sw_lng && ne_lat && ne_lng
56
+ {:conditions => Geocoder::Sql.within_bounding_box(
57
+ sw_lat, sw_lng, ne_lat, ne_lng,
58
+ full_column_name(geocoder_options[:latitude]),
59
+ full_column_name(geocoder_options[:longitude])
60
+ )}
56
61
  else
57
- "#{geocoder_options[:longitude]} BETWEEN #{sw_lng} AND #{ne_lng}"
62
+ select(select_clause(nil, "NULL", "NULL")).where(false_condition)
58
63
  end
59
- { :conditions => spans }
60
64
  }
61
65
  end
62
66
  end
@@ -69,7 +73,7 @@ module Geocoder::Store
69
73
  def distance_from_sql(location, *args)
70
74
  latitude, longitude = Geocoder::Calculations.extract_coordinates(location)
71
75
  if Geocoder::Calculations.coordinates_present?(latitude, longitude)
72
- distance_from_sql_options(latitude, longitude, *args)
76
+ distance_sql(latitude, longitude, *args)
73
77
  end
74
78
  end
75
79
 
@@ -83,170 +87,99 @@ module Geocoder::Store
83
87
  # * +:units+ - <tt>:mi</tt> or <tt>:km</tt>; to be used.
84
88
  # for interpreting radius as well as the +distance+ attribute which
85
89
  # is added to each found nearby object.
86
- # See Geocoder::Configuration to know how configure default units.
90
+ # Use Geocoder.configure[:units] to configure default units.
87
91
  # * +:bearing+ - <tt>:linear</tt> or <tt>:spherical</tt>.
88
92
  # the method to be used for calculating the bearing (direction)
89
93
  # between the given point and each found nearby point;
90
- # set to false for no bearing calculation.
91
- # See Geocoder::Configuration to know how configure default method.
94
+ # set to false for no bearing calculation. Use
95
+ # Geocoder.configure[:distances] to configure default calculation method.
92
96
  # * +:select+ - string with the SELECT SQL fragment (e.g. “id, name”)
93
- # * +:order+ - column(s) for ORDER BY SQL clause; default is distance
97
+ # * +:order+ - column(s) for ORDER BY SQL clause; default is distance;
98
+ # set to false or nil to omit the ORDER BY clause
94
99
  # * +:exclude+ - an object to exclude (used by the +nearbys+ method)
95
100
  #
96
101
  def near_scope_options(latitude, longitude, radius = 20, options = {})
97
- if using_sqlite?
98
- approx_near_scope_options(latitude, longitude, radius, options)
99
- else
100
- full_near_scope_options(latitude, longitude, radius, options)
101
- end
102
- end
102
+ options[:units] ||= (geocoder_options[:units] || Geocoder.config.units)
103
+ bearing = bearing_sql(latitude, longitude, options)
104
+ distance = distance_sql(latitude, longitude, options)
105
+
106
+ b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
107
+ args = b + [
108
+ full_column_name(geocoder_options[:latitude]),
109
+ full_column_name(geocoder_options[:longitude])
110
+ ]
111
+ bounding_box_conditions = Geocoder::Sql.within_bounding_box(*args)
103
112
 
104
- def distance_from_sql_options(latitude, longitude, options = {})
105
113
  if using_sqlite?
106
- approx_distance_from_sql(latitude, longitude, options)
114
+ conditions = bounding_box_conditions
107
115
  else
108
- full_distance_from_sql(latitude, longitude, options)
116
+ conditions = [bounding_box_conditions + " AND #{distance} <= ?", radius]
109
117
  end
118
+ {
119
+ :select => select_clause(options[:select], distance, bearing),
120
+ :conditions => add_exclude_condition(conditions, options[:exclude]),
121
+ :order => options.include?(:order) ? options[:order] : "distance ASC"
122
+ }
110
123
  end
111
124
 
112
125
  ##
113
- # Scope options hash for use with a database that supports POWER(),
114
- # SQRT(), PI(), and trigonometric functions SIN(), COS(), ASIN(),
115
- # ATAN2(), DEGREES(), and RADIANS().
116
- #
117
- # Bearing calculation based on:
118
- # http://www.beginningspatial.com/calculating_bearing_one_point_another
126
+ # SQL for calculating distance based on the current database's
127
+ # capabilities (trig functions?).
119
128
  #
120
- def full_near_scope_options(latitude, longitude, radius, options)
121
- lat_attr = geocoder_options[:latitude]
122
- lon_attr = geocoder_options[:longitude]
123
- options[:bearing] ||= (options[:method] ||
124
- geocoder_options[:method] ||
125
- Geocoder::Configuration.distances)
126
- bearing = case options[:bearing]
127
- when :linear
128
- "CAST(" +
129
- "DEGREES(ATAN2( " +
130
- "RADIANS(#{full_column_name(lon_attr)} - #{longitude}), " +
131
- "RADIANS(#{full_column_name(lat_attr)} - #{latitude})" +
132
- ")) + 360 " +
133
- "AS decimal) % 360"
134
- when :spherical
135
- "CAST(" +
136
- "DEGREES(ATAN2( " +
137
- "SIN(RADIANS(#{full_column_name(lon_attr)} - #{longitude})) * " +
138
- "COS(RADIANS(#{full_column_name(lat_attr)})), (" +
139
- "COS(RADIANS(#{latitude})) * SIN(RADIANS(#{full_column_name(lat_attr)}))" +
140
- ") - (" +
141
- "SIN(RADIANS(#{latitude})) * COS(RADIANS(#{full_column_name(lat_attr)})) * " +
142
- "COS(RADIANS(#{full_column_name(lon_attr)} - #{longitude}))" +
143
- ")" +
144
- ")) + 360 " +
145
- "AS decimal) % 360"
146
- end
147
- options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units)
148
- distance = full_distance_from_sql(latitude, longitude, options)
149
- conditions = ["#{distance} <= ?", radius]
150
- default_near_scope_options(latitude, longitude, radius, options).merge(
151
- :select => "#{options[:select] || full_column_name("*")}, " +
152
- "#{distance} AS distance" +
153
- (bearing ? ", #{bearing} AS bearing" : ""),
154
- :conditions => add_exclude_condition(conditions, options[:exclude])
129
+ def distance_sql(latitude, longitude, options = {})
130
+ method_prefix = using_sqlite? ? "approx" : "full"
131
+ Geocoder::Sql.send(
132
+ method_prefix + "_distance",
133
+ latitude, longitude,
134
+ full_column_name(geocoder_options[:latitude]),
135
+ full_column_name(geocoder_options[:longitude]),
136
+ options
155
137
  )
156
138
  end
157
139
 
158
-
159
- # Distance calculations based on the excellent tutorial at:
160
- # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
161
-
162
- def full_distance_from_sql(latitude, longitude, options)
163
- lat_attr = geocoder_options[:latitude]
164
- lon_attr = geocoder_options[:longitude]
165
-
166
- earth = Geocoder::Calculations.earth_radius(options[:units] || :mi)
167
-
168
- "#{earth} * 2 * ASIN(SQRT(" +
169
- "POWER(SIN((#{latitude} - #{full_column_name(lat_attr)}) * PI() / 180 / 2), 2) + " +
170
- "COS(#{latitude} * PI() / 180) * COS(#{full_column_name(lat_attr)} * PI() / 180) * " +
171
- "POWER(SIN((#{longitude} - #{full_column_name(lon_attr)}) * PI() / 180 / 2), 2) ))"
172
- end
173
-
174
- def approx_distance_from_sql(latitude, longitude, options)
175
- lat_attr = geocoder_options[:latitude]
176
- lon_attr = geocoder_options[:longitude]
177
-
178
- dx = Geocoder::Calculations.longitude_degree_distance(30, options[:units] || :mi)
179
- dy = Geocoder::Calculations.latitude_degree_distance(options[:units] || :mi)
180
-
181
- # sin of 45 degrees = average x or y component of vector
182
- factor = Math.sin(Math::PI / 4)
183
-
184
- "(#{dy} * ABS(#{full_column_name(lat_attr)} - #{latitude}) * #{factor}) + " +
185
- "(#{dx} * ABS(#{full_column_name(lon_attr)} - #{longitude}) * #{factor})"
186
- end
187
-
188
140
  ##
189
- # Scope options hash for use with a database without trigonometric
190
- # functions, like SQLite. Approach is to find objects within a square
191
- # rather than a circle, so results are very approximate (will include
192
- # objects outside the given radius).
193
- #
194
- # Distance and bearing calculations are *extremely inaccurate*. They
195
- # only exist for interface consistency--not intended for production!
141
+ # SQL for calculating bearing based on the current database's
142
+ # capabilities (trig functions?).
196
143
  #
197
- def approx_near_scope_options(latitude, longitude, radius, options)
198
- lat_attr = geocoder_options[:latitude]
199
- lon_attr = geocoder_options[:longitude]
200
- unless options.include?(:bearing)
201
- options[:bearing] = (options[:method] || \
202
- geocoder_options[:method] || \
203
- Geocoder::Configuration.distances)
144
+ def bearing_sql(latitude, longitude, options = {})
145
+ if !options.include?(:bearing)
146
+ options[:bearing] = Geocoder.config.distances
204
147
  end
205
148
  if options[:bearing]
206
- bearing = "CASE " +
207
- "WHEN (#{full_column_name(lat_attr)} >= #{latitude} AND #{full_column_name(lon_attr)} >= #{longitude}) THEN 45.0 " +
208
- "WHEN (#{full_column_name(lat_attr)} < #{latitude} AND #{full_column_name(lon_attr)} >= #{longitude}) THEN 135.0 " +
209
- "WHEN (#{full_column_name(lat_attr)} < #{latitude} AND #{full_column_name(lon_attr)} < #{longitude}) THEN 225.0 " +
210
- "WHEN (#{full_column_name(lat_attr)} >= #{latitude} AND #{full_column_name(lon_attr)} < #{longitude}) THEN 315.0 " +
211
- "END"
212
- else
213
- bearing = false
149
+ method_prefix = using_sqlite? ? "approx" : "full"
150
+ Geocoder::Sql.send(
151
+ method_prefix + "_bearing",
152
+ latitude, longitude,
153
+ full_column_name(geocoder_options[:latitude]),
154
+ full_column_name(geocoder_options[:longitude]),
155
+ options
156
+ )
214
157
  end
215
-
216
- options[:units] ||= (geocoder_options[:units] || Geocoder::Configuration.units)
217
- distance = approx_distance_from_sql(latitude, longitude, options)
218
-
219
- b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
220
- conditions = [
221
- "#{full_column_name(lat_attr)} BETWEEN ? AND ? AND #{full_column_name(lon_attr)} BETWEEN ? AND ?"] +
222
- [b[0], b[2], b[1], b[3]
223
- ]
224
- default_near_scope_options(latitude, longitude, radius, options).merge(
225
- :select => "#{options[:select] || full_column_name("*")}, " +
226
- "#{distance} AS distance" +
227
- (bearing ? ", #{bearing} AS bearing" : ""),
228
- :conditions => add_exclude_condition(conditions, options[:exclude])
229
- )
230
158
  end
231
159
 
232
160
  ##
233
- # Options used for any near-like scope.
161
+ # Generate the SELECT clause.
234
162
  #
235
- def default_near_scope_options(latitude, longitude, radius, options)
236
- {
237
- :order => options[:order] || "distance",
238
- :limit => options[:limit],
239
- :offset => options[:offset]
240
- }
163
+ def select_clause(columns, distance = nil, bearing = nil)
164
+ if columns == :id_only
165
+ return full_column_name(primary_key)
166
+ elsif columns == :geo_only
167
+ clause = ""
168
+ else
169
+ clause = (columns || full_column_name("*")) + ", "
170
+ end
171
+ clause + "#{distance} AS distance" +
172
+ (bearing ? ", #{bearing} AS bearing" : "")
241
173
  end
242
174
 
243
175
  ##
244
176
  # Adds a condition to exclude a given object by ID.
245
- # The given conditions MUST be an array.
177
+ # Expects conditions as an array or string. Returns array.
246
178
  #
247
179
  def add_exclude_condition(conditions, exclude)
180
+ conditions = [conditions] if conditions.is_a?(String)
248
181
  if exclude
249
- conditions[0] << " AND #{full_column_name(:id)} != ?"
182
+ conditions[0] << " AND #{full_column_name(primary_key)} != ?"
250
183
  conditions << exclude.id
251
184
  end
252
185
  conditions
@@ -280,8 +213,8 @@ module Geocoder::Store
280
213
  do_lookup(false) do |o,rs|
281
214
  if r = rs.first
282
215
  unless r.latitude.nil? or r.longitude.nil?
283
- o.send :write_attribute, self.class.geocoder_options[:latitude], r.latitude
284
- o.send :write_attribute, self.class.geocoder_options[:longitude], r.longitude
216
+ o.__send__ "#{self.class.geocoder_options[:latitude]}=", r.latitude
217
+ o.__send__ "#{self.class.geocoder_options[:longitude]}=", r.longitude
285
218
  end
286
219
  r.coordinates
287
220
  end
@@ -298,7 +231,7 @@ module Geocoder::Store
298
231
  do_lookup(true) do |o,rs|
299
232
  if r = rs.first
300
233
  unless r.address.nil?
301
- o.send :write_attribute, self.class.geocoder_options[:fetched_address], r.address
234
+ o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
302
235
  end
303
236
  r.address
304
237
  end
@@ -58,10 +58,11 @@ module Geocoder
58
58
  ##
59
59
  # Get nearby geocoded objects.
60
60
  # Takes the same options hash as the near class method (scope).
61
+ # Returns nil if the object is not geocoded.
61
62
  #
62
63
  def nearbys(radius = 20, options = {})
63
- return [] unless geocoded?
64
- options.merge!(:exclude => self)
64
+ return nil unless geocoded?
65
+ options.merge!(:exclude => self) unless send(self.class.primary_key).nil?
65
66
  self.class.near(self, radius, options)
66
67
  end
67
68
 
@@ -59,7 +59,7 @@ module Geocoder::Store
59
59
  do_lookup(false) do |o,rs|
60
60
  if r = rs.first
61
61
  unless r.coordinates.nil?
62
- o.send :write_attribute, self.class.geocoder_options[:coordinates], r.coordinates.reverse
62
+ o.__send__ "#{self.class.geocoder_options[:coordinates]}=", r.coordinates.reverse
63
63
  end
64
64
  r.coordinates
65
65
  end
@@ -74,7 +74,7 @@ module Geocoder::Store
74
74
  do_lookup(true) do |o,rs|
75
75
  if r = rs.first
76
76
  unless r.address.nil?
77
- o.send :write_attribute, self.class.geocoder_options[:fetched_address], r.address
77
+ o.__send__ "#{self.class.geocoder_options[:fetched_address]}=", r.address
78
78
  end
79
79
  r.address
80
80
  end
@@ -1,3 +1,3 @@
1
1
  module Geocoder
2
- VERSION = "1.1.3"
2
+ VERSION = "1.1.6"
3
3
  end
data/lib/geocoder.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require "geocoder/configuration"
2
+ require "geocoder/query"
2
3
  require "geocoder/calculations"
3
4
  require "geocoder/exceptions"
4
5
  require "geocoder/cache"
5
6
  require "geocoder/request"
7
+ require "geocoder/lookup"
6
8
  require "geocoder/models/active_record" if defined?(::ActiveRecord)
7
9
  require "geocoder/models/mongoid" if defined?(::Mongoid)
8
10
  require "geocoder/models/mongo_mapper" if defined?(::MongoMapper)
@@ -13,15 +15,16 @@ module Geocoder
13
15
  ##
14
16
  # Search for information about an address or a set of coordinates.
15
17
  #
16
- def search(query)
17
- blank_query?(query) ? [] : lookup(query).search(query)
18
+ def search(query, options = {})
19
+ query = Geocoder::Query.new(query, options) unless query.is_a?(Geocoder::Query)
20
+ query.blank? ? [] : query.execute
18
21
  end
19
22
 
20
23
  ##
21
24
  # Look up the coordinates of the given street or IP address.
22
25
  #
23
- def coordinates(address)
24
- if (results = search(address)).size > 0
26
+ def coordinates(address, options = {})
27
+ if (results = search(address, options)).size > 0
25
28
  results.first.coordinates
26
29
  end
27
30
  end
@@ -30,8 +33,8 @@ module Geocoder
30
33
  # Look up the address of the given coordinates ([lat,lon])
31
34
  # or IP address (string).
32
35
  #
33
- def address(query)
34
- if (results = search(query)).size > 0
36
+ def address(query, options = {})
37
+ if (results = search(query, options)).size > 0
35
38
  results.first.address
36
39
  end
37
40
  end
@@ -40,89 +43,8 @@ module Geocoder
40
43
  # The working Cache object, or +nil+ if none configured.
41
44
  #
42
45
  def cache
43
- if @cache.nil? and store = Configuration.cache
44
- @cache = Cache.new(store, Configuration.cache_prefix)
45
- end
46
- @cache
47
- end
48
-
49
- ##
50
- # Array of valid Lookup names.
51
- #
52
- def valid_lookups
53
- street_lookups + ip_lookups
54
- end
55
-
56
- ##
57
- # All street address lookups, default first.
58
- #
59
- def street_lookups
60
- [:google, :google_premier, :yahoo, :bing, :geocoder_ca, :yandex, :nominatim, :mapquest, :test]
61
- end
62
-
63
- ##
64
- # All IP address lookups, default first.
65
- #
66
- def ip_lookups
67
- [:freegeoip]
68
- end
69
-
70
-
71
- private # -----------------------------------------------------------------
72
-
73
- ##
74
- # Get a Lookup object (which communicates with the remote geocoding API).
75
- # Takes a search query and returns an IP or street address Lookup
76
- # depending on the query contents.
77
- #
78
- def lookup(query)
79
- if ip_address?(query)
80
- get_lookup(ip_lookups.first)
81
- else
82
- get_lookup(Configuration.lookup || street_lookups.first)
83
- end
84
- end
85
-
86
- ##
87
- # Retrieve a Lookup object from the store.
88
- #
89
- def get_lookup(name)
90
- @lookups = {} unless defined?(@lookups)
91
- @lookups[name] = spawn_lookup(name) unless @lookups.include?(name)
92
- @lookups[name]
93
- end
94
-
95
- ##
96
- # Spawn a Lookup of the given name.
97
- #
98
- def spawn_lookup(name)
99
- if valid_lookups.include?(name)
100
- name = name.to_s
101
- require "geocoder/lookups/#{name}"
102
- klass = name.split("_").map{ |i| i[0...1].upcase + i[1..-1] }.join
103
- Geocoder::Lookup.const_get(klass).new
104
- else
105
- valids = valid_lookups.map(&:inspect).join(", ")
106
- raise ConfigurationError, "Please specify a valid lookup for Geocoder " +
107
- "(#{name.inspect} is not one of: #{valids})."
108
- end
109
- end
110
-
111
- ##
112
- # Does the given value look like an IP address?
113
- #
114
- # Does not check for actual validity, just the appearance of four
115
- # dot-delimited numbers.
116
- #
117
- def ip_address?(value)
118
- !!value.to_s.match(/^(::ffff:)?(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
119
- end
120
-
121
- ##
122
- # Is the given search query blank? (ie, should we not bother searching?)
123
- #
124
- def blank_query?(value)
125
- !!value.to_s.match(/^\s*$/)
46
+ warn "WARNING: Calling Geocoder.cache is DEPRECATED. The #cache method now belongs to the Geocoder::Lookup object."
47
+ Geocoder::Lookup.get(Geocoder.config.lookup).send(:configuration).cache
126
48
  end
127
49
  end
128
50
 
@@ -0,0 +1,74 @@
1
+ #
2
+ # = Hash Recursive Merge
3
+ #
4
+ # Merges a Ruby Hash recursively, Also known as deep merge.
5
+ # Recursive version of Hash#merge and Hash#merge!.
6
+ #
7
+ # Category:: Ruby
8
+ # Package:: Hash
9
+ # Author:: Simone Carletti <weppos@weppos.net>
10
+ # Copyright:: 2007-2008 The Authors
11
+ # License:: MIT License
12
+ # Link:: http://www.simonecarletti.com/
13
+ # Source:: http://gist.github.com/gists/6391/
14
+ #
15
+ module HashRecursiveMerge
16
+
17
+ #
18
+ # Recursive version of Hash#merge!
19
+ #
20
+ # Adds the contents of +other_hash+ to +hsh+,
21
+ # merging entries in +hsh+ with duplicate keys with those from +other_hash+.
22
+ #
23
+ # Compared with Hash#merge!, this method supports nested hashes.
24
+ # When both +hsh+ and +other_hash+ contains an entry with the same key,
25
+ # it merges and returns the values from both arrays.
26
+ #
27
+ # h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
28
+ # h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
29
+ # h1.rmerge!(h2) #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
30
+ #
31
+ # Simply using Hash#merge! would return
32
+ #
33
+ # h1.merge!(h2) #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
34
+ #
35
+ def rmerge!(other_hash)
36
+ merge!(other_hash) do |key, oldval, newval|
37
+ oldval.class == self.class ? oldval.rmerge!(newval) : newval
38
+ end
39
+ end
40
+
41
+ #
42
+ # Recursive version of Hash#merge
43
+ #
44
+ # Compared with Hash#merge!, this method supports nested hashes.
45
+ # When both +hsh+ and +other_hash+ contains an entry with the same key,
46
+ # it merges and returns the values from both arrays.
47
+ #
48
+ # Compared with Hash#merge, this method provides a different approch
49
+ # for merging nasted hashes.
50
+ # If the value of a given key is an Hash and both +other_hash+ abd +hsh
51
+ # includes the same key, the value is merged instead replaced with
52
+ # +other_hash+ value.
53
+ #
54
+ # h1 = {"a" => 100, "b" => 200, "c" => {"c1" => 12, "c2" => 14}}
55
+ # h2 = {"b" => 254, "c" => {"c1" => 16, "c3" => 94}}
56
+ # h1.rmerge(h2) #=> {"a" => 100, "b" => 254, "c" => {"c1" => 16, "c2" => 14, "c3" => 94}}
57
+ #
58
+ # Simply using Hash#merge would return
59
+ #
60
+ # h1.merge(h2) #=> {"a" => 100, "b" = >254, "c" => {"c1" => 16, "c3" => 94}}
61
+ #
62
+ def rmerge(other_hash)
63
+ r = {}
64
+ merge(other_hash) do |key, oldval, newval|
65
+ r[key] = oldval.class == self.class ? oldval.rmerge(newval) : newval
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+
72
+ class Hash
73
+ include HashRecursiveMerge
74
+ end