acts_as_geocodable 2.0.3 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +25 -0
- data/.rspec +2 -0
- data/.travis.yml +36 -0
- data/CHANGELOG.md +68 -0
- data/Gemfile +12 -0
- data/{MIT-LICENSE → LICENSE.txt} +5 -3
- data/README.md +160 -0
- data/Rakefile +6 -0
- data/acts_as_geocodable.gemspec +27 -0
- data/gemfiles/rails30.gemfile +15 -0
- data/gemfiles/rails31.gemfile +14 -0
- data/gemfiles/rails32.gemfile +14 -0
- data/gemfiles/rails40.gemfile +14 -0
- data/gemfiles/rails41.gemfile +14 -0
- data/lib/acts_as_geocodable.rb +104 -105
- data/lib/acts_as_geocodable/geocode.rb +8 -8
- data/lib/acts_as_geocodable/geocoding.rb +3 -3
- data/lib/acts_as_geocodable/remote_location.rb +8 -8
- data/lib/acts_as_geocodable/version.rb +1 -1
- data/lib/generators/acts_as_geocodable/USAGE +1 -1
- data/lib/generators/acts_as_geocodable/acts_as_geocodable_generator.rb +5 -6
- data/lib/generators/acts_as_geocodable/templates/migration.rb +15 -15
- data/spec/acts_as_geocodable_generator_spec.rb +23 -0
- data/spec/acts_as_geocodable_spec.rb +318 -0
- data/spec/db/database.yml.example +8 -0
- data/spec/db/schema.rb +59 -0
- data/spec/geocode_spec.rb +84 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/factories.rb +110 -0
- data/spec/support/geocoder.rb +52 -0
- data/spec/support/models.rb +35 -0
- metadata +122 -67
- data/CHANGELOG +0 -18
- data/README.textile +0 -129
data/lib/acts_as_geocodable.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
|
1
|
+
require "active_record"
|
2
|
+
require "active_support"
|
3
|
+
require "graticule"
|
4
|
+
|
5
|
+
require "acts_as_geocodable/geocoding"
|
6
|
+
require "acts_as_geocodable/geocode"
|
7
|
+
require "acts_as_geocodable/remote_location"
|
5
8
|
|
6
9
|
module ActiveSupport::Callbacks::ClassMethods
|
7
10
|
def without_callback(*args, &block)
|
@@ -11,7 +14,7 @@ module ActiveSupport::Callbacks::ClassMethods
|
|
11
14
|
end
|
12
15
|
end
|
13
16
|
|
14
|
-
module ActsAsGeocodable
|
17
|
+
module ActsAsGeocodable
|
15
18
|
# Make a model geocodable.
|
16
19
|
#
|
17
20
|
# class Event < ActiveRecord::Base
|
@@ -29,25 +32,24 @@ module ActsAsGeocodable #:nodoc:
|
|
29
32
|
#
|
30
33
|
def acts_as_geocodable(options = {})
|
31
34
|
options = {
|
32
|
-
:
|
33
|
-
:
|
34
|
-
:
|
35
|
-
:
|
36
|
-
:
|
37
|
-
:
|
35
|
+
address: {
|
36
|
+
street: :street, locality: :locality, region: :region,
|
37
|
+
postal_code: :postal_code, country: :country},
|
38
|
+
normalize_address: false,
|
39
|
+
distance_column: "distance",
|
40
|
+
units: :miles
|
38
41
|
}.merge(options)
|
39
42
|
|
40
|
-
|
41
|
-
|
42
|
-
self.acts_as_geocodable_options = options
|
43
|
-
else
|
44
|
-
write_inheritable_attribute :acts_as_geocodable_options, options
|
45
|
-
class_inheritable_reader :acts_as_geocodable_options
|
46
|
-
end
|
43
|
+
class_attribute :acts_as_geocodable_options
|
44
|
+
self.acts_as_geocodable_options = options
|
47
45
|
|
48
46
|
define_callbacks :geocoding
|
49
47
|
|
50
|
-
|
48
|
+
if ActiveRecord::VERSION::MAJOR >= 4
|
49
|
+
has_one :geocoding, -> { includes :geocode }, as: :geocodable, dependent: :destroy
|
50
|
+
else
|
51
|
+
has_one :geocoding, as: :geocodable, include: :geocode, dependent: :destroy
|
52
|
+
end
|
51
53
|
|
52
54
|
after_save :attach_geocode
|
53
55
|
|
@@ -63,7 +65,7 @@ module ActsAsGeocodable #:nodoc:
|
|
63
65
|
|
64
66
|
# Use ActiveRecord ARel style syntax for finding records.
|
65
67
|
#
|
66
|
-
# Model.origin("Chicago, IL", :
|
68
|
+
# Model.origin("Chicago, IL", within: 10)
|
67
69
|
#
|
68
70
|
# a +distance+ attribute indicating the distance
|
69
71
|
# to the origin is added to each of the results:
|
@@ -80,34 +82,35 @@ module ActsAsGeocodable #:nodoc:
|
|
80
82
|
# Default is <tt>:miles</tt> unless specified otherwise in the +acts_as_geocodable+
|
81
83
|
# declaration.
|
82
84
|
#
|
83
|
-
scope :origin, lambda {|*args|
|
85
|
+
scope :origin, lambda { |*args|
|
84
86
|
origin = location_to_geocode(args[0])
|
85
87
|
options = {
|
86
|
-
:
|
88
|
+
units: acts_as_geocodable_options[:units],
|
87
89
|
}.merge(args[1] || {})
|
88
90
|
distance_sql = sql_for_distance(origin, options[:units])
|
89
91
|
|
90
92
|
scope = with_geocode_fields.select("#{table_name}.*, #{distance_sql} AS
|
91
93
|
#{acts_as_geocodable_options[:distance_column]}")
|
92
94
|
|
93
|
-
scope = scope.where("#{distance_sql} >
|
95
|
+
scope = scope.where("#{distance_sql} > #{options[:beyond]}") if options[:beyond]
|
94
96
|
if options[:within]
|
95
|
-
scope = scope.where("(geocodes.latitude = :lat AND geocodes.longitude = :long) OR (#{distance_sql} <= #{options[:within]})", {:
|
97
|
+
scope = scope.where("(geocodes.latitude = :lat AND geocodes.longitude = :long) OR (#{distance_sql} <= #{options[:within]})", { lat: origin.latitude, long: origin.longitude })
|
96
98
|
end
|
97
99
|
scope
|
98
100
|
}
|
99
101
|
|
100
|
-
scope :near, order("#{acts_as_geocodable_options[:distance_column]} ASC")
|
101
|
-
scope :far, order("#{acts_as_geocodable_options[:distance_column]} DESC")
|
102
|
+
scope :near, -> { order("#{acts_as_geocodable_options[:distance_column]} ASC") }
|
103
|
+
scope :far, -> { order("#{acts_as_geocodable_options[:distance_column]} DESC") }
|
102
104
|
|
103
105
|
include ActsAsGeocodable::Model
|
104
106
|
end
|
105
107
|
|
106
108
|
module Model
|
107
|
-
|
109
|
+
def self.included(base)
|
110
|
+
base.extend(ClassMethods)
|
111
|
+
end
|
108
112
|
|
109
113
|
module ClassMethods
|
110
|
-
|
111
114
|
# Find the nearest location to the given origin
|
112
115
|
#
|
113
116
|
# Model.origin("Grand Rapids, MI").nearest
|
@@ -128,7 +131,7 @@ module ActsAsGeocodable #:nodoc:
|
|
128
131
|
def location_to_geocode(location)
|
129
132
|
case location
|
130
133
|
when Geocode then location
|
131
|
-
when
|
134
|
+
when ActsAsGeocodable::Model then location.geocode
|
132
135
|
when String, Fixnum then Geocode.find_or_create_by_query(location.to_s)
|
133
136
|
end
|
134
137
|
end
|
@@ -149,120 +152,116 @@ module ActsAsGeocodable #:nodoc:
|
|
149
152
|
# end
|
150
153
|
#
|
151
154
|
def validates_as_geocodable(options = {})
|
152
|
-
options = options.reverse_merge :
|
155
|
+
options = options.reverse_merge message: "Address could not be geocoded.", allow_nil: false
|
153
156
|
validate do |model|
|
154
157
|
is_blank = model.to_location.attributes.except(:precision).all?(&:blank?)
|
155
158
|
unless options[:allow_nil] && is_blank
|
156
|
-
geocode = model.send
|
159
|
+
geocode = model.send(:attach_geocode)
|
157
160
|
if !geocode ||
|
158
161
|
(options[:precision] && geocode.precision < options[:precision]) ||
|
159
162
|
(block_given? && yield(geocode) == false)
|
160
|
-
model.errors.add
|
163
|
+
model.errors.add(:base, options[:message])
|
161
164
|
end
|
162
165
|
end
|
163
166
|
end
|
164
167
|
end
|
165
168
|
|
166
|
-
|
169
|
+
private
|
167
170
|
|
168
171
|
def sql_for_distance(origin, units = acts_as_geocodable_options[:units])
|
169
172
|
Graticule::Distance::Spherical.to_sql(
|
170
|
-
:
|
171
|
-
:
|
172
|
-
:
|
173
|
-
:
|
174
|
-
:
|
173
|
+
latitude: origin.latitude,
|
174
|
+
longitude: origin.longitude,
|
175
|
+
latitude_column: "geocodes.latitude",
|
176
|
+
longitude_column: "geocodes.longitude",
|
177
|
+
units: units
|
175
178
|
)
|
176
179
|
end
|
177
180
|
end
|
178
181
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
geocoding.geocode if geocoding
|
184
|
-
end
|
182
|
+
# Get the geocode for this model
|
183
|
+
def geocode
|
184
|
+
geocoding.geocode if geocoding
|
185
|
+
end
|
185
186
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
end
|
187
|
+
# Create a Graticule::Location
|
188
|
+
def to_location
|
189
|
+
Graticule::Location.new.tap do |location|
|
190
|
+
[:street, :locality, :region, :postal_code, :country].each do |attr|
|
191
|
+
location.send("#{attr}=", geo_attribute(attr))
|
192
192
|
end
|
193
193
|
end
|
194
|
+
end
|
194
195
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
196
|
+
# Get the distance to the given destination. The destination can be an
|
197
|
+
# acts_as_geocodable model, a Geocode, or a string
|
198
|
+
#
|
199
|
+
# myhome.distance_to "Chicago, IL"
|
200
|
+
# myhome.distance_to "49423"
|
201
|
+
# myhome.distance_to other_model
|
202
|
+
#
|
203
|
+
# == Options
|
204
|
+
# * <tt>:units</tt>: <tt>:miles</tt> or <tt>:kilometers</tt>
|
205
|
+
# * <tt>:formula</tt>: The formula to use to calculate the distance. This can
|
206
|
+
# be any formula supported by Graticule. The default is <tt>:haversine</tt>.
|
207
|
+
#
|
208
|
+
def distance_to(destination, options = {})
|
209
|
+
units = options[:units] || acts_as_geocodable_options[:units]
|
210
|
+
formula = options[:formula] || :haversine
|
210
211
|
|
211
|
-
|
212
|
-
|
213
|
-
|
212
|
+
geocode = self.class.location_to_geocode(destination)
|
213
|
+
self.geocode.distance_to(geocode, units, formula)
|
214
|
+
end
|
214
215
|
|
215
216
|
protected
|
216
217
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
end
|
225
|
-
elsif !new_geocode && self.geocoding
|
226
|
-
self.geocoding.destroy
|
218
|
+
# Perform the geocoding
|
219
|
+
def attach_geocode
|
220
|
+
new_geocode = Geocode.find_or_create_by_location(self.to_location) unless self.to_location.blank?
|
221
|
+
if new_geocode && self.geocode != new_geocode
|
222
|
+
run_callbacks :geocoding do
|
223
|
+
self.geocoding = Geocoding.new(geocode: new_geocode)
|
224
|
+
self.update_address self.acts_as_geocodable_options[:normalize_address]
|
227
225
|
end
|
228
|
-
|
229
|
-
|
230
|
-
logger.warn e.message
|
226
|
+
elsif !new_geocode && self.geocoding
|
227
|
+
self.geocoding.destroy
|
231
228
|
end
|
229
|
+
new_geocode
|
230
|
+
rescue Graticule::Error => error
|
231
|
+
logger.warn error.message
|
232
|
+
end
|
232
233
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
234
|
+
def update_address(force = false)
|
235
|
+
unless self.geocode.blank?
|
236
|
+
if self.acts_as_geocodable_options[:address].is_a? Symbol
|
237
|
+
method = self.acts_as_geocodable_options[:address]
|
238
|
+
if self.respond_to?("#{method}=") && (self.send(method).blank? || force)
|
239
|
+
self.send("#{method}=", self.geocode.to_location.to_s)
|
240
|
+
end
|
241
|
+
else
|
242
|
+
self.acts_as_geocodable_options[:address].each do |attribute,method|
|
238
243
|
if self.respond_to?("#{method}=") && (self.send(method).blank? || force)
|
239
|
-
self.send
|
240
|
-
end
|
241
|
-
else
|
242
|
-
self.acts_as_geocodable_options[:address].each do |attribute,method|
|
243
|
-
if self.respond_to?("#{method}=") && (self.send(method).blank? || force)
|
244
|
-
self.send "#{method}=", self.geocode.send(attribute)
|
245
|
-
end
|
244
|
+
self.send("#{method}=", self.geocode.send(attribute))
|
246
245
|
end
|
247
246
|
end
|
247
|
+
end
|
248
248
|
|
249
|
-
|
250
|
-
|
251
|
-
end
|
249
|
+
self.class.without_callback(:save, :after, :attach_geocode) do
|
250
|
+
save
|
252
251
|
end
|
253
252
|
end
|
253
|
+
end
|
254
254
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
end
|
255
|
+
def geo_attribute(attr_key)
|
256
|
+
if self.acts_as_geocodable_options[:address].is_a? Symbol
|
257
|
+
attr_name = self.acts_as_geocodable_options[:address]
|
258
|
+
attr_key == :street ? self.send(attr_name) : nil
|
259
|
+
else
|
260
|
+
attr_name = self.acts_as_geocodable_options[:address][attr_key]
|
261
|
+
attr_name && self.respond_to?(attr_name) ? self.send(attr_name) : nil
|
263
262
|
end
|
264
263
|
end
|
265
264
|
end
|
266
265
|
end
|
267
266
|
|
268
|
-
ActiveRecord::Base.send
|
267
|
+
ActiveRecord::Base.send(:extend, ActsAsGeocodable)
|
@@ -1,5 +1,5 @@
|
|
1
1
|
class Geocode < ActiveRecord::Base
|
2
|
-
has_many :geocodings, :
|
2
|
+
has_many :geocodings, dependent: :destroy
|
3
3
|
|
4
4
|
validates_uniqueness_of :query
|
5
5
|
|
@@ -20,7 +20,7 @@ class Geocode < ActiveRecord::Base
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.create_by_query(query)
|
23
|
-
create geocoder.locate(query).attributes.merge(:
|
23
|
+
create geocoder.locate(query).attributes.merge(query: query)
|
24
24
|
end
|
25
25
|
|
26
26
|
def self.find_or_create_by_location(location)
|
@@ -28,9 +28,9 @@ class Geocode < ActiveRecord::Base
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.create_from_location(location)
|
31
|
-
create geocoder.locate(location).attributes.merge(:
|
32
|
-
rescue Graticule::Error =>
|
33
|
-
logger.warn
|
31
|
+
create geocoder.locate(location).attributes.merge(query: location.to_s)
|
32
|
+
rescue Graticule::Error => error
|
33
|
+
logger.warn error.message
|
34
34
|
nil
|
35
35
|
end
|
36
36
|
|
@@ -47,7 +47,7 @@ class Geocode < ActiveRecord::Base
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def on(geocodable)
|
50
|
-
geocodings.create
|
50
|
+
geocodings.create(geocodable: geocodable)
|
51
51
|
end
|
52
52
|
|
53
53
|
def coordinates
|
@@ -60,6 +60,6 @@ class Geocode < ActiveRecord::Base
|
|
60
60
|
|
61
61
|
# Create a Graticule::Location
|
62
62
|
def to_location
|
63
|
-
Graticule::Location.new(attributes.except(
|
63
|
+
Graticule::Location.new(attributes.except("id", "query"))
|
64
64
|
end
|
65
|
-
end
|
65
|
+
end
|
@@ -1,12 +1,12 @@
|
|
1
1
|
class Geocoding < ActiveRecord::Base
|
2
2
|
belongs_to :geocode
|
3
|
-
belongs_to :geocodable, :
|
3
|
+
belongs_to :geocodable, polymorphic: true
|
4
4
|
|
5
5
|
def self.geocoded_class(geocodable)
|
6
6
|
ActiveRecord::Base.send(:class_name_of_active_record_descendant, geocodable.class).to_s
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def self.find_geocodable(geocoded_class, geocoded_id)
|
10
10
|
geocoded_class.constantize.find(geocoded_id)
|
11
11
|
end
|
12
|
-
end
|
12
|
+
end
|
@@ -1,20 +1,20 @@
|
|
1
|
-
|
2
|
-
module RemoteLocation #:nodoc:
|
1
|
+
require "action_controller"
|
3
2
|
|
3
|
+
module ActsAsGeocodable
|
4
|
+
module RemoteLocation
|
4
5
|
# Get the remote location of the request IP using http://hostip.info
|
5
6
|
def remote_location
|
6
|
-
if request.remote_ip ==
|
7
|
+
if request.remote_ip == "127.0.0.1"
|
7
8
|
# otherwise people would complain that it doesn't work
|
8
|
-
Graticule::Location.new(:
|
9
|
+
Graticule::Location.new(locality: "localhost")
|
9
10
|
else
|
10
11
|
Graticule.service(:host_ip).new.locate(request.remote_ip)
|
11
12
|
end
|
12
|
-
rescue Graticule::Error =>
|
13
|
-
logger.warn "An error occurred while looking up the location of '#{request.remote_ip}': #{
|
13
|
+
rescue Graticule::Error => error
|
14
|
+
logger.warn "An error occurred while looking up the location of '#{request.remote_ip}': #{error.message}"
|
14
15
|
nil
|
15
16
|
end
|
16
|
-
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
ActionController::Base.send
|
20
|
+
ActionController::Base.send(:include, ActsAsGeocodable::RemoteLocation)
|
@@ -1,16 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "rails/generators"
|
2
|
+
require "rails/generators/migration"
|
3
3
|
|
4
4
|
class ActsAsGeocodableGenerator < Rails::Generators::Base
|
5
5
|
include Rails::Generators::Migration
|
6
6
|
|
7
7
|
def self.source_root
|
8
|
-
@source_root ||= File.join(File.dirname(__FILE__),
|
8
|
+
@source_root ||= File.join(File.dirname(__FILE__), "templates")
|
9
9
|
end
|
10
10
|
|
11
11
|
# Implement the required interface for Rails::Generators::Migration.
|
12
12
|
#
|
13
|
-
def self.next_migration_number(dirname)
|
13
|
+
def self.next_migration_number(dirname)
|
14
14
|
next_migration_number = current_migration_number(dirname) + 1
|
15
15
|
if ActiveRecord::Base.timestamped_migrations
|
16
16
|
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max
|
@@ -21,8 +21,7 @@ class ActsAsGeocodableGenerator < Rails::Generators::Base
|
|
21
21
|
|
22
22
|
def create_migration_file
|
23
23
|
if defined?(ActiveRecord)
|
24
|
-
migration_template
|
24
|
+
migration_template "migration.rb", "db/migrate/add_geocodable_tables.rb"
|
25
25
|
end
|
26
26
|
end
|
27
|
-
|
28
27
|
end
|