rails-geocoder 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Alex Reisner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,71 @@
1
+ = Geocoder
2
+
3
+ Geocoder adds database-agnostic object geocoding to Rails (via Google). It does not rely on proprietary database functions so reasonably accurate distances can be calculated in MySQL or even SQLite.
4
+
5
+ == Setup
6
+
7
+ Install either *as a plugin*:
8
+
9
+ script/plugin install git://github.com/alexreisner/geocoder.git
10
+
11
+ or *as a gem*:
12
+
13
+ # add to config/environment.rb:
14
+ config.gem "rails-geocoder", :lib => "geocoder", :source => "http://gemcutter.org/"
15
+
16
+ # at command prompt:
17
+ sudo rake gems:install
18
+
19
+ To add geocoding features to a class:
20
+
21
+ geocoded_by :location
22
+
23
+ Be sure your class defines attributes for storing latitude and longitude (use +float+ or +double+ database columns) and a location (human-readable address to be geocoded). These attribute names are all configurable; for example, to use +address+, +lat+, and +lon+ respectively:
24
+
25
+ geocoded_by :address, :latitude => :lat, :longitude => :lon
26
+
27
+ A geocodable string is anything you'd use to search Google Maps. Any of the following are acceptable:
28
+
29
+ 714 Green St, Big Town, MO
30
+ Eiffel Tower, Paris, FR
31
+ Paris, TX, US
32
+
33
+ If your model has +address+, +city+, +state+, and +country+ attributes your +location+ method might look something like this:
34
+
35
+ def location
36
+ [address, city, state, country].compact.join(', ')
37
+ end
38
+
39
+
40
+ == Features
41
+
42
+ Assuming +Venue+ is a geocoded model:
43
+
44
+ Venue.find_near('Omaha, NE, US', 20) # venues within 20 miles of Omaha
45
+ Venue.find_near([40.71, 100.23], 20) # venues within 20 miles of a point
46
+ Venue.geocoded # venues with coordinates
47
+ Venue.not_geocoded # venues without coordinates
48
+
49
+ Assuming +obj+ has a valid string for its +location+:
50
+
51
+ obj.fetch_coordinates # returns coordinates [lat, lon]
52
+ obj.fetch_coordinates! # also writes coordinates to object
53
+
54
+ Assuming +obj+ is geocoded (has latitude and longitude):
55
+
56
+ obj.nearbys(30) # other objects within 30 miles
57
+ obj.distance_to(40.714, -100.234) # distance to arbitrary point
58
+
59
+ Some utility methods are also available:
60
+
61
+ # distance (in miles) between Eiffel Tower and Empire State Building
62
+ Geocoder.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
63
+
64
+ # look up coordinates of some location (like searching Google Maps)
65
+ Geocoder.fetch_coordinates("25 Main St, Cooperstown, NY")
66
+
67
+
68
+ Please see the code for more methods and detailed information about arguments (eg, working with kilometers).
69
+
70
+
71
+ Copyright (c) 2009 Alex Reisner, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rails-geocoder"
8
+ gem.summary = %Q{Add geocoding functionality to Rails models.}
9
+ gem.description = %Q{Add geocoding functionality to Rails models.}
10
+ gem.email = "alex@alexreisner.com"
11
+ gem.homepage = "http://github.com/alexreisner/geocoder"
12
+ gem.authors = ["Alex Reisner"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/*_test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ if File.exist?('VERSION')
47
+ version = File.read('VERSION')
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "geocoder #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.8.0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'geocoder'
data/lib/geocoder.rb ADDED
@@ -0,0 +1,254 @@
1
+ ##
2
+ # Add geocoding functionality (via Google) to any object.
3
+ #
4
+ module Geocoder
5
+
6
+ ##
7
+ # Implementation of 'included' hook method.
8
+ #
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ base.class_eval do
12
+
13
+ # named scope: geocoded objects
14
+ named_scope :geocoded,
15
+ :conditions => "#{geocoder_options[:latitude]} IS NOT NULL " +
16
+ "AND #{geocoder_options[:longitude]} IS NOT NULL"
17
+
18
+ # named scope: not-geocoded objects
19
+ named_scope :not_geocoded,
20
+ :conditions => "#{geocoder_options[:latitude]} IS NULL " +
21
+ "OR #{geocoder_options[:longitude]} IS NULL"
22
+ end
23
+ end
24
+
25
+ ##
26
+ # Methods which will be class methods of the including class.
27
+ #
28
+ module ClassMethods
29
+
30
+ ##
31
+ # Find all objects within a radius (in miles) of the given location
32
+ # (address string). Location (the first argument) may be either a string
33
+ # to geocode or an array of coordinates (<tt>[lat,long]</tt>).
34
+ #
35
+ def find_near(location, radius = 20, options = {})
36
+ latitude, longitude = location.is_a?(Array) ?
37
+ location : Geocoder.fetch_coordinates(location)
38
+ return [] unless (latitude and longitude)
39
+ all(find_near_options(latitude, longitude, radius, options))
40
+ end
41
+
42
+ ##
43
+ # Get options hash suitable for passing to ActiveRecord.find to get
44
+ # records within a radius (in miles) of the given point.
45
+ # Taken from excellent tutorial at:
46
+ # http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
47
+ #
48
+ # Options hash may include:
49
+ #
50
+ # +order+ :: column(s) for ORDER BY SQL clause
51
+ # +limit+ :: number of records to return (for LIMIT SQL clause)
52
+ # +offset+ :: number of records to skip (for LIMIT SQL clause)
53
+ #
54
+ def find_near_options(latitude, longitude, radius = 20, options = {})
55
+
56
+ # set defaults/clean up arguments
57
+ options[:order] ||= 'distance ASC'
58
+ radius = radius.to_i
59
+
60
+ # constrain search to a (radius x radius) square
61
+ factor = (Math::cos(latitude * Math::PI / 180.0) * 69.0).abs
62
+ lon_lo = longitude - (radius / factor);
63
+ lon_hi = longitude + (radius / factor);
64
+ lat_lo = latitude - (radius / 69.0);
65
+ lat_hi = latitude + (radius / 69.0);
66
+
67
+ # build limit clause
68
+ limit = nil
69
+ if options[:limit] or options[:offset]
70
+ options[:offset] ||= 0
71
+ limit = "#{options[:offset]},#{options[:limit]}"
72
+ end
73
+
74
+ # generate hash
75
+ lat_attr = geocoder_options[:latitude]
76
+ lon_attr = geocoder_options[:longitude]
77
+ {
78
+ :select => "*, 3956 * 2 * ASIN(SQRT(" +
79
+ "POWER(SIN((#{latitude} - #{lat_attr}) * " +
80
+ "PI() / 180 / 2), 2) + COS(#{latitude} * PI()/180) * " +
81
+ "COS(#{lat_attr} * PI() / 180) * " +
82
+ "POWER(SIN((#{longitude} - #{lon_attr}) * " +
83
+ "PI() / 180 / 2), 2) )) as distance",
84
+ :conditions => [
85
+ "#{lat_attr} BETWEEN ? AND ? AND " +
86
+ "#{lon_attr} BETWEEN ? AND ?",
87
+ lat_lo, lat_hi, lon_lo, lon_hi],
88
+ :having => "distance <= #{radius}",
89
+ :order => options[:order],
90
+ :limit => limit
91
+ }
92
+ end
93
+
94
+ ##
95
+ # Get the coordinates [lat,lon] of an object. This is not great but it
96
+ # seems cleaner than polluting the object method namespace.
97
+ #
98
+ def _get_coordinates(object)
99
+ [object.send(geocoder_options[:latitude]),
100
+ object.send(geocoder_options[:longitude])]
101
+ end
102
+ end
103
+
104
+ ##
105
+ # Is this object geocoded? (Does it have latitude and longitude?)
106
+ #
107
+ def geocoded?
108
+ self.class._get_coordinates(self).compact.size > 0
109
+ end
110
+
111
+ ##
112
+ # Calculate the distance from the object to a point (lat,lon). Valid units
113
+ # are defined in <tt>distance_between</tt> class method.
114
+ #
115
+ def distance_to(lat, lon, units = :mi)
116
+ return nil unless geocoded?
117
+ mylat,mylon = self.class._get_coordinates(self)
118
+ Geocoder.distance_between(mylat, mylon, lat, lon, :units => units)
119
+ end
120
+
121
+ ##
122
+ # Get other geocoded objects within a given radius.
123
+ # The object must be geocoded before this method is called.
124
+ #
125
+ def nearbys(radius = 20)
126
+ return [] unless geocoded?
127
+ lat,lon = self.class._get_coordinates(self)
128
+ self.class.find_near([lat, lon], radius) - [self]
129
+ end
130
+
131
+ ##
132
+ # Fetch coordinates based on the object's location.
133
+ # Returns an array <tt>[lat,lon]</tt>.
134
+ #
135
+ def fetch_coordinates
136
+ location = read_attribute(self.class.geocoder_options[:method_name])
137
+ Geocoder.fetch_coordinates(location)
138
+ end
139
+
140
+ ##
141
+ # Fetch coordinates and assign +latitude+ and +longitude+.
142
+ #
143
+ def fetch_coordinates!
144
+ returning fetch_coordinates do |c|
145
+ unless c.blank?
146
+ write_attribute(self.class.geocoder_options[:latitude], c[0])
147
+ write_attribute(self.class.geocoder_options[:longitude], c[1])
148
+ end
149
+ end
150
+ end
151
+
152
+ ##
153
+ # Query Google for the coordinates of the given phrase.
154
+ # Returns array [lat,lon] if found, nil if not found or if network error.
155
+ #
156
+ def self.fetch_coordinates(query)
157
+ return nil unless doc = self.search(query)
158
+
159
+ # make sure search found a result
160
+ e = doc.elements['kml/Response/Status/code']
161
+ return nil unless (e and e.text == "200")
162
+
163
+ # isolate the relevant part of the result
164
+ place = doc.elements['kml/Response/Placemark']
165
+
166
+ # if there are multiple results, blindly use the first
167
+ coords = place.elements['Point/coordinates'].text
168
+ coords.split(',')[0...2].reverse.map{ |i| i.to_f }
169
+ end
170
+
171
+ ##
172
+ # Calculate the distance between two points on Earth (Haversine formula).
173
+ # Takes two sets of coordinates and an options hash:
174
+ #
175
+ # +units+ :: <tt>:mi</tt> for miles (default), <tt>:km</tt> for kilometers
176
+ #
177
+ def self.distance_between(lat1, lon1, lat2, lon2, options = {})
178
+
179
+ # set default options
180
+ options[:units] ||= :mi
181
+
182
+ # define conversion factors
183
+ units = { :mi => 3956, :km => 6371 }
184
+
185
+ # convert degrees to radians
186
+ lat1 = to_radians(lat1)
187
+ lon1 = to_radians(lon1)
188
+ lat2 = to_radians(lat2)
189
+ lon2 = to_radians(lon2)
190
+
191
+ # compute distances
192
+ dlat = (lat1 - lat2).abs
193
+ dlon = (lon1 - lon2).abs
194
+
195
+ a = (Math.sin(dlat / 2))**2 + Math.cos(lat1) *
196
+ (Math.sin(dlon / 2))**2 * Math.cos(lat2)
197
+ c = 2 * Math.atan2( Math.sqrt(a), Math.sqrt(1-a))
198
+ c * units[options[:units]]
199
+ end
200
+
201
+ ##
202
+ # Convert degrees to radians.
203
+ #
204
+ def self.to_radians(degrees)
205
+ degrees * (Math::PI / 180)
206
+ end
207
+
208
+ ##
209
+ # Query Google for geographic information about the given phrase.
210
+ # Returns the XML response as a hash. This method is not intended for
211
+ # general use (prefer Geocoder.search).
212
+ #
213
+ def self.search(query)
214
+ params = { :q => query, :output => "xml" }
215
+ url = "http://maps.google.com/maps/geo?" + params.to_query
216
+
217
+ # Query geocoder and make sure it responds quickly.
218
+ begin
219
+ resp = nil
220
+ timeout(3) do
221
+ resp = Net::HTTP.get_response(URI.parse(url))
222
+ end
223
+ rescue SocketError, TimeoutError
224
+ return nil
225
+ end
226
+
227
+ # Google's XML document has incorrect encoding (says UTF-8 but is actually
228
+ # ISO 8859-1). Have to fix this or REXML won't parse correctly.
229
+ # This may be fixed in the future; see the bug report at:
230
+ # http://code.google.com/p/gmaps-api-issues/issues/detail?id=233
231
+ doc = resp.body.sub('UTF-8', 'ISO-8859-1')
232
+
233
+ REXML::Document.new(doc)
234
+ end
235
+ end
236
+
237
+ ##
238
+ # Add geocoded_by method to ActiveRecord::Base so Geocoder is accessible.
239
+ #
240
+ ActiveRecord::Base.class_eval do
241
+
242
+ ##
243
+ # Set attribute names and include the Geocoder module.
244
+ #
245
+ def self.geocoded_by(method_name = :location, options = {})
246
+ class_inheritable_reader :geocoder_options
247
+ write_inheritable_attribute :geocoder_options, {
248
+ :method_name => method_name,
249
+ :latitude => options[:latitude] || :latitude,
250
+ :longitude => options[:longitude] || :longitude
251
+ }
252
+ include Geocoder
253
+ end
254
+ end
@@ -0,0 +1,51 @@
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{rails-geocoder}
8
+ s.version = "0.8.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Alex Reisner"]
12
+ s.date = %q{2009-10-01}
13
+ s.description = %q{Add geocoding functionality to Rails models.}
14
+ s.email = %q{alex@alexreisner.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "init.rb",
27
+ "lib/geocoder.rb",
28
+ "rails-geocoder.gemspec",
29
+ "test/geocoder_test.rb",
30
+ "test/test_helper.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/alexreisner/geocoder}
33
+ s.rdoc_options = ["--charset=UTF-8"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.5}
36
+ s.summary = %q{Add geocoding functionality to Rails models.}
37
+ s.test_files = [
38
+ "test/geocoder_test.rb",
39
+ "test/test_helper.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ else
48
+ end
49
+ else
50
+ end
51
+ end
@@ -0,0 +1,8 @@
1
+ require 'test_helper'
2
+
3
+ class GeocoderTest < Test::Unit::TestCase
4
+ # Replace this with your real tests.
5
+ def test_this_plugin
6
+ flunk
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ require 'geocoder'
7
+
8
+ class Test::Unit::TestCase
9
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-geocoder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Reisner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-01 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Add geocoding functionality to Rails models.
17
+ email: alex@alexreisner.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - .document
27
+ - .gitignore
28
+ - LICENSE
29
+ - README.rdoc
30
+ - Rakefile
31
+ - VERSION
32
+ - init.rb
33
+ - lib/geocoder.rb
34
+ - rails-geocoder.gemspec
35
+ - test/geocoder_test.rb
36
+ - test/test_helper.rb
37
+ has_rdoc: true
38
+ homepage: http://github.com/alexreisner/geocoder
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options:
43
+ - --charset=UTF-8
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.3.5
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Add geocoding functionality to Rails models.
65
+ test_files:
66
+ - test/geocoder_test.rb
67
+ - test/test_helper.rb