acts_as_geocodable 1.0.4 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +5 -0
- data/README +31 -37
- data/lib/acts_as_geocodable.rb +233 -278
- data/lib/acts_as_geocodable/geocode.rb +13 -13
- data/lib/acts_as_geocodable/remote_location.rb +6 -4
- data/lib/acts_as_geocodable/version.rb +3 -0
- data/lib/generators/acts_as_geocodable/USAGE +12 -0
- data/lib/generators/acts_as_geocodable/acts_as_geocodable_generator.rb +28 -0
- data/{generators/geocodable_migration → lib/generators/acts_as_geocodable}/templates/migration.rb +2 -2
- metadata +110 -41
- data/.gitignore +0 -3
- data/Gemfile +0 -2
- data/Gemfile.lock +0 -23
- data/Rakefile +0 -39
- data/VERSION +0 -1
- data/about.yml +0 -7
- data/acts_as_geocodable.gemspec +0 -75
- data/generators/geocodable_migration/USAGE +0 -12
- data/generators/geocodable_migration/geocodable_migration_generator.rb +0 -7
- data/install.rb +0 -1
- data/lib/acts_as_geocodable/tasks/acts_as_geocodable_tasks.rake +0 -4
- data/rails/init.rb +0 -5
- data/test/acts_as_geocodable_test.rb +0 -382
- data/test/db/database.yml +0 -18
- data/test/db/schema.rb +0 -60
- data/test/fixtures/cities.yml +0 -12
- data/test/fixtures/geocodes.yml +0 -51
- data/test/fixtures/geocodings.yml +0 -15
- data/test/fixtures/vacations.yml +0 -15
- data/test/geocode_test.rb +0 -97
- data/test/test_helper.rb +0 -46
- data/uninstall.rb +0 -1
data/CHANGELOG
CHANGED
data/README
CHANGED
@@ -1,58 +1,53 @@
|
|
1
1
|
= acts_as_geocodable
|
2
2
|
|
3
|
-
acts_as_geocodable
|
3
|
+
acts_as_geocodable helps you build geo-aware applications. It automatically geocodes your models when they are saved, giving you the ability to search by location and calculate distances between records.
|
4
|
+
|
5
|
+
Beginning with version 2, we require Rails 3. Use one of the 1.0.x tags to work with Rails 2.3.
|
6
|
+
|
7
|
+
Also, we've adopted the ARel style syntax for finding records.
|
4
8
|
|
5
9
|
== Usage
|
6
10
|
|
7
11
|
event = Event.create :street => "777 NE Martin Luther King, Jr. Blvd.",
|
8
12
|
:locality => "Portland", :region => "Oregon", :postal_code => 97232
|
9
|
-
|
13
|
+
|
10
14
|
event.geocode.latitude #=> 45.529100000000
|
11
15
|
event.geocode.longitude #=> -122.644200000000
|
12
|
-
|
13
|
-
event.distance_to "49423" #=> 1807.66560483205
|
14
|
-
|
15
|
-
Event.find(:all, :within => 50, :origin => "97232")
|
16
|
-
|
17
|
-
Event.find(:nearest, :origin => "Portland, OR")
|
18
|
-
|
19
|
-
== Upgrading
|
20
|
-
|
21
|
-
If you're upgrading from a previous version of this plugin, note that :city has been renamed to :locality to be consistent with Graticule 0.2. Create a migration that has:
|
22
16
|
|
23
|
-
|
17
|
+
event.distance_to "49423" #=> 1807.66560483205
|
24
18
|
|
25
|
-
|
19
|
+
Event.origin("97232", :within => 50)
|
26
20
|
|
27
|
-
|
28
|
-
acts_as_geocodable :address => {:street => :address1, :locality => :city,
|
29
|
-
:region => :state, :postal_code => :zip}
|
30
|
-
end
|
21
|
+
Event.origin("Portland, OR").nearest
|
31
22
|
|
32
23
|
== Installation
|
33
24
|
|
34
|
-
|
25
|
+
Install as a gem
|
35
26
|
|
36
|
-
gem install
|
27
|
+
gem install acts_as_geocodable
|
37
28
|
|
38
|
-
|
39
|
-
|
40
|
-
script/plugin install git://github.com/collectiveidea/acts_as_geocodable.git
|
41
|
-
|
42
|
-
Or, install as a gem
|
43
|
-
|
44
|
-
gem install acts_as_geocodable --source=http://gemcutter.org
|
45
|
-
|
29
|
+
Graticule[link:http://rubyforge.org/projects/graticule] is used for all the heavy lifting and will be installed too.
|
46
30
|
|
47
31
|
== Upgrading
|
48
|
-
|
32
|
+
|
49
33
|
Before October 2008, precision wasn't included in the Geocode model. Make sure you add a string precision column to your geocode table if you're upgrading from an older version, and update Graticule.
|
50
34
|
|
35
|
+
Also, if you're upgrading from a previous version of this plugin, note that :city has been renamed to :locality to be consistent with Graticule 0.2. Create a migration that has:
|
36
|
+
|
37
|
+
rename_column :geocodes, :city, :locality
|
38
|
+
|
39
|
+
Also remember to change your mapping in your geocodable classes to use the :locality key instead of :city:
|
40
|
+
|
41
|
+
class Event < ActiveRecord::Base
|
42
|
+
acts_as_geocodable :address => {:street => :address1, :locality => :city,
|
43
|
+
:region => :state, :postal_code => :zip}
|
44
|
+
end
|
45
|
+
|
51
46
|
== Configuration
|
52
47
|
|
53
48
|
Create the required tables
|
54
|
-
|
55
|
-
|
49
|
+
|
50
|
+
rails generate acts_as_geocodable
|
56
51
|
rake db:migrate
|
57
52
|
|
58
53
|
Set the default geocoder in your environment.rb file.
|
@@ -62,13 +57,13 @@ Set the default geocoder in your environment.rb file.
|
|
62
57
|
Then, in each model you want to make geocodable, add acts_as_geocodable.
|
63
58
|
|
64
59
|
class Event < ActiveRecord::Base
|
65
|
-
|
60
|
+
acts_as_geocodable
|
66
61
|
end
|
67
62
|
|
68
63
|
The only requirement is that your model must have address fields. By default, acts_as_geocodable looks for attributes called +street+, +locality+, +region+, +postal_code+, and +country+. To change these, you can provide a mapping in the <tt>:address</tt> option:
|
69
64
|
|
70
65
|
class Event < ActiveRecord::Base
|
71
|
-
|
66
|
+
acts_as_geocodable :address => {:street => :address1, :locality => :city, :region => :state, :postal_code => :zip}
|
72
67
|
end
|
73
68
|
|
74
69
|
If that doesn't meet your needs, simply override the default +to_location+ method in your model, and return a Graticule::Location with those attributes set.
|
@@ -76,7 +71,7 @@ If that doesn't meet your needs, simply override the default +to_location+ metho
|
|
76
71
|
acts_as_geocodable can also update your address fields with the data returned from the geocoding service:
|
77
72
|
|
78
73
|
class Event < ActiveRecord::Base
|
79
|
-
|
74
|
+
acts_as_geocodable :normalize_address => true
|
80
75
|
end
|
81
76
|
|
82
77
|
== IP-based Geocoding
|
@@ -84,8 +79,8 @@ acts_as_geocodable can also update your address fields with the data returned fr
|
|
84
79
|
acts_as_geocodable adds a remote_location method in your controllers that uses http://hostip.info to guess remote users location based on their IP address.
|
85
80
|
|
86
81
|
def index
|
87
|
-
@nearest = Store.
|
88
|
-
@stores = Store.
|
82
|
+
@nearest = Store.origin(remote_location).nearest if remote_location
|
83
|
+
@stores = Store.all
|
89
84
|
end
|
90
85
|
|
91
86
|
Keep in mind that IP-based geocoding is not always accurate, and often will not return any results.
|
@@ -100,5 +95,4 @@ Patches and suggestions are welcome!
|
|
100
95
|
|
101
96
|
== To Do
|
102
97
|
|
103
|
-
* Documentation!!!
|
104
98
|
* configurable formulas
|
data/lib/acts_as_geocodable.rb
CHANGED
@@ -1,307 +1,262 @@
|
|
1
|
+
require 'graticule'
|
1
2
|
require 'acts_as_geocodable/geocoding'
|
2
3
|
require 'acts_as_geocodable/geocode'
|
3
4
|
require 'acts_as_geocodable/remote_location'
|
4
5
|
|
5
|
-
module
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
module ActiveSupport::Callbacks::ClassMethods
|
7
|
+
def without_callback(*args, &block)
|
8
|
+
skip_callback(*args)
|
9
|
+
yield
|
10
|
+
set_callback(*args)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ActsAsGeocodable #:nodoc:
|
15
|
+
# Make a model geocodable.
|
16
|
+
#
|
17
|
+
# class Event < ActiveRecord::Base
|
18
|
+
# acts_as_geocodable
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# == Options
|
22
|
+
# * <tt>:address</tt>: A hash that maps geocodable attirbutes (<tt>:street</tt>,
|
23
|
+
# <tt>:locality</tt>, <tt>:region</tt>, <tt>:postal_code</tt>, <tt>:country</tt>)
|
24
|
+
# to your model's address fields, or a symbol to store the entire address in one field
|
25
|
+
# * <tt>:normalize_address</tt>: If set to true, you address fields will be updated
|
26
|
+
# using the address fields returned by the geocoder. (Default is +false+)
|
27
|
+
# * <tt>:units</tt>: Default units-<tt>:miles</tt> or <tt>:kilometers</tt>-used for
|
28
|
+
# distance calculations and queries. (Default is <tt>:miles</tt>)
|
29
|
+
#
|
30
|
+
def acts_as_geocodable(options = {})
|
31
|
+
options = {
|
32
|
+
:address => {
|
33
|
+
:street => :street, :locality => :locality, :region => :region,
|
34
|
+
:postal_code => :postal_code, :country => :country},
|
35
|
+
:normalize_address => false,
|
36
|
+
:distance_column => 'distance',
|
37
|
+
:units => :miles
|
38
|
+
}.merge(options)
|
39
|
+
|
40
|
+
write_inheritable_attribute :acts_as_geocodable_options, options
|
41
|
+
class_inheritable_reader :acts_as_geocodable_options
|
42
|
+
|
43
|
+
define_callbacks :geocoding
|
44
|
+
|
45
|
+
has_one :geocoding, :as => :geocodable, :include => :geocode, :dependent => :destroy
|
46
|
+
|
47
|
+
after_save :attach_geocode
|
48
|
+
|
49
|
+
# Would love to do a simpler scope here, like:
|
50
|
+
# scope :with_geocode_fields, includes(:geocoding)
|
51
|
+
# But we need to use select() and it would get overwritten.
|
52
|
+
scope :with_geocode_fields, lambda {
|
53
|
+
joins("JOIN geocodings ON
|
54
|
+
#{table_name}.#{primary_key} = geocodings.geocodable_id AND
|
55
|
+
geocodings.geocodable_type = '#{model_name}'
|
56
|
+
JOIN geocodes ON geocodings.geocode_id = geocodes.id")
|
57
|
+
}
|
58
|
+
|
59
|
+
# Use ActiveRecord ARel style syntax for finding records.
|
60
|
+
#
|
61
|
+
# Model.origin("Chicago, IL", :within => 10)
|
62
|
+
#
|
63
|
+
# a +distance+ attribute indicating the distance
|
64
|
+
# to the origin is added to each of the results:
|
65
|
+
#
|
66
|
+
# Model.origin("Portland, OR").first.distance #=> 388.383
|
67
|
+
#
|
68
|
+
# == Options
|
69
|
+
#
|
70
|
+
# * <tt>origin</tt>: A Geocode, String, or geocodable model that specifies
|
71
|
+
# the origin
|
72
|
+
# * <tt>:within</tt>: Limit to results within this radius of the origin
|
73
|
+
# * <tt>:beyond</tt>: Limit to results outside of this radius from the origin
|
74
|
+
# * <tt>:units</tt>: Units to use for <tt>:within</tt> or <tt>:beyond</tt>.
|
75
|
+
# Default is <tt>:miles</tt> unless specified otherwise in the +acts_as_geocodable+
|
76
|
+
# declaration.
|
77
|
+
#
|
78
|
+
scope :origin, lambda {|*args|
|
79
|
+
origin = args[0]
|
80
|
+
options = {
|
81
|
+
:units => acts_as_geocodable_options[:units],
|
82
|
+
}.merge(args[1] || {})
|
83
|
+
distance_sql = sql_for_distance(origin, options[:units])
|
84
|
+
|
85
|
+
scope = with_geocode_fields.select("#{table_name}.*, #{distance_sql} AS
|
86
|
+
#{acts_as_geocodable_options[:distance_column]}")
|
87
|
+
|
88
|
+
scope = scope.where("#{distance_sql} > #{options[:beyond]}") if options[:beyond]
|
89
|
+
scope = scope.where("#{distance_sql} <= #{options[:within]}") if options[:within]
|
90
|
+
scope
|
91
|
+
}
|
92
|
+
|
93
|
+
scope :near, order("#{acts_as_geocodable_options[:distance_column]} ASC")
|
94
|
+
scope :far, order("#{acts_as_geocodable_options[:distance_column]} DESC")
|
95
|
+
|
96
|
+
include ActsAsGeocodable::Model
|
97
|
+
end
|
98
|
+
|
99
|
+
module Model
|
100
|
+
extend ActiveSupport::Concern
|
101
|
+
|
102
|
+
module ClassMethods
|
103
|
+
|
104
|
+
# Find the nearest location to the given origin
|
105
|
+
#
|
106
|
+
# Model.origin("Grand Rapids, MI").nearest
|
107
|
+
#
|
108
|
+
def nearest
|
109
|
+
near.first
|
11
110
|
end
|
12
111
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# == Options
|
22
|
-
# * <tt>:address</tt>: A hash that maps geocodable attirbutes (<tt>:street</tt>,
|
23
|
-
# <tt>:locality</tt>, <tt>:region</tt>, <tt>:postal_code</tt>, <tt>:country</tt>)
|
24
|
-
# to your model's address fields, or a symbol to store the entire address in one field
|
25
|
-
# * <tt>:normalize_address</tt>: If set to true, you address fields will be updated
|
26
|
-
# using the address fields returned by the geocoder. (Default is +false+)
|
27
|
-
# * <tt>:units</tt>: Default units-<tt>:miles</tt> or <tt>:kilometers</tt>-used for
|
28
|
-
# distance calculations and queries. (Default is <tt>:miles</tt>)
|
29
|
-
#
|
30
|
-
def acts_as_geocodable(options = {})
|
31
|
-
options = {
|
32
|
-
:address => {
|
33
|
-
:street => :street, :locality => :locality, :region => :region,
|
34
|
-
:postal_code => :postal_code, :country => :country},
|
35
|
-
:normalize_address => false,
|
36
|
-
:distance_column => 'distance',
|
37
|
-
:units => :miles
|
38
|
-
}.merge(options)
|
39
|
-
|
40
|
-
write_inheritable_attribute :acts_as_geocodable_options, options
|
41
|
-
class_inheritable_reader :acts_as_geocodable_options
|
42
|
-
|
43
|
-
define_callbacks :after_geocoding
|
44
|
-
|
45
|
-
has_one :geocoding, :as => :geocodable, :include => :geocode, :dependent => :destroy
|
46
|
-
|
47
|
-
after_save :attach_geocode
|
48
|
-
|
49
|
-
include CollectiveIdea::Acts::Geocodable::InstanceMethods
|
50
|
-
extend CollectiveIdea::Acts::Geocodable::SingletonMethods
|
51
|
-
end
|
52
|
-
|
112
|
+
# Find the farthest location to the given origin
|
113
|
+
#
|
114
|
+
# Model.origin("Grand Rapids, MI").farthest
|
115
|
+
#
|
116
|
+
def farthest
|
117
|
+
far.first
|
53
118
|
end
|
54
119
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# Whenever find is called with an <tt>:origin</tt>, a +distance+ attribute
|
62
|
-
# indicating the distance to the origin is added to each of the results:
|
63
|
-
#
|
64
|
-
# Model.find(:first, :origin => "Portland, OR").distance #=> 388.383
|
65
|
-
#
|
66
|
-
# +acts_as_geocodable+ adds 2 other retrieval approaches to ActiveRecord's default
|
67
|
-
# find by id, find <tt>:first</tt>, and find <tt>:all</tt>:
|
68
|
-
#
|
69
|
-
# * <tt>:nearest</tt>: find the nearest location to the given origin
|
70
|
-
# * <tt>:farthest</tt>: find the farthest location from the given origin
|
71
|
-
#
|
72
|
-
# Model.find(:nearest, :origin => "Grand Rapids, MI")
|
73
|
-
#
|
74
|
-
# == Options
|
75
|
-
#
|
76
|
-
# * <tt>:origin</tt>: A Geocode, String, or geocodable model that specifies
|
77
|
-
# the origin
|
78
|
-
# * <tt>:within</tt>: Limit to results within this radius of the origin
|
79
|
-
# * <tt>:beyond</tt>: Limit to results outside of this radius from the origin
|
80
|
-
# * <tt>:units</tt>: Units to use for <tt>:within</tt> or <tt>:beyond</tt>.
|
81
|
-
# Default is <tt>:miles</tt> unless specified otherwise in the +acts_as_geocodable+
|
82
|
-
# declaration.
|
83
|
-
#
|
84
|
-
def find(*args)
|
85
|
-
options = args.extract_options!
|
86
|
-
origin = location_to_geocode options.delete(:origin)
|
87
|
-
if origin
|
88
|
-
options[:units] ||= acts_as_geocodable_options[:units]
|
89
|
-
add_distance_to_select!(origin, options)
|
90
|
-
with_proximity!(args, options) do
|
91
|
-
geocode_conditions!(options, origin) do
|
92
|
-
join_geocodes { super *args.push(options) }
|
93
|
-
end
|
94
|
-
end
|
95
|
-
else
|
96
|
-
super *args.push(options)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
# Extends ActiveRecord's count method to be geo-aware.
|
101
|
-
#
|
102
|
-
# Model.count(:within => 10, :origin => "Chicago, IL")
|
103
|
-
#
|
104
|
-
# == Options
|
105
|
-
#
|
106
|
-
# * <tt>:origin</tt>: A Geocode, String, or geocodable model that specifies
|
107
|
-
# the origin
|
108
|
-
# * <tt>:within</tt>: Limit to results within this radius of the origin
|
109
|
-
# * <tt>:beyond</tt>: Limit to results outside of this radius from the origin
|
110
|
-
# * <tt>:units</tt>: Units to use for <tt>:within</tt> or <tt>:beyond</tt>.
|
111
|
-
# Default is <tt>:miles</tt> unless specified otherwise in the +acts_as_geocodable+
|
112
|
-
# declaration.
|
113
|
-
#
|
114
|
-
def count(*args)
|
115
|
-
options = args.extract_options!
|
116
|
-
origin = location_to_geocode options.delete(:origin)
|
117
|
-
if origin
|
118
|
-
options[:units] ||= acts_as_geocodable_options[:units]
|
119
|
-
with_proximity!(args, options) do
|
120
|
-
geocode_conditions!(options, origin) do
|
121
|
-
join_geocodes { super *args.push(options) }
|
122
|
-
end
|
123
|
-
end
|
124
|
-
else
|
125
|
-
super *args.push(options)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
# Convert the given location to a Geocode
|
130
|
-
def location_to_geocode(location)
|
131
|
-
case location
|
132
|
-
when Geocode then location
|
133
|
-
when InstanceMethods then location.geocode
|
134
|
-
when String, Fixnum then Geocode.find_or_create_by_query(location)
|
135
|
-
end
|
120
|
+
# Convert the given location to a Geocode
|
121
|
+
def location_to_geocode(location)
|
122
|
+
case location
|
123
|
+
when Geocode then location
|
124
|
+
when InstanceMethods then location.geocode
|
125
|
+
when String, Fixnum then Geocode.find_or_create_by_query(location)
|
136
126
|
end
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
private
|
169
|
-
|
170
|
-
def add_distance_to_select!(origin, options)
|
171
|
-
(options[:select] ||= "#{table_name}.*") <<
|
172
|
-
", #{sql_for_distance(origin, options[:units])} AS
|
173
|
-
#{acts_as_geocodable_options[:distance_column]}"
|
174
|
-
end
|
175
|
-
|
176
|
-
def with_proximity!(args, options)
|
177
|
-
if [:nearest, :farthest].include?(args.first)
|
178
|
-
raise ArgumentError, ":include cannot be specified with :nearest and :farthest" if options[:include]
|
179
|
-
direction = args.first == :nearest ? "ASC" : "DESC"
|
180
|
-
args[0] = :first
|
181
|
-
with_scope :find => { :order => "#{acts_as_geocodable_options[:distance_column]} #{direction}"} do
|
182
|
-
yield
|
127
|
+
end
|
128
|
+
|
129
|
+
# Validate that the model can be geocoded
|
130
|
+
#
|
131
|
+
# Options:
|
132
|
+
# * <tt>:message</tt>: Added to errors base (Default: Address could not be geocoded.)
|
133
|
+
# * <tt>:allow_nil</tt>: If all the address attributes are blank, then don't try to
|
134
|
+
# validate the geocode (Default: false)
|
135
|
+
# * <tt>:precision</tt>: Require a minimum geocoding precision
|
136
|
+
#
|
137
|
+
# validates_as_geocodable also takes a block that you can use to performa additional
|
138
|
+
# checks on the geocode. If this block returns false, then validation will fail.
|
139
|
+
#
|
140
|
+
# validates_as_geocodable do |geocode|
|
141
|
+
# geocode.country == "US"
|
142
|
+
# end
|
143
|
+
#
|
144
|
+
def validates_as_geocodable(options = {})
|
145
|
+
options = options.reverse_merge :message => "Address could not be geocoded.", :allow_nil => false
|
146
|
+
validate do |model|
|
147
|
+
is_blank = model.to_location.attributes.except(:precision).all?(&:blank?)
|
148
|
+
unless options[:allow_nil] && is_blank
|
149
|
+
geocode = model.send :attach_geocode
|
150
|
+
if !geocode ||
|
151
|
+
(options[:precision] && geocode.precision < options[:precision]) ||
|
152
|
+
(block_given? && yield(geocode) == false)
|
153
|
+
model.errors.add :base, options[:message]
|
183
154
|
end
|
184
|
-
else
|
185
|
-
yield
|
186
155
|
end
|
187
156
|
end
|
188
|
-
|
189
|
-
def join_geocodes(&block)
|
190
|
-
with_scope :find => { :joins => "JOIN geocodings ON
|
191
|
-
#{table_name}.#{primary_key} = geocodings.geocodable_id AND
|
192
|
-
geocodings.geocodable_type = '#{class_name}'
|
193
|
-
JOIN geocodes ON geocodings.geocode_id = geocodes.id" } do
|
194
|
-
yield
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def geocode_conditions!(options, origin)
|
199
|
-
units = options.delete(:units)
|
200
|
-
conditions = []
|
201
|
-
conditions << "#{sql_for_distance(origin, units)} <= #{options.delete(:within)}" if options[:within]
|
202
|
-
conditions << "#{sql_for_distance(origin, units)} > #{options.delete(:beyond)}" if options[:beyond]
|
203
|
-
if conditions.empty?
|
204
|
-
yield
|
205
|
-
else
|
206
|
-
with_scope(:find => { :conditions => conditions.join(" AND ") }) { yield }
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
def sql_for_distance(origin, units = acts_as_geocodable_options[:units])
|
211
|
-
Graticule::Distance::Spherical.to_sql(
|
212
|
-
:latitude => origin.latitude,
|
213
|
-
:longitude => origin.longitude,
|
214
|
-
:latitude_column => "geocodes.latitude",
|
215
|
-
:longitude_column => "geocodes.longitude",
|
216
|
-
:units => units
|
217
|
-
)
|
218
|
-
end
|
219
|
-
|
220
157
|
end
|
221
158
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
159
|
+
private
|
160
|
+
|
161
|
+
def sql_for_distance(origin, units = acts_as_geocodable_options[:units])
|
162
|
+
origin = location_to_geocode(origin)
|
163
|
+
Graticule::Distance::Spherical.to_sql(
|
164
|
+
:latitude => origin.latitude,
|
165
|
+
:longitude => origin.longitude,
|
166
|
+
:latitude_column => "geocodes.latitude",
|
167
|
+
:longitude_column => "geocodes.longitude",
|
168
|
+
:units => units
|
169
|
+
)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
module InstanceMethods
|
174
|
+
|
175
|
+
# Get the geocode for this model
|
176
|
+
def geocode
|
177
|
+
geocoding.geocode if geocoding
|
178
|
+
end
|
179
|
+
|
180
|
+
# Create a Graticule::Location
|
181
|
+
def to_location
|
182
|
+
Graticule::Location.new.tap do |location|
|
183
|
+
[:street, :locality, :region, :postal_code, :country].each do |attr|
|
184
|
+
location.send "#{attr}=", geo_attribute(attr)
|
235
185
|
end
|
236
186
|
end
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
187
|
+
end
|
188
|
+
|
189
|
+
# Get the distance to the given destination. The destination can be an
|
190
|
+
# acts_as_geocodable model, a Geocode, or a string
|
191
|
+
#
|
192
|
+
# myhome.distance_to "Chicago, IL"
|
193
|
+
# myhome.distance_to "49423"
|
194
|
+
# myhome.distance_to other_model
|
195
|
+
#
|
196
|
+
# == Options
|
197
|
+
# * <tt>:units</tt>: <tt>:miles</tt> or <tt>:kilometers</tt>
|
198
|
+
# * <tt>:formula</tt>: The formula to use to calculate the distance. This can
|
199
|
+
# be any formula supported by Graticule. The default is <tt>:haversine</tt>.
|
200
|
+
#
|
201
|
+
def distance_to(destination, options = {})
|
202
|
+
units = options[:units] || acts_as_geocodable_options[:units]
|
203
|
+
formula = options[:formula] || :haversine
|
204
|
+
|
205
|
+
geocode = self.class.location_to_geocode(destination)
|
206
|
+
self.geocode.distance_to(geocode, units, formula)
|
207
|
+
end
|
208
|
+
|
209
|
+
protected
|
210
|
+
|
211
|
+
# Perform the geocoding
|
212
|
+
def attach_geocode
|
213
|
+
new_geocode = Geocode.find_or_create_by_location self.to_location unless self.to_location.blank?
|
214
|
+
if new_geocode && self.geocode != new_geocode
|
215
|
+
run_callbacks :geocoding do
|
264
216
|
self.geocoding = Geocoding.new :geocode => new_geocode
|
265
217
|
self.update_address self.acts_as_geocodable_options[:normalize_address]
|
266
|
-
callback :after_geocoding
|
267
|
-
elsif !new_geocode && self.geocoding
|
268
|
-
self.geocoding.destroy
|
269
218
|
end
|
270
|
-
|
271
|
-
|
272
|
-
logger.warn e.message
|
219
|
+
elsif !new_geocode && self.geocoding
|
220
|
+
self.geocoding.destroy
|
273
221
|
end
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
222
|
+
new_geocode
|
223
|
+
rescue Graticule::Error => e
|
224
|
+
logger.warn e.message
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
def update_address(force = false) #:nodoc:
|
229
|
+
unless self.geocode.blank?
|
230
|
+
if self.acts_as_geocodable_options[:address].is_a? Symbol
|
231
|
+
method = self.acts_as_geocodable_options[:address]
|
232
|
+
if self.respond_to?("#{method}=") && (self.send(method).blank? || force)
|
233
|
+
self.send "#{method}=", self.geocode.to_location.to_s
|
234
|
+
end
|
235
|
+
else
|
236
|
+
self.acts_as_geocodable_options[:address].each do |attribute,method|
|
280
237
|
if self.respond_to?("#{method}=") && (self.send(method).blank? || force)
|
281
|
-
self.send "#{method}=", self.geocode.
|
282
|
-
end
|
283
|
-
else
|
284
|
-
self.acts_as_geocodable_options[:address].each do |attribute,method|
|
285
|
-
if self.respond_to?("#{method}=") && (self.send(method).blank? || force)
|
286
|
-
self.send "#{method}=", self.geocode.send(attribute)
|
287
|
-
end
|
238
|
+
self.send "#{method}=", self.geocode.send(attribute)
|
288
239
|
end
|
289
240
|
end
|
290
|
-
|
291
|
-
update_without_callbacks
|
292
241
|
end
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
if self.acts_as_geocodable_options[:address].is_a? Symbol
|
297
|
-
attr_name = self.acts_as_geocodable_options[:address]
|
298
|
-
attr_key == :street ? self.send(attr_name) : nil
|
299
|
-
else
|
300
|
-
attr_name = self.acts_as_geocodable_options[:address][attr_key]
|
301
|
-
attr_name && self.respond_to?(attr_name) ? self.send(attr_name) : nil
|
242
|
+
|
243
|
+
self.class.without_callback(:save, :after, :attach_geocode) do
|
244
|
+
save
|
302
245
|
end
|
303
246
|
end
|
304
247
|
end
|
248
|
+
|
249
|
+
def geo_attribute(attr_key) #:nodoc:
|
250
|
+
if self.acts_as_geocodable_options[:address].is_a? Symbol
|
251
|
+
attr_name = self.acts_as_geocodable_options[:address]
|
252
|
+
attr_key == :street ? self.send(attr_name) : nil
|
253
|
+
else
|
254
|
+
attr_name = self.acts_as_geocodable_options[:address][attr_key]
|
255
|
+
attr_name && self.respond_to?(attr_name) ? self.send(attr_name) : nil
|
256
|
+
end
|
257
|
+
end
|
305
258
|
end
|
306
259
|
end
|
307
|
-
end
|
260
|
+
end
|
261
|
+
|
262
|
+
ActiveRecord::Base.send :extend, ActsAsGeocodable
|