geocoder 0.1.1 → 0.9.8
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of geocoder might be problematic. Click here for more details.
- data/CHANGELOG.rdoc +96 -0
- data/LICENSE +1 -1
- data/README.rdoc +215 -0
- data/Rakefile +46 -43
- data/lib/geocoder.rb +17 -321
- data/lib/geocoder/active_record.rb +235 -0
- data/lib/geocoder/calculations.rb +94 -0
- data/lib/geocoder/configuration.rb +8 -0
- data/lib/geocoder/lookup.rb +90 -0
- data/lib/geocoder/railtie.rb +68 -0
- data/lib/geocoder/result.rb +42 -0
- data/lib/tasks/geocoder.rake +15 -0
- data/test/geocoder_test.rb +36 -0
- data/test/test_helper.rb +97 -0
- metadata +68 -51
- data/CHANGELOG +0 -10
- data/README +0 -122
- data/TODO +0 -10
- data/bin/geocode +0 -4
- data/test/mocks/csv?address=2038+damen+ave+chicago+il +0 -2
- data/test/mocks/csv?address=2125+w+north+ave+chicago+il +0 -1
- data/test/mocks/csv?address=donotleaveitisnotreal +0 -1
- data/test/mocks/geocode?appid=YahooDemo&location= +0 -4
- data/test/mocks/geocode?appid=YahooDemo&location=2038+damen+ave+chicago+il +0 -2
- data/test/mocks/geocode?appid=YahooDemo&location=2125+w+north+ave+chicago+il +0 -2
- data/test/mocks/geocode?appid=YahooDemo&location=donotleaveitisnotreal +0 -4
- data/test/mocks/http.rb +0 -19
- data/test/sample.xml +0 -2
- data/test/tc_geocoderus.rb +0 -105
- data/test/tc_yahoo.rb +0 -110
- data/test/ts_geocoder.rb +0 -4
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
= Changelog
|
2
|
+
|
3
|
+
Per-release changes to Geocoder.
|
4
|
+
|
5
|
+
== 0.9.8 (2011 Feb 8)
|
6
|
+
|
7
|
+
* Include geocode:all Rake task in gem (was missing!).
|
8
|
+
* Add Geocoder.search for access to Google's full response.
|
9
|
+
* Add ability to configure Google connection timeout.
|
10
|
+
* Emit warnings on Google connection problems and errors.
|
11
|
+
* Refactor: insert Geocoder into ActiveRecord via Railtie.
|
12
|
+
|
13
|
+
== 0.9.7 (2011 Feb 1)
|
14
|
+
|
15
|
+
* Add reverse geocoding (+reverse_geocoded_by+).
|
16
|
+
* Prevent exception (uninitialized constant Geocoder::Net) when net/http not already required (sleepycat).
|
17
|
+
* Refactor: split monolithic Geocoder module into several smaller ones.
|
18
|
+
|
19
|
+
== 0.9.6 (2011 Jan 19)
|
20
|
+
|
21
|
+
* Fix incompatibility with will_paginate gem.
|
22
|
+
* Include table names in GROUP BY clause of nearby scope to avoid ambiguity in joins (matchu).
|
23
|
+
|
24
|
+
== 0.9.5 (2010 Oct 15)
|
25
|
+
|
26
|
+
* Fix broken PostgreSQL compatibility (now 100% compatible).
|
27
|
+
* Switch from Google's XML to JSON geocoding API.
|
28
|
+
* Separate Rails 2 and Rails 3-compatible branches.
|
29
|
+
* Don't allow :conditions hash in 'options' argument to 'nearbys' method (was deprecated in 0.9.3).
|
30
|
+
|
31
|
+
== 0.9.4 (2010 Aug 2)
|
32
|
+
|
33
|
+
* Google Maps API key no longer required (uses geocoder v3).
|
34
|
+
|
35
|
+
== 0.9.3 (2010 Aug 2)
|
36
|
+
|
37
|
+
* Fix incompatibility with Rails 3 RC 1.
|
38
|
+
* Deprecate 'options' argument to 'nearbys' method.
|
39
|
+
* Allow inclusion of 'nearbys' in Arel method chains.
|
40
|
+
|
41
|
+
== 0.9.2 (2010 Jun 3)
|
42
|
+
|
43
|
+
* Fix LIMIT clause bug in PostgreSQL (reported by kenzie).
|
44
|
+
|
45
|
+
== 0.9.1 (2010 May 4)
|
46
|
+
|
47
|
+
* Use scope instead of named_scope in Rails 3.
|
48
|
+
|
49
|
+
== 0.9.0 (2010 Apr 2)
|
50
|
+
|
51
|
+
* Fix bug in PostgreSQL support (caused "PGError: ERROR: column "distance" does not exist"), reported by developish.
|
52
|
+
|
53
|
+
== 0.8.9 (2010 Feb 11)
|
54
|
+
|
55
|
+
* Add Rails 3 compatibility.
|
56
|
+
* Avoid querying Google when query would be an empty string.
|
57
|
+
|
58
|
+
== 0.8.8 (2009 Dec 7)
|
59
|
+
|
60
|
+
* Automatically select a less accurate but compatible distance algorithm when SQLite database detected (fixes SQLite incompatibility).
|
61
|
+
|
62
|
+
== 0.8.7 (2009 Nov 4)
|
63
|
+
|
64
|
+
* Added Geocoder.geographic_center method.
|
65
|
+
* Replaced _get_coordinates class method with read_coordinates instance method.
|
66
|
+
|
67
|
+
== 0.8.6 (2009 Oct 27)
|
68
|
+
|
69
|
+
* The fetch_coordinates method now assigns coordinates to attributes (behaves like fetch_coordinates! used to) and fetch_coordinates! both assigns and saves the attributes.
|
70
|
+
* Added geocode:all rake task.
|
71
|
+
|
72
|
+
== 0.8.5 (2009 Oct 26)
|
73
|
+
|
74
|
+
* Avoid calling deprecated method from within Geocoder itself.
|
75
|
+
|
76
|
+
== 0.8.4 (2009 Oct 23)
|
77
|
+
|
78
|
+
* Deprecate <tt>find_near</tt> class method in favor of +near+ named scope.
|
79
|
+
|
80
|
+
== 0.8.3 (2009 Oct 23)
|
81
|
+
|
82
|
+
* Update Google URL query string parameter to reflect recent changes in Google's API.
|
83
|
+
|
84
|
+
== 0.8.2 (2009 Oct 12)
|
85
|
+
|
86
|
+
* Allow a model's geocoder search string method to be something other than an ActiveRecord attribute.
|
87
|
+
* Clean up documentation.
|
88
|
+
|
89
|
+
== 0.8.1 (2009 Oct 8)
|
90
|
+
|
91
|
+
* Extract XML-fetching code from Geocoder.search and place in Geocoder._fetch_xml (for ease of mocking).
|
92
|
+
* Add tests for coordinate-fetching instance methods.
|
93
|
+
|
94
|
+
== 0.8.0 (2009 Oct 1)
|
95
|
+
|
96
|
+
First release.
|
data/LICENSE
CHANGED
data/README.rdoc
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
= Geocoder
|
2
|
+
|
3
|
+
Geocoder adds object geocoding and database-agnostic distance calculations to Ruby on Rails. It's as simple as calling <tt>fetch_coordinates!</tt> on your objects, and then using a scope like <tt>Venue.near("Billings, MT")</tt>. Since it does not rely on proprietary database functions finding geocoded objects in a given area works with out-of-the-box MySQL or even SQLite.
|
4
|
+
|
5
|
+
Geocoder is compatible with Rails 2.x and 3.x. <b>This is the README for the 3.x branch.</b> Please see the 2.x branch for installation instructions, documentation, and issues.
|
6
|
+
|
7
|
+
|
8
|
+
== 1. Install
|
9
|
+
|
10
|
+
=== As a Gem
|
11
|
+
|
12
|
+
Add this to your Gemfile:
|
13
|
+
|
14
|
+
gem "rails-geocoder", :require => "geocoder"
|
15
|
+
|
16
|
+
and run this at the command prompt:
|
17
|
+
|
18
|
+
bundle install
|
19
|
+
|
20
|
+
=== Or As a Plugin
|
21
|
+
|
22
|
+
At the command prompt:
|
23
|
+
|
24
|
+
rails plugin install git://github.com/alexreisner/geocoder.git
|
25
|
+
|
26
|
+
|
27
|
+
== 2. Configure
|
28
|
+
|
29
|
+
A) Add +latitude+ and +longitude+ columns to your model:
|
30
|
+
|
31
|
+
rails generate migration AddLatitudeAndLongitudeToYourModel latitude:float longitude:float
|
32
|
+
rake db:migrate
|
33
|
+
|
34
|
+
B) Tell geocoder where your model stores its address:
|
35
|
+
|
36
|
+
geocoded_by :address
|
37
|
+
|
38
|
+
C) Optionally, auto-fetch coordinates every time your model is saved:
|
39
|
+
|
40
|
+
after_validation :fetch_coordinates
|
41
|
+
|
42
|
+
<i>Note that you are not stuck with the +latitude+ and +longitude+ column names, or the +address+ method. See "More On Configuration" below for details.</i>
|
43
|
+
|
44
|
+
|
45
|
+
== 3. Use
|
46
|
+
|
47
|
+
Assuming +obj+ is an instance of a geocoded class, you can get its coordinates:
|
48
|
+
|
49
|
+
obj.fetch_coordinates # fetches and assigns coordinates
|
50
|
+
obj.fetch_coordinates! # also saves lat, lon attributes
|
51
|
+
|
52
|
+
If you have a lot of objects you can use this Rake task to geocode them all:
|
53
|
+
|
54
|
+
rake geocode:all CLASS=YourModel
|
55
|
+
|
56
|
+
Once +obj+ is geocoded you can do things like this:
|
57
|
+
|
58
|
+
obj.nearbys(30) # other objects within 30 miles
|
59
|
+
obj.distance_to(40.714, -100.234) # distance to arbitrary point
|
60
|
+
|
61
|
+
To find objects by location, use the following scopes:
|
62
|
+
|
63
|
+
Venue.near('Omaha, NE, US', 20) # venues within 20 miles of Omaha
|
64
|
+
Venue.near([40.71, 100.23], 20) # venues within 20 miles of a point
|
65
|
+
Venue.geocoded # venues with coordinates
|
66
|
+
Venue.not_geocoded # venues without coordinates
|
67
|
+
|
68
|
+
Some utility methods are also available:
|
69
|
+
|
70
|
+
# distance (in miles) between Eiffel Tower and Empire State Building
|
71
|
+
Geocoder::Calculations.distance_between( 48.858205,2.294359, 40.748433,-73.985655 )
|
72
|
+
|
73
|
+
# look up coordinates of some location (like searching Google Maps)
|
74
|
+
Geocoder.fetch_coordinates("25 Main St, Cooperstown, NY")
|
75
|
+
|
76
|
+
# find the geographic center (aka center of gravity) of objects or points
|
77
|
+
Geocoder::Calculations.geographic_center([ city1, city2, city3, [40.22,-73.99], city4 ])
|
78
|
+
|
79
|
+
|
80
|
+
== More On Configuration
|
81
|
+
|
82
|
+
You are not stuck with using the +latitude+ and +longitude+ database column names for storing coordinates. For example, to use +lat+ and +lon+:
|
83
|
+
|
84
|
+
geocoded_by :address, :latitude => :lat, :longitude => :lon
|
85
|
+
|
86
|
+
The string to use for geocoding can be anything you'd use to search Google Maps. For example, any of the following are acceptable:
|
87
|
+
|
88
|
+
714 Green St, Big Town, MO
|
89
|
+
Eiffel Tower, Paris, FR
|
90
|
+
Paris, TX, US
|
91
|
+
|
92
|
+
If your model has +address+, +city+, +state+, and +country+ attributes you might do something like this:
|
93
|
+
|
94
|
+
geocoded_by :location
|
95
|
+
|
96
|
+
def location
|
97
|
+
[address, city, state, country].compact.join(', ')
|
98
|
+
end
|
99
|
+
|
100
|
+
Please see the code (<tt>lib/geocoder/active_record.rb</tt>) for more methods and detailed information about arguments (eg, working with kilometers).
|
101
|
+
|
102
|
+
You can also set the timeout used for connections to Google's geocoding service. The default is 3 seconds, but if you want to set it to 5 you could put the following in an initializer:
|
103
|
+
|
104
|
+
Geocoder::Configuration.timeout = 5
|
105
|
+
|
106
|
+
|
107
|
+
== Reverse Geocoding
|
108
|
+
|
109
|
+
If you need reverse geocoding (lat/long coordinates to address), do something like the following in your model:
|
110
|
+
|
111
|
+
reverse_geocoded_by :latitude, :longitude
|
112
|
+
after_validation :fetch_address
|
113
|
+
|
114
|
+
and make sure it has +latitude+ and +longitude+ attributes, as well as an +address+ attribute. As with regular geocoding, you can specify alternate names for all of these attributes, for example:
|
115
|
+
|
116
|
+
reverse_geocoded_by :lat, :lon, :address => :location
|
117
|
+
|
118
|
+
|
119
|
+
== Forward and Reverse Geocoding in the Same Model
|
120
|
+
|
121
|
+
If you apply both forward and reverse geocoding functionality to the same model, you can provide different methods for storing the fetched address (reverse geocoding) and providing an address to use when fetching coordinates (forward geocoding), for example:
|
122
|
+
|
123
|
+
class Venue
|
124
|
+
|
125
|
+
# build an address from street, city, and state attributes
|
126
|
+
geocoded_by :address_from_components
|
127
|
+
|
128
|
+
# store the Google-provided address in the full_address attribute
|
129
|
+
reverse_geocoded_by :latitude, :longitude, :address => :full_address
|
130
|
+
end
|
131
|
+
|
132
|
+
However, there can be only one set of latitude/longitude attributes, and whichever you specify last will be used. For example:
|
133
|
+
|
134
|
+
class Venue
|
135
|
+
|
136
|
+
geocoded_by :address,
|
137
|
+
:latitude => :fetched_latitude, # this will be overridden by the below
|
138
|
+
:longitude => :fetched_longitude # same here
|
139
|
+
|
140
|
+
reverse_geocoded_by :latitude, :longitude
|
141
|
+
end
|
142
|
+
|
143
|
+
The reason for this is that we don't want ambiguity when doing distance calculations. We need a single, authoritative source for coordinates!
|
144
|
+
|
145
|
+
|
146
|
+
== Getting More Information
|
147
|
+
|
148
|
+
Those familiar with Google's Geocoding API know that it returns much more information than just an address or set of coordinates. If you want access to the entire response you can use the <tt>Geocoder.search</tt> method:
|
149
|
+
|
150
|
+
results = Geocoder.search("McCarren Park, Brooklyn, NY")
|
151
|
+
r = results.first
|
152
|
+
|
153
|
+
+r+ is now a Geocoder::Result object which has methods like the following:
|
154
|
+
|
155
|
+
r.geometry
|
156
|
+
=> {
|
157
|
+
"location"=>{"lng"=>-79.3801601, "lat"=>43.6619568},
|
158
|
+
"location_type"=>"ROOFTOP",
|
159
|
+
"viewport"=>{
|
160
|
+
"northeast"=>{"lng"=>-79.3770125, "lat"=>43.6651044},
|
161
|
+
"southwest"=>{"lng"=>-79.3833077, "lat"=>43.6588092}
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
r.address_components_of_type(:neighborhood)
|
166
|
+
=> [{
|
167
|
+
"long_name"=>"Greenpoint",
|
168
|
+
"short_name"=>"Greenpoint",
|
169
|
+
"types"=>["neighborhood", "political"]
|
170
|
+
}]
|
171
|
+
|
172
|
+
Please see the Geocoder::Result class for more information, as well as Google's API documentation (http://code.google.com/apis/maps/documentation/geocoding/#JSON).
|
173
|
+
|
174
|
+
|
175
|
+
== SQLite
|
176
|
+
|
177
|
+
SQLite's lack of trigonometric functions requires an alternate implementation of the +near+ method (scope). When using SQLite, Geocoder will automatically use a less accurate algorithm for finding objects near a given point. Results of this algorithm should not be trusted too much as it will return objects that are outside the given radius.
|
178
|
+
|
179
|
+
It is also not possible to calculate distances between points without the trig functions so you cannot sort results by "nearness."
|
180
|
+
|
181
|
+
|
182
|
+
=== Discussion
|
183
|
+
|
184
|
+
There are few options for finding objects near a given point in SQLite without installing extensions:
|
185
|
+
|
186
|
+
1. Use a square instead of a circle for finding nearby points. For example, if you want to find points near 40.71, 100.23, search for objects with latitude between 39.71 and 41.71 and longitude between 99.23 and 101.23. One degree of latitude or longitude is at most 69 miles so divide your radius (in miles) by 69.0 to get the amount to add and subtract from your center coordinates to get the upper and lower bounds. The results will not be very accurate (you'll get points outside the desired radius--at worst 29% farther away), but you will get all the points within the required radius.
|
187
|
+
|
188
|
+
2. Load all objects into memory and compute distances between them using the <tt>Geocoder::Calculations.distance_between</tt> method. This will produce accurate results but will be very slow (and use a lot of memory) if you have a lot of objects in your database.
|
189
|
+
|
190
|
+
3. If you have a large number of objects (so you can't use approach #2) and you need accurate results (better than approach #1 will give), you can use a combination of the two. Get all the objects within a square around your center point, and then eliminate the ones that are too far away using <tt>Geocoder::Calculations.distance_between</tt>.
|
191
|
+
|
192
|
+
Because Geocoder needs to provide this functionality as a scope, we must go with option #1, but feel free to implement #2 or #3 if you need more accuracy.
|
193
|
+
|
194
|
+
|
195
|
+
== Known Issue
|
196
|
+
|
197
|
+
You cannot use the +near+ scope with another scope that provides an +includes+ option because the +SELECT+ clause generated by +near+ will overwrite it (or vice versa). Instead, try using +joins+ and pass a <tt>:select</tt> option to the +near+ scope to get the columns you want. For example, in Rails 2 syntax:
|
198
|
+
|
199
|
+
# instead of :includes => :venues:
|
200
|
+
City.near("Omaha, NE", 20, :select => "venues.*").all(:joins => :venues)
|
201
|
+
|
202
|
+
If anyone has a more elegant solution to this problem I am very interested in seeing it.
|
203
|
+
|
204
|
+
|
205
|
+
== To-do List
|
206
|
+
|
207
|
+
* support different ORMs (DataMapper, Mongoid, etc)
|
208
|
+
* use completely separate "drivers" for different AR adapters?
|
209
|
+
* seems reasonable since we're using very DB-specific features
|
210
|
+
* also need to make sure 'mysql2' is supported
|
211
|
+
* make 'near' scope work with AR associations
|
212
|
+
* http://stackoverflow.com/questions/3266358/geocoder-rails-plugin-near-search-problem-with-activerecord
|
213
|
+
|
214
|
+
|
215
|
+
Copyright (c) 2009-11 Alex Reisner, released under the MIT license
|
data/Rakefile
CHANGED
@@ -1,53 +1,56 @@
|
|
1
|
-
require 'rake/rdoctask'
|
2
|
-
require 'rake/gempackagetask'
|
3
1
|
require 'rubygems'
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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{Geocoder adds object geocoding and database-agnostic distance calculations to Ruby on Rails. It does not rely on proprietary database functions so finding geocoded objects in a given area is easily done using out-of-the-box MySQL or even SQLite.}
|
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"
|
16
18
|
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
21
25
|
end
|
22
26
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
s.rubyforge_project = "geocoder"
|
35
|
-
s.homepage = "http://geocoder.rubyforge.org"
|
36
|
-
s.bindir = "bin"
|
37
|
-
s.executables = ["geocode"]
|
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
38
|
end
|
39
39
|
|
40
|
-
|
41
|
-
pkg.need_zip = true
|
42
|
-
pkg.need_tar = true
|
43
|
-
end
|
40
|
+
task :test => :check_dependencies
|
44
41
|
|
45
|
-
|
46
|
-
task :install => [ :repackage ] do
|
47
|
-
sh "sudo gem install -l pkg/geocoder-#{GEOCODER_VERSION}.gem"
|
48
|
-
end
|
42
|
+
task :default => :test
|
49
43
|
|
50
|
-
|
51
|
-
|
52
|
-
|
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')
|
53
56
|
end
|
data/lib/geocoder.rb
CHANGED
@@ -1,327 +1,23 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
-
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
-
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
-
# the following conditions:
|
11
|
-
#
|
12
|
-
# The above copyright notice and this permission notice shall be
|
13
|
-
# included in all copies or substantial portions of the Software.
|
14
|
-
#
|
15
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
-
#++
|
23
|
-
#
|
24
|
-
# = Geocoder -- Geocoding library for Ruby
|
25
|
-
#
|
26
|
-
require 'cgi'
|
27
|
-
require 'net/http'
|
28
|
-
require 'rexml/document'
|
29
|
-
require 'timeout'
|
1
|
+
require "geocoder/configuration"
|
2
|
+
require "geocoder/calculations"
|
3
|
+
require "geocoder/lookup"
|
4
|
+
require "geocoder/result"
|
5
|
+
require "geocoder/active_record"
|
6
|
+
require "geocoder/railtie"
|
30
7
|
|
31
8
|
module Geocoder
|
9
|
+
extend self
|
32
10
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
["address", "Address"],
|
39
|
-
["city", "City"],
|
40
|
-
["state", "State"],
|
41
|
-
["zip", "ZIP Code"] ].freeze
|
42
|
-
|
43
|
-
class Base
|
44
|
-
# +location+ is a string, any of the following:
|
45
|
-
# * city, state
|
46
|
-
# * city, state, zip
|
47
|
-
# * zip
|
48
|
-
# * street, city, state
|
49
|
-
# * street, city, state, zip
|
50
|
-
# * street, zip
|
51
|
-
def geocode location, *args
|
52
|
-
options = { :timeout => nil }
|
53
|
-
options.update(args.pop) if args.last.is_a?(Hash)
|
54
|
-
@options = options
|
55
|
-
if location.nil? or location.empty?
|
56
|
-
raise BlankLocationString
|
57
|
-
end
|
58
|
-
location = String location
|
59
|
-
results = parse request(location)
|
60
|
-
create_response results
|
61
|
-
end
|
62
|
-
|
63
|
-
def create_response results
|
64
|
-
Response.new results
|
65
|
-
end
|
66
|
-
|
67
|
-
# Makes an HTTP GET request on URL and returns the body
|
68
|
-
# of the response
|
69
|
-
def get url, timeout=5
|
70
|
-
url = URI.parse url
|
71
|
-
http = Net::HTTP.new url.host, url.port
|
72
|
-
res = Timeout::timeout(timeout) {
|
73
|
-
http.get url.request_uri
|
74
|
-
}
|
75
|
-
res.body
|
76
|
-
end
|
77
|
-
|
78
|
-
def request location
|
79
|
-
get url(location), @options[:timeout]
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
class GeoCoderUs < Base
|
84
|
-
def initialize *args
|
85
|
-
#
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
|
90
|
-
def parse csv_text
|
91
|
-
if csv_text =~ /^2: /
|
92
|
-
raise GeocodingError, csv_text.split(": ")[1]
|
93
|
-
end
|
94
|
-
results = []
|
95
|
-
csv_text.split("\n").each do |line|
|
96
|
-
latitude, longitude, address, city, state, zip = line.split ","
|
97
|
-
result = Result.new
|
98
|
-
result.latitude = latitude
|
99
|
-
result.longitude = longitude
|
100
|
-
result.address = address
|
101
|
-
result.city = city
|
102
|
-
result.state = state
|
103
|
-
result.zip = zip
|
104
|
-
results << result
|
105
|
-
end
|
106
|
-
results
|
107
|
-
end
|
108
|
-
|
109
|
-
# Returns URL of geocoder.us web service
|
110
|
-
def url address
|
111
|
-
"http://rpc.geocoder.us/service/csv?address=#{CGI.escape address}"
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
class Yahoo < Base
|
116
|
-
include REXML
|
117
|
-
# Requires a Y! Application ID
|
118
|
-
# http://developer.yahoo.net/faq/index.html#appid
|
119
|
-
def initialize appid
|
120
|
-
@appid = appid
|
121
|
-
end
|
122
|
-
|
123
|
-
private
|
124
|
-
|
125
|
-
# return array of results
|
126
|
-
def parse xml
|
127
|
-
# Create a new REXML::Document object from the raw XML text
|
128
|
-
xml = Document.new xml
|
129
|
-
#
|
130
|
-
# Normally, Y! will return an XML document with the root node
|
131
|
-
# <ResultSet>; if the request bombs, they return one with the
|
132
|
-
# root node <Error>
|
133
|
-
if is_error? xml
|
134
|
-
msgs = []
|
135
|
-
# Bubble up an exception using the error messages from Y!
|
136
|
-
xml.root.elements.each("Message") { |e| msgs << e.get_text.value }
|
137
|
-
raise GeocodingError, msgs.join(", ")
|
138
|
-
else
|
139
|
-
results = []
|
140
|
-
xml.root.elements.each "Result" do |e|
|
141
|
-
result = Result.new
|
142
|
-
# add fields
|
143
|
-
fields.each do |field|
|
144
|
-
text = e.elements[field.capitalize].get_text
|
145
|
-
if text.respond_to? :value
|
146
|
-
result.send "#{field}=", text.value
|
147
|
-
end
|
148
|
-
end
|
149
|
-
# add attributes
|
150
|
-
attributes.each do |attribute|
|
151
|
-
result.send "#{attribute}=", e.attributes[attribute]
|
152
|
-
end
|
153
|
-
results << result
|
154
|
-
end
|
155
|
-
results
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def fields
|
160
|
-
%w| latitude longitude address city state zip country |
|
161
|
-
end
|
162
|
-
|
163
|
-
def attributes
|
164
|
-
%w| precision warning |
|
165
|
-
end
|
166
|
-
|
167
|
-
def is_error? document
|
168
|
-
document.root.name == "Error"
|
169
|
-
end
|
170
|
-
|
171
|
-
# Returns URL of Y! Geocoding web service
|
172
|
-
def url location
|
173
|
-
"http://api.local.yahoo.com/MapsService/V1/geocode?appid=#{@appid}&location=#{CGI.escape location}"
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
SERVICES = { :yahoo => Yahoo,
|
178
|
-
:geocoderus => GeoCoderUs }.freeze
|
179
|
-
|
180
|
-
class Result < Struct.new :latitude, :longitude, :address, :city,
|
181
|
-
:state, :zip, :country, :precision,
|
182
|
-
:warning
|
183
|
-
alias :lat :latitude
|
184
|
-
alias :lng :longitude
|
11
|
+
##
|
12
|
+
# Alias for Geocoder::Lookup.search.
|
13
|
+
#
|
14
|
+
def search(*args)
|
15
|
+
Lookup.search(*args)
|
185
16
|
end
|
186
17
|
|
187
|
-
#
|
188
|
-
|
189
|
-
|
190
|
-
class Response < Array
|
191
|
-
def initialize results
|
192
|
-
results.each do |result|
|
193
|
-
self << result
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
# Geocoding was an unqualified success if one result in the result
|
198
|
-
# set is retured and there is no warning attribute in that result
|
199
|
-
def success?
|
200
|
-
size == 1 and self[0].warning.nil?
|
201
|
-
end
|
202
|
-
|
203
|
-
def bullseye?
|
204
|
-
success?
|
205
|
-
end
|
206
|
-
|
207
|
-
# Returns latitude in degrees decimal
|
208
|
-
def latitude
|
209
|
-
self[0].latitude if bullseye?
|
210
|
-
end
|
211
|
-
|
212
|
-
# Returns longitude in degrees decimal
|
213
|
-
def longitude
|
214
|
-
self[0].longitude if bullseye?
|
215
|
-
end
|
216
|
-
|
217
|
-
# Returns normalized street address, capitalized
|
218
|
-
def address
|
219
|
-
self[0].address if bullseye?
|
220
|
-
end
|
221
|
-
|
222
|
-
# Returns normalized city name, capitalized
|
223
|
-
def city
|
224
|
-
self[0].city if bullseye?
|
225
|
-
end
|
226
|
-
|
227
|
-
# Returns normalized two-letter USPS state abbreviation
|
228
|
-
def state
|
229
|
-
self[0].state if bullseye?
|
230
|
-
end
|
231
|
-
|
232
|
-
alias_method :array_zip, :zip
|
233
|
-
|
234
|
-
# Returns normalized ZIP Code, or postal code
|
235
|
-
def zip
|
236
|
-
self[0].zip if bullseye?
|
237
|
-
end
|
238
|
-
|
239
|
-
# Returns two-letter country code abbreviation
|
240
|
-
def country
|
241
|
-
self[0].country if bullseye?
|
242
|
-
end
|
243
|
-
|
244
|
-
alias :lat :latitude
|
245
|
-
alias :lng :longitude
|
246
|
-
end
|
247
|
-
|
248
|
-
class Cli
|
249
|
-
require 'optparse'
|
250
|
-
require 'ostruct'
|
251
|
-
|
252
|
-
def self.parse args
|
253
|
-
options = OpenStruct.new
|
254
|
-
# default values
|
255
|
-
options.appid = "YahooDemo"
|
256
|
-
options.service = Yahoo
|
257
|
-
options.timeout = 5
|
258
|
-
opts = OptionParser.new do |opts|
|
259
|
-
opts.banner = "Usage: geocode [options] location"
|
260
|
-
opts.separator ""
|
261
|
-
opts.separator "Options:"
|
262
|
-
opts.on "-a appid", "--appid appid", "Yahoo! Application ID" do |a|
|
263
|
-
options.appid = a
|
264
|
-
end
|
265
|
-
opts.on "-s service", "--service service", "`yahoo' or `geocoderus'" do |s|
|
266
|
-
options.service = SERVICES[s]
|
267
|
-
end
|
268
|
-
opts.on "-t secs", "--timeout secs", Integer, "Timeout in seconds" do |t|
|
269
|
-
options.timeout = t
|
270
|
-
end
|
271
|
-
opts.on "-q", "--quiet", "Quiet output" do |q|
|
272
|
-
options.quiet = q
|
273
|
-
end
|
274
|
-
opts.on_tail "-h", "--help", "Show this message" do
|
275
|
-
puts opts
|
276
|
-
exit
|
277
|
-
end
|
278
|
-
opts.parse! args
|
279
|
-
end
|
280
|
-
[options, opts]
|
281
|
-
end
|
282
|
-
|
283
|
-
def initialize cli_args
|
284
|
-
@options, @opt_parser = Cli::parse cli_args
|
285
|
-
@location = cli_args.join " "
|
286
|
-
end
|
287
|
-
|
288
|
-
def report result
|
289
|
-
buffer = []
|
290
|
-
if @options.quiet
|
291
|
-
result.each do |r|
|
292
|
-
buffer << FIELDS.collect do |k,v|
|
293
|
-
r.send k
|
294
|
-
end.join(",")
|
295
|
-
end
|
296
|
-
else
|
297
|
-
buffer << "Found #{result.size} result(s)."
|
298
|
-
buffer << buffer.last.gsub(/./, "-")
|
299
|
-
buffer << result.collect do |r|
|
300
|
-
FIELDS.collect do |k,v|
|
301
|
-
"#{v}: #{r.send k}"
|
302
|
-
end.join("\n")
|
303
|
-
end.join("\n- - - -\n")
|
304
|
-
end
|
305
|
-
puts buffer.join("\n")
|
306
|
-
end
|
307
|
-
|
308
|
-
def go!
|
309
|
-
g = @options.service.new @options.appid
|
310
|
-
begin
|
311
|
-
result = g.geocode @location, :timeout => @options.timeout
|
312
|
-
report result
|
313
|
-
rescue BlankLocationString
|
314
|
-
STDERR.puts "You have to give an address to geocode!"
|
315
|
-
puts
|
316
|
-
puts @opt_parser
|
317
|
-
exit
|
318
|
-
rescue Timeout::Error
|
319
|
-
STDERR.puts "The remote geocoding service timed-out. Try increasing the timeout value (-t)."
|
320
|
-
exit
|
321
|
-
rescue Geocoder::GeocodingError => e
|
322
|
-
STDERR.puts "Geocoder: #{e}"
|
323
|
-
exit
|
324
|
-
end
|
325
|
-
end
|
326
|
-
end
|
18
|
+
# exception classes
|
19
|
+
class Error < StandardError; end
|
20
|
+
class ConfigurationError < Error; end
|
327
21
|
end
|
22
|
+
|
23
|
+
Geocoder::Railtie.insert
|