Permalink
Browse files

add specs, get mixin working, start on adding find methods

  • Loading branch information...
Matt King authored and foysavas committed Jun 8, 2009
1 parent 86cb197 commit cde4b9190446bc4c49c05c81faa146cfa989091e
Showing with 168 additions and 22 deletions.
  1. +6 −1 README
  2. +3 −3 Rakefile
  3. +1 −2 lib/dm-geokit.rb
  4. +80 −9 lib/dm-geokit/resource.rb
  5. 0 lib/jeweler/templates/.gitignore
  6. +58 −0 spec/dm_geokit_spec.rb
  7. +0 −7 spec/merb_geokit_spec.rb
  8. +20 −0 spec/spec_helper.rb
View
7 README
@@ -1,4 +1,9 @@
dm-geokit
=========
-DataMapper plugin for acts_as_mappable stuff.
+A mixin for Datamapper models that enables geographic functionality.
+
+* Search for content via simple query methods, e.g. Location.all(:origin => 'Portland, OR', :within => 5.miles)
+
+
+Requires the GeoKit gem.
View
@@ -3,9 +3,9 @@ begin
Jeweler::Tasks.new do |s|
s.name = 'dm-geokit'
s.summary = "DataMapper plugin for geokit stuff forked from Foy Savas's project. Now relies on the geokit gem rather than Foy's gem."
- s.authors = ['Foy Savas', 'Daniel Higginbotham']
- s.email = 'daniel@flyingmachinestudios.com'
- s.homepage = "http://github.com/flyingmachine/dm-geokit/tree/master"
+ s.authors = ['Foy Savas', 'Daniel Higginbotham', 'Matt King']
+ s.email = 'matt@mattking.org'
+ s.homepage = "http://github.com/mattking17/dm-geokit/tree/master"
s.description = "Simple and opinionated helper for creating Rubygem projects on GitHub"
s.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore']
s.require_path = 'lib'
View
@@ -1,6 +1,5 @@
require 'rubygems'
-require 'geokit-core'
+require 'geokit'
require 'dm-core'
require File.join(File.dirname(__FILE__),'dm-geokit','resource')
-
View
@@ -1,22 +1,33 @@
module DataMapper
module GeoKit
+ PROPERTY_NAMES = %w(lat lng street_address city state zip country_code full_address)
+
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def has_geographic_location(name, options = {})
- include InstanceMethods
+ return if self.included_modules.include?(DataMapper::GeoKit::InstanceMethods)
+ send :include, InstanceMethods
+ send :include, ::GeoKit::Mappable
+
+ property name.to_sym, String, :size => 255
+ PROPERTY_NAMES.each do |p|
+ if p.match(/l(at|ng)/)
+ property "#{name}_#{p}".to_sym, Float, :precision => 15, :scale => 12, :index => true
+ else
+ property "#{name}_#{p}".to_sym, String, :size => 255
+ end
+ end
- property name.to_sym, DM::Text
- property "#{name}_lat".to_sym, Float, :precision => 9, :scale => 6
- property "#{name}_lng".to_sym, Float, :precision => 9, :scale => 6
+ DataMapper.auto_upgrade!
define_method "#{name}" do
if(value = attribute_get(name.to_sym)).nil?
nil
else
- ::YAML.load(value)
+ GeographicLocation.new(name, self)
end
end
@@ -25,16 +36,76 @@ def has_geographic_location(name, options = {})
nil
else value.is_a?(String)
geo = ::GeoKit::Geocoders::MultiGeocoder.geocode(value)
- attribute_set("#{name}".to_sym, geo.to_yaml)
- attribute_set("#{name}_lat".to_sym, geo.lat)
- attribute_set("#{name}_lng".to_sym, geo.lng)
- end
+ if geo.success?
+ attribute_set(name.to_sym, geo.full_address)
+ PROPERTY_NAMES.each do |p|
+ attribute_set("#{name}_#{p}".to_sym, geo.send(p.to_sym))
+ end
+ end
+ end
+ end
+
+ cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name
+ self.distance_column_name = options[:distance_column_name] || 'distance'
+ self.default_units = options[:default_units] || ::GeoKit::default_units
+ self.default_formula = options[:default_formula] || ::GeoKit::default_formula
+ self.lat_column_name = "#{name}_lat"
+ self.lng_column_name = "#{name}_lng"
+ self.qualified_lat_column_name = "#{storage_name}.#{lat_column_name}"
+ self.qualified_lng_column_name = "#{storage_name}.#{lng_column_name}"
+ if options.include?(:auto_geocode) && options[:auto_geocode]
+ # if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash
+ options[:auto_geocode] = {} if options[:auto_geocode] == true
+ cattr_accessor :auto_geocode_field, :auto_geocode_error_message
+ self.auto_geocode_field = options[:auto_geocode][:field] || 'address'
+ self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address'
+
+ # set the actual callback here
+# before_validation_on_create :auto_geocode_address
end
+
end
+ alias acts_as_mappable has_geographic_location
end
module InstanceMethods
+ # Mix class methods into module.
+ def self.included(base) # :nodoc:
+ base.extend SingletonMethods
+ end
+
+ # Class singleton methods to mix into ActiveRecord.
+ module SingletonMethods # :nodoc:
+ # Extends the existing find method in potentially two ways:
+ # - If a mappable instance exists in the options, adds a distance column.
+ # - If a mappable instance exists in the options and the distance column exists in the
+ # conditions, substitutes the distance sql for the distance column -- this saves
+ # having to write the gory SQL.
+ def all(query = {})
+ super(prepare_query(query))
+ end
+
+ private
+
+ def prepare_query(query)
+ origin = query.delete(:origin)
+ within = query.delete(:within)
+ query
+ end
+
+ end
+ end
+ class GeographicLocation
+ attr_accessor :full_address, :lat, :lng, :street_address, :city, :state, :zip, :country_code
+ def initialize(field, obj)
+ PROPERTY_NAMES.each do |p|
+ instance_variable_set("@#{p}",obj.send("#{field}_#{p}"))
+ end
+ end
+ def to_s
+ @full_address
+ end
end
end
No changes.
View
@@ -0,0 +1,58 @@
+require File.dirname(__FILE__) + '/spec_helper'
+
+describe "dm-geokit" do
+ it "should add address fields after calling has_geographic_location" do
+ u = UninitializedLocation.new
+ u.should_not respond_to(:address)
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
+ u.should_not respond_to("address_#{p}".to_sym)
+ end
+ UninitializedLocation.send(:has_geographic_location, :address)
+ u = UninitializedLocation.new
+ u.should respond_to(:address)
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
+ u.should respond_to("address_#{p}".to_sym)
+ end
+ end
+
+ it "should respond to acts_as_mappable" do
+ Location.should respond_to(:acts_as_mappable)
+ end
+
+ it "should have a geocode method" do
+ Location.should respond_to(:geocode)
+ end
+
+ it "should have the address field return a GeographicLocation object" do
+ l = Location.create(:address => "5119 NE 27th ave portland, or 97211")
+ l.address.should be_a(DataMapper::GeoKit::GeographicLocation)
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
+ l.address.should respond_to("#{p}".to_sym)
+ end
+ end
+
+ it "should convert to LatLng" do
+ l = Location.create(:address => "5119 NE 27th ave portland, or 97211")
+ l.should respond_to(:to_lat_lng)
+ l.to_lat_lng.should be_a(::GeoKit::LatLng)
+ l.to_lat_lng.lat.should == l.address.lat
+ l.to_lat_lng.lng.should == l.address.lng
+ end
+
+ it "should set address fields on geocode" do
+ l = Location.new
+ l.address.should be(nil)
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
+ l.send("address_#{p}").should be(nil)
+ end
+ l.address = '5119 NE 27th ave portland, or 97211'
+ DataMapper::GeoKit::PROPERTY_NAMES.each do |p|
+ l.send("address_#{p}").should_not be(nil)
+ end
+ end
+
+ it "should find a location" do
+ Location.all(:origin => 'portland, or', :within => 5).size.should == 2
+ end
+
+end
View
@@ -1,7 +0,0 @@
-require File.dirname(__FILE__) + '/spec_helper'
-
-describe "merb_geokit" do
- it "should do nothing" do
- true.should == true
- end
-end
View
@@ -1,2 +1,22 @@
$TESTING=true
$:.push File.join(File.dirname(__FILE__), '..', 'lib')
+%w(dm-geokit).each{|l| require l}
+
+DataMapper::Logger.new(STDOUT, :debug)
+DataMapper.setup(:default, "mysql://root@localhost/dm_geokit_test")
+
+class Location
+ include DataMapper::Resource
+ include DataMapper::GeoKit
+ property :id, Serial
+ has_geographic_location :address
+end
+
+class UninitializedLocation
+ include DataMapper::Resource
+ include DataMapper::GeoKit
+ property :id, Serial
+end
+
+DataMapper.auto_migrate!
+

0 comments on commit cde4b91

Please sign in to comment.