acts_as_geocodable 2.0.3 → 2.1.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.
- 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
|