While making GaymerCon we knew location search would be a priority. In all the gaymer meetups we’ve seen, awesome people who never would have found each other any other way have been brought together.
But you get tired of writing the same get_nearby_x
query over and over. Wouldn’t it be nicer if there were just a mixin that could handle all that jazz? Well, enter Locatable:
module Locatable
def self.included(base)
base.extend(ClassMethods)
end
def location
@location ||= Location.find_or_create_by(item_id: self.id, item_class: self.class.name.underscore)
@location
end
def place=(new_place)
prev_place = self.location.place
return if prev_place == new_place
coords = PlaceFinder.coords(new_place)
location.place = new_place
location.coords = coords
location.save
end
def place
self.location.place
end
def coords=(new_coords)
raise ArgumentError.new("Coordinates must by a [lat, lng] array!") unless new_coords.is_a?(Array) && new_coords.count == 2
return true if location.coords == new_coords
location.coords = new_coords
location.save
end
def coords
return nil if location.coords == [199.9, 199.9]
location.coords
end
def distance_from(point_arr)
Geocalc.distance_between(self.coords, point_arr)
end
def nearby(max_miles_away = 50)
location.nearby(max_miles_away, self.class.name.underscore)
end
def method_missing(method, *args, &block)
return location.send(:nearby, args[0], $1.downcase.singularize) if method.to_s =~ /nearby_(\w+)/
super(method, *args, &block)
end
module ClassMethods
def nearby(coords, max_miles_away = 50)
Location.nearby(coords, max_miles_away, self.name.underscore)
end
def method_missing(method, *args, &block)
return Location.nearby(args[0], args[1], $1.downcase.singularize) if method.to_s =~ /nearby_(\w+)/
super(method, *args, &block)
end
end
end
And its cousin class, the Location model:
class Location
include Mongoid::Document
field :coords, :type => Array, :default => lambda{ [199.9, 199.9] }
index({ coords: "2d" }, { min: -200, max: 200 })
field :place, type: String
field :item_id, type: Integer
field :item_class, type: String
def self.nearby(coords, max_miles_away = 50, item_class = "user")
# convert to lat/lng coord distance, as per http://www.mongodb.org/display/DOCS/Geospatial+Indexing
max_miles_away = 50 unless max_miles_away.present?
item_ids = self.where(:coords => {"$near" => coords , '$maxDistance' => max_miles_away.fdiv(69)}, :item_class => item_class).to_a
item_ids.collect!(&:item_id)
klass = item_class.classify.constantize
klass.where("id IN (?)", item_ids)
end
def nearby(max_miles_away = 50, item_class = "user")
return [] if self.coords.nil? || self.coords == [199.9, 199.9]
self.class.nearby(self.coords, max_miles_away, item_class)
end
def self.method_missing(method, *args, &block)
return self.nearby(args[0], args[1], $1.downcase.singularize) if method.to_s =~ /nearby_(\w+)/
super(method, *args, &block)
end
def method_missing(method, *args, &block)
return self.nearby(args[0], $1.downcase.singularize) if method.to_s =~ /nearby_(\w+)/
super(method, *args, &block)
end
end
Here’s how it works:
- You create the location model using Mongoid. Mongo has built-in geo-indexing and geo-search capabilities, so we make a generic class to do that.
- Add the “Locatable” mixin to whatever class you want to make findable.
- Anywhere in your models / controllers, you can call “get_nearby_x” on your locatable class, where x is the underscored name of any other locatable class! For example, “get_nearby_users” and “get_nearby_groups” both work for any given user or group. Neat, right?
With Locatable, you can paginate and search through any model, starting from any model, as long as you at some point input coordinates for both of them. So far, it’s proving pretty nice.
Cheers,