acts_as_geocodable 1.0.0
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.
- data/.gitignore +2 -0
- data/CHANGELOG +13 -0
- data/MIT-LICENSE +25 -0
- data/README +104 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/about.yml +7 -0
- data/acts_as_geocodable.gemspec +69 -0
- data/generators/geocodable_migration/USAGE +12 -0
- data/generators/geocodable_migration/geocodable_migration_generator.rb +7 -0
- data/generators/geocodable_migration/templates/migration.rb +40 -0
- data/install.rb +1 -0
- data/lib/acts_as_geocodable.rb +286 -0
- data/lib/acts_as_geocodable/geocode.rb +61 -0
- data/lib/acts_as_geocodable/geocoding.rb +12 -0
- data/lib/acts_as_geocodable/remote_location.rb +18 -0
- data/lib/acts_as_geocodable/tasks/acts_as_geocodable_tasks.rake +4 -0
- data/rails/init.rb +5 -0
- data/test/acts_as_geocodable_test.rb +340 -0
- data/test/db/database.yml +18 -0
- data/test/db/schema.rb +60 -0
- data/test/fixtures/cities.yml +12 -0
- data/test/fixtures/geocodes.yml +51 -0
- data/test/fixtures/geocodings.yml +15 -0
- data/test/fixtures/vacations.yml +15 -0
- data/test/geocode_test.rb +97 -0
- data/test/test_helper.rb +46 -0
- data/uninstall.rb +1 -0
- metadata +86 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
class Geocode < ActiveRecord::Base
|
2
|
+
has_many :geocodings, :dependent => :destroy
|
3
|
+
|
4
|
+
validates_uniqueness_of :query
|
5
|
+
|
6
|
+
cattr_accessor :geocoder
|
7
|
+
|
8
|
+
def distance_to(destination, units = :miles, formula = :haversine)
|
9
|
+
if destination && destination.latitude && destination.longitude
|
10
|
+
Graticule::Distance.const_get(formula.to_s.camelize).distance(self, destination, units)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def geocoded?
|
15
|
+
!latitude.blank? && !longitude.blank?
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.find_or_create_by_query(query)
|
19
|
+
find_by_query(query) || create_by_query(query)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.create_by_query(query)
|
23
|
+
create geocoder.locate(query).attributes.merge(:query => query)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.find_or_create_by_location(location)
|
27
|
+
find_by_query(location.to_s) || create_from_location(location)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.create_from_location(location)
|
31
|
+
create geocoder.locate(location).attributes.merge(:query => location.to_s)
|
32
|
+
rescue Graticule::Error => e
|
33
|
+
logger.warn e.message
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def precision=(name)
|
38
|
+
self[:precision] = name.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
def geocoded
|
42
|
+
@geocoded ||= geocodings.collect { |geocoding| geocoding.geocodable }
|
43
|
+
end
|
44
|
+
|
45
|
+
def on(geocodable)
|
46
|
+
geocodings.create :geocodable => geocodable
|
47
|
+
end
|
48
|
+
|
49
|
+
def coordinates
|
50
|
+
"#{longitude},#{latitude}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_s
|
54
|
+
coordinates
|
55
|
+
end
|
56
|
+
|
57
|
+
# Create a Graticule::Location
|
58
|
+
def to_location
|
59
|
+
Graticule::Location.new(attributes.except('id', 'query'))
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Geocoding < ActiveRecord::Base
|
2
|
+
belongs_to :geocode
|
3
|
+
belongs_to :geocodable, :polymorphic => true
|
4
|
+
|
5
|
+
def self.geocoded_class(geocodable)
|
6
|
+
ActiveRecord::Base.send(:class_name_of_active_record_descendant, geocodable.class).to_s
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.find_geocodable(geocoded_class, geocoded_id)
|
10
|
+
geocoded_class.constantize.find(geocoded_id)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module CollectiveIdea #:nodoc:
|
2
|
+
module RemoteLocation #:nodoc:
|
3
|
+
|
4
|
+
# Get the remote location of the request IP using http://hostip.info
|
5
|
+
def remote_location
|
6
|
+
if request.remote_ip == '127.0.0.1'
|
7
|
+
# otherwise people would complain that it doesn't work
|
8
|
+
Graticule::Location.new(:locality => 'localhost')
|
9
|
+
else
|
10
|
+
Graticule.service(:host_ip).new.locate(request.remote_ip)
|
11
|
+
end
|
12
|
+
rescue Graticule::Error => e
|
13
|
+
logger.warn "An error occurred while looking up the location of '#{request.remote_ip}': #{e.message}"
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,340 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'shoulda/rails'
|
3
|
+
|
4
|
+
class Vacation < ActiveRecord::Base
|
5
|
+
acts_as_geocodable :normalize_address => true
|
6
|
+
belongs_to :nearest_city, :class_name => 'City', :foreign_key => 'city_id'
|
7
|
+
end
|
8
|
+
|
9
|
+
class City < ActiveRecord::Base
|
10
|
+
acts_as_geocodable :address => {:postal_code => :zip}
|
11
|
+
end
|
12
|
+
|
13
|
+
class ValidatedVacation < ActiveRecord::Base
|
14
|
+
acts_as_geocodable
|
15
|
+
validates_as_geocodable
|
16
|
+
end
|
17
|
+
|
18
|
+
class AddressBlobVacation < ActiveRecord::Base
|
19
|
+
acts_as_geocodable :address => :address, :normalize_address => true
|
20
|
+
end
|
21
|
+
|
22
|
+
class CallbackLocation < ActiveRecord::Base
|
23
|
+
acts_as_geocodable :address => :address
|
24
|
+
after_geocoding :done_geocoding
|
25
|
+
|
26
|
+
def done_geocoding
|
27
|
+
true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ActsAsGeocodableTest < ActiveSupport::TestCase
|
32
|
+
fixtures :vacations, :cities, :geocodes, :geocodings
|
33
|
+
|
34
|
+
# enable Should macros
|
35
|
+
def self.model_class
|
36
|
+
Vacation
|
37
|
+
end
|
38
|
+
|
39
|
+
should_have_one :geocoding
|
40
|
+
|
41
|
+
context "geocode" do
|
42
|
+
setup do
|
43
|
+
@location = vacations(:whitehouse)
|
44
|
+
end
|
45
|
+
|
46
|
+
should "be the geocode from the geocoding" do
|
47
|
+
@location.geocode.should == @location.geocoding.geocode
|
48
|
+
end
|
49
|
+
|
50
|
+
should "be nil without a geocoding" do
|
51
|
+
Vacation.new.geocode.should be(nil)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
context "to_location" do
|
57
|
+
should "return a graticule location" do
|
58
|
+
expected = Graticule::Location.new :street => '1600 Pennsylvania Ave NW',
|
59
|
+
:locality => 'Washington', :region => 'DC', :postal_code => '20502',
|
60
|
+
:country => nil
|
61
|
+
vacations(:whitehouse).to_location.should == expected
|
62
|
+
end
|
63
|
+
|
64
|
+
should "return a graticule location for mapped locations" do
|
65
|
+
cities(:holland).to_location.should == Graticule::Location.new(:postal_code => '49423')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "with address normalization" do
|
70
|
+
setup do
|
71
|
+
Vacation.acts_as_geocodable_options[:normalize_address] = true
|
72
|
+
|
73
|
+
Geocode.geocoder.stubs(:locate).returns(
|
74
|
+
Graticule::Location.new(:locality => "San Clemente", :region => "CA")
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
should "update address fields with result" do
|
79
|
+
vacation = Vacation.create! :locality => 'sanclemente', :region => 'ca'
|
80
|
+
vacation.locality.should == 'San Clemente'
|
81
|
+
vacation.region.should == 'CA'
|
82
|
+
end
|
83
|
+
|
84
|
+
should "update address blob" do
|
85
|
+
Geocode.geocoder.expects(:locate).returns(
|
86
|
+
Graticule::Location.new(:locality => "Grand Rapids", :region => "MI", :country => "US")
|
87
|
+
)
|
88
|
+
|
89
|
+
vacation = AddressBlobVacation.create! :address => "grand rapids, mi"
|
90
|
+
vacation.address.should == "Grand Rapids, MI US"
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
context "without address normalization" do
|
96
|
+
setup do
|
97
|
+
Vacation.acts_as_geocodable_options[:normalize_address] = false
|
98
|
+
|
99
|
+
Geocode.geocoder.stubs(:locate).returns(
|
100
|
+
Graticule::Location.new(:locality => "Portland", :region => "OR", :postal_code => '97212')
|
101
|
+
)
|
102
|
+
|
103
|
+
@vacation = Vacation.create! :locality => 'portland', :region => 'or'
|
104
|
+
end
|
105
|
+
|
106
|
+
should "not update address attributes" do
|
107
|
+
@vacation.locality.should == 'portland'
|
108
|
+
@vacation.region.should == 'or'
|
109
|
+
end
|
110
|
+
|
111
|
+
should "fill in blank attributes" do
|
112
|
+
@vacation.postal_code.should == '97212'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "with blank location attributes" do
|
117
|
+
should "not create geocode" do
|
118
|
+
Geocode.geocoder.expects(:locate).never
|
119
|
+
assert_no_difference 'Geocode.count + Geocoding.count' do
|
120
|
+
Vacation.create!(:locality => "\n", :region => " ").geocoding.should be(nil)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
should "destroy existing geocoding" do
|
125
|
+
whitehouse = vacations(:whitehouse)
|
126
|
+
[:name, :street, :locality, :region, :postal_code].each do |attribute|
|
127
|
+
whitehouse.send("#{attribute}=", nil)
|
128
|
+
end
|
129
|
+
assert_difference 'Geocoding.count', -1 do
|
130
|
+
whitehouse.save!
|
131
|
+
end
|
132
|
+
whitehouse.reload
|
133
|
+
whitehouse.geocoding.should be(nil)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context "on save" do
|
138
|
+
should "not create geocode without changes" do
|
139
|
+
whitehouse = vacations(:whitehouse)
|
140
|
+
assert_not_nil whitehouse.geocoding
|
141
|
+
original_geocode = whitehouse.geocode
|
142
|
+
|
143
|
+
assert_no_difference 'Geocode.count + Geocoding.count' do
|
144
|
+
whitehouse.save!
|
145
|
+
whitehouse.reload
|
146
|
+
end
|
147
|
+
|
148
|
+
assert_equal original_geocode, whitehouse.geocode
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
context "on save with an existing geocode" do
|
154
|
+
setup do
|
155
|
+
@location = vacations(:saugatuck)
|
156
|
+
@location.attributes = {:locality => 'Beverly Hills', :postal_code => '90210'}
|
157
|
+
end
|
158
|
+
|
159
|
+
should "destroy the old geocoding" do
|
160
|
+
assert_no_difference('Geocoding.count') { @location.save! }
|
161
|
+
end
|
162
|
+
|
163
|
+
should "set the new geocode" do
|
164
|
+
@location.save!
|
165
|
+
@location.geocode.should == geocodes(:beverly_hills)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
context "validates_as_geocodable" do
|
171
|
+
setup do
|
172
|
+
@vacation = ValidatedVacation.new :locality => "Grand Rapids", :region => "MI"
|
173
|
+
end
|
174
|
+
|
175
|
+
should "be invalid without geocodable address" do
|
176
|
+
Geocode.geocoder.expects(:locate).raises(Graticule::Error)
|
177
|
+
assert !@vacation.valid?
|
178
|
+
assert_equal 1, @vacation.errors.size
|
179
|
+
assert_equal "Address could not be geocoded.", @vacation.errors.on(:base)
|
180
|
+
end
|
181
|
+
|
182
|
+
should "be valid with geocodable address" do
|
183
|
+
assert @vacation.valid?
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context "find with origin" do
|
188
|
+
should "add distance to result" do
|
189
|
+
Vacation.find(1, :origin => "49406").distance.to_f.should be_close(0.794248231790402, 0.2)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context "find within" do
|
194
|
+
setup do
|
195
|
+
@results = Vacation.find(:all, :origin => 49406, :within => 10)
|
196
|
+
end
|
197
|
+
|
198
|
+
should "find locations within radius" do
|
199
|
+
@results.should include(vacations(:saugatuck))
|
200
|
+
end
|
201
|
+
|
202
|
+
should "add distance to results" do
|
203
|
+
@results.first.distance.to_f.should be_close(0.794248231790402, 0.2)
|
204
|
+
end
|
205
|
+
|
206
|
+
def test_find_within
|
207
|
+
spots = Vacation.find(:all, :origin => "49406", :within => 3)
|
208
|
+
assert_equal 1, spots.size
|
209
|
+
assert_equal vacations(:saugatuck), spots.first
|
210
|
+
end
|
211
|
+
|
212
|
+
def test_count_within
|
213
|
+
spots_count = Vacation.count(:origin => "49406", :within => 3)
|
214
|
+
assert_equal 1, spots_count
|
215
|
+
end
|
216
|
+
|
217
|
+
def test_within_kilometers
|
218
|
+
saugatuck = Vacation.find(:first, :within => 2, :units => :kilometers, :origin => "49406")
|
219
|
+
assert_equal vacations(:saugatuck), saugatuck
|
220
|
+
assert_in_delta 1.27821863, saugatuck.distance, 0.2
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
context "distance_to" do
|
226
|
+
setup do
|
227
|
+
@saugatuck = vacations(:saugatuck)
|
228
|
+
@douglas = Vacation.create!(:name => 'Douglas', :postal_code => '49406')
|
229
|
+
end
|
230
|
+
|
231
|
+
should 'calculate distance from a string' do
|
232
|
+
@douglas.distance_to(geocodes(:saugatuck_geocode).query).should be_close(0.794248231790402, 0.2)
|
233
|
+
end
|
234
|
+
should 'calculate distance from a geocode' do
|
235
|
+
@douglas.distance_to(geocodes(:saugatuck_geocode)).should be_close(0.794248231790402, 0.2)
|
236
|
+
end
|
237
|
+
|
238
|
+
should 'calculate distance from a geocodable model' do
|
239
|
+
@douglas.distance_to(@saugatuck).should be_close(0.794248231790402, 0.2)
|
240
|
+
@saugatuck.distance_to(@douglas).should be_close(0.794248231790402, 0.2)
|
241
|
+
end
|
242
|
+
|
243
|
+
should 'calculate distance in default miles' do
|
244
|
+
@douglas.distance_to(@saugatuck, :units => :miles).should be_close(0.794248231790402, 0.2)
|
245
|
+
end
|
246
|
+
|
247
|
+
should 'calculate distance in default kilometers' do
|
248
|
+
@douglas.distance_to(@saugatuck, :units => :kilometers).should be_close(1.27821863, 0.2)
|
249
|
+
end
|
250
|
+
|
251
|
+
should 'return nil with invalid geocode' do
|
252
|
+
@douglas.distance_to(Geocode.new).should be(nil)
|
253
|
+
@douglas.distance_to(nil).should be(nil)
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_find_beyond
|
259
|
+
spots = Vacation.find(:all, :origin => "49406", :beyond => 3)
|
260
|
+
assert_equal 1, spots.size
|
261
|
+
assert_equal vacations(:whitehouse), spots.first
|
262
|
+
end
|
263
|
+
|
264
|
+
def test_count_beyond
|
265
|
+
spots = Vacation.count(:origin => "49406", :beyond => 3)
|
266
|
+
assert_equal 1, spots
|
267
|
+
end
|
268
|
+
|
269
|
+
def test_find_beyond_in_kilometers
|
270
|
+
whitehouse = Vacation.find(:first, :beyond => 3, :units => :kilometers, :origin => "49406")
|
271
|
+
assert_equal vacations(:whitehouse), whitehouse
|
272
|
+
assert_in_delta 877.554975851074, whitehouse.distance, 1
|
273
|
+
end
|
274
|
+
|
275
|
+
def test_find_nearest
|
276
|
+
assert_equal vacations(:saugatuck), Vacation.find(:nearest, :origin => "49406")
|
277
|
+
end
|
278
|
+
|
279
|
+
def test_find_nearest
|
280
|
+
assert_equal vacations(:whitehouse), Vacation.find(:farthest, :origin => "49406")
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_find_nearest_with_include_raises_error
|
284
|
+
assert_raises(ArgumentError) { Vacation.find(:nearest, :origin => '49406', :include => :nearest_city) }
|
285
|
+
end
|
286
|
+
|
287
|
+
def test_uses_units_set_in_declared_options
|
288
|
+
Vacation.acts_as_geocodable_options.merge! :units => :kilometers
|
289
|
+
saugatuck = Vacation.find(:first, :within => 2, :units => :kilometers, :origin => "49406")
|
290
|
+
assert_in_delta 1.27821863, saugatuck.distance, 0.2
|
291
|
+
end
|
292
|
+
|
293
|
+
def test_find_with_order
|
294
|
+
expected = [vacations(:saugatuck), vacations(:whitehouse)]
|
295
|
+
actual = Vacation.find(:all, :origin => '49406', :order => 'distance')
|
296
|
+
assert_equal expected, actual
|
297
|
+
end
|
298
|
+
|
299
|
+
def test_location_to_geocode_nil
|
300
|
+
assert_nil Vacation.send(:location_to_geocode, nil)
|
301
|
+
end
|
302
|
+
|
303
|
+
def test_location_to_geocode_with_geocode
|
304
|
+
g = Geocode.new
|
305
|
+
assert(g === Vacation.send(:location_to_geocode, g))
|
306
|
+
end
|
307
|
+
|
308
|
+
def test_location_to_geocode_with_string
|
309
|
+
assert_equal geocodes(:douglas), Vacation.send(:location_to_geocode, '49406')
|
310
|
+
end
|
311
|
+
|
312
|
+
def test_location_to_geocode_with_fixnum
|
313
|
+
assert_equal geocodes(:douglas), Vacation.send(:location_to_geocode, 49406)
|
314
|
+
end
|
315
|
+
|
316
|
+
def test_location_to_geocode_with_geocodable
|
317
|
+
assert_equal geocodes(:white_house_geocode),
|
318
|
+
Vacation.send(:location_to_geocode, vacations(:whitehouse))
|
319
|
+
end
|
320
|
+
|
321
|
+
def test_find_nearest_raises_error_with_include
|
322
|
+
assert_raises(ArgumentError) { Vacation.find(:nearest, :include => :nearest_city, :origin => 49406) }
|
323
|
+
end
|
324
|
+
|
325
|
+
def test_callback_after_geocoding
|
326
|
+
location = CallbackLocation.new :address => "Holland, MI"
|
327
|
+
assert_nil location.geocoding
|
328
|
+
location.expects(:done_geocoding).once.returns(true)
|
329
|
+
assert location.save!
|
330
|
+
end
|
331
|
+
|
332
|
+
def test_does_not_run_the_callback_after_geocoding_if_object_dont_change
|
333
|
+
location = CallbackLocation.create(:address => "Holland, MI")
|
334
|
+
assert_not_nil location.geocoding
|
335
|
+
location.expects(:done_geocoding).never
|
336
|
+
assert location.save!
|
337
|
+
end
|
338
|
+
|
339
|
+
|
340
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
sqlite:
|
2
|
+
:adapter: sqlite
|
3
|
+
:dbfile: acts_as_geocodable_plugin.sqlite.db
|
4
|
+
sqlite3:
|
5
|
+
:adapter: sqlite3
|
6
|
+
:dbfile: acts_as_geocodable_plugin.sqlite3.db
|
7
|
+
postgresql:
|
8
|
+
:adapter: postgresql
|
9
|
+
:username: postgres
|
10
|
+
:password: postgres
|
11
|
+
:database: acts_as_geocodable_plugin_test
|
12
|
+
:min_messages: ERROR
|
13
|
+
mysql:
|
14
|
+
:adapter: mysql
|
15
|
+
:host: localhost
|
16
|
+
:username: root
|
17
|
+
:password:
|
18
|
+
:database: acts_as_geocodable_plugin_test
|