Skip to content
This repository

Added geocoded_through #66

Open
wants to merge 17 commits into from
Vince Puzzella
class User < ActiveRecord::Base
  has_many :inventory_items
  geocoded_by :postal_code
  reverse_geocoded_by :latitude, :longitude
  after_validation :geocode
end

class InventoryItem < ActiveRecord::Base
  belongs_to :user
  geocoded_through :user
end

irb(main):013:0> InventoryItem.near('M4J3J8').first.user.postal_code
=> "M4J3J8"
irb(main):014:0> InventoryItem.near('M4J3J8').first.reverse_geocode
=> "127-135 Dewhurst Blvd, East York, ON M4J 3J9, Canada"
irb(main):015:0>

Stefan Wrobel

Has this been reviewed? This would be great functionality to have.

Ozkar

I tried this out, but it kept returning nil for distance.

I have this in my gemfile:

gem 'geocoder', :git => 'git://github.com/vpuzzella/geocoder.git'

class Address < ActiveRecord::Base
    has_many :items
    geocoded_by :full_address
end

class Item < ActiveRecord::Base
    belongs_to :address
    geocoded_through :address
end

Now if I do Address.near, it functions normally:

addresses = Address.near('12345 sample st, CA, 12345', 5, :order => :distance)

addresses.first.distance   ## returns a float for me

Now on the other hand if I did Item.near (the model that is using geocoded_through), calling 'distance' always returns nil:

items = Item.near('12345 sample st, CA, 12345', 5, :order => :distance)

items.first                                       ## returns nil
items.map(&:distance).compact    ## returns [] 

Tried it in sqlite and mysql

alexreisner / vpuzzella Any ideas?

Ozkar

Update: With geocoded_through, the array of returned items IS CORRECTLY sorted by distance, but that distance float just isn't automatically assigned to each item for some reason

My temporary solution was this

add attr_accessor :distance to Item:

class Item < ActiveRecord::Base
    belongs_to :address
    geocoded_through :address

    attr_accessor :distance
end

Then manually assign the distance to each item in the array afterwards using distance_to()

@items = Item.near(@address, @within, :order => :distance)

@items.each{|item| item.distance = item.distance_to(@address) }

Super inefficient I'm sure, but it will have to do for me for now, hopefully one of you guys figures out how to do it properly =)

btw, thx for your hard work guys, really appreciate it!

Vince Puzzella

Cool, I'll see if I can get this stuff merged in. Sorry, ultra busy these days... Patches are welcome!

Ozkar

Ahhh nooo dont merge my stuff in, it was just a hack i did to get distance to show up =|

I'm not sure maybe there is a better, more efficient way then this =|

Does anyone know how where in the code the Model is suppose to get assigned the 'distance' value?

Justin Sunsri

Thanks!!!!! need to get this into production

Tomás Arribas

Any chance to get this into master? I'm using it extensively in my app and I'm having to bundle Geocoder from vpuzzella's fork since 5 months! Would love to have geocoded_through + all the latest improvements on Geocoder, but until this is integrated I'm stuck using vpuzzella's outdated fork.

geocoded_through has worked beautifully for me so far. Distance was correctly assigned for each item as well. Please get it merged!

Vince Puzzella

@raymccoy: I tried to rebase upstream mater, but there are too many conflicts for me to deal with at this time. Feel free to fork and have a crack at it.

Vince Puzzella

@raymccoy: I merged upstream master in so the fork is no longer outdated. I didn't have time to thoroughly test the merge so you'll have to confirm that everything is working as it should.

Sorry, I'm just too busy these days....

Tomás Arribas

Thank you! Unfortunately it's not working now. When using Model.near it tries to use the (non-existing) model's latitude and longitude db columns instead of the colums from the geocoded_through model.

Don't worry, will fork yours and try to get it working with the current codebase, thanks for your time!

Vince Puzzella

@raymccoy: Sorry about that. Let me know when you have a fix and I will merge it.

Tomás Arribas

@vpuzzella unfortunately it seems the changes are too substantial to attempt a fix from your version, so in the end I have forked the main repo and I'm adapting your changes to the new codebase from scratch.

I already have it working for what I use in my app (Model.near), but I'm trying to make it work for all other methods, i.e. Model.geocode, Model.to_coordinates and other methods that are failing right now when using them on a model with geocoded_though

Vince Puzzella

@raymccoy: I made another feeble attempt to fix the problem. :) It's totally untested because I don't have access to my proper dev env at the moment.

Tomás Arribas

Thanks! But it's still broken, now it spits:

undefined local variable or method `through' for #<Class:0x000000045fe868>

When using Model.near. This is the relevant part of the trace

/lib/geocoder/stores/active_record.rb:237:in `default_near_scope_options'
/lib/geocoder/stores/active_record.rb:144:in `full_near_scope_options'
/lib/geocoder/stores/active_record.rb:96:in `near_scope_options'
/lib/geocoder/stores/active_record.rb:37:in `block (2 levels) in included'
Tomás Arribas

Great, that seems solved, but now it fails in the SQL query, it is built wrong.

It is doing "model_table.through_table.latitude" instead of "through_table.latitude" for the selects. I believe this happens where there is #{table_name}.#{lat_attr} in the code.

Same for longitude, for the record :)

Tomás Arribas

Yup, I changed all #{table_name}.#{lat_attr} to #{lat_attr} and .near seems to work now :) Will try other methods to see that all is working.

Tomás Arribas

Tried .geocode, .to_coordinates and .nearbys and they don't work.

I think commit 726fc47 messes things up. geocoder_options[:latitude] is used sometimes for accessing the database, but other times it is used to access the model's attributes, for example

  def to_coordinates
    [:latitude, :longitude].map do |i|
      if assoc = self.class.geocoder_options[:through]
        send assoc.name
      else
        self
      end.send self.class.geocoder_options[i]
    end
  end

So in there, when using geocoded_through is trying to access Model.table_name.latitude, instead of Model.through_relation.latitude.

In my case, my through model is "Address", so instead of doing Model.address.latitude it is doing Model.adresses.latitude (adresses being the table name) and fails.

I think it is safer to leave geocoder_options[:latitude] alone and use

through_table = geocoder_options[:through] ? geocoder_options[:through].table_name + '.' : ''

"POWER(SIN((#{latitude} - #{through_table}#{lat_attr}) * PI() / 180 / 2), 2) + "
[...]

In every method where lat_attr and lon_attr are used. Not very DRY I know...

Can do it for you and pull request if you don't want to waste more time with me! :)

Vince Puzzella

@raymccoy Please go ahead and make the necessary changes :) Send me a pull request when you're done. Thank you for handling this!

Tomás Arribas

For the record:

Model.near, Model.to_coordinates, Model.geocode and Model.nearbys work now for geocoded_through models.

Some of these methods work only if you map latitude and longitude to the geocoded_through model manually, i.e.

has_one :address
geocoded_through :address

def latitude
  address.latitude
end

def longitude
  address.longitude
end

Docs would need to mention this if this pull is accepted.

Justin Sunsri
David Fisher

+1 for this feature. Very needed for me.

Edo Balvers

+1 as well, thanks for the work!

Daniel Staudigel

+1 - when will this be released?

Thomas Kienlen

+1 ... great feature !

Matt Ledom
mledom commented

Does anyone know if this was merged in?

Nico Ritsche
ncri commented

I'd also like to see this merged. I used a hack on master for near queries with the location in an associated record:

class User < ActiveRecord::Base
  has_many :inventory_items
  geocoded_by :postal_code
end

class InventoryItem < ActiveRecord::Base
  belongs_to :user

  def self.near location, distance
    scope_options = User.send(:near_scope_options, location[0], location[1], distance, units: :km)
    joins(user).
    where(scope_options[:conditions]).
    select('inventory_items.*').
    select(scope_options[:select]).
    order(scope_options[:order])
  end

end

Works well for my simple use case.

Harrison Sweeney

+1, would be good to have this functionality merged with master

Thomas Kienlen

I did the merge in my 'merge_all' branch.
Now the code has diverged but it might be possible to cherry pick what you need.

will

I'm attempting to use with kaminari for pagination - and i'm getting issues with pagination when using geocodes_through only the first page is returned, but with no pagination links. If i comment out the geocode near part and click through to page 2, then uncomment the line the expected results (or at least some results) are returned. I think its the count part thats going wrong.

logging the active relations count shows it is

{"151.2872618"=>1}

when using geocode_through

but

10

without. Which looks suspicous. Is this an issue with the geocde_through fork, or might i be doing it wrong?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
8  CHANGELOG.rdoc
Source Rendered
@@ -2,6 +2,14 @@
2 2
 
3 3
 Per-release changes to Geocoder.
4 4
 
  5
+== 1.1.1 (2012 Feb 16)
  6
+
  7
+* Add distance_from_sql class method to geocoded class (thanks github.com/dwilkie).
  8
+* Add OverQueryLimitError and raise when relevant for Google lookup.
  9
+* Fix: don't cache API data if response indicates an error.
  10
+* Fix: within_bounding_box now uses correct lat/lon DB columns (thanks github.com/kongo).
  11
+* Fix: error accessing city in some cases with Yandex result (thanks github.com/kor6n and sld).
  12
+
5 13
 == 1.1.0 (2011 Dec 3)
6 14
 
7 15
 * A block passed to geocoded_by is now always executed, even if the geocoding service returns no results. This means you need to make sure you have results before trying to assign data to your object.
5  README.rdoc
Source Rendered
@@ -53,6 +53,11 @@ For reverse geocoding, tell Geocoder which attributes store latitude and longitu
53 53
   reverse_geocoded_by :latitude, :longitude
54 54
   after_validation :reverse_geocode  # auto-fetch address
55 55
 
  56
+To specify that model is geocoded through a :belongs_to association:
  57
+  
  58
+  belongs_to :user
  59
+  geocoded_through :user
  60
+
56 61
 === Mongoid
57 62
 
58 63
 First, your model must have an array field for storing coordinates:
5  lib/geocoder/models/active_record.rb
@@ -31,6 +31,11 @@ def reverse_geocoded_by(latitude_attr, longitude_attr, options = {}, &block)
31 31
         )
32 32
       end
33 33
 
  34
+      def geocoded_through(assoc_name)
  35
+        assoc = reflect_on_association assoc_name
  36
+        # TODO: Raise an error if assoc is not a belongs_to
  37
+        geocoder_init assoc.klass.geocoder_options.merge(:through => assoc)
  38
+      end
34 39
 
35 40
       private # --------------------------------------------------------------
36 41
 
5  lib/geocoder/models/base.rb
@@ -24,7 +24,10 @@ def reverse_geocoded_by
24 24
         fail
25 25
       end
26 26
 
27  
-
  27
+      def geocoded_through(assoc_name)
  28
+        fail
  29
+      end
  30
+      
28 31
       private # ----------------------------------------------------------------
29 32
 
30 33
       def geocoder_init(options)
2  lib/geocoder/results/nominatim.rb
@@ -20,7 +20,7 @@ def city
20 20
     end
21 21
 
22 22
     def village
23  
-      @data['address']['villiage']
  23
+      @data['address']['village']
24 24
     end
25 25
 
26 26
     def town
84  lib/geocoder/stores/active_record.rb
@@ -116,8 +116,15 @@ def distance_from_sql_options(latitude, longitude, options = {})
116 116
       def full_near_scope_options(latitude, longitude, radius, options)
117 117
         lat_attr = geocoder_options[:latitude]
118 118
         lon_attr = geocoder_options[:longitude]
  119
+
  120
+        if assoc = geocoder_options[:through]          
  121
+          lat_attr = "#{assoc.table_name}.#{lat_attr}"
  122
+          lon_attr = "#{assoc.table_name}.#{lon_attr}"
  123
+        end
  124
+
119 125
         options[:bearing] = :linear unless options.include?(:bearing)
120 126
         bearing = case options[:bearing]
  127
+          
121 128
         when :linear
122 129
           "CAST(" +
123 130
             "DEGREES(ATAN2( " +
@@ -141,10 +148,7 @@ def full_near_scope_options(latitude, longitude, radius, options)
141 148
 
142 149
         distance = full_distance_from_sql(latitude, longitude, options)
143 150
         conditions = ["#{distance} <= ?", radius]
144  
-        default_near_scope_options(latitude, longitude, radius, options).merge(
145  
-          :select => "#{options[:select] || "#{table_name}.*"}, " +
146  
-            "#{distance} AS distance" +
147  
-            (bearing ? ", #{bearing} AS bearing" : ""),
  151
+        default_near_scope_options(latitude, longitude, radius, distance, bearing, options).merge(
148 152
           :conditions => add_exclude_condition(conditions, options[:exclude])
149 153
         )
150 154
       end
@@ -156,18 +160,28 @@ def full_near_scope_options(latitude, longitude, radius, options)
156 160
       def full_distance_from_sql(latitude, longitude, options)
157 161
         lat_attr = geocoder_options[:latitude]
158 162
         lon_attr = geocoder_options[:longitude]
  163
+        
  164
+        if assoc = geocoder_options[:through]
  165
+          lat_attr = "#{assoc.table_name}.#{lat_attr}"
  166
+          lon_attr = "#{assoc.table_name}.#{lon_attr}"
  167
+        end        
159 168
 
160 169
         earth = Geocoder::Calculations.earth_radius(options[:units] || :mi)
161 170
 
162 171
         "#{earth} * 2 * ASIN(SQRT(" +
163  
-          "POWER(SIN((#{latitude} - #{table_name}.#{lat_attr}) * PI() / 180 / 2), 2) + " +
164  
-          "COS(#{latitude} * PI() / 180) * COS(#{table_name}.#{lat_attr} * PI() / 180) * " +
165  
-          "POWER(SIN((#{longitude} - #{table_name}.#{lon_attr}) * PI() / 180 / 2), 2) ))"
  172
+          "POWER(SIN((#{latitude} - #{lat_attr}) * PI() / 180 / 2), 2) + " +
  173
+          "COS(#{latitude} * PI() / 180) * COS(#{lat_attr} * PI() / 180) * " +
  174
+          "POWER(SIN((#{longitude} - #{lon_attr}) * PI() / 180 / 2), 2) ))"
166 175
       end
167 176
 
168 177
       def approx_distance_from_sql(latitude, longitude, options)
169 178
         lat_attr = geocoder_options[:latitude]
170 179
         lon_attr = geocoder_options[:longitude]
  180
+        
  181
+        if assoc = geocoder_options[:through]
  182
+          lat_attr = "#{assoc.table_name}.#{lat_attr}"
  183
+          lon_attr = "#{assoc.table_name}.#{lon_attr}"
  184
+        end
171 185
 
172 186
         dx = Geocoder::Calculations.longitude_degree_distance(30, options[:units] || :mi)
173 187
         dy = Geocoder::Calculations.latitude_degree_distance(options[:units] || :mi)
@@ -175,8 +189,8 @@ def approx_distance_from_sql(latitude, longitude, options)
175 189
         # sin of 45 degrees = average x or y component of vector
176 190
         factor = Math.sin(Math::PI / 4)
177 191
 
178  
-        "(#{dy} * ABS(#{table_name}.#{lat_attr} - #{latitude}) * #{factor}) + " +
179  
-          "(#{dx} * ABS(#{table_name}.#{lon_attr} - #{longitude}) * #{factor})"
  192
+        "(#{dy} * ABS(#{lat_attr} - #{latitude}) * #{factor}) + " +
  193
+          "(#{dx} * ABS(#{lon_attr} - #{longitude}) * #{factor})"
180 194
       end
181 195
 
182 196
       ##
@@ -191,6 +205,12 @@ def approx_distance_from_sql(latitude, longitude, options)
191 205
       def approx_near_scope_options(latitude, longitude, radius, options)
192 206
         lat_attr = geocoder_options[:latitude]
193 207
         lon_attr = geocoder_options[:longitude]
  208
+        
  209
+        if assoc = geocoder_options[:through]
  210
+          lat_attr = "#{assoc.table_name}.#{lat_attr}"
  211
+          lon_attr = "#{assoc.table_name}.#{lon_attr}"
  212
+        end
  213
+        
194 214
         options[:bearing] = :linear unless options.include?(:bearing)
195 215
         if options[:bearing]
196 216
           bearing = "CASE " +
@@ -210,10 +230,7 @@ def approx_near_scope_options(latitude, longitude, radius, options)
210 230
           "#{lat_attr} BETWEEN ? AND ? AND #{lon_attr} BETWEEN ? AND ?"] +
211 231
           [b[0], b[2], b[1], b[3]
212 232
         ]
213  
-        default_near_scope_options(latitude, longitude, radius, options).merge(
214  
-          :select => "#{options[:select] || "#{table_name}.*"}, " +
215  
-            "#{distance} AS distance" +
216  
-            (bearing ? ", #{bearing} AS bearing" : ""),
  233
+        default_near_scope_options(latitude, longitude, radius, distance, bearing, options).merge(
217 234
           :conditions => add_exclude_condition(conditions, options[:exclude])
218 235
         )
219 236
       end
@@ -221,12 +238,43 @@ def approx_near_scope_options(latitude, longitude, radius, options)
221 238
       ##
222 239
       # Options used for any near-like scope.
223 240
       #
224  
-      def default_near_scope_options(latitude, longitude, radius, options)
  241
+      def default_near_scope_options(latitude, longitude, radius, distance, bearing, options)
  242
+        lat_attr = geocoder_options[:latitude]
  243
+        lon_attr = geocoder_options[:longitude]
  244
+        
  245
+        if assoc = geocoder_options[:through]
  246
+          lat_attr = "#{assoc.table_name}.#{lat_attr}"
  247
+          lon_attr = "#{assoc.table_name}.#{lon_attr}"
  248
+        end
  249
+
  250
+        b = Geocoder::Calculations.bounding_box([latitude, longitude], radius, options)
  251
+
  252
+        conditions = \
  253
+          ["#{lat_attr} BETWEEN ? AND ? AND #{lon_attr} BETWEEN ? AND ?"] +
  254
+          [b[0], b[2], b[1], b[3]]
  255
+
  256
+        if obj = options[:exclude]
  257
+          conditions[0] << " AND #{table_name}.id != ?"
  258
+          conditions << obj.id
  259
+        end
  260
+
  261
+        select = "#{options[:select] || "#{table_name}.*"}, #{distance} AS distance"
  262
+        select << ", #{bearing} AS bearing" if bearing
  263
+
  264
+        group = columns.map{ |c| "#{table_name}.#{c.name}" }.join(',')
  265
+        
  266
+        if through = geocoder_options[:through]
  267
+          group << ", #{lat_attr}, #{lon_attr}"
  268
+        end
  269
+
225 270
         {
226  
-          :order  => options[:order] || "distance",
227  
-          :limit  => options[:limit],
228  
-          :offset => options[:offset]
229  
-        }
  271
+          :select     =>  select,
  272
+          :group      =>  group,
  273
+          :order      =>  options[:order] || "distance",
  274
+          :limit      =>  options[:limit],
  275
+          :offset     =>  options[:offset],
  276
+          :conditions =>  conditions
  277
+        }.merge!(through ? {:joins =>  through.name} : {})
230 278
       end
231 279
 
232 280
       ##
17  lib/geocoder/stores/base.rb
@@ -13,7 +13,13 @@ def geocoded?
13 13
       # Coordinates [lat,lon] of the object.
14 14
       #
15 15
       def to_coordinates
16  
-        [:latitude, :longitude].map{ |i| send self.class.geocoder_options[i] }
  16
+        [:latitude, :longitude].map do |i|
  17
+          if assoc = self.class.geocoder_options[:through]
  18
+            send assoc.name
  19
+          else
  20
+            self
  21
+          end.send self.class.geocoder_options[i]
  22
+        end
17 23
       end
18 24
 
19 25
       ##
@@ -90,10 +96,13 @@ def reverse_geocode
90 96
       #
91 97
       def do_lookup(reverse = false)
92 98
         options = self.class.geocoder_options
  99
+
  100
+        target = options[:through] ? (send options[:through].name) : self
  101
+
93 102
         if reverse and options[:reverse_geocode]
94 103
           query = to_coordinates
95 104
         elsif !reverse and options[:geocode]
96  
-          query = send(options[:user_address])
  105
+          query = target.send(options[:user_address])
97 106
         else
98 107
           return
99 108
         end
@@ -103,12 +112,12 @@ def do_lookup(reverse = false)
103 112
         # execute custom block, if specified in configuration
104 113
         block_key = reverse ? :reverse_block : :geocode_block
105 114
         if custom_block = options[block_key]
106  
-          custom_block.call(self, results)
  115
+          custom_block.call(target, results)
107 116
 
108 117
         # else execute block passed directly to this method,
109 118
         # which generally performs the "auto-assigns"
110 119
         elsif block_given?
111  
-          yield(self, results)
  120
+          yield(target, results)
112 121
         end
113 122
       end
114 123
     end
2  lib/geocoder/version.rb
... ...
@@ -1,3 +1,3 @@
1 1
 module Geocoder
2  
-  VERSION = "1.1.0"
  2
+  VERSION = "1.1.1"
3 3
 end
2  test/test_helper.rb
@@ -150,7 +150,7 @@ def fetch_raw_data(query, reverse = false)
150 150
       end
151 151
     end
152 152
 
153  
-  class Nominatim < Base
  153
+    class Nominatim < Base
154 154
       private #-----------------------------------------------------------------
155 155
       def fetch_raw_data(query, reverse = false)
156 156
         raise TimeoutError if query == "timeout"
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.