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:

  1. 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.
  2. Add the “Locatable” mixin to whatever class you want to make findable.
  3. 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,