geocoder 1.0.1 → 1.0.2

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.

@@ -2,6 +2,12 @@
2
2
 
3
3
  Per-release changes to Geocoder.
4
4
 
5
+ == 1.0.2 (2011 June 25)
6
+
7
+ * Add support for MongoMapper (thanks github.com/spagalloco).
8
+ * Fix: user-specified coordinates field wasn't working with Mongoid (thanks github.com/thisduck).
9
+ * Fix: invalid location given to near scope was returning all results (Active Record) or error (Mongoid) (thanks github.com/ogennadi).
10
+
5
11
  == 1.0.1 (2011 May 17)
6
12
 
7
13
  * Add option to not rescue from certain exceptions (thanks github.com/ahmedrb).
@@ -77,6 +77,10 @@ Reverse geocoding is similar:
77
77
  reverse_geocoded_by :coordinates
78
78
  after_validation :reverse_geocode # auto-fetch address
79
79
 
80
+ === MongoMapper
81
+
82
+ MongoMapper is very similar to Mongoid, just be sure to include <tt>Geocoder::Model::Mongoid</tt>.
83
+
80
84
  === Bulk Geocoding
81
85
 
82
86
  If you have just added geocoding to an existing application with a lot of objects you can use this Rake task to geocode them all:
@@ -152,7 +156,7 @@ You can convert these numbers to compass point names by using the utility method
152
156
 
153
157
  <i>Note: when using SQLite +distance+ and +bearing+ values are provided for interface consistency only. They are not very accurate.</i>
154
158
 
155
- To calculate accurate distance and bearing with SQLite or Mongoid:
159
+ To calculate accurate distance and bearing with SQLite or MongoDB:
156
160
 
157
161
  obj.distance_to([43.9,-98.6]) # distance from obj to point
158
162
  obj.bearing_to([43.9,-98.6]) # bearing from obj to point
@@ -163,10 +167,10 @@ The <tt>bearing_from/to</tt> methods take a single argument which can be: a <tt>
163
167
 
164
168
  == More on Configuration
165
169
 
166
- You are not stuck with using the +latitude+ and +longitude+ database column names (with ActiveRecord) or the +coordinates+ array (Mongoid) for storing coordinates. For example:
170
+ You are not stuck with using the +latitude+ and +longitude+ database column names (with ActiveRecord) or the +coordinates+ array (Mongo) for storing coordinates. For example:
167
171
 
168
172
  geocoded_by :address, :latitude => :lat, :longitude => :lon # ActiveRecord
169
- geocoded_by :address, :coordinates => :coords # Mongoid
173
+ geocoded_by :address, :coordinates => :coords # MongoDB
170
174
 
171
175
  The +address+ method can return any string you'd use to search Google Maps. For example, any of the following are acceptable:
172
176
 
@@ -185,7 +189,7 @@ If your model has +street+, +city+, +state+, and +country+ attributes you might
185
189
  For reverse geocoding you can also specify an alternate name attribute where the address will be stored, for example:
186
190
 
187
191
  reverse_geocoded_by :lat, :lon, :address => :location # ActiveRecord
188
- reverse_geocoded_by :coordinates, :address => :loc # Mongoid
192
+ reverse_geocoded_by :coordinates, :address => :loc # MongoDB
189
193
 
190
194
 
191
195
  == Advanced Geocoding
@@ -211,7 +215,7 @@ Every <tt>Geocoder::Result</tt> object, +result+, provides the following data:
211
215
  * <tt>result.state</tt> - string
212
216
  * <tt>result.state_code</tt> - string
213
217
  * <tt>result.postal_code</tt> - string
214
- * <tt>result.country_name</tt> - string
218
+ * <tt>result.country</tt> - string
215
219
  * <tt>result.country_code</tt> - string
216
220
 
217
221
  If you're familiar with the results returned by the geocoding service you're using you can access even more data, but you'll need to be familiar with the particular <tt>Geocoder::Result</tt> object you're using and the structure of your geocoding service's responses. (See below for links to geocoding service documentation.)
@@ -410,15 +414,15 @@ When you install the Geocoder gem it adds a +geocode+ command to your shell. You
410
414
  There are also a number of options for setting the geocoding API, key, and language, viewing the raw JSON reponse, and more. Please run <tt>geocode -h</tt> for details.
411
415
 
412
416
 
413
- == Notes on Mongoid
417
+ == Notes on MongoDB
414
418
 
415
419
  === The Near Method
416
420
 
417
- Mongoid document classes have a built-in +near+ scope, but since it only works two-dimensions Geocoder overrides it with its own spherical +near+ method in geocoded classes.
421
+ Mongo document classes (Mongoid and MongoMapper) have a built-in +near+ scope, but since it only works two-dimensions Geocoder overrides it with its own spherical +near+ method in geocoded classes.
418
422
 
419
423
  === Latitude/Longitude Order
420
424
 
421
- Coordinates are generally printed and spoken as latitude, then logitude ([lat,lon]). Geocoder respects this convention and always expects method arguments to be given in [lat,lon] order. However, MongoDB requires that coordinates be stored in [lon,lat] order as per the GeoJSON spec (http://geojson.org/geojson-spec.html#positions), so internally they are stored "backwards." However, this does not affect order of arguments to methods when using Mongoid.
425
+ Coordinates are generally printed and spoken as latitude, then logitude ([lat,lon]). Geocoder respects this convention and always expects method arguments to be given in [lat,lon] order. However, MongoDB requires that coordinates be stored in [lon,lat] order as per the GeoJSON spec (http://geojson.org/geojson-spec.html#positions), so internally they are stored "backwards." However, this does not affect order of arguments to methods when using Mongoid or MongoMapper.
422
426
 
423
427
 
424
428
  == Distance Queries in SQLite
@@ -4,6 +4,7 @@ require "geocoder/cache"
4
4
  require "geocoder/request"
5
5
  require "geocoder/models/active_record"
6
6
  require "geocoder/models/mongoid"
7
+ require "geocoder/models/mongo_mapper"
7
8
 
8
9
  module Geocoder
9
10
  extend self
@@ -0,0 +1,55 @@
1
+ require 'geocoder'
2
+
3
+ module Geocoder
4
+
5
+ ##
6
+ # Methods for invoking Geocoder in a model.
7
+ #
8
+ module Model
9
+ module MongoBase
10
+
11
+ ##
12
+ # Set attribute names and include the Geocoder module.
13
+ #
14
+ def geocoded_by(address_attr, options = {}, &block)
15
+ geocoder_init(
16
+ :geocode => true,
17
+ :user_address => address_attr,
18
+ :coordinates => options[:coordinates] || :coordinates,
19
+ :geocode_block => block
20
+ )
21
+ end
22
+
23
+ ##
24
+ # Set attribute names and include the Geocoder module.
25
+ #
26
+ def reverse_geocoded_by(coordinates_attr, options = {}, &block)
27
+ geocoder_init(
28
+ :reverse_geocode => true,
29
+ :fetched_address => options[:address] || :address,
30
+ :coordinates => coordinates_attr,
31
+ :reverse_block => block
32
+ )
33
+ end
34
+
35
+ private # ----------------------------------------------------------------
36
+
37
+ def geocoder_init(options)
38
+ unless geocoder_initialized?
39
+ @geocoder_options = {}
40
+ require "geocoder/stores/#{geocoder_file_name}"
41
+ include eval("Geocoder::Store::" + geocoder_module_name)
42
+ end
43
+ @geocoder_options.merge! options
44
+ end
45
+
46
+ def geocoder_initialized?
47
+ begin
48
+ included_modules.include? eval("Geocoder::Store::" + geocoder_module_name)
49
+ rescue NameError
50
+ false
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ require 'geocoder/models/base'
2
+ require 'geocoder/models/mongo_base'
3
+
4
+ module Geocoder
5
+ module Model
6
+ module MongoMapper
7
+ include Base
8
+ include MongoBase
9
+
10
+ def self.included(base); base.extend(self); end
11
+
12
+ private # --------------------------------------------------------------
13
+
14
+ def geocoder_file_name; "mongo_mapper"; end
15
+ def geocoder_module_name; "MongoMapper"; end
16
+
17
+ def geocoder_init(options)
18
+ super(options)
19
+ ensure_index [[ geocoder_options[:coordinates], Mongo::GEO2D ]],
20
+ :min => -180, :max => 180 # create 2d index
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,37 +1,14 @@
1
1
  require 'geocoder/models/base'
2
+ require 'geocoder/models/mongo_base'
2
3
 
3
4
  module Geocoder
4
5
  module Model
5
6
  module Mongoid
6
7
  include Base
8
+ include MongoBase
7
9
 
8
10
  def self.included(base); base.extend(self); end
9
11
 
10
- ##
11
- # Set attribute names and include the Geocoder module.
12
- #
13
- def geocoded_by(address_attr, options = {}, &block)
14
- geocoder_init(
15
- :geocode => true,
16
- :user_address => address_attr,
17
- :coordinates => options[:coordinates] || :coordinates,
18
- :geocode_block => block
19
- )
20
- end
21
-
22
- ##
23
- # Set attribute names and include the Geocoder module.
24
- #
25
- def reverse_geocoded_by(coordinates_attr, options = {}, &block)
26
- geocoder_init(
27
- :reverse_geocode => true,
28
- :fetched_address => options[:address] || :address,
29
- :coordinates => coordinates_attr,
30
- :reverse_block => block
31
- )
32
- end
33
-
34
-
35
12
  private # --------------------------------------------------------------
36
13
 
37
14
  def geocoder_file_name; "mongoid"; end
@@ -36,7 +36,7 @@ module Geocoder::Store
36
36
  if latitude and longitude
37
37
  near_scope_options(latitude, longitude, *args)
38
38
  else
39
- {}
39
+ where(:id => false) # no results if no lat/lon given
40
40
  end
41
41
  }
42
42
  end
@@ -0,0 +1,81 @@
1
+ module Geocoder::Store
2
+ module MongoBase
3
+
4
+ def self.included_by_model(base)
5
+ base.class_eval do
6
+
7
+ scope :geocoded, lambda {
8
+ where(geocoder_options[:coordinates].ne => nil)
9
+ }
10
+
11
+ scope :not_geocoded, lambda {
12
+ where(geocoder_options[:coordinates] => nil)
13
+ }
14
+
15
+ scope :near, lambda{ |location, *args|
16
+ coords = Geocoder::Calculations.extract_coordinates(location)
17
+
18
+ # no results if no lat/lon given
19
+ return where(:id => false) unless coords.is_a?(Array)
20
+
21
+ radius = args.size > 0 ? args.shift : 20
22
+ options = args.size > 0 ? args.shift : {}
23
+
24
+ # Use BSON::OrderedHash if Ruby's hashes are unordered.
25
+ # Conditions must be in order required by indexes (see mongo gem).
26
+ empty = RUBY_VERSION.split('.')[1].to_i < 9 ? BSON::OrderedHash.new : {}
27
+
28
+ conds = empty.clone
29
+ field = geocoder_options[:coordinates]
30
+ conds[field] = empty.clone
31
+ conds[field]["$nearSphere"] = coords.reverse
32
+ conds[field]["$maxDistance"] = \
33
+ Geocoder::Calculations.distance_to_radians(radius, options[:units] || :mi)
34
+
35
+ if obj = options[:exclude]
36
+ conds[:_id.ne] = obj.id
37
+ end
38
+ where(conds)
39
+ }
40
+ end
41
+ end
42
+
43
+ ##
44
+ # Coordinates [lat,lon] of the object.
45
+ # This method always returns coordinates in lat,lon order,
46
+ # even though internally they are stored in the opposite order.
47
+ #
48
+ def to_coordinates
49
+ coords = send(self.class.geocoder_options[:coordinates])
50
+ coords.is_a?(Array) ? coords.reverse : []
51
+ end
52
+
53
+ ##
54
+ # Look up coordinates and assign to +latitude+ and +longitude+ attributes
55
+ # (or other as specified in +geocoded_by+). Returns coordinates (array).
56
+ #
57
+ def geocode
58
+ do_lookup(false) do |o,rs|
59
+ r = rs.first
60
+ unless r.coordinates.nil?
61
+ o.send :write_attribute, self.class.geocoder_options[:coordinates], r.coordinates.reverse
62
+ end
63
+ r.coordinates
64
+ end
65
+ end
66
+
67
+ ##
68
+ # Look up address and assign to +address+ attribute (or other as specified
69
+ # in +reverse_geocoded_by+). Returns address (string).
70
+ #
71
+ def reverse_geocode
72
+ do_lookup(true) do |o,rs|
73
+ r = rs.first
74
+ unless r.address.nil?
75
+ o.send :write_attribute, self.class.geocoder_options[:fetched_address], r.address
76
+ end
77
+ r.address
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,13 @@
1
+ require 'geocoder/stores/base'
2
+ require 'geocoder/stores/mongo_base'
3
+
4
+ module Geocoder::Store
5
+ module MongoMapper
6
+ include Base
7
+ include MongoBase
8
+
9
+ def self.included(base)
10
+ MongoBase.included_by_model(base)
11
+ end
12
+ end
13
+ end
@@ -1,79 +1,13 @@
1
1
  require 'geocoder/stores/base'
2
+ require 'geocoder/stores/mongo_base'
2
3
 
3
4
  module Geocoder::Store
4
5
  module Mongoid
5
6
  include Base
7
+ include MongoBase
6
8
 
7
9
  def self.included(base)
8
- base.class_eval do
9
-
10
- scope :geocoded, lambda {
11
- where(geocoder_options[:coordinates].ne => nil)
12
- }
13
-
14
- scope :not_geocoded, lambda {
15
- where(geocoder_options[:coordinates] => nil)
16
- }
17
-
18
- scope :near, lambda{ |location, *args|
19
- coords = Geocoder::Calculations.extract_coordinates(location)
20
- radius = args.size > 0 ? args.shift : 20
21
- options = args.size > 0 ? args.shift : {}
22
-
23
- # Use BSON::OrderedHash if Ruby's hashes are unordered.
24
- # Conditions must be in order required by indexes (see mongo gem).
25
- empty = RUBY_VERSION.split('.')[1].to_i < 9 ? BSON::OrderedHash.new : {}
26
-
27
- conds = empty.clone
28
- conds[:coordinates] = empty.clone
29
- conds[:coordinates]["$nearSphere"] = coords.reverse
30
- conds[:coordinates]["$maxDistance"] = \
31
- Geocoder::Calculations.distance_to_radians(radius, options[:units] || :mi)
32
-
33
- if obj = options[:exclude]
34
- conds[:_id.ne] = obj.id
35
- end
36
- criteria.where(conds)
37
- }
38
- end
39
- end
40
-
41
- ##
42
- # Coordinates [lat,lon] of the object.
43
- # This method always returns coordinates in lat,lon order,
44
- # even though internally they are stored in the opposite order.
45
- #
46
- def to_coordinates
47
- coords = send(self.class.geocoder_options[:coordinates])
48
- coords.is_a?(Array) ? coords.reverse : []
49
- end
50
-
51
- ##
52
- # Look up coordinates and assign to +latitude+ and +longitude+ attributes
53
- # (or other as specified in +geocoded_by+). Returns coordinates (array).
54
- #
55
- def geocode
56
- do_lookup(false) do |o,rs|
57
- r = rs.first
58
- unless r.coordinates.nil?
59
- o.send :write_attribute, self.class.geocoder_options[:coordinates], r.coordinates.reverse
60
- end
61
- r.coordinates
62
- end
63
- end
64
-
65
- ##
66
- # Look up address and assign to +address+ attribute (or other as specified
67
- # in +reverse_geocoded_by+). Returns address (string).
68
- #
69
- def reverse_geocode
70
- do_lookup(true) do |o,rs|
71
- r = rs.first
72
- unless r.address.nil?
73
- o.send :write_attribute, self.class.geocoder_options[:fetched_address], r.address
74
- end
75
- r.address
76
- end
10
+ MongoBase.included_by_model(base)
77
11
  end
78
12
  end
79
13
  end
@@ -1,3 +1,3 @@
1
1
  module Geocoder
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.2"
3
3
  end
@@ -0,0 +1,147 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+
4
+ class CalculationsTest < Test::Unit::TestCase
5
+
6
+
7
+ # --- degree distance ---
8
+
9
+ def test_longitude_degree_distance_at_equator
10
+ assert_equal 69, Geocoder::Calculations.longitude_degree_distance(0).round
11
+ end
12
+
13
+ def test_longitude_degree_distance_at_new_york
14
+ assert_equal 53, Geocoder::Calculations.longitude_degree_distance(40).round
15
+ end
16
+
17
+ def test_longitude_degree_distance_at_north_pole
18
+ assert_equal 0, Geocoder::Calculations.longitude_degree_distance(89.98).round
19
+ end
20
+
21
+
22
+ # --- distance between ---
23
+
24
+ def test_distance_between_in_miles
25
+ assert_equal 69, Geocoder::Calculations.distance_between([0,0], [0,1]).round
26
+ la_to_ny = Geocoder::Calculations.distance_between([34.05,-118.25], [40.72,-74]).round
27
+ assert (la_to_ny - 2444).abs < 10
28
+ end
29
+
30
+ def test_distance_between_in_kilometers
31
+ assert_equal 111, Geocoder::Calculations.distance_between([0,0], [0,1], :units => :km).round
32
+ la_to_ny = Geocoder::Calculations.distance_between([34.05,-118.25], [40.72,-74], :units => :km).round
33
+ assert (la_to_ny - 3942).abs < 10
34
+ end
35
+
36
+
37
+ # --- geographic center ---
38
+
39
+ def test_geographic_center_with_arrays
40
+ assert_equal [0.0, 0.5],
41
+ Geocoder::Calculations.geographic_center([[0,0], [0,1]])
42
+ assert_equal [0.0, 1.0],
43
+ Geocoder::Calculations.geographic_center([[0,0], [0,1], [0,2]])
44
+ end
45
+
46
+ def test_geographic_center_with_mixed_arguments
47
+ p1 = [0, 0]
48
+ p2 = Landmark.new("Some Cold Place", 0, 1)
49
+ assert_equal [0.0, 0.5], Geocoder::Calculations.geographic_center([p1, p2])
50
+ end
51
+
52
+
53
+ # --- bounding box ---
54
+
55
+ def test_bounding_box_calculation_in_miles
56
+ center = [51, 7] # Cologne, DE
57
+ radius = 10 # miles
58
+ dlon = radius / Geocoder::Calculations.latitude_degree_distance
59
+ dlat = radius / Geocoder::Calculations.longitude_degree_distance(center[0])
60
+ corners = [50.86, 6.77, 51.14, 7.23]
61
+ assert_equal corners.map{ |i| (i * 100).round },
62
+ Geocoder::Calculations.bounding_box(center, radius).map{ |i| (i * 100).round }
63
+ end
64
+
65
+ def test_bounding_box_calculation_in_kilometers
66
+ center = [51, 7] # Cologne, DE
67
+ radius = 111 # kilometers (= 1 degree latitude)
68
+ dlon = radius / Geocoder::Calculations.latitude_degree_distance(:km)
69
+ dlat = radius / Geocoder::Calculations.longitude_degree_distance(center[0], :km)
70
+ corners = [50, 5.41, 52, 8.59]
71
+ assert_equal corners.map{ |i| (i * 100).round },
72
+ Geocoder::Calculations.bounding_box(center, radius, :units => :km).map{ |i| (i * 100).round }
73
+ end
74
+
75
+ def test_bounding_box_calculation_with_object
76
+ center = [51, 7] # Cologne, DE
77
+ radius = 10 # miles
78
+ dlon = radius / Geocoder::Calculations.latitude_degree_distance
79
+ dlat = radius / Geocoder::Calculations.longitude_degree_distance(center[0])
80
+ corners = [50.86, 6.77, 51.14, 7.23]
81
+ obj = Landmark.new("Cologne", center[0], center[1])
82
+ assert_equal corners.map{ |i| (i * 100).round },
83
+ Geocoder::Calculations.bounding_box(obj, radius).map{ |i| (i * 100).round }
84
+ end
85
+
86
+ def test_bounding_box_calculation_with_address_string
87
+ assert_nothing_raised do
88
+ Geocoder::Calculations.bounding_box("4893 Clay St, San Francisco, CA", 50)
89
+ end
90
+ end
91
+
92
+
93
+ # --- bearing ---
94
+
95
+ def test_compass_points
96
+ assert_equal "N", Geocoder::Calculations.compass_point(0)
97
+ assert_equal "N", Geocoder::Calculations.compass_point(1.0)
98
+ assert_equal "N", Geocoder::Calculations.compass_point(360)
99
+ assert_equal "N", Geocoder::Calculations.compass_point(361)
100
+ assert_equal "N", Geocoder::Calculations.compass_point(-22)
101
+ assert_equal "NW", Geocoder::Calculations.compass_point(-23)
102
+ assert_equal "S", Geocoder::Calculations.compass_point(180)
103
+ assert_equal "S", Geocoder::Calculations.compass_point(181)
104
+ end
105
+
106
+ def test_bearing_between
107
+ bearings = {
108
+ :n => 0,
109
+ :e => 90,
110
+ :s => 180,
111
+ :w => 270
112
+ }
113
+ points = {
114
+ :n => [41, -75],
115
+ :e => [40, -74],
116
+ :s => [39, -75],
117
+ :w => [40, -76]
118
+ }
119
+ directions = [:n, :e, :s, :w]
120
+ methods = [:linear, :spherical]
121
+
122
+ methods.each do |m|
123
+ directions.each_with_index do |d,i|
124
+ opp = directions[(i + 2) % 4] # opposite direction
125
+ b = Geocoder::Calculations.bearing_between(
126
+ points[d], points[opp], :method => m)
127
+ assert (b - bearings[opp]).abs < 1,
128
+ "Bearing (#{m}) should be close to #{bearings[opp]} but was #{b}."
129
+ end
130
+ end
131
+ end
132
+
133
+ def test_spherical_bearing_to
134
+ l = Landmark.new(*landmark_params(:msg))
135
+ assert_equal 324, l.bearing_to([50,-85], :method => :spherical).round
136
+ end
137
+
138
+ def test_spherical_bearing_from
139
+ l = Landmark.new(*landmark_params(:msg))
140
+ assert_equal 136, l.bearing_from([50,-85], :method => :spherical).round
141
+ end
142
+
143
+ def test_linear_bearing_from_and_to_are_exactly_opposite
144
+ l = Landmark.new(*landmark_params(:msg))
145
+ assert_equal l.bearing_from([50,-86.1]), l.bearing_to([50,-86.1]) - 180
146
+ end
147
+ end