rails-geocoder 0.8.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.
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