acts_as_geocodable 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/.gitignore
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
0.2.1 - 2008-8-8
|
2
|
+
* Results are now WillPaginate compatible
|
3
|
+
|
4
|
+
0.2.0 - 2007-10-27
|
5
|
+
* Added validates_as_geocodable (Mark Van Holstyn)
|
6
|
+
* Allow address mapping to be a single field (Mark Van Holstyn)
|
7
|
+
|
8
|
+
0.1.0
|
9
|
+
* Added remote_location to get a users location based on their remote_ip
|
10
|
+
* renamed :city to :locality in address mapping to be consistent with Graticule 0.2
|
11
|
+
create a migration with:
|
12
|
+
rename_column :geocodes, :city, :locality
|
13
|
+
* replace #full_address with #to_location
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Copyright (c) 2006 Daniel Morrison, Collective Idea
|
2
|
+
http://collectiveidea.com
|
3
|
+
|
4
|
+
Testing code was heavily influenced by Rick Olson. Thanks, Rick!
|
5
|
+
Testing Portions Copyright (c) 2006 Rick Olson
|
6
|
+
http://techno-weenie.net
|
7
|
+
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
9
|
+
a copy of this software and associated documentation files (the
|
10
|
+
"Software"), to deal in the Software without restriction, including
|
11
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
12
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
13
|
+
permit persons to whom the Software is furnished to do so, subject to
|
14
|
+
the following conditions:
|
15
|
+
|
16
|
+
The above copyright notice and this permission notice shall be
|
17
|
+
included in all copies or substantial portions of the Software.
|
18
|
+
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
20
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
21
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
22
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
23
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
24
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
25
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
= acts_as_geocodable
|
2
|
+
|
3
|
+
acts_as_geocodable is a plugin to help 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
|
+
== Usage
|
6
|
+
|
7
|
+
event = Event.create :street => "777 NE Martin Luther King, Jr. Blvd.",
|
8
|
+
:locality => "Portland", :region => "Oregon", :postal_code => 97232
|
9
|
+
|
10
|
+
event.geocode.latitude #=> 45.529100000000
|
11
|
+
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
|
+
|
23
|
+
rename_column :geocodes, :city, :locality
|
24
|
+
|
25
|
+
Also remember to change your mapping in your geocodable classes to use the :locality key instead of :city:
|
26
|
+
|
27
|
+
class Event < ActiveRecord::Base
|
28
|
+
acts_as_geocodable :address => {:street => :address1, :locality => :city,
|
29
|
+
:region => :state, :postal_code => :zip}
|
30
|
+
end
|
31
|
+
|
32
|
+
== Installation
|
33
|
+
|
34
|
+
Graticule[link:http://rubyforge.org/projects/graticule] is used for all the heavy lifting.
|
35
|
+
|
36
|
+
gem install graticule --include-dependencies
|
37
|
+
|
38
|
+
Install as a plugin
|
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
|
+
|
46
|
+
|
47
|
+
== Upgrading
|
48
|
+
|
49
|
+
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
|
+
|
51
|
+
== Configuration
|
52
|
+
|
53
|
+
Create the required tables
|
54
|
+
|
55
|
+
script/generate geocodable_migration add_geocodable_tables
|
56
|
+
rake db:migrate
|
57
|
+
|
58
|
+
Set the default geocoder in your environment.rb file.
|
59
|
+
|
60
|
+
Geocode.geocoder = Graticule.service(:yahoo).new 'your_api_key'
|
61
|
+
|
62
|
+
Then, in each model you want to make geocodable, add acts_as_geocodable.
|
63
|
+
|
64
|
+
class Event < ActiveRecord::Base
|
65
|
+
acts_as_geocodable
|
66
|
+
end
|
67
|
+
|
68
|
+
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
|
+
|
70
|
+
class Event < ActiveRecord::Base
|
71
|
+
acts_as_geocodable :address => {:street => :address1, :locality => :city, :region => :state, :postal_code => :zip}
|
72
|
+
end
|
73
|
+
|
74
|
+
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.
|
75
|
+
|
76
|
+
acts_as_geocodable can also update your address fields with the data returned from the geocoding service:
|
77
|
+
|
78
|
+
class Event < ActiveRecord::Base
|
79
|
+
acts_as_geocodable :normalize_address => true
|
80
|
+
end
|
81
|
+
|
82
|
+
== IP-based Geocoding
|
83
|
+
|
84
|
+
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
|
+
|
86
|
+
def index
|
87
|
+
@nearest = Store.find(:nearest, :origin => remote_location) if remote_location
|
88
|
+
@stores = Store.find(:all)
|
89
|
+
end
|
90
|
+
|
91
|
+
Keep in mind that IP-based geocoding is not always accurate, and often will not return any results.
|
92
|
+
|
93
|
+
== Development
|
94
|
+
|
95
|
+
The source code is available at:
|
96
|
+
http://github.com/collectiveidea/acts_as_geocodable
|
97
|
+
git://github.com/collectiveidea/acts_as_geocodable.git
|
98
|
+
|
99
|
+
Patches and suggestions are welcome!
|
100
|
+
|
101
|
+
== To Do
|
102
|
+
|
103
|
+
* Documentation!!!
|
104
|
+
* configurable formulas
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'load_multi_rails_rake_tasks'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
|
6
|
+
desc 'Default: run unit tests.'
|
7
|
+
task :default => :test
|
8
|
+
|
9
|
+
desc 'Test the acts_as_geocodable plugin.'
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Generate documentation for the acts_as_geocodable plugin.'
|
17
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'ActsAsGeocodable'
|
20
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
21
|
+
rdoc.rdoc_files.include('README')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
require 'jeweler'
|
27
|
+
Jeweler::Tasks.new do |gemspec|
|
28
|
+
gemspec.name = 'acts_as_geocodable'
|
29
|
+
gemspec.summary = 'Simple geocoding for Rails ActiveRecord models'
|
30
|
+
gemspec.description = 'Simple geocoding for Rails ActiveRecord models. See the README for more details.'
|
31
|
+
gemspec.email = 'info@collectiveidea.com'
|
32
|
+
gemspec.homepage = 'http://github.com/collectiveidea/acts_as_geocodable'
|
33
|
+
gemspec.authors = ['Daniel Morrison', 'Brandon Keepers']
|
34
|
+
end
|
35
|
+
Jeweler::GemcutterTasks.new
|
36
|
+
rescue LoadError
|
37
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
38
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/about.yml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
author:
|
2
|
+
name: Brandon Keepers and Daniel Morrison, Collective Idea
|
3
|
+
homepage: http://collectiveidea.com
|
4
|
+
summary: A plugin to help build geo-aware applications
|
5
|
+
homepage: http://opensoul.org/2007/2/13/geocoding-as-easy-as-1-2
|
6
|
+
plugin: http://source.collectiveidea.com/public/rails/plugins/acts_as_geocodable
|
7
|
+
license: MIT
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{acts_as_geocodable}
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Daniel Morrison", "Brandon Keepers"]
|
12
|
+
s.date = %q{2009-10-21}
|
13
|
+
s.description = %q{Simple geocoding for Rails ActiveRecord models. See the README for more details.}
|
14
|
+
s.email = %q{info@collectiveidea.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"CHANGELOG",
|
21
|
+
"MIT-LICENSE",
|
22
|
+
"README",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION",
|
25
|
+
"about.yml",
|
26
|
+
"acts_as_geocodable.gemspec",
|
27
|
+
"generators/geocodable_migration/USAGE",
|
28
|
+
"generators/geocodable_migration/geocodable_migration_generator.rb",
|
29
|
+
"generators/geocodable_migration/templates/migration.rb",
|
30
|
+
"install.rb",
|
31
|
+
"lib/acts_as_geocodable.rb",
|
32
|
+
"lib/acts_as_geocodable/geocode.rb",
|
33
|
+
"lib/acts_as_geocodable/geocoding.rb",
|
34
|
+
"lib/acts_as_geocodable/remote_location.rb",
|
35
|
+
"lib/acts_as_geocodable/tasks/acts_as_geocodable_tasks.rake",
|
36
|
+
"rails/init.rb",
|
37
|
+
"test/acts_as_geocodable_test.rb",
|
38
|
+
"test/db/database.yml",
|
39
|
+
"test/db/schema.rb",
|
40
|
+
"test/fixtures/cities.yml",
|
41
|
+
"test/fixtures/geocodes.yml",
|
42
|
+
"test/fixtures/geocodings.yml",
|
43
|
+
"test/fixtures/vacations.yml",
|
44
|
+
"test/geocode_test.rb",
|
45
|
+
"test/test_helper.rb",
|
46
|
+
"uninstall.rb"
|
47
|
+
]
|
48
|
+
s.homepage = %q{http://github.com/collectiveidea/acts_as_geocodable}
|
49
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
50
|
+
s.require_paths = ["lib"]
|
51
|
+
s.rubygems_version = %q{1.3.5}
|
52
|
+
s.summary = %q{Simple geocoding for Rails ActiveRecord models}
|
53
|
+
s.test_files = [
|
54
|
+
"test/acts_as_geocodable_test.rb",
|
55
|
+
"test/db/schema.rb",
|
56
|
+
"test/geocode_test.rb",
|
57
|
+
"test/test_helper.rb"
|
58
|
+
]
|
59
|
+
|
60
|
+
if s.respond_to? :specification_version then
|
61
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
62
|
+
s.specification_version = 3
|
63
|
+
|
64
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
65
|
+
else
|
66
|
+
end
|
67
|
+
else
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Description:
|
2
|
+
The geocodable migration generator creates a migration which you can use to generate two tables:
|
3
|
+
|
4
|
+
geocodes - Contains the latitude and longitude coordinates, with the address they refer to.
|
5
|
+
geocodings - The polymorphic join table.
|
6
|
+
|
7
|
+
|
8
|
+
Example:
|
9
|
+
./script/generate geocodable_migration add_geocodable_tables
|
10
|
+
|
11
|
+
With 4 existing migrations, this will create an AddGeocodableTables migration in the
|
12
|
+
file db/migrate/5_add_geocodable_tables.rb
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class <%= class_name %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table "geocodes" do |t|
|
4
|
+
t.column "latitude", :decimal, :precision => 15, :scale => 12
|
5
|
+
t.column "longitude", :decimal, :precision => 15, :scale => 12
|
6
|
+
t.column "query", :string
|
7
|
+
t.column "street", :string
|
8
|
+
t.column "locality", :string
|
9
|
+
t.column "region", :string
|
10
|
+
t.column "postal_code", :string
|
11
|
+
t.column "country", :string
|
12
|
+
t.column "precision", :string
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index "geocodes", ["longitude"], :name => "geocodes_longitude_index"
|
16
|
+
add_index "geocodes", ["latitude"], :name => "geocodes_latitude_index"
|
17
|
+
add_index "geocodes", ["query"], :name => "geocodes_query_index", :unique => true
|
18
|
+
add_index "geocodes", ["locality"], :name => "geocodes_locality_index"
|
19
|
+
add_index "geocodes", ["region"], :name => "geocodes_region_index"
|
20
|
+
add_index "geocodes", ["postal_code"], :name => "geocodes_postal_code_index"
|
21
|
+
add_index "geocodes", ["country"], :name => "geocodes_country_index"
|
22
|
+
add_index "geocodes", ["precision"], :name => "geocodes_precision_index"
|
23
|
+
|
24
|
+
|
25
|
+
create_table "geocodings" do |t|
|
26
|
+
t.column "geocodable_id", :integer
|
27
|
+
t.column "geocode_id", :integer
|
28
|
+
t.column "geocodable_type", :string
|
29
|
+
end
|
30
|
+
|
31
|
+
add_index "geocodings", ["geocodable_type"], :name => "geocodings_geocodable_type_index"
|
32
|
+
add_index "geocodings", ["geocode_id"], :name => "geocodings_geocode_id_index"
|
33
|
+
add_index "geocodings", ["geocodable_id"], :name => "geocodings_geocodable_id_index"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.down
|
37
|
+
drop_table :geocodes
|
38
|
+
drop_table :geocodings
|
39
|
+
end
|
40
|
+
end
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,286 @@
|
|
1
|
+
require 'acts_as_geocodable/geocoding'
|
2
|
+
require 'acts_as_geocodable/geocode'
|
3
|
+
require 'acts_as_geocodable/remote_location'
|
4
|
+
|
5
|
+
module CollectiveIdea #:nodoc:
|
6
|
+
module Acts #:nodoc:
|
7
|
+
module Geocodable #:nodoc:
|
8
|
+
|
9
|
+
def self.included(mod)
|
10
|
+
mod.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
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 :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
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
module SingletonMethods
|
56
|
+
|
57
|
+
# Extends ActiveRecord's find method to be geo-aware.
|
58
|
+
#
|
59
|
+
# Model.find(:all, :within => 10, :origin => "Chicago, IL")
|
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
|
136
|
+
end
|
137
|
+
|
138
|
+
def validates_as_geocodable(options = {})
|
139
|
+
options = options.reverse_merge :message => "Address could not be geocoded.", :allow_nil => false
|
140
|
+
validate do |geocodable|
|
141
|
+
if !(options[:allow_nil] && geocodable.to_location.attributes.all?(&:blank?)) &&
|
142
|
+
!Geocode.find_or_create_by_location(geocodable.to_location)
|
143
|
+
geocodable.errors.add_to_base options[:message]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def add_distance_to_select!(origin, options)
|
151
|
+
(options[:select] ||= "#{table_name}.*") <<
|
152
|
+
", #{sql_for_distance(origin, options[:units])} AS
|
153
|
+
#{acts_as_geocodable_options[:distance_column]}"
|
154
|
+
end
|
155
|
+
|
156
|
+
def with_proximity!(args, options)
|
157
|
+
if [:nearest, :farthest].include?(args.first)
|
158
|
+
raise ArgumentError, ":include cannot be specified with :nearest and :farthest" if options[:include]
|
159
|
+
direction = args.first == :nearest ? "ASC" : "DESC"
|
160
|
+
args[0] = :first
|
161
|
+
with_scope :find => { :order => "#{acts_as_geocodable_options[:distance_column]} #{direction}"} do
|
162
|
+
yield
|
163
|
+
end
|
164
|
+
else
|
165
|
+
yield
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def join_geocodes(&block)
|
170
|
+
with_scope :find => { :joins => "JOIN geocodings ON
|
171
|
+
#{table_name}.#{primary_key} = geocodings.geocodable_id AND
|
172
|
+
geocodings.geocodable_type = '#{class_name}'
|
173
|
+
JOIN geocodes ON geocodings.geocode_id = geocodes.id" } do
|
174
|
+
yield
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def geocode_conditions!(options, origin)
|
179
|
+
units = options.delete(:units)
|
180
|
+
conditions = []
|
181
|
+
conditions << "#{sql_for_distance(origin, units)} <= #{options.delete(:within)}" if options[:within]
|
182
|
+
conditions << "#{sql_for_distance(origin, units)} > #{options.delete(:beyond)}" if options[:beyond]
|
183
|
+
if conditions.empty?
|
184
|
+
yield
|
185
|
+
else
|
186
|
+
with_scope(:find => { :conditions => conditions.join(" AND ") }) { yield }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def sql_for_distance(origin, units = acts_as_geocodable_options[:units])
|
191
|
+
Graticule::Distance::Spherical.to_sql(
|
192
|
+
:latitude => origin.latitude,
|
193
|
+
:longitude => origin.longitude,
|
194
|
+
:latitude_column => "geocodes.latitude",
|
195
|
+
:longitude_column => "geocodes.longitude",
|
196
|
+
:units => units
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
module InstanceMethods
|
203
|
+
|
204
|
+
# Get the geocode for this model
|
205
|
+
def geocode
|
206
|
+
geocoding.geocode if geocoding
|
207
|
+
end
|
208
|
+
|
209
|
+
# Create a Graticule::Location
|
210
|
+
def to_location
|
211
|
+
returning Graticule::Location.new do |location|
|
212
|
+
[:street, :locality, :region, :postal_code, :country].each do |attr|
|
213
|
+
location.send "#{attr}=", geo_attribute(attr)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Get the distance to the given destination. The destination can be an
|
219
|
+
# acts_as_geocodable model, a Geocode, or a string
|
220
|
+
#
|
221
|
+
# myhome.distance_to "Chicago, IL"
|
222
|
+
# myhome.distance_to "49423"
|
223
|
+
# myhome.distance_to other_model
|
224
|
+
#
|
225
|
+
# == Options
|
226
|
+
# * <tt>:units</tt>: <tt>:miles</tt> or <tt>:kilometers</tt>
|
227
|
+
# * <tt>:formula</tt>: The formula to use to calculate the distance. This can
|
228
|
+
# be any formula supported by Graticule. The default is <tt>:haversine</tt>.
|
229
|
+
#
|
230
|
+
def distance_to(destination, options = {})
|
231
|
+
units = options[:units] || acts_as_geocodable_options[:units]
|
232
|
+
formula = options[:formula] || :haversine
|
233
|
+
|
234
|
+
geocode = self.class.location_to_geocode(destination)
|
235
|
+
self.geocode.distance_to(geocode, units, formula)
|
236
|
+
end
|
237
|
+
|
238
|
+
protected
|
239
|
+
|
240
|
+
# Perform the geocoding
|
241
|
+
def attach_geocode
|
242
|
+
new_geocode = Geocode.find_or_create_by_location self.to_location unless self.to_location.blank?
|
243
|
+
if new_geocode && self.geocode != new_geocode
|
244
|
+
self.geocoding = Geocoding.new :geocode => new_geocode
|
245
|
+
self.update_address self.acts_as_geocodable_options[:normalize_address]
|
246
|
+
callback :after_geocoding
|
247
|
+
elsif !new_geocode && self.geocoding
|
248
|
+
self.geocoding.destroy
|
249
|
+
end
|
250
|
+
rescue Graticule::Error => e
|
251
|
+
logger.warn e.message
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
def update_address(force = false)
|
256
|
+
unless self.geocode.blank?
|
257
|
+
if self.acts_as_geocodable_options[:address].is_a? Symbol
|
258
|
+
method = self.acts_as_geocodable_options[:address]
|
259
|
+
if self.respond_to?("#{method}=") && (self.send(method).blank? || force)
|
260
|
+
self.send "#{method}=", self.geocode.to_location.to_s
|
261
|
+
end
|
262
|
+
else
|
263
|
+
self.acts_as_geocodable_options[:address].each do |attribute,method|
|
264
|
+
if self.respond_to?("#{method}=") && (self.send(method).blank? || force)
|
265
|
+
self.send "#{method}=", self.geocode.send(attribute)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
update_without_callbacks
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def geo_attribute(attr_key)
|
275
|
+
if self.acts_as_geocodable_options[:address].is_a? Symbol
|
276
|
+
attr_name = self.acts_as_geocodable_options[:address]
|
277
|
+
attr_key == :street ? self.send(attr_name) : nil
|
278
|
+
else
|
279
|
+
attr_name = self.acts_as_geocodable_options[:address][attr_key]
|
280
|
+
attr_name && self.respond_to?(attr_name) ? self.send(attr_name) : nil
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|