├── public ├── favicon.ico ├── images │ └── rails.png ├── javascripts │ ├── application.js │ └── dragdrop.js ├── robots.txt ├── dispatch.rb ├── dispatch.cgi ├── dispatch.fcgi ├── 422.html ├── 404.html ├── 500.html └── index.html ├── vendor └── plugins │ ├── geokit-rails │ ├── test │ │ ├── fixtures │ │ │ ├── stores.yml │ │ │ ├── mock_organizations.yml │ │ │ ├── companies.yml │ │ │ ├── mock_addresses.yml │ │ │ ├── locations.yml │ │ │ └── custom_locations.yml │ │ ├── database.yml │ │ ├── test_helper.rb │ │ ├── schema.rb │ │ ├── ip_geocode_lookup_test.rb │ │ └── acts_as_mappable_test.rb │ ├── .gitignore │ ├── about.yml │ ├── VERSION_HISTORY.txt │ ├── Rakefile │ ├── lib │ │ └── geokit-rails │ │ │ ├── defaults.rb │ │ │ ├── ip_geocode_lookup.rb │ │ │ └── acts_as_mappable.rb │ ├── init.rb │ ├── install.rb │ ├── MIT-LICENSE │ ├── assets │ │ └── api_keys_template │ └── README.markdown │ └── stateaware_api │ ├── install.rb │ ├── uninstall.rb │ ├── init.rb │ ├── tasks │ └── stateaware_api_tasks.rake │ ├── test │ └── stateaware_api_test.rb │ ├── README │ ├── Rakefile │ ├── lib │ └── stateaware_api.rb │ └── MIT-LICENSE ├── app ├── helpers │ ├── data_groups_helper.rb │ ├── data_points_helper.rb │ └── application_helper.rb ├── models │ ├── data_group.rb │ ├── scraper_task.rb │ ├── data_point.rb │ ├── geo_location.rb │ ├── settings.rb │ ├── api │ │ └── they_work_for_you.rb │ ├── api.rb │ ├── scraper.rb │ └── scraper_parser.rb └── controllers │ ├── data_points_controller.rb │ ├── data_groups_controller.rb │ └── application.rb ├── config ├── settings.yml.example ├── initializers │ ├── mime_types.rb │ ├── inflections.rb │ ├── new_rails_defaults.rb │ └── geokit_config.rb ├── database.yml.example ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── routes.rb ├── boot.rb └── environment.rb ├── db ├── development.sqlite3 ├── migrate │ ├── 20090307112230_create_data_groups.rb │ ├── 20090307112508_create_apis.rb │ ├── 20090307112850_create_scraper_tasks.rb │ ├── 20090307112804_create_scrapers.rb │ └── 20090307114640_create_data_points.rb └── schema.rb ├── script ├── plugin ├── runner ├── server ├── console ├── dbconsole ├── destroy ├── generate ├── process │ ├── reaper │ ├── spawner │ └── inspector ├── performance │ ├── profiler │ ├── request │ └── benchmarker └── about ├── test ├── fixtures │ ├── scrapers.yml │ ├── data_groups.yml │ ├── scraper_tasks.yml │ ├── apis.yml │ └── data_points.yml ├── unit │ ├── api_test.rb │ ├── scraper_test.rb │ ├── data_group_test.rb │ ├── data_point_test.rb │ └── scraper_task_test.rb ├── functional │ ├── data_groups_controller_test.rb │ └── data_points_controller_test.rb └── test_helper.rb ├── doc └── README_FOR_APP ├── lib ├── tasks │ └── api.rake └── scrapers │ ├── sam_knows.scraper │ └── flood_warnings.scraper ├── Rakefile └── README.textile /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/fixtures/stores.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/.gitignore: -------------------------------------------------------------------------------- 1 | test/debug.log 2 | .svn 3 | -------------------------------------------------------------------------------- /app/helpers/data_groups_helper.rb: -------------------------------------------------------------------------------- 1 | module DataGroupsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/helpers/data_points_helper.rb: -------------------------------------------------------------------------------- 1 | module DataPointsHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/models/data_group.rb: -------------------------------------------------------------------------------- 1 | class DataGroup < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /config/settings.yml.example: -------------------------------------------------------------------------------- 1 | apis: 2 | theyworkforyou: 3 | key: 4 | -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/install.rb: -------------------------------------------------------------------------------- 1 | # Install hook code here 2 | -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/uninstall.rb: -------------------------------------------------------------------------------- 1 | # Uninstall hook code here 2 | -------------------------------------------------------------------------------- /app/models/scraper_task.rb: -------------------------------------------------------------------------------- 1 | class ScraperTask < ActiveRecord::Base 2 | belongs_to :scraper 3 | end 4 | -------------------------------------------------------------------------------- /db/development.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/stateaware/master/db/development.sqlite3 -------------------------------------------------------------------------------- /public/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/stateaware/master/public/images/rails.png -------------------------------------------------------------------------------- /app/models/data_point.rb: -------------------------------------------------------------------------------- 1 | class DataPoint < ActiveRecord::Base 2 | acts_as_mappable 3 | belongs_to :api 4 | end 5 | -------------------------------------------------------------------------------- /script/plugin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/plugin' 4 | -------------------------------------------------------------------------------- /script/runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/runner' 4 | -------------------------------------------------------------------------------- /script/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/server' 4 | -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/init.rb: -------------------------------------------------------------------------------- 1 | # Include hook code here 2 | require 'stateaware_api' 3 | StateawareApi.enable 4 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/console' 4 | -------------------------------------------------------------------------------- /script/dbconsole: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/dbconsole' 4 | -------------------------------------------------------------------------------- /script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/destroy' 4 | -------------------------------------------------------------------------------- /script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/generate' 4 | -------------------------------------------------------------------------------- /script/process/reaper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/reaper' 4 | -------------------------------------------------------------------------------- /script/process/spawner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/spawner' 4 | -------------------------------------------------------------------------------- /script/process/inspector: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/inspector' 4 | -------------------------------------------------------------------------------- /script/performance/profiler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/profiler' 4 | -------------------------------------------------------------------------------- /script/performance/request: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/request' 4 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/fixtures/mock_organizations.yml: -------------------------------------------------------------------------------- 1 | starbucks: 2 | name: Starbucks 3 | 4 | barnes_and_noble: 5 | name: Barnes & Noble -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # Methods added to this helper will be available to all templates in the application. 2 | module ApplicationHelper 3 | end 4 | -------------------------------------------------------------------------------- /script/performance/benchmarker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/benchmarker' 4 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/fixtures/companies.yml: -------------------------------------------------------------------------------- 1 | starbucks: 2 | id: 1 3 | name: Starbucks 4 | 5 | barnes_and_noble: 6 | id: 2 7 | name: Barnes & Noble -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/tasks/stateaware_api_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :stateaware_api do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /script/about: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | $LOAD_PATH.unshift "#{RAILTIES_PATH}/builtin/rails_info" 4 | require 'commands/about' -------------------------------------------------------------------------------- /test/fixtures/scrapers.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | datagroup_id: 1 5 | 6 | two: 7 | datagroup_id: 1 8 | -------------------------------------------------------------------------------- /test/fixtures/data_groups.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | description: MyText 5 | 6 | two: 7 | description: MyText 8 | -------------------------------------------------------------------------------- /test/fixtures/scraper_tasks.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | # one: 4 | # column: value 5 | # 6 | # two: 7 | # column: value 8 | -------------------------------------------------------------------------------- /public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place your application-specific JavaScript functions and classes here 2 | // This file is automatically included by javascript_include_tag :defaults 3 | -------------------------------------------------------------------------------- /test/unit/api_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ApiTest < ActiveSupport::TestCase 4 | # Replace this with your real tests. 5 | def test_truth 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/unit/scraper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ScraperTest < ActiveSupport::TestCase 4 | # Replace this with your real tests. 5 | def test_truth 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/unit/data_group_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DataGroupTest < ActiveSupport::TestCase 4 | # Replace this with your real tests. 5 | def test_truth 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/unit/data_point_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DataPointTest < ActiveSupport::TestCase 4 | # Replace this with your real tests. 5 | def test_truth 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/unit/scraper_task_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ScraperTaskTest < ActiveSupport::TestCase 4 | # Replace this with your real tests. 5 | def test_truth 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /doc/README_FOR_APP: -------------------------------------------------------------------------------- 1 | Use this README file to introduce your application and point to useful places in the API for learning more. 2 | Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test/functional/data_groups_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DataGroupsControllerTest < ActionController::TestCase 4 | # Replace this with your real tests. 5 | def test_truth 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/functional/data_points_controller_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class DataPointsControllerTest < ActionController::TestCase 4 | # Replace this with your real tests. 5 | def test_truth 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/test/stateaware_api_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | class StateawareApiTest < Test::Unit::TestCase 4 | # Replace this with your real tests. 5 | def test_this_plugin 6 | flunk 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /lib/tasks/api.rake: -------------------------------------------------------------------------------- 1 | require RAILS_ROOT + '/config/environment' 2 | 3 | namespace :api do 4 | desc 'Imports available API classes from models/api into the database' 5 | task :import do 6 | Api.import 7 | Scraper.import 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/README: -------------------------------------------------------------------------------- 1 | StateawareApi 2 | ============= 3 | 4 | Introduction goes here. 5 | 6 | 7 | Example 8 | ======= 9 | 10 | Example goes here. 11 | 12 | 13 | Copyright (c) 2009 [name of plugin creator], released under the MIT license 14 | -------------------------------------------------------------------------------- /test/fixtures/apis.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | datagroup_id: 1 5 | class_name: MyString 6 | class_file_name: MyString 7 | 8 | two: 9 | datagroup_id: 1 10 | class_name: MyString 11 | class_file_name: MyString 12 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/database.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | adapter: mysql 3 | host: localhost 4 | username: root 5 | password: 6 | database: geokit_plugin_test 7 | postgresql: 8 | adapter: postgresql 9 | host: localhost 10 | username: root 11 | password: 12 | database: geokit_plugin_test -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require(File.join(File.dirname(__FILE__), 'config', 'boot')) 5 | 6 | require 'rake' 7 | require 'rake/testtask' 8 | require 'rake/rdoctask' 9 | 10 | require 'tasks/rails' 11 | -------------------------------------------------------------------------------- /app/models/geo_location.rb: -------------------------------------------------------------------------------- 1 | class GeoLocation 2 | def self.text_search(text) 3 | end 4 | 5 | def self.long_lat_search(position) 6 | end 7 | 8 | def self.search(location) 9 | case location 10 | when String: 11 | text_search location 12 | when Array: 13 | long_lat_search location 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/about.yml: -------------------------------------------------------------------------------- 1 | author: 2 | name_1: Bill Eisenhauer 3 | homepage_1: http://blog.billeisenhauer.com 4 | name_2: Andre Lewis 5 | homepage_2: http://www.earthcode.com 6 | summary: Geo distance calculations, distance calculation query support, geocoding for physical and ip addresses. 7 | version: 1.1.0 8 | rails_version: 1.0+ 9 | license: MIT -------------------------------------------------------------------------------- /db/migrate/20090307112230_create_data_groups.rb: -------------------------------------------------------------------------------- 1 | class CreateDataGroups < ActiveRecord::Migration 2 | def self.up 3 | create_table :data_groups do |t| 4 | t.text :description 5 | t.string :name 6 | t.string :icon_file_name 7 | t.timestamps 8 | end 9 | end 10 | 11 | def self.down 12 | drop_table :data_groups 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20090307112508_create_apis.rb: -------------------------------------------------------------------------------- 1 | class CreateApis < ActiveRecord::Migration 2 | def self.up 3 | create_table :apis do |t| 4 | t.integer :data_group_id 5 | t.string :description 6 | t.string :type 7 | t.text :method_specs 8 | t.timestamps 9 | end 10 | end 11 | 12 | def self.down 13 | drop_table :apis 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/data_points.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | 3 | one: 4 | original_data: MyText 5 | data_group_id: 1 6 | original_location: MyText 7 | longitude: 1.5 8 | latitude: 1.5 9 | description: MyText 10 | 11 | two: 12 | original_data: MyText 13 | data_group_id: 1 14 | original_location: MyText 15 | longitude: 1.5 16 | latitude: 1.5 17 | description: MyText 18 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /app/controllers/data_points_controller.rb: -------------------------------------------------------------------------------- 1 | class DataPointsController < ApplicationController 2 | # Requires a search string 3 | def index 4 | # TODO: Search for geographical data 5 | @data_points = DataPoint.find :all, :conditions => [''] 6 | end 7 | 8 | # Search for data. At the moment postcode searches are accepted, 9 | # but in the future it should allow different data types 10 | def search 11 | Api.search_all_apis params[:postcode] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20090307112850_create_scraper_tasks.rb: -------------------------------------------------------------------------------- 1 | class CreateScraperTasks < ActiveRecord::Migration 2 | def self.up 3 | create_table :scraper_tasks do |t| 4 | t.column :name, :string 5 | t.column :scraper_id, :integer 6 | t.column :description, :text 7 | t.column :created_at, :datetime 8 | t.column :updated_at, :datetime 9 | t.timestamps 10 | end 11 | end 12 | 13 | def self.down 14 | drop_table :scraper_tasks 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/fixtures/mock_addresses.yml: -------------------------------------------------------------------------------- 1 | address_starbucks: 2 | addressable: starbucks (MockOrganization) 3 | street: 106 N Denton Tap Rd # 350 4 | city: Coppell 5 | state: TX 6 | postal_code: 75019 7 | lat: 32.969527 8 | lng: -96.990159 9 | 10 | address_barnes_and_noble: 11 | addressable: barnes_and_noble (MockOrganization) 12 | street: 5904 N Macarthur Blvd # 160 13 | city: Irving 14 | state: TX 15 | postal_code: 75039 16 | lat: 32.895155 17 | lng: -96.958444 -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/VERSION_HISTORY.txt: -------------------------------------------------------------------------------- 1 | 12/18/08 Split Rails plugin from geocoder gem, updated for Rails 2.2.2 2 | 01/20/08 Version 1.0.1. Further fix of distance calculation, this time in SQL. Now uses least() function, which is available in MySQL version 3.22.5+ and postgres versions 8.1+ 3 | 01/16/08 fixed the "zero-distance" bug (calculating between two points that are the same) 4 | 12/11/07 fixed a small but with queries crossing meridian, and also fixed find(:closest) 5 | 10/11/07 Fixed Rails2/Edge compatability -------------------------------------------------------------------------------- /public/dispatch.rb: -------------------------------------------------------------------------------- 1 | #!/opt/local/bin/ruby 2 | 3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) 4 | 5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: 6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired 7 | require "dispatcher" 8 | 9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) 10 | Dispatcher.dispatch -------------------------------------------------------------------------------- /public/dispatch.cgi: -------------------------------------------------------------------------------- 1 | #!/opt/local/bin/ruby 2 | 3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) 4 | 5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: 6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired 7 | require "dispatcher" 8 | 9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) 10 | Dispatcher.dispatch -------------------------------------------------------------------------------- /app/models/settings.rb: -------------------------------------------------------------------------------- 1 | # Internal settings 2 | class Settings 3 | @@loaded = false 4 | @@config = [] 5 | 6 | class << self 7 | def [](group) 8 | load_config unless @@loaded 9 | @@config[group] 10 | end 11 | 12 | private 13 | def load_config 14 | @@loaded = true 15 | @@config = YAML.load(File.open("#{RAILS_ROOT}/config/settings.yml")) 16 | end 17 | 18 | def method_missing(sym, *args) 19 | Settings[sym.to_s][args.first] 20 | end 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /db/migrate/20090307112804_create_scrapers.rb: -------------------------------------------------------------------------------- 1 | class CreateScrapers < ActiveRecord::Migration 2 | def self.up 3 | create_table :scrapers do |t| 4 | t.integer :data_group_id 5 | t.column :name, :string 6 | t.column :namespace, :string 7 | t.column :script, :text 8 | t.column :default_scraper_task_id, :integer 9 | t.column :description, :text 10 | t.column :created_at, :datetime 11 | t.column :updated_at, :datetime 12 | t.timestamps 13 | end 14 | end 15 | 16 | def self.down 17 | drop_table :scrapers 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/data_groups_controller.rb: -------------------------------------------------------------------------------- 1 | class DataGroupsController < ApplicationController 2 | # Return a list of all data groups 3 | def index 4 | @data_groups = DataGroup.find :all, :order => 'name' 5 | 6 | respond_to do |wants| 7 | wants.xml { render :xml => @data_groups } 8 | wants.json { render :json => @data_groups } 9 | end 10 | end 11 | 12 | # Return a specific data group 13 | def show 14 | @data_group = DataGroup.find params[:id] 15 | 16 | respond_to do |wants| 17 | wants.xml { render :xml => @data_group } 18 | wants.json { render :json => @data_group } 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the GeoKit plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for the GeoKit plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'rdoc' 18 | rdoc.title = 'GeoKit' 19 | rdoc.options << '--line-numbers' << '--inline-source' 20 | rdoc.rdoc_files.include('README') 21 | rdoc.rdoc_files.include('lib/**/*.rb') 22 | end -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/lib/geokit-rails/defaults.rb: -------------------------------------------------------------------------------- 1 | module Geokit 2 | # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable 3 | @@default_units = :miles 4 | @@default_formula = :sphere 5 | 6 | [:default_units, :default_formula].each do |sym| 7 | class_eval <<-EOS, __FILE__, __LINE__ 8 | def self.#{sym} 9 | if defined?(#{sym.to_s.upcase}) 10 | #{sym.to_s.upcase} 11 | else 12 | @@#{sym} 13 | end 14 | end 15 | 16 | def self.#{sym}=(obj) 17 | @@#{sym} = obj 18 | end 19 | EOS 20 | end 21 | Geokit::Geocoders.logger = ActiveRecord::Base.logger 22 | end 23 | -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test the stateaware_api plugin.' 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << 'lib' 11 | t.pattern = 'test/**/*_test.rb' 12 | t.verbose = true 13 | end 14 | 15 | desc 'Generate documentation for the stateaware_api plugin.' 16 | Rake::RDocTask.new(:rdoc) do |rdoc| 17 | rdoc.rdoc_dir = 'rdoc' 18 | rdoc.title = 'StateawareApi' 19 | rdoc.options << '--line-numbers' << '--inline-source' 20 | rdoc.rdoc_files.include('README') 21 | rdoc.rdoc_files.include('lib/**/*.rb') 22 | end 23 | -------------------------------------------------------------------------------- /config/database.yml.example: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3-ruby (not necessary on OS X Leopard) 3 | development: 4 | adapter: mysql 5 | database: stateaware_development 6 | username: root 7 | password: 8 | socket: /tmp/mysql.sock 9 | 10 | # Warning: The database defined as "test" will be erased and 11 | # re-generated from your development database when you run "rake". 12 | # Do not set this db to the same as development or production. 13 | test: 14 | adapter: mysql 15 | database: stateaware_test 16 | username: root 17 | password: 18 | socket: /tmp/mysql.sock 19 | 20 | production: 21 | adapter: sqlite3 22 | database: db/production.sqlite3 23 | timeout: 5000 24 | -------------------------------------------------------------------------------- /config/initializers/new_rails_defaults.rb: -------------------------------------------------------------------------------- 1 | # These settings change the behavior of Rails 2 apps and will be defaults 2 | # for Rails 3. You can remove this initializer when Rails 3 is released. 3 | 4 | if defined?(ActiveRecord) 5 | # Include Active Record class name as root for JSON serialized output. 6 | ActiveRecord::Base.include_root_in_json = true 7 | 8 | # Store the full class name (including module namespace) in STI type column. 9 | ActiveRecord::Base.store_full_sti_class = true 10 | end 11 | 12 | # Use ISO 8601 format for JSON serialized times and dates. 13 | ActiveSupport.use_standard_json_time_format = true 14 | 15 | # Don't escape HTML entities in JSON, leave that for the #json_escape helper. 16 | # if you're including raw json in an HTML page. 17 | ActiveSupport.escape_html_entities_in_json = false -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # In the development environment your application's code is reloaded on 4 | # every request. This slows down response time but is perfect for development 5 | # since you don't have to restart the webserver when you make code changes. 6 | config.cache_classes = false 7 | 8 | # Log error messages when you accidentally call methods on nil. 9 | config.whiny_nils = true 10 | 11 | # Show full error reports and disable caching 12 | config.action_controller.consider_all_requests_local = true 13 | config.action_view.debug_rjs = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false -------------------------------------------------------------------------------- /lib/scrapers/sam_knows.scraper: -------------------------------------------------------------------------------- 1 | description 'Sam Knows broadband checker' 2 | name 'Sam Knows' 3 | data_group_name 'Telecomms' 4 | default 'search' 5 | hits 10 6 | 7 | site do 8 | home 'http://www.samknows.com/' 9 | search 'http://www.samknows.com/broadband/checker2.php?p=summary' 10 | end 11 | 12 | task 'search', :args => 'query' do 13 | post(site.search, :postcode => query) 14 | 15 | # Get results 16 | broadband_enabled = 'No' 17 | for_each('div#results', :find_first => ['.flashMsg']).each do |status_message| 18 | if status_message and status_message.match /Congratulations/ 19 | broadband_enabled = 'Yes' 20 | end 21 | end 22 | 23 | save({ :link => site.home, :data_summary => {'Broadband Available' => broadband_enabled }, :entity_type => 'Boolean', :address => 'site.search', :name => 'Broadband Available' }) 24 | end 25 | 26 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/init.rb: -------------------------------------------------------------------------------- 1 | # Load modules and classes needed to automatically mix in ActiveRecord and 2 | # ActionController helpers. All other functionality must be explicitly 3 | # required. 4 | # 5 | # Note that we don't explicitly require the geokit gem. You should specify gem dependencies in your config/environment.rb 6 | # with this line: config.gem "andre-geokit", :lib=>'geokit', :source => 'http://gems.github.com' 7 | # 8 | if defined? Geokit 9 | require 'geokit-rails/defaults' 10 | require 'geokit-rails/acts_as_mappable' 11 | require 'geokit-rails/ip_geocode_lookup' 12 | 13 | # Automatically mix in distance finder support into ActiveRecord classes. 14 | ActiveRecord::Base.send :include, GeoKit::ActsAsMappable 15 | 16 | # Automatically mix in ip geocoding helpers into ActionController classes. 17 | ActionController::Base.send :include, GeoKit::IpGeocodeLookup 18 | end -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/install.rb: -------------------------------------------------------------------------------- 1 | # Display to the console the contents of the README file. 2 | puts IO.read(File.join(File.dirname(__FILE__), 'README.markdown')) 3 | 4 | # place the api_keys_template in the application's /config/initializers/geokit_config.rb 5 | path=File.expand_path(File.join(File.dirname(__FILE__), '../../../config/initializers/geokit_config.rb')) 6 | template_path=File.join(File.dirname(__FILE__), '/assets/api_keys_template') 7 | if File.exists?(path) 8 | puts "It looks like you already have a configuration file at #{path}. We've left it as-is. Recommended: check #{template_path} to see if anything has changed, and update config file accordingly." 9 | else 10 | File.open(path, "w") do |f| 11 | f.puts IO.read(template_path) 12 | puts "We created a configuration file for you in config/initializers/geokit_config.rb. Add your Google API keys, etc there." 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/lib/stateaware_api.rb: -------------------------------------------------------------------------------- 1 | # StateawareApi 2 | module StateawareApi 3 | def self.enable 4 | ActiveRecord::Base.class_eval { extend ActiveRecordClassMethods } 5 | end 6 | 7 | def self.classes 8 | @classes 9 | end 10 | 11 | def self.add_api(klass, api_description, data_group_name, methods) 12 | @classes ||= {} 13 | @classes[klass.name] ||= {} 14 | @classes[klass.name][:description] = api_description 15 | @classes[klass.name][:methods] = methods 16 | @classes[klass.name][:data_group_name] = data_group_name 17 | end 18 | 19 | def self.[](class_name) 20 | return if @classes.nil? 21 | @classes[class_name] 22 | end 23 | 24 | module ActiveRecordClassMethods 25 | def stateaware_api(api_description, options) 26 | StateawareApi.add_api self, api_description, options[:data_group_name], options[:methods] 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require "test/unit" 3 | require "rubygems" 4 | #require File.dirname(__FILE__) + '/../init' 5 | require 'geokit' 6 | 7 | plugin_test_dir = File.dirname(__FILE__) 8 | 9 | # Load the Rails environment 10 | require ENV['environment'] || File.join(plugin_test_dir, '../../../../config/environment') 11 | require 'test_help' 12 | #require 'active_record/fixtures' 13 | databases = YAML::load(IO.read(plugin_test_dir + '/database.yml')) 14 | ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log") 15 | 16 | # A specific database can be used by setting the DB environment variable 17 | ActiveRecord::Base.establish_connection(databases[ENV['DB'] || 'mysql']) 18 | 19 | # Load the test schema into the database 20 | load(File.join(plugin_test_dir, 'schema.rb')) 21 | 22 | # Load fixtures from the plugin 23 | #Test::Unit::TestCase.fixture_path = File.join(plugin_test_dir, 'fixtures/') 24 | -------------------------------------------------------------------------------- /public/dispatch.fcgi: -------------------------------------------------------------------------------- 1 | #!/opt/local/bin/ruby 2 | # 3 | # You may specify the path to the FastCGI crash log (a log of unhandled 4 | # exceptions which forced the FastCGI instance to exit, great for debugging) 5 | # and the number of requests to process before running garbage collection. 6 | # 7 | # By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log 8 | # and the GC period is nil (turned off). A reasonable number of requests 9 | # could range from 10-100 depending on the memory footprint of your app. 10 | # 11 | # Example: 12 | # # Default log path, normal GC behavior. 13 | # RailsFCGIHandler.process! 14 | # 15 | # # Default log path, 50 requests between GC. 16 | # RailsFCGIHandler.process! nil, 50 17 | # 18 | # # Custom log path, normal GC behavior. 19 | # RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' 20 | # 21 | require File.dirname(__FILE__) + "/../config/environment" 22 | require 'fcgi_handler' 23 | 24 | RailsFCGIHandler.process! 25 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The production environment is meant for finished, "live" apps. 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Use a different logger for distributed setups 8 | # config.logger = SyslogLogger.new 9 | 10 | # Full error reports are disabled and caching is turned on 11 | config.action_controller.consider_all_requests_local = false 12 | config.action_controller.perform_caching = true 13 | config.action_view.cache_template_loading = true 14 | 15 | # Use a different cache store in production 16 | # config.cache_store = :mem_cache_store 17 | 18 | # Enable serving of images, stylesheets, and javascripts from an asset server 19 | # config.action_controller.asset_host = "http://assets.example.com" 20 | 21 | # Disable delivery errors, bad email addresses will be ignored 22 | # config.action_mailer.raise_delivery_errors = false 23 | -------------------------------------------------------------------------------- /public/422.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | The change you wanted was rejected (422) 9 | 21 | 22 | 23 | 24 | 25 |
26 |

The change you wanted was rejected.

27 |

Maybe you tried to change something you didn't have access to.

28 |
29 | 30 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | The page you were looking for doesn't exist (404) 9 | 21 | 22 | 23 | 24 | 25 |
26 |

The page you were looking for doesn't exist.

27 |

You may have mistyped the address or the page may have moved.

28 |
29 | 30 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | We're sorry, but something went wrong (500) 9 | 21 | 22 | 23 | 24 | 25 |
26 |

We're sorry, but something went wrong.

27 |

We've been notified about this issue and we'll take a look at it shortly.

28 |
29 | 30 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | config.cache_classes = true 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.action_controller.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Disable request forgery protection in test environment 17 | config.action_controller.allow_forgery_protection = false 18 | 19 | # Tell Action Mailer not to deliver emails to the real world. 20 | # The :test delivery method accumulates sent emails in the 21 | # ActionMailer::Base.deliveries array. 22 | config.action_mailer.delivery_method = :test 23 | -------------------------------------------------------------------------------- /db/migrate/20090307114640_create_data_points.rb: -------------------------------------------------------------------------------- 1 | class CreateDataPoints < ActiveRecord::Migration 2 | def self.up 3 | 4 | # data_points.create :link => result.mp_website, 5 | # :name => result.name, 6 | # :address => postcode, 7 | # :api => data_group, 8 | # :entity_type => 'Person', 9 | # :additional_links => { 'Wikipedia' => result.wikipedia_url }, 10 | # :data_summary => { 'Debates spoken in over the last year' => result.debate_sectionsspoken_inlastyear }, 11 | # :original_data => result 12 | 13 | 14 | create_table :data_points do |t| 15 | t.string :name 16 | t.text :address 17 | t.text :link 18 | t.integer :api_id 19 | t.integer :scraper_id 20 | t.string :entity_type 21 | t.text :additional_links 22 | t.text :data_summary 23 | t.text :original_data 24 | t.decimal :lng, :precision => 15, :scale => 10 25 | t.decimal :lat, :precision => 15, :scale => 10 26 | t.timestamps 27 | end 28 | end 29 | 30 | def self.down 31 | drop_table :data_points 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/scrapers/flood_warnings.scraper: -------------------------------------------------------------------------------- 1 | description 'Flood Warnings Search' 2 | name 'Flood Warnings Search' 3 | data_group_name 'Acts of God' 4 | default 'search' 5 | hits 10 6 | 7 | site do 8 | home 'http://www.environment-agency.gov.uk/' 9 | search 'http://www.environment-agency.gov.uk/homeandleisure/floods/31618.aspx' 10 | end 11 | 12 | task 'search', :args => 'query' do 13 | get(site.search, '') 14 | view_state = find_value('#__VIEWSTATE').first[:value] 15 | validation = find_value('#__EVENTVALIDATION').first[:value] 16 | post(site.search, { 'ctl00$txtTerm' => query, 'ctl00$ddlSearchOption' => 'Postcode', '__VIEWSTATE' => view_state, '__EVENTVALIDATION' => validation, '__EVENTARGUMENT' => '', '__EVENTTARGET' => '', 'ctl00$btnSearch.x' => 6, 'ctl00$btnSearch.y' => 4}) 17 | # Get results 18 | for_each('.ResultItem li', :find_first => ['.text', '.TitleInfoBox a']).each do |status, link| 19 | p status 20 | p link 21 | save({ :link => link, :data_summary => {'Status' => status }, :entity_type => 'Geographical', :address => 'site.search', :name => 'Flood Status' }) 22 | end 23 | end 24 | 25 | 26 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/fixtures/locations.yml: -------------------------------------------------------------------------------- 1 | a: 2 | id: 1 3 | company_id: 1 4 | street: 7979 N MacArthur Blvd 5 | city: Irving 6 | state: TX 7 | postal_code: 75063 8 | lat: 32.918593 9 | lng: -96.958444 10 | b: 11 | id: 2 12 | company_id: 1 13 | street: 7750 N Macarthur Blvd # 160 14 | city: Irving 15 | state: TX 16 | postal_code: 75063 17 | lat: 32.914144 18 | lng: -96.958444 19 | c: 20 | id: 3 21 | company_id: 1 22 | street: 5904 N Macarthur Blvd # 160 23 | city: Irving 24 | state: TX 25 | postal_code: 75039 26 | lat: 32.895155 27 | lng: -96.958444 28 | d: 29 | id: 4 30 | company_id: 1 31 | street: 817 S Macarthur Blvd # 145 32 | city: Coppell 33 | state: TX 34 | postal_code: 75019 35 | lat: 32.951613 36 | lng: -96.958444 37 | e: 38 | id: 5 39 | company_id: 1 40 | street: 106 N Denton Tap Rd # 350 41 | city: Coppell 42 | state: TX 43 | postal_code: 75019 44 | lat: 32.969527 45 | lng: -96.990159 46 | f: 47 | id: 6 48 | company_id: 2 49 | street: 5904 N Macarthur Blvd # 160 50 | city: Irving 51 | state: TX 52 | postal_code: 75039 53 | lat: 32.895155 54 | lng: -96.958444 -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Bill Eisenhauer & Andre Lewis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /vendor/plugins/stateaware_api/MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 [name of plugin creator] 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app/controllers/application.rb: -------------------------------------------------------------------------------- 1 | # Filters added to this controller apply to all controllers in the application. 2 | # Likewise, all the methods added will be available for all controllers. 3 | 4 | class ApplicationController < ActionController::Base 5 | helper :all # include all helpers, all the time 6 | 7 | # See ActionController::RequestForgeryProtection for details 8 | # Uncomment the :secret if you're not using the cookie session store 9 | protect_from_forgery # :secret => '5f6ba937d9d0fbcdf8e8a74afe14ebf1' 10 | 11 | # See ActionController::Base for details 12 | # Uncomment this to filter the contents of submitted sensitive data parameters 13 | # from your application log (in this case, all fields with names like "password"). 14 | # filter_parameter_logging :password 15 | 16 | rescue_from ActiveRecord::RecordNotFound do |exception| 17 | api_error({ 'Error' => 'Record not found' }) 18 | end 19 | 20 | protected 21 | def api_error(error, status = 500) 22 | respond_to do |wants| 23 | wants.xml { render :xml => error, :status => status } 24 | wants.json { render :xml => error, :status => status } 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/fixtures/custom_locations.yml: -------------------------------------------------------------------------------- 1 | a: 2 | id: 1 3 | company_id: 1 4 | street: 7979 N MacArthur Blvd 5 | city: Irving 6 | state: TX 7 | postal_code: 75063 8 | latitude: 32.918593 9 | longitude: -96.958444 10 | b: 11 | id: 2 12 | company_id: 1 13 | street: 7750 N Macarthur Blvd # 160 14 | city: Irving 15 | state: TX 16 | postal_code: 75063 17 | latitude: 32.914144 18 | longitude: -96.958444 19 | c: 20 | id: 3 21 | company_id: 1 22 | street: 5904 N Macarthur Blvd # 160 23 | city: Irving 24 | state: TX 25 | postal_code: 75039 26 | latitude: 32.895155 27 | longitude: -96.958444 28 | d: 29 | id: 4 30 | company_id: 1 31 | street: 817 S Macarthur Blvd # 145 32 | city: Coppell 33 | state: TX 34 | postal_code: 75019 35 | latitude: 32.951613 36 | longitude: -96.958444 37 | e: 38 | id: 5 39 | company_id: 1 40 | street: 106 N Denton Tap Rd # 350 41 | city: Coppell 42 | state: TX 43 | postal_code: 75019 44 | latitude: 32.969527 45 | longitude: -96.990159 46 | f: 47 | id: 6 48 | company_id: 2 49 | street: 5904 N Macarthur Blvd # 160 50 | city: Irving 51 | state: TX 52 | postal_code: 75039 53 | latitude: 32.895155 54 | longitude: -96.958444 -------------------------------------------------------------------------------- /app/models/api/they_work_for_you.rb: -------------------------------------------------------------------------------- 1 | class Api::TheyWorkForYou < Api 2 | 3 | stateaware_api 'TheyWorkForYou provides information on MPs', :data_group_name => 'MP Data', :methods => { 4 | :get_data_for_postcode => { :arguments => [:postcode], :description => 'Find an MP for a location' } 5 | } 6 | 7 | def get_data_for_postcode(postcode) 8 | # access they work for you and get an MP's data 9 | data_point = data_points.find_by_address postcode 10 | 11 | if data_point 12 | # TODO: This could refetch data every so often to update it 13 | data_point 14 | else 15 | client = Twfy::Client.new(Settings['apis']['theyworkforyou']['key']) 16 | mp = client.mp(:postcode => postcode) 17 | # if data was found, store it as a DataPoint entry 18 | if mp 19 | map_result_to_data_point postcode, client.mp_info(:id => mp.person_id) 20 | end 21 | end 22 | rescue Twfy::Client::APIError => exception 23 | errors.add_to_base exception.to_s 24 | end 25 | 26 | def map_result_to_data_point(postcode, result) 27 | return if result.nil? 28 | data_points.create :link => result.mp_website, 29 | :name => result.name, 30 | :address => postcode, 31 | :api => self, 32 | :entity_type => 'Person', 33 | :additional_links => { 'Wikipedia' => result.wikipedia_url }, 34 | :data_summary => { 'Debates spoken in over the last year' => result.debate_sectionsspoken_inlastyear }, 35 | :original_data => result 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/models/api.rb: -------------------------------------------------------------------------------- 1 | class Api < ActiveRecord::Base 2 | belongs_to :data_group 3 | has_many :data_points 4 | serialize :method_specs 5 | 6 | def self.import 7 | require_api_files 8 | 9 | StateawareApi.classes.each do |class_name, spec| 10 | method_specs = spec[:methods] 11 | data_group_name = spec[:data_group_name] 12 | description = spec[:description] 13 | 14 | # Find or create the DataGroup for this class and method 15 | data_group = DataGroup.find_or_create_by_name data_group_name 16 | klass = class_name.constantize 17 | api = klass.create :description => description, :method_specs => method_specs, :data_group => data_group 18 | end 19 | end 20 | 21 | def self.search_all_apis(value, datatype = :postcode) 22 | results = [] 23 | Api.find(:all).each do |api| 24 | api.method_specs.each do |method_name, method_spec| 25 | if method_spec[:arguments].include? datatype 26 | results.push api.send(method_name, value) 27 | end 28 | end 29 | end 30 | 31 | Scraper.find(:all).each do |scraper| 32 | data_point = DataPoint.find_by_address_and_scraper_id(value, scraper.id) 33 | if data_point 34 | # TODO: update result 35 | else 36 | scraper_results = scraper.run_task :search, value 37 | data_point = scraper.data_points.create scraper_results 38 | end 39 | 40 | if data_point 41 | results.push data_point 42 | end 43 | end 44 | results 45 | end 46 | 47 | private 48 | def self.require_api_files 49 | path = RAILS_ROOT + '/app/models/api/' 50 | Dir.open(path).each do |file_name| 51 | if file_name.match /.*\.rb$/ 52 | require File.join(path, file_name) 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /app/models/scraper.rb: -------------------------------------------------------------------------------- 1 | class Scraper < ActiveRecord::Base 2 | has_many :scraper_tasks 3 | has_many :data_points 4 | has_one :default_task, :class_name => 'ScraperTask' 5 | belongs_to :data_group 6 | 7 | def run_task(task, value) 8 | ScraperParser.new(script).send(:search, value) 9 | end 10 | 11 | def self.import 12 | Dir.glob(RAILS_ROOT + '/lib/scrapers/*.scraper').each do |file_name| 13 | puts "Importing #{file_name}" 14 | 15 | script = File.open(file_name).read 16 | scraper_parser = ScraperParser.new(script) 17 | data_group = DataGroup.find_or_create_by_name scraper_parser._data_group_name 18 | scraper = if scraper = Scraper.find_by_name_and_namespace(scraper_parser._name, 'System') 19 | scraper.update_attributes(:name => scraper_parser._name, 20 | :namespace => 'System', 21 | :script => script, 22 | :data_group => data_group, 23 | :description => scraper_parser._description) 24 | scraper 25 | else 26 | Scraper.create( 27 | :name => scraper_parser._name, 28 | :namespace => 'System', 29 | :script => script, 30 | :data_group => data_group, 31 | :description => scraper_parser._description) 32 | end 33 | 34 | scraper_parser.tasks.each do |name, t| 35 | name = name.to_s 36 | 37 | next if t[:private] 38 | task = if task = ScraperTask.find_by_name_and_scraper_id(name, scraper.id) 39 | task.update_attributes(:scraper => scraper, :name => name) 40 | task 41 | else 42 | ScraperTask.create(:scraper => scraper, :name => name) 43 | end 44 | 45 | if task.name == scraper_parser._default.to_s 46 | scraper.update_attribute(:default_scraper_task_id, task.id) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | require File.expand_path(File.dirname(__FILE__) + "/../config/environment") 3 | require 'test_help' 4 | 5 | class Test::Unit::TestCase 6 | # Transactional fixtures accelerate your tests by wrapping each test method 7 | # in a transaction that's rolled back on completion. This ensures that the 8 | # test database remains unchanged so your fixtures don't have to be reloaded 9 | # between every test method. Fewer database queries means faster tests. 10 | # 11 | # Read Mike Clark's excellent walkthrough at 12 | # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting 13 | # 14 | # Every Active Record database supports transactions except MyISAM tables 15 | # in MySQL. Turn off transactional fixtures in this case; however, if you 16 | # don't care one way or the other, switching from MyISAM to InnoDB tables 17 | # is recommended. 18 | # 19 | # The only drawback to using transactional fixtures is when you actually 20 | # need to test transactions. Since your test is bracketed by a transaction, 21 | # any transactions started in your code will be automatically rolled back. 22 | self.use_transactional_fixtures = true 23 | 24 | # Instantiated fixtures are slow, but give you @david where otherwise you 25 | # would need people(:david). If you don't want to migrate your existing 26 | # test cases which use the @david style and don't mind the speed hit (each 27 | # instantiated fixtures translates to a database query per test method), 28 | # then set this back to true. 29 | self.use_instantiated_fixtures = false 30 | 31 | # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. 32 | # 33 | # Note: You'll currently still have to declare fixtures explicitly in integration tests 34 | # -- they do not yet inherit this setting 35 | fixtures :all 36 | 37 | # Add more helper methods to be used by all tests here... 38 | end 39 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/lib/geokit-rails/ip_geocode_lookup.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Geokit 4 | # Contains a class method geocode_ip_address which can be used to enable automatic geocoding 5 | # for request IP addresses. The geocoded information is stored in a cookie and in the 6 | # session to minimize web service calls. The point of the helper is to enable location-based 7 | # websites to have a best-guess for new visitors. 8 | module IpGeocodeLookup 9 | # Mix below class methods into ActionController. 10 | def self.included(base) # :nodoc: 11 | base.extend ClassMethods 12 | end 13 | 14 | # Class method to mix into active record. 15 | module ClassMethods # :nodoc: 16 | def geocode_ip_address(filter_options = {}) 17 | before_filter :store_ip_location, filter_options 18 | end 19 | end 20 | 21 | private 22 | 23 | # Places the IP address' geocode location into the session if it 24 | # can be found. Otherwise, looks for a geo location cookie and 25 | # uses that value. The last resort is to call the web service to 26 | # get the value. 27 | def store_ip_location 28 | session[:geo_location] ||= retrieve_location_from_cookie_or_service 29 | cookies[:geo_location] = { :value => session[:geo_location].to_yaml, :expires => 30.days.from_now } if session[:geo_location] 30 | end 31 | 32 | # Uses the stored location value from the cookie if it exists. If 33 | # no cookie exists, calls out to the web service to get the location. 34 | def retrieve_location_from_cookie_or_service 35 | return YAML.load(cookies[:geo_location]) if cookies[:geo_location] 36 | location = Geocoders::IpGeocoder.geocode(get_ip_address) 37 | return location.success ? location : nil 38 | end 39 | 40 | # Returns the real ip address, though this could be the localhost ip 41 | # address. No special handling here anymore. 42 | def get_ip_address 43 | request.remote_ip 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 0) do 2 | create_table :companies, :force => true do |t| 3 | t.column :name, :string 4 | end 5 | 6 | create_table :locations, :force => true do |t| 7 | t.column :company_id, :integer, :default => 0, :null => false 8 | t.column :street, :string, :limit => 60 9 | t.column :city, :string, :limit => 60 10 | t.column :state, :string, :limit => 2 11 | t.column :postal_code, :string, :limit => 16 12 | t.column :lat, :decimal, :precision => 15, :scale => 10 13 | t.column :lng, :decimal, :precision => 15, :scale => 10 14 | end 15 | 16 | create_table :custom_locations, :force => true do |t| 17 | t.column :company_id, :integer, :default => 0, :null => false 18 | t.column :street, :string, :limit => 60 19 | t.column :city, :string, :limit => 60 20 | t.column :state, :string, :limit => 2 21 | t.column :postal_code, :string, :limit => 16 22 | t.column :latitude, :decimal, :precision => 15, :scale => 10 23 | t.column :longitude, :decimal, :precision => 15, :scale => 10 24 | end 25 | 26 | create_table :stores, :force=> true do |t| 27 | t.column :address, :string 28 | t.column :lat, :decimal, :precision => 15, :scale => 10 29 | t.column :lng, :decimal, :precision => 15, :scale => 10 30 | end 31 | 32 | create_table :mock_organizations, :force => true do |t| 33 | t.column :name, :string 34 | end 35 | 36 | create_table :mock_addresses, :force => true do |t| 37 | t.column :addressable_id, :integer, :null => false 38 | t.column :addressable_type, :string, :null => false 39 | t.column :street, :string, :limit => 60 40 | t.column :city, :string, :limit => 60 41 | t.column :state, :string, :limit => 2 42 | t.column :postal_code, :string, :limit => 16 43 | t.column :lat, :decimal, :precision => 15, :scale => 10 44 | t.column :lng, :decimal, :precision => 15, :scale => 10 45 | end 46 | end -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | map.resources :data_groups 3 | map.resources :data_points 4 | 5 | # The priority is based upon order of creation: first created -> highest priority. 6 | 7 | # Sample of regular route: 8 | # map.connect 'products/:id', :controller => 'catalog', :action => 'view' 9 | # Keep in mind you can assign values other than :controller and :action 10 | 11 | # Sample of named route: 12 | # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' 13 | # This route can be invoked with purchase_url(:id => product.id) 14 | 15 | # Sample resource route (maps HTTP verbs to controller actions automatically): 16 | # map.resources :products 17 | 18 | # Sample resource route with options: 19 | # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get } 20 | 21 | # Sample resource route with sub-resources: 22 | # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller 23 | 24 | # Sample resource route with more complex sub-resources 25 | # map.resources :products do |products| 26 | # products.resources :comments 27 | # products.resources :sales, :collection => { :recent => :get } 28 | # end 29 | 30 | # Sample resource route within a namespace: 31 | # map.namespace :admin do |admin| 32 | # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb) 33 | # admin.resources :products 34 | # end 35 | 36 | # You can have the root of your site routed with map.root -- just remember to delete public/index.html. 37 | # map.root :controller => "welcome" 38 | 39 | # See how all your routes lay out with "rake routes" 40 | 41 | # Install the default routes as the lowest priority. 42 | # Note: These default routes make all actions in every controller accessible via GET requests. You should 43 | # consider removing the them or commenting them out if you're using named routes and resources. 44 | map.connect ':controller/:action/:id' 45 | map.connect ':controller/:action/:id.:format' 46 | end 47 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead of editing this file, 2 | # please use the migrations feature of Active Record to incrementally modify your database, and 3 | # then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your database schema. If you need 6 | # to create the application database on another system, you should be using db:schema:load, not running 7 | # all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations 8 | # you'll amass, the slower it'll run and the greater likelihood for issues). 9 | # 10 | # It's strongly recommended to check this file into your version control system. 11 | 12 | ActiveRecord::Schema.define(:version => 20090307114640) do 13 | 14 | create_table "apis", :force => true do |t| 15 | t.integer "data_group_id" 16 | t.string "description" 17 | t.string "type" 18 | t.text "method_specs" 19 | t.datetime "created_at" 20 | t.datetime "updated_at" 21 | end 22 | 23 | create_table "data_groups", :force => true do |t| 24 | t.text "description" 25 | t.string "name" 26 | t.string "icon_file_name" 27 | t.datetime "created_at" 28 | t.datetime "updated_at" 29 | end 30 | 31 | create_table "data_points", :force => true do |t| 32 | t.string "name" 33 | t.text "address" 34 | t.text "link" 35 | t.integer "api_id" 36 | t.integer "scraper_id" 37 | t.string "entity_type" 38 | t.text "additional_links" 39 | t.text "data_summary" 40 | t.text "original_data" 41 | t.decimal "lng", :precision => 15, :scale => 10 42 | t.decimal "lat", :precision => 15, :scale => 10 43 | t.datetime "created_at" 44 | t.datetime "updated_at" 45 | end 46 | 47 | create_table "scraper_tasks", :force => true do |t| 48 | t.string "name" 49 | t.integer "scraper_id" 50 | t.text "description" 51 | t.datetime "created_at" 52 | t.datetime "updated_at" 53 | end 54 | 55 | create_table "scrapers", :force => true do |t| 56 | t.integer "data_group_id" 57 | t.string "name" 58 | t.string "namespace" 59 | t.text "script" 60 | t.integer "default_scraper_task_id" 61 | t.text "description" 62 | t.datetime "created_at" 63 | t.datetime "updated_at" 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/ip_geocode_lookup_test.rb: -------------------------------------------------------------------------------- 1 | require ENV['environment'] || File.join(File.dirname(__FILE__), '../../../../config/environment') 2 | require 'action_controller/test_process' 3 | require 'test/unit' 4 | require 'rubygems' 5 | require 'mocha' 6 | 7 | 8 | class LocationAwareController < ActionController::Base #:nodoc: all 9 | geocode_ip_address 10 | 11 | def index 12 | render :nothing => true 13 | end 14 | end 15 | 16 | class ActionController::TestRequest #:nodoc: all 17 | attr_accessor :remote_ip 18 | end 19 | 20 | # Re-raise errors caught by the controller. 21 | class LocationAwareController #:nodoc: all 22 | def rescue_action(e) raise e end; 23 | end 24 | 25 | class IpGeocodeLookupTest < Test::Unit::TestCase #:nodoc: all 26 | 27 | def setup 28 | @success = GeoKit::GeoLoc.new 29 | @success.provider = "hostip" 30 | @success.lat = 41.7696 31 | @success.lng = -88.4588 32 | @success.city = "Sugar Grove" 33 | @success.state = "IL" 34 | @success.country_code = "US" 35 | @success.success = true 36 | 37 | @failure = GeoKit::GeoLoc.new 38 | @failure.provider = "hostip" 39 | @failure.city = "(Private Address)" 40 | @failure.success = false 41 | 42 | @controller = LocationAwareController.new 43 | @request = ActionController::TestRequest.new 44 | @response = ActionController::TestResponse.new 45 | end 46 | 47 | def test_no_location_in_cookie_or_session 48 | Geokit::Geocoders::IpGeocoder.expects(:geocode).with("good ip").returns(@success) 49 | @request.remote_ip = "good ip" 50 | get :index 51 | verify 52 | end 53 | 54 | def test_location_in_cookie 55 | @request.remote_ip = "good ip" 56 | @request.cookies['geo_location'] = CGI::Cookie.new('geo_location', @success.to_yaml) 57 | get :index 58 | verify 59 | end 60 | 61 | def test_location_in_session 62 | @request.remote_ip = "good ip" 63 | @request.session[:geo_location] = @success 64 | @request.cookies['geo_location'] = CGI::Cookie.new('geo_location', @success.to_yaml) 65 | get :index 66 | verify 67 | end 68 | 69 | def test_ip_not_located 70 | Geokit::Geocoders::IpGeocoder.expects(:geocode).with("bad ip").returns(@failure) 71 | @request.remote_ip = "bad ip" 72 | get :index 73 | assert_nil @request.session[:geo_location] 74 | end 75 | 76 | private 77 | 78 | def verify 79 | assert_response :success 80 | assert_equal @success, @request.session[:geo_location] 81 | assert_not_nil cookies['geo_location'] 82 | assert_equal @success, YAML.load(cookies['geo_location'].join) 83 | end 84 | end -------------------------------------------------------------------------------- /config/initializers/geokit_config.rb: -------------------------------------------------------------------------------- 1 | if defined? Geokit 2 | 3 | # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable 4 | Geokit::default_units = :miles 5 | Geokit::default_formula = :sphere 6 | 7 | # This is the timeout value in seconds to be used for calls to the geocoder web 8 | # services. For no timeout at all, comment out the setting. The timeout unit 9 | # is in seconds. 10 | Geokit::Geocoders::timeout = 3 11 | 12 | # These settings are used if web service calls must be routed through a proxy. 13 | # These setting can be nil if not needed, otherwise, addr and port must be 14 | # filled in at a minimum. If the proxy requires authentication, the username 15 | # and password can be provided as well. 16 | Geokit::Geocoders::proxy_addr = nil 17 | Geokit::Geocoders::proxy_port = nil 18 | Geokit::Geocoders::proxy_user = nil 19 | Geokit::Geocoders::proxy_pass = nil 20 | 21 | # This is your yahoo application key for the Yahoo Geocoder. 22 | # See http://developer.yahoo.com/faq/index.html#appid 23 | # and http://developer.yahoo.com/maps/rest/V1/geocode.html 24 | Geokit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' 25 | 26 | # This is your Google Maps geocoder key. 27 | # See http://www.google.com/apis/maps/signup.html 28 | # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples 29 | Geokit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' 30 | 31 | # This is your username and password for geocoder.us. 32 | # To use the free service, the value can be set to nil or false. For 33 | # usage tied to an account, the value should be set to username:password. 34 | # See http://geocoder.us 35 | # and http://geocoder.us/user/signup 36 | Geokit::Geocoders::geocoder_us = false 37 | 38 | # This is your authorization key for geocoder.ca. 39 | # To use the free service, the value can be set to nil or false. For 40 | # usage tied to an account, set the value to the key obtained from 41 | # Geocoder.ca. 42 | # See http://geocoder.ca 43 | # and http://geocoder.ca/?register=1 44 | Geokit::Geocoders::geocoder_ca = false 45 | 46 | # Uncomment to use a username with the Geonames geocoder 47 | #Geokit::Geocoders::geonames="REPLACE_WITH_YOUR_GEONAMES_USERNAME" 48 | 49 | # This is the order in which the geocoders are called in a failover scenario 50 | # If you only want to use a single geocoder, put a single symbol in the array. 51 | # Valid symbols are :google, :yahoo, :us, and :ca. 52 | # Be aware that there are Terms of Use restrictions on how you can use the 53 | # various geocoders. Make sure you read up on relevant Terms of Use for each 54 | # geocoder you are going to use. 55 | Geokit::Geocoders::provider_order = [:google,:us] 56 | end 57 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/assets/api_keys_template: -------------------------------------------------------------------------------- 1 | if defined? Geokit 2 | 3 | # These defaults are used in Geokit::Mappable.distance_to and in acts_as_mappable 4 | Geokit::default_units = :miles 5 | Geokit::default_formula = :sphere 6 | 7 | # This is the timeout value in seconds to be used for calls to the geocoder web 8 | # services. For no timeout at all, comment out the setting. The timeout unit 9 | # is in seconds. 10 | Geokit::Geocoders::timeout = 3 11 | 12 | # These settings are used if web service calls must be routed through a proxy. 13 | # These setting can be nil if not needed, otherwise, addr and port must be 14 | # filled in at a minimum. If the proxy requires authentication, the username 15 | # and password can be provided as well. 16 | Geokit::Geocoders::proxy_addr = nil 17 | Geokit::Geocoders::proxy_port = nil 18 | Geokit::Geocoders::proxy_user = nil 19 | Geokit::Geocoders::proxy_pass = nil 20 | 21 | # This is your yahoo application key for the Yahoo Geocoder. 22 | # See http://developer.yahoo.com/faq/index.html#appid 23 | # and http://developer.yahoo.com/maps/rest/V1/geocode.html 24 | Geokit::Geocoders::yahoo = 'REPLACE_WITH_YOUR_YAHOO_KEY' 25 | 26 | # This is your Google Maps geocoder key. 27 | # See http://www.google.com/apis/maps/signup.html 28 | # and http://www.google.com/apis/maps/documentation/#Geocoding_Examples 29 | Geokit::Geocoders::google = 'REPLACE_WITH_YOUR_GOOGLE_KEY' 30 | 31 | # This is your username and password for geocoder.us. 32 | # To use the free service, the value can be set to nil or false. For 33 | # usage tied to an account, the value should be set to username:password. 34 | # See http://geocoder.us 35 | # and http://geocoder.us/user/signup 36 | Geokit::Geocoders::geocoder_us = false 37 | 38 | # This is your authorization key for geocoder.ca. 39 | # To use the free service, the value can be set to nil or false. For 40 | # usage tied to an account, set the value to the key obtained from 41 | # Geocoder.ca. 42 | # See http://geocoder.ca 43 | # and http://geocoder.ca/?register=1 44 | Geokit::Geocoders::geocoder_ca = false 45 | 46 | # Uncomment to use a username with the Geonames geocoder 47 | #Geokit::Geocoders::geonames="REPLACE_WITH_YOUR_GEONAMES_USERNAME" 48 | 49 | # This is the order in which the geocoders are called in a failover scenario 50 | # If you only want to use a single geocoder, put a single symbol in the array. 51 | # Valid symbols are :google, :yahoo, :us, and :ca. 52 | # Be aware that there are Terms of Use restrictions on how you can use the 53 | # various geocoders. Make sure you read up on relevant Terms of Use for each 54 | # geocoder you are going to use. 55 | Geokit::Geocoders::provider_order = [:google,:us] 56 | end 57 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h2. Installation 2 | 3 | # Uses geokit: http://geokit.rubyforge.org/readme.html 4 | # Needs MySQL or Postgres (due to geokit's requirements) 5 | # Add a Google Maps API key to config/initializers/geokit_config.rb 6 | # Sign up and get a key from: http://www.theyworkforyou.com/api/ and put it in config/settings.yml (copy my example file and edit it) 7 | # Copy the database.yml.example file and edit it 8 | 9 | h2. Background 10 | 11 | This is a project for "Rewired State":http://rewiredstate.org/. 12 | 13 | *StateAware* aims to *collect, combine and enrich government data* through APIs and screen scraping. I wanted StateAware to achieve two things: 14 | 15 | * The government won't make sensible APIs, so let's do it for them 16 | * Make people more aware of locally relevant government data (through a web interface and iPhone app) 17 | 18 | This is the result of a one day hacking session and isn't even at the proof of concept stage yet, but I'm uploading it in case anyone wants to use the code for something else. I was in the middle of reworking the ScraperParser to cope with Cookies and copying headers, so it needs to be finished off. 19 | 20 | h3. Architecture 21 | 22 | * StateAware is written in Rails 23 | * It collects values from various APIs based on user input and serializes the data in a model called DataPoint 24 | * If data matching DataPoints have been found recently, it doesn't refetch it from a particular API (effectively caching it so APIs/sites don't get hammered) 25 | * DataPoints are grouped by DataGroups, APIs and Scrapers. DataGroups are a generic group that might appear in a user interface (categories really, this could easily have been tags) 26 | 27 | h3. Data collection 28 | 29 | StateAware collects data through API and Scraper classes. The APIs are currently subclasses of an ActiveRecord model called Api. Scrapers are also models, but use a scraper DSL that should make it easier for people to contribute scrapers. 30 | 31 | The Scraper DSL was just a quick one I knocked up for the prototype, but isn't friendly enough yet. 32 | 33 | h3. Enriching data 34 | 35 | I installed GeoKit with the aim of automatically geocoding data. I also wanted to collect Twitter search info about a particular DataPoint for its location. Another interesting search would be news items (perhaps from Google News or the BBC). 36 | 37 | The first API I supported was "TheyWorkForYou.com":http://www.theyworkforyou.com/. I thought it would be interesting to see news links based on MPs and the things they've talked about. 38 | 39 | I also tried to support UK flood warnings so I could have Twitter searches for people in those areas talking about the floods, but I got stuck trying to scrape their site. 40 | 41 | h3. Future expansion 42 | 43 | * I didn't do any work towards *enriching* data, but this could easily be added 44 | * I designed an iPhone map that would show local data (that's why a lot of the code refers to postcode searches currently) 45 | * Each API and Scraper should define the datatype inputs it takes for relevant searching 46 | * APIs and Scraper stubs need to including licensing details 47 | * I started adding controllers that speak JSON and XML. The ultimate goal was trusted remote clients that can contribute data -- this would help if a particular site blocks the IP of the scrapers 48 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Don't change this file! 2 | # Configure your app in config/environment.rb and config/environments/*.rb 3 | 4 | RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) 5 | 6 | module Rails 7 | class << self 8 | def boot! 9 | unless booted? 10 | preinitialize 11 | pick_boot.run 12 | end 13 | end 14 | 15 | def booted? 16 | defined? Rails::Initializer 17 | end 18 | 19 | def pick_boot 20 | (vendor_rails? ? VendorBoot : GemBoot).new 21 | end 22 | 23 | def vendor_rails? 24 | File.exist?("#{RAILS_ROOT}/vendor/rails") 25 | end 26 | 27 | def preinitialize 28 | load(preinitializer_path) if File.exist?(preinitializer_path) 29 | end 30 | 31 | def preinitializer_path 32 | "#{RAILS_ROOT}/config/preinitializer.rb" 33 | end 34 | end 35 | 36 | class Boot 37 | def run 38 | load_initializer 39 | Rails::Initializer.run(:set_load_path) 40 | end 41 | end 42 | 43 | class VendorBoot < Boot 44 | def load_initializer 45 | require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" 46 | Rails::Initializer.run(:install_gem_spec_stubs) 47 | end 48 | end 49 | 50 | class GemBoot < Boot 51 | def load_initializer 52 | self.class.load_rubygems 53 | load_rails_gem 54 | require 'initializer' 55 | end 56 | 57 | def load_rails_gem 58 | if version = self.class.gem_version 59 | gem 'rails', version 60 | else 61 | gem 'rails' 62 | end 63 | rescue Gem::LoadError => load_error 64 | $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) 65 | exit 1 66 | end 67 | 68 | class << self 69 | def rubygems_version 70 | Gem::RubyGemsVersion if defined? Gem::RubyGemsVersion 71 | end 72 | 73 | def gem_version 74 | if defined? RAILS_GEM_VERSION 75 | RAILS_GEM_VERSION 76 | elsif ENV.include?('RAILS_GEM_VERSION') 77 | ENV['RAILS_GEM_VERSION'] 78 | else 79 | parse_gem_version(read_environment_rb) 80 | end 81 | end 82 | 83 | def load_rubygems 84 | require 'rubygems' 85 | min_version = '1.1.1' 86 | unless rubygems_version >= min_version 87 | $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.) 88 | exit 1 89 | end 90 | 91 | rescue LoadError 92 | $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org) 93 | exit 1 94 | end 95 | 96 | def parse_gem_version(text) 97 | $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ 98 | end 99 | 100 | private 101 | def read_environment_rb 102 | File.read("#{RAILS_ROOT}/config/environment.rb") 103 | end 104 | end 105 | end 106 | end 107 | 108 | # All that for this: 109 | Rails.boot! 110 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file 2 | 3 | # Uncomment below to force Rails into production mode when 4 | # you don't control web/app server and can't set it the proper way 5 | # ENV['RAILS_ENV'] ||= 'production' 6 | 7 | # Specifies gem version of Rails to use when vendor/rails is not present 8 | RAILS_GEM_VERSION = '2.1.1' unless defined? RAILS_GEM_VERSION 9 | 10 | # Bootstrap the Rails environment, frameworks, and default configuration 11 | require File.join(File.dirname(__FILE__), 'boot') 12 | 13 | Rails::Initializer.run do |config| 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | # See Rails::Configuration for more options. 18 | 19 | # Skip frameworks you're not going to use. To use Rails without a database 20 | # you must remove the Active Record framework. 21 | # config.frameworks -= [ :active_record, :active_resource, :action_mailer ] 22 | 23 | # Specify gems that this application depends on. 24 | # They can then be installed with "rake gems:install" on new installations. 25 | # config.gem "bj" 26 | # config.gem "hpricot", :version => '0.6', :source => "http://code.whytheluckystiff.net" 27 | # config.gem "aws-s3", :lib => "aws/s3" 28 | 29 | # Only load the plugins named here, in the order given. By default, all plugins 30 | # in vendor/plugins are loaded in alphabetical order. 31 | # :all can be used as a placeholder for all plugins not explicitly named 32 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 33 | 34 | # Add additional load paths for your own custom dirs 35 | # config.load_paths += %W( #{RAILS_ROOT}/extras ) 36 | 37 | # Force all environments to use the same logger level 38 | # (by default production uses :info, the others :debug) 39 | # config.log_level = :debug 40 | 41 | # Make Time.zone default to the specified zone, and make Active Record store time values 42 | # in the database in UTC, and return them converted to the specified local zone. 43 | # Run "rake -D time" for a list of tasks for finding time zone names. Comment line to use default local time. 44 | config.time_zone = 'UTC' 45 | 46 | # Your secret key for verifying cookie session data integrity. 47 | # If you change this key, all old sessions will become invalid! 48 | # Make sure the secret is at least 30 characters and all random, 49 | # no regular words or you'll be exposed to dictionary attacks. 50 | config.action_controller.session = { 51 | :session_key => '_stateaware_session', 52 | :secret => '568421d4bcadf1f4691d972011863777016d5c4f8ed4867e615dfce109c1f310b6204a24b4f48e3dcce0788a83e7e4e55f0a8ff592db4579bc1bb21539d8dea3' 53 | } 54 | 55 | # Use the database for sessions instead of the cookie-based default, 56 | # which shouldn't be used to store highly confidential information 57 | # (create the session table with "rake db:sessions:create") 58 | # config.action_controller.session_store = :active_record_store 59 | 60 | # Use SQL instead of Active Record's schema dumper when creating the test database. 61 | # This is necessary if your schema can't be completely dumped by the schema dumper, 62 | # like if you have constraints or database-specific column types 63 | # config.active_record.schema_format = :sql 64 | 65 | # Activate observers that should always be running 66 | # config.active_record.observers = :cacher, :garbage_collector 67 | 68 | config.gem 'andre-geokit', :lib => 'geokit', :source => 'http://gems.github.com' 69 | config.gem 'twfy' 70 | end 71 | -------------------------------------------------------------------------------- /app/models/scraper_parser.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'hpricot' 3 | require 'ostruct' 4 | require 'net/http' 5 | require 'uri' 6 | 7 | class ScraperParser 8 | attr_accessor :_description, :_name, :_default, :_hits, :_data_group_name 9 | attr_reader :site, :data, :tasks 10 | 11 | def initialize(script = nil) 12 | @hits = 10 13 | @data = '' 14 | @tasks = {} 15 | @results = [] 16 | @trace = [] 17 | @site = OpenStruct.new() 18 | @args = {} 19 | load(script) if script 20 | end 21 | 22 | def load(script) 23 | instance_eval script 24 | end 25 | 26 | def task(name, options = {}, &block) 27 | method_name = name.to_s.gsub(/^private\s+/, '').to_sym 28 | 29 | @tasks[method_name] = {} 30 | @tasks[method_name][:args] = options[:args] if options[:args] 31 | @tasks[method_name][:private] = name.match(/^private/) ? true : false 32 | 33 | define_method(method_name) do |*args| 34 | @results = [] 35 | @trace.push current_method 36 | 37 | @tasks[method_name][:args].each_with_index do |param_name, index| 38 | unless @args[method_name] 39 | klass = Struct.new(*@tasks[method_name][:args].collect { |p| p.to_sym }) unless @args[method_name] 40 | @args[method_name] = klass.new 41 | end 42 | @args[method_name].send(param_name + '=', args[index]) 43 | end 44 | 45 | instance_eval &block 46 | 47 | @trace.pop 48 | @results 49 | end 50 | end 51 | 52 | def params(sym) 53 | @args[@current_method].send(sym) 54 | end 55 | 56 | def site(&block) 57 | if block 58 | @defining_site = true 59 | instance_eval &block 60 | @defining_site = false 61 | end 62 | return @site 63 | end 64 | 65 | def get(url, *args) 66 | url = insert_url_params(url, args.first) if args.first 67 | http = Net::HTTP.new(URI.parse(url), 80) 68 | response, data = http.get(path, nil) 69 | @cookie = response.response['set-cookie'] 70 | @last_url = url 71 | @data = Hpricot open(url) 72 | end 73 | 74 | def post(url, *args) 75 | headers = { 76 | 'Cookie' => @cookie, 77 | 'Referer' => @last_url, 78 | 'Content-Type' => 'application/x-www-form-urlencoded' 79 | } 80 | 81 | 82 | result = Net::HTTP.post_form(URI.parse(url), args, headers) 83 | @data = Hpricot result.body 84 | end 85 | 86 | # This works as an iterator when a block is passed 87 | def find_links_to(url, &block) 88 | url, selector = if url.kind_of? Array 89 | [url[1], url[0]] 90 | else 91 | [url, 'a'] 92 | end 93 | 94 | links = (@data/selector).find_all do |link| 95 | link[:href].match(strip_url_params(url)) if link and link[:href] 96 | end 97 | 98 | links = links.compact.collect { |link| [link[:href], link.inner_text] }.uniq[0..@_hits - 1] 99 | 100 | links.collect do |link| 101 | yield link[0] 102 | end 103 | end 104 | 105 | def for_each(selector, options = {}, &block) 106 | results = [] 107 | 108 | (@data/selector).collect do |item| 109 | items = if options[:find_first].kind_of? Array 110 | options[:find_first].collect do |sub_selector| 111 | result = find_first(sub_selector, item) 112 | replace_tags result if result 113 | end.compact 114 | elsif options[:find_first] 115 | find_first(options[:find_first], item) 116 | else 117 | replace_tags item.inner_text 118 | end 119 | 120 | items.empty? ? nil : items 121 | end.compact.uniq[0..@_hits - 1] 122 | end 123 | 124 | def find_value(selector) 125 | (@data/selector) 126 | end 127 | 128 | # Searches the last page fetched with get and returns html in tags using Hpricot 129 | def find_first(selector, data) 130 | data = @data unless data 131 | results = (data/selector) 132 | if results and results.first 133 | results.first.inner_html.strip 134 | else 135 | nil 136 | end 137 | end 138 | 139 | def find(selector, data = nil, &block) 140 | data = @data unless data 141 | 142 | if selector.kind_of? Array 143 | return selector.collect { |s| find(s, data, &block) } 144 | end 145 | 146 | (data/selector).collect do |elem| 147 | if block 148 | yield elem 149 | else 150 | elem.inner_text 151 | end 152 | end 153 | end 154 | 155 | def find_text(selector, data = nil, &block) 156 | data = @data unless data 157 | 158 | if selector.kind_of? Array 159 | return selector.collect { |s| find_text(s, data, &block).first } 160 | end 161 | 162 | (data/selector).collect do |elem| 163 | if block 164 | yield elem.inner_text 165 | else 166 | elem.inner_text 167 | end 168 | end 169 | end 170 | 171 | def collect_result(data) 172 | @results.push data 173 | end 174 | 175 | def save(data) 176 | @results = data 177 | end 178 | 179 | def replace_tags(text, replace = '') 180 | text.gsub(/(<[^>]+?>| )/, replace).strip.gsub(/[\s\t]+/, ' ').gsub(/\243/, '£') 181 | end 182 | 183 | private 184 | 185 | def metaclass 186 | class << self; self; end 187 | end 188 | 189 | def method_missing(sym, *args) 190 | if @defining_site 191 | @site.send(sym.to_s + '=', args[0]) 192 | elsif [:description, :name, :default, :hits, :data_group_name].include? sym 193 | send('_' + sym.to_s + '=', args[0]) 194 | elsif respond_to? sym 195 | send(sym, args) 196 | elsif @args[@trace.last] and @args[@trace.last].members.include? sym.to_s 197 | return @args[@trace.last].send(sym) 198 | else 199 | puts "ScraperParser#method_missing: not found" 200 | p @args[@trace.last].members 201 | p sym.to_s + ' not found' 202 | super 203 | end 204 | end 205 | 206 | def define_method(name, &block) 207 | metaclass.send(:define_method, name, &block) 208 | end 209 | 210 | def strip_url_params(url) 211 | url.gsub(/%s/, '') 212 | end 213 | 214 | def insert_url_params(url, *args) 215 | args.each do |arg| 216 | arg ||= '' 217 | url.gsub!(/%s/, CGI.escape(arg)) 218 | end 219 | 220 | return url 221 | end 222 | 223 | def current_method 224 | caller[0].sub(/.*`([^']+)'/, '\1').to_sym 225 | end 226 | end 227 | 228 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Ruby on Rails: Welcome aboard 7 | 181 | 182 | 183 | 204 | 205 | 206 |
207 | 236 | 237 |
238 | 242 | 243 | 247 | 248 |
249 |

Getting started

250 |

Here’s how to get rolling:

251 | 252 |
    253 |
  1. 254 |

    Use script/generate to create your models and controllers

    255 |

    To see all available options, run it without parameters.

    256 |
  2. 257 | 258 |
  3. 259 |

    Set up a default route and remove or rename this file

    260 |

    Routes are set up in config/routes.rb.

    261 |
  4. 262 | 263 |
  5. 264 |

    Create your database

    265 |

    Run rake db:migrate to create your database. If you're not using SQLite (the default), edit config/database.yml with your username and password.

    266 |
  6. 267 |
268 |
269 |
270 | 271 | 272 |
273 | 274 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/README.markdown: -------------------------------------------------------------------------------- 1 | ## INSTALLATION 2 | 3 | Geokit consists of a Gem ([geokit-gem](http://github.com/andre/geokit-gem/tree/master)) and a Rails plugin ([geokit-rails](http://github.com/andre/geokit-rails/tree/master)). 4 | 5 | #### 1. Install the Rails plugin: 6 | 7 | cd [YOUR_RAILS_APP_ROOT] 8 | script/plugin install git://github.com/andre/geokit-rails.git 9 | 10 | #### 2. Add this line to your environment.rb 11 | (inside the Rails::Initializer.run do |config| block) 12 | 13 | config.gem "andre-geokit", :lib=>'geokit', :source => 'http://gems.github.com' 14 | 15 | This informs Rails of the gem dependency. 16 | 17 | #### 3. Tell Rails to install the gem: 18 | 19 | rake gems:install 20 | 21 | And you're good to go! 22 | 23 | ## FEATURE SUMMARY 24 | 25 | Geokit provides key functionality for location-oriented Rails applications: 26 | 27 | - Distance calculations, for both flat and spherical environments. For example, 28 | given the location of two points on the earth, you can calculate the miles/KM 29 | between them. 30 | - ActiveRecord distance-based finders. For example, you can find all the points 31 | in your database within a 50-mile radius. 32 | - IP-based location lookup utilizing hostip.info. Provide an IP address, and get 33 | city name and latitude/longitude in return 34 | - A before_filter helper to geocoder the user's location based on IP address, 35 | and retain the location in a cookie. 36 | - Geocoding from multiple providers. It currently supports Google, Yahoo, 37 | Geocoder.us, and Geocoder.ca geocoders, and it provides a uniform response 38 | structure from all of them. It also provides a fail-over mechanism, in case 39 | your input fails to geocode in one service. Geocoding is provided buy the Geokit 40 | gem, which you must have installed 41 | 42 | The goal of this plugin is to provide the common functionality for location-oriented 43 | applications (geocoding, location lookup, distance calculation) in an easy-to-use 44 | package. 45 | 46 | ## A NOTE ON TERMINOLOGY 47 | 48 | Throughout the code and API, latitude and longitude are referred to as lat 49 | and lng. We've found over the long term the abbreviation saves lots of typing time. 50 | 51 | ## DISTANCE CALCULATIONS AND QUERIES 52 | 53 | If you want only distance calculation services, you need only mix in the Mappable 54 | module like so: 55 | 56 | class Location 57 | include Geokit::Mappable 58 | end 59 | 60 | After doing so, you can do things like: 61 | 62 | Location.distance_between(from, to) 63 | 64 | with optional parameters :units and :formula. Values for :units can be :miles, 65 | :kms (kilometers), or :nms (nautical miles) with :miles as the default. Values for :formula can be :sphere or :flat with 66 | :sphere as the default. :sphere gives you Haversine calculations, while :flat 67 | gives the Pythagoreum Theory. These defaults persist through out the plug-in. 68 | 69 | You can also do: 70 | 71 | location.distance_to(other) 72 | 73 | The real power and utility of the plug-in is in its query support. This is 74 | achieved through mixing into an ActiveRecord model object: 75 | 76 | class Location < ActiveRecord::Base 77 | acts_as_mappable 78 | end 79 | 80 | The plug-in uses the above-mentioned defaults, but can be modified to use 81 | different units and a different formulae. This is done through the `:default_units` 82 | and `:default_formula` keys which accept the same values as mentioned above. 83 | 84 | The plug-in creates a calculated column and potentially a calculated condition. 85 | By default, these are known as "distance" but this can be changed through the 86 | `:distance_field_name` key. 87 | 88 | So, an alternative invocation would look as below: 89 | 90 | class Location < ActiveRecord::Base 91 | acts_as_mappable :default_units => :kms, 92 | :default_formula => :flat, 93 | :distance_field_name => :distance 94 | end 95 | 96 | You can also define alternative column names for latitude and longitude using 97 | the `:lat_column_name` and `:lng_column_name` keys. The defaults are 'lat' and 98 | 'lng' respectively. 99 | 100 | Thereafter, a set of finder methods are made available. Below are the 101 | different combinations: 102 | 103 | Origin as a two-element array of latititude/longitude: 104 | 105 | find(:all, :origin => [37.792,-122.393]) 106 | 107 | Origin as a geocodeable string: 108 | 109 | find(:all, :origin => '100 Spear st, San Francisco, CA') 110 | 111 | Origin as an object which responds to lat and lng methods, 112 | or latitude and longitude methods, or whatever methods you have 113 | specified for `lng_column_name` and `lat_column_name`: 114 | 115 | find(:all, :origin=>my_store) # my_store.lat and my_store.lng methods exist 116 | 117 | Often you will need to find within a certain distance. The prefered syntax is: 118 | 119 | find(:all, :origin => @somewhere, :within => 5) 120 | 121 | . . . however these syntaxes will also work: 122 | 123 | find_within(5, :origin => @somewhere) 124 | find(:all, :origin => @somewhere, :conditions => "distance < 5") 125 | 126 | Note however that the third form should be avoided. With either of the first two, 127 | Geokit automatically adds a bounding box to speed up the radial query in the database. 128 | With the third form, it does not. 129 | 130 | If you need to combine distance conditions with other conditions, you should do 131 | so like this: 132 | 133 | find(:all, :origin => @somewhere, :within => 5, :conditions=>['state=?',state]) 134 | 135 | If :origin is not provided in the finder call, the find method 136 | works as normal. Further, the key is removed 137 | from the :options hash prior to invoking the superclass behavior. 138 | 139 | Other convenience methods work intuitively and are as follows: 140 | 141 | find_within(distance, :origin => @somewhere) 142 | find_beyond(distance, :origin => @somewhere) 143 | find_closest(:origin => @somewhere) 144 | find_farthest(:origin => @somewhere) 145 | 146 | where the options respect the defaults, but can be overridden if 147 | desired. 148 | 149 | Lastly, if all that is desired is the raw SQL for distance 150 | calculations, you can use the following: 151 | 152 | distance_sql(origin, units=default_units, formula=default_formula) 153 | 154 | Thereafter, you are free to use it in find_by_sql as you wish. 155 | 156 | There are methods available to enable you to get the count based upon 157 | the find condition that you have provided. These all work similarly to 158 | the finders. So for instance: 159 | 160 | count(:origin, :conditions => "distance < 5") 161 | count_within(distance, :origin => @somewhere) 162 | count_beyond(distance, :origin => @somewhere) 163 | 164 | ## FINDING WITHIN A BOUNDING BOX 165 | 166 | If you are displaying points on a map, you probably need to query for whatever falls within the rectangular bounds of the map: 167 | 168 | Store.find :all, :bounds=>[sw_point,ne_point] 169 | 170 | The input to :bounds can be array with the two points or a Bounds object. However you provide them, the order should always be the southwest corner, northeast corner of the rectangle. Typically, you will be getting the sw_point and ne_point from a map that is displayed on a web page. 171 | 172 | If you need to calculate the bounding box from a point and radius, you can do that: 173 | 174 | bounds=Bounds.from_point_and_radius(home,5) 175 | Store.find :all, :bounds=>bounds 176 | 177 | ## USING INCLUDES 178 | 179 | You can use includes along with your distance finders: 180 | 181 | stores=Store.find :all, :origin=>home, :include=>[:reviews,:cities] :within=>5, :order=>'distance' 182 | 183 | *However*, ActiveRecord drops the calculated distance column when you use include. So, if you need to 184 | use the distance column, you'll have to re-calculate it post-query in Ruby: 185 | 186 | stores.sort_by_distance_from(home) 187 | 188 | In this case, you may want to just use the bounding box 189 | condition alone in your SQL (there's no use calculating the distance twice): 190 | 191 | bounds=Bounds.from_point_and_radius(home,5) 192 | stores=Store.find :all, :include=>[:reviews,:cities] :bounds=>bounds 193 | stores.sort_by_distance_from(home) 194 | 195 | ## USING :through 196 | 197 | You can also specify a model as mappable "through" another associated model. In other words, that associated model is the 198 | actual mappable model with "lat" and "lng" attributes, but this "through" model can still utilize all of the above find methods 199 | to search for records. 200 | 201 | class Location < ActiveRecord::Base 202 | belongs_to :locatable, :polymorphic => true 203 | acts_as_mappable 204 | end 205 | 206 | class Company < ActiveRecord::Base 207 | has_one :location, :as => :locatable # also works for belongs_to associations 208 | acts_as_mappable :through => :location 209 | end 210 | 211 | Then you can still call: 212 | 213 | Company.find_within(distance, :origin => @somewhere) 214 | 215 | Remember that the notes above about USING INCLUDES apply to the results from this find, since an include is automatically used. 216 | 217 | ## IP GEOCODING 218 | 219 | You can obtain the location for an IP at any time using the geocoder 220 | as in the following example: 221 | 222 | location = IpGeocoder.geocode('12.215.42.19') 223 | 224 | where Location is a GeoLoc instance containing the latitude, 225 | longitude, city, state, and country code. Also, the success 226 | value is true. 227 | 228 | If the IP cannot be geocoded, a GeoLoc instance is returned with a 229 | success value of false. 230 | 231 | It should be noted that the IP address needs to be visible to the 232 | Rails application. In other words, you need to ensure that the 233 | requesting IP address is forwarded by any front-end servers that 234 | are out in front of the Rails app. Otherwise, the IP will always 235 | be that of the front-end server. 236 | 237 | ## IP GEOCODING HELPER 238 | 239 | A class method called geocode_ip_address has been mixed into the 240 | ActionController::Base. This enables before_filter style lookup of 241 | the IP address. Since it is a filter, it can accept any of the 242 | available filter options. 243 | 244 | Usage is as below: 245 | 246 | class LocationAwareController < ActionController::Base 247 | geocode_ip_address 248 | end 249 | 250 | A first-time lookup will result in the GeoLoc class being stored 251 | in the session as `:geo_location` as well as in a cookie called 252 | `:geo_session`. Subsequent lookups will use the session value if it 253 | exists or the cookie value if it doesn't exist. The last resort is 254 | to make a call to the web service. Clients are free to manage the 255 | cookie as they wish. 256 | 257 | The intent of this feature is to be able to provide a good guess as 258 | to a new visitor's location. 259 | 260 | ## INTEGRATED FIND AND GEOCODING 261 | 262 | Geocoding has been integrated with the finders enabling you to pass 263 | a physical address or an IP address. This would look the following: 264 | 265 | Location.find_farthest(:origin => '217.15.10.9') 266 | Location.find_farthest(:origin => 'Irving, TX') 267 | 268 | where the IP or physical address would be geocoded to a location and 269 | then the resulting latitude and longitude coordinates would be used 270 | in the find. This is not expected to be common usage, but it can be 271 | done nevertheless. 272 | 273 | ## ADDRESS GEOCODING 274 | 275 | Geocoding is provided by the Geokit gem, which is required for this plugin. 276 | See the top of this file for instructions on installing the Geokit gem. 277 | 278 | Geokit can geocode addresses using multiple geocodeing web services. 279 | Currently, Geokit supports Google, Yahoo, and Geocoder.us geocoding 280 | services. 281 | 282 | These geocoder services are made available through the following classes: 283 | GoogleGeocoder, YahooGeocoder, UsGeocoder, CaGeocoder, and GeonamesGeocoder. 284 | Further, an additional geocoder class called MultiGeocoder incorporates an ordered failover 285 | sequence to increase the probability of successful geocoding. 286 | 287 | All classes are called using the following signature: 288 | 289 | include Geokit::Geocoders 290 | location = XxxGeocoder.geocode(address) 291 | 292 | where you replace Xxx Geocoder with the appropriate class. A GeoLoc 293 | instance is the result of the call. This class has a "success" 294 | attribute which will be true if a successful geocoding occurred. 295 | If successful, the lat and lng properties will be populated. 296 | 297 | Geocoders are named with the naming convention NameGeocoder. This 298 | naming convention enables Geocoder to auto-detect its sub-classes 299 | in order to create methods called `name_geocoder(address)` so that 300 | all geocoders are called through the base class. This is done 301 | purely for convenience; the individual geocoder classes are expected 302 | to be used independently. 303 | 304 | The MultiGeocoder class requires the configuration of a provider 305 | order which dictates what order to use the various geocoders. Ordering 306 | is done through `Geokit::Geocoders::provider_order`, found in 307 | `config/initializers/geokit_config.rb`. 308 | 309 | If you don't already have a `geokit_config.rb` file, the plugin creates one 310 | when it is first installed. 311 | 312 | Make sure your failover configuration matches the usage characteristics 313 | of your application -- for example, if you routinely get bogus input to 314 | geocode, your code will be much slower if you have to failover among 315 | multiple geocoders before determining that the input was in fact bogus. 316 | 317 | The Geocoder.geocode method returns a GeoLoc object. Basic usage: 318 | 319 | loc=Geocoder.geocode('100 Spear St, San Francisco, CA') 320 | if loc.success 321 | puts loc.lat 322 | puts loc.lng 323 | puts loc.full_address 324 | end 325 | 326 | ## REVERSE GEOCODING 327 | 328 | Currently, only the Google Geocoder supports reverse geocoding. Pass the lat/lng as a string, array or LatLng instance: 329 | 330 | res=Geokit::Geocoders::GoogleGeocoder.reverse_geocode "37.791821,-122.394679" 331 | => # '100 Spear st, San Francisco, CA') 344 | 345 | where the physical address would be geocoded to a location and then the 346 | resulting latitude and longitude coordinates would be used in the 347 | find. 348 | 349 | Note that if the address fails to geocode, the find method will raise an 350 | ActiveRecord::GeocodeError you must be prepared to catch. Alternatively, 351 | You can geocoder the address beforehand, and pass the resulting lat/lng 352 | into the finder if successful. 353 | 354 | ## Auto Geocoding 355 | 356 | If your geocoding needs are simple, you can tell your model to automatically 357 | geocode itself on create: 358 | 359 | class Store < ActiveRecord::Base 360 | acts_as_mappable :auto_geocode=>true 361 | end 362 | 363 | It takes two optional params: 364 | 365 | class Store < ActiveRecord::Base 366 | acts_as_mappable :auto_geocode=>{:field=>:address, :error_message=>'Could not geocode address'} 367 | end 368 | 369 | . . . which is equivilent to: 370 | 371 | class Store << ActiveRecord::Base 372 | acts_as_mappable 373 | before_validation_on_create :geocode_address 374 | 375 | private 376 | def geocode_address 377 | geo=Geokit::Geocoders::MultiGeocoder.geocode (address) 378 | errors.add(:address, "Could not Geocode address") if !geo.success 379 | self.lat, self.lng = geo.lat,geo.lng if geo.success 380 | end 381 | end 382 | 383 | If you need any more complicated geocoding behavior for your model, you should roll your own 384 | `before_validate` callback. 385 | 386 | 387 | ## Distances, headings, endpoints, and midpoints 388 | 389 | distance=home.distance_from(work, :units=>:miles) 390 | heading=home.heading_to(work) # result is in degrees, 0 is north 391 | endpoint=home.endpoint(90,2) # two miles due east 392 | midpoing=home.midpoint_to(work) 393 | 394 | ## Cool stuff you can do with bounds 395 | 396 | bounds=Bounds.new(sw_point,ne_point) 397 | bounds.contains?(home) 398 | puts bounds.center 399 | 400 | 401 | HOW TO . . . 402 | ================================================================================= 403 | 404 | A few quick examples to get you started .... 405 | 406 | ## How to install the Geokit Rails plugin 407 | (See the very top of this file) 408 | 409 | ## How to find all stores within a 10-mile radius of a given lat/lng 410 | 1. ensure your stores table has lat and lng columns with numeric or float 411 | datatypes to store your latitude/longitude 412 | 413 | 3. use `acts_as_mappable` on your store model: 414 | 415 | class Store < ActiveRecord::Base 416 | acts_as_mappable 417 | ... 418 | end 419 | 420 | 3. finders now have extra capabilities: 421 | 422 | Store.find(:all, :origin =>[32.951613,-96.958444], :within=>10) 423 | 424 | ## How to geocode an address 425 | 426 | 1. configure your geocoder key(s) in `config/initializers/geokit_config.rb` 427 | 428 | 2. also in `geokit_config.rb`, make sure that `Geokit::Geocoders::provider_order` reflects the 429 | geocoder(s). If you only want to use one geocoder, there should 430 | be only one symbol in the array. For example: 431 | 432 | Geokit::Geocoders::provider_order=[:google] 433 | 434 | 3. Test it out in script/console 435 | 436 | include Geokit::Geocoders 437 | res = MultiGeocoder.geocode('100 Spear St, San Francisco, CA') 438 | puts res.lat 439 | puts res.lng 440 | puts res.full_address 441 | ... etc. The return type is GeoLoc, see the API for 442 | all the methods you can call on it. 443 | 444 | ## How to find all stores within 10 miles of a given address 445 | 446 | 1. as above, ensure your table has the lat/lng columns, and you've 447 | applied `acts_as_mappable` to the Store model. 448 | 449 | 2. configure and test out your geocoder, as above 450 | 451 | 3. pass the address in under the :origin key 452 | 453 | Store.find(:all, :origin=>'100 Spear st, San Francisco, CA', 454 | :within=>10) 455 | 456 | 4. you can also use a zipcode, or anything else that's geocodable: 457 | 458 | Store.find(:all, :origin=>'94117', 459 | :conditions=>'distance<10') 460 | 461 | ## How to sort a query by distance from an origin 462 | 463 | You now have access to a 'distance' column, and you can use it 464 | as you would any other column. For example: 465 | Store.find(:all, :origin=>'94117', :order=>'distance') 466 | 467 | ## How to elements of an array according to distance from a common point 468 | 469 | Usually, you can do your sorting in the database as part of your find call. 470 | If you need to sort things post-query, you can do so: 471 | 472 | stores=Store.find :all 473 | stores.sort_by_distance_from(home) 474 | puts stores.first.distance 475 | 476 | Obviously, each of the items in the array must have a latitude/longitude so 477 | they can be sorted by distance. 478 | 479 | ## Database Compatability 480 | 481 | * Geokit works with MySQL (tested with version 5.0.41) or PostgreSQL (tested with version 8.2.6) 482 | * Geokit does *not* work with SQLite, as it lacks the necessary geometry functions. 483 | * Geokit is known to *not* work with Postgres versions under 8.1 -- it uses the least() funciton. 484 | 485 | 486 | ## HIGH-LEVEL NOTES ON WHAT'S WHERE 487 | 488 | `acts_as_mappable.rb`, as you'd expect, contains the ActsAsMappable 489 | module which gets mixed into your models to provide the 490 | location-based finder goodness. 491 | 492 | `ip_geocode_lookup.rb` contains the before_filter helper method which 493 | enables auto lookup of the requesting IP address. 494 | 495 | ### The Geokit gem provides the building blocks of distance-based operations: 496 | 497 | The Mappable module, which provides basic 498 | distance calculation methods, i.e., calculating the distance 499 | between two points. 500 | 501 | The LatLng class is a simple container for latitude and longitude, but 502 | it's made more powerful by mixing in the above-mentioned Mappable 503 | module -- therefore, you can calculate easily the distance between two 504 | LatLng ojbects with `distance = first.distance_to(other)` 505 | 506 | GeoLoc represents an address or location which 507 | has been geocoded. You can get the city, zipcode, street address, etc. 508 | from a GeoLoc object. GeoLoc extends LatLng, so you also get lat/lng 509 | AND the Mappable modeule goodness for free. 510 | 511 | ## GOOGLE GROUP 512 | 513 | Follow the Google Group for updates and discussion on Geokit: http://groups.google.com/group/geokit 514 | 515 | ## IMPORTANT POST-INSTALLATION NOTES: 516 | 517 | *1. The configuration file*: Geokit for Rails uses a configuration file in config/initializers. 518 | You *must* add your own keys for the various geocoding services if you want to use geocoding. 519 | If you need to refer to the original template again, see the `assets/api_keys_template` file. 520 | 521 | *2. The gem dependency*: Geokit for Rails depends on the Geokit gem. Tell Rails about this 522 | dependency in `config/environment.rb`, within the initializer block: 523 | config.gem "andre-geokit", :lib=>'geokit', :source => 'http://gems.github.com' 524 | 525 | *If you're having trouble with dependencies ....* 526 | 527 | Try installing the gem manually, then adding a `require 'geokit'` to the top of 528 | `vendor/plugins/geokit-rails/init.rb` and/or `config/geokit_config.rb`. 529 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/test/acts_as_mappable_test.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'mocha' 3 | require File.join(File.dirname(__FILE__), 'test_helper') 4 | 5 | Geokit::Geocoders::provider_order=[:google,:us] 6 | 7 | # Uses defaults 8 | class Company < ActiveRecord::Base #:nodoc: all 9 | has_many :locations 10 | end 11 | 12 | # Configures everything. 13 | class Location < ActiveRecord::Base #:nodoc: all 14 | belongs_to :company 15 | acts_as_mappable 16 | end 17 | 18 | # for auto_geocode 19 | class Store < ActiveRecord::Base 20 | acts_as_mappable :auto_geocode=>true 21 | end 22 | 23 | # Uses deviations from conventions. 24 | class CustomLocation < ActiveRecord::Base #:nodoc: all 25 | belongs_to :company 26 | acts_as_mappable :distance_column_name => 'dist', 27 | :default_units => :kms, 28 | :default_formula => :flat, 29 | :lat_column_name => 'latitude', 30 | :lng_column_name => 'longitude' 31 | 32 | def to_s 33 | "lat: #{latitude} lng: #{longitude} dist: #{dist}" 34 | end 35 | end 36 | 37 | # Uses :through 38 | class MockOrganization < ActiveRecord::Base #:nodoc: all 39 | has_one :mock_address, :as => :addressable 40 | acts_as_mappable :through => :mock_address 41 | end 42 | 43 | # Used by :through 44 | class MockAddress < ActiveRecord::Base #:nodoc: all 45 | belongs_to :addressable, :polymorphic => true 46 | acts_as_mappable 47 | end 48 | 49 | class ActsAsMappableTest < ActiveSupport::TestCase #:nodoc: all 50 | 51 | LOCATION_A_IP = "217.10.83.5" 52 | 53 | self.fixture_path = File.dirname(__FILE__) + '/fixtures' 54 | self.use_transactional_fixtures = true 55 | self.use_instantiated_fixtures = false 56 | self.pre_loaded_fixtures = true 57 | fixtures :companies, :locations, :custom_locations, :stores, :mock_organizations, :mock_addresses 58 | 59 | def setup 60 | @location_a = GeoKit::GeoLoc.new 61 | @location_a.lat = 32.918593 62 | @location_a.lng = -96.958444 63 | @location_a.city = "Irving" 64 | @location_a.state = "TX" 65 | @location_a.country_code = "US" 66 | @location_a.success = true 67 | 68 | @sw = GeoKit::LatLng.new(32.91663,-96.982841) 69 | @ne = GeoKit::LatLng.new(32.96302,-96.919495) 70 | @bounds_center=GeoKit::LatLng.new((@sw.lat+@ne.lat)/2,(@sw.lng+@ne.lng)/2) 71 | 72 | @starbucks = companies(:starbucks) 73 | @loc_a = locations(:a) 74 | @custom_loc_a = custom_locations(:a) 75 | @loc_e = locations(:e) 76 | @custom_loc_e = custom_locations(:e) 77 | 78 | @barnes_and_noble = mock_organizations(:barnes_and_noble) 79 | @address = mock_addresses(:address_barnes_and_noble) 80 | end 81 | 82 | def test_override_default_units_the_hard_way 83 | Location.default_units = :kms 84 | locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") 85 | assert_equal 5, locations.size 86 | locations = Location.count(:origin => @loc_a, :conditions => "distance < 3.97") 87 | assert_equal 5, locations 88 | Location.default_units = :miles 89 | end 90 | 91 | def test_include 92 | locations = Location.find(:all, :origin => @loc_a, :include => :company, :conditions => "company_id = 1") 93 | assert !locations.empty? 94 | assert_equal 1, locations[0].company.id 95 | assert_equal 'Starbucks', locations[0].company.name 96 | end 97 | 98 | def test_distance_between_geocoded 99 | GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) 100 | GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("San Francisco, CA").returns(@location_a) 101 | assert_equal 0, Location.distance_between("Irving, TX", "San Francisco, CA") 102 | end 103 | 104 | def test_distance_to_geocoded 105 | GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) 106 | assert_equal 0, @custom_loc_a.distance_to("Irving, TX") 107 | end 108 | 109 | def test_distance_to_geocoded_error 110 | GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(GeoKit::GeoLoc.new) 111 | assert_raise(GeoKit::Geocoders::GeocodeError) { @custom_loc_a.distance_to("Irving, TX") } 112 | end 113 | 114 | def test_custom_attributes_distance_calculations 115 | assert_equal 0, @custom_loc_a.distance_to(@loc_a) 116 | assert_equal 0, CustomLocation.distance_between(@custom_loc_a, @loc_a) 117 | end 118 | 119 | def test_distance_column_in_select 120 | locations = Location.find(:all, :origin => @loc_a, :order => "distance ASC") 121 | assert_equal 6, locations.size 122 | assert_equal 0, @loc_a.distance_to(locations.first) 123 | assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 124 | end 125 | 126 | def test_find_with_distance_condition 127 | locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") 128 | assert_equal 5, locations.size 129 | locations = Location.count(:origin => @loc_a, :conditions => "distance < 3.97") 130 | assert_equal 5, locations 131 | end 132 | 133 | def test_find_with_distance_condition_with_units_override 134 | locations = Location.find(:all, :origin => @loc_a, :units => :kms, :conditions => "distance < 6.387") 135 | assert_equal 5, locations.size 136 | locations = Location.count(:origin => @loc_a, :units => :kms, :conditions => "distance < 6.387") 137 | assert_equal 5, locations 138 | end 139 | 140 | def test_find_with_distance_condition_with_formula_override 141 | locations = Location.find(:all, :origin => @loc_a, :formula => :flat, :conditions => "distance < 6.387") 142 | assert_equal 6, locations.size 143 | locations = Location.count(:origin => @loc_a, :formula => :flat, :conditions => "distance < 6.387") 144 | assert_equal 6, locations 145 | end 146 | 147 | def test_find_within 148 | locations = Location.find_within(3.97, :origin => @loc_a) 149 | assert_equal 5, locations.size 150 | locations = Location.count_within(3.97, :origin => @loc_a) 151 | assert_equal 5, locations 152 | end 153 | 154 | def test_find_within_with_token 155 | locations = Location.find(:all, :within => 3.97, :origin => @loc_a) 156 | assert_equal 5, locations.size 157 | locations = Location.count(:within => 3.97, :origin => @loc_a) 158 | assert_equal 5, locations 159 | end 160 | 161 | def test_find_within_with_coordinates 162 | locations = Location.find_within(3.97, :origin =>[@loc_a.lat,@loc_a.lng]) 163 | assert_equal 5, locations.size 164 | locations = Location.count_within(3.97, :origin =>[@loc_a.lat,@loc_a.lng]) 165 | assert_equal 5, locations 166 | end 167 | 168 | def test_find_with_compound_condition 169 | locations = Location.find(:all, :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") 170 | assert_equal 2, locations.size 171 | locations = Location.count(:origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") 172 | assert_equal 2, locations 173 | end 174 | 175 | def test_find_with_secure_compound_condition 176 | locations = Location.find(:all, :origin => @loc_a, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) 177 | assert_equal 2, locations.size 178 | locations = Location.count(:origin => @loc_a, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) 179 | assert_equal 2, locations 180 | end 181 | 182 | def test_find_beyond 183 | locations = Location.find_beyond(3.95, :origin => @loc_a) 184 | assert_equal 1, locations.size 185 | locations = Location.count_beyond(3.95, :origin => @loc_a) 186 | assert_equal 1, locations 187 | end 188 | 189 | def test_find_beyond_with_token 190 | locations = Location.find(:all, :beyond => 3.95, :origin => @loc_a) 191 | assert_equal 1, locations.size 192 | locations = Location.count(:beyond => 3.95, :origin => @loc_a) 193 | assert_equal 1, locations 194 | end 195 | 196 | def test_find_beyond_with_coordinates 197 | locations = Location.find_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) 198 | assert_equal 1, locations.size 199 | locations = Location.count_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) 200 | assert_equal 1, locations 201 | end 202 | 203 | def test_find_range_with_token 204 | locations = Location.find(:all, :range => 0..10, :origin => @loc_a) 205 | assert_equal 6, locations.size 206 | locations = Location.count(:range => 0..10, :origin => @loc_a) 207 | assert_equal 6, locations 208 | end 209 | 210 | def test_find_range_with_token_with_conditions 211 | locations = Location.find(:all, :origin => @loc_a, :range => 0..10, :conditions => ["city = ?", 'Coppell']) 212 | assert_equal 2, locations.size 213 | locations = Location.count(:origin => @loc_a, :range => 0..10, :conditions => ["city = ?", 'Coppell']) 214 | assert_equal 2, locations 215 | end 216 | 217 | def test_find_range_with_token_excluding_end 218 | locations = Location.find(:all, :range => 0...10, :origin => @loc_a) 219 | assert_equal 6, locations.size 220 | locations = Location.count(:range => 0...10, :origin => @loc_a) 221 | assert_equal 6, locations 222 | end 223 | 224 | def test_find_nearest 225 | assert_equal @loc_a, Location.find_nearest(:origin => @loc_a) 226 | end 227 | 228 | def test_find_nearest_through_find 229 | assert_equal @loc_a, Location.find(:nearest, :origin => @loc_a) 230 | end 231 | 232 | def test_find_nearest_with_coordinates 233 | assert_equal @loc_a, Location.find_nearest(:origin =>[@loc_a.lat, @loc_a.lng]) 234 | end 235 | 236 | def test_find_farthest 237 | assert_equal @loc_e, Location.find_farthest(:origin => @loc_a) 238 | end 239 | 240 | def test_find_farthest_through_find 241 | assert_equal @loc_e, Location.find(:farthest, :origin => @loc_a) 242 | end 243 | 244 | def test_find_farthest_with_coordinates 245 | assert_equal @loc_e, Location.find_farthest(:origin =>[@loc_a.lat, @loc_a.lng]) 246 | end 247 | 248 | def test_scoped_distance_column_in_select 249 | locations = @starbucks.locations.find(:all, :origin => @loc_a, :order => "distance ASC") 250 | assert_equal 5, locations.size 251 | assert_equal 0, @loc_a.distance_to(locations.first) 252 | assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 253 | end 254 | 255 | def test_scoped_find_with_distance_condition 256 | locations = @starbucks.locations.find(:all, :origin => @loc_a, :conditions => "distance < 3.97") 257 | assert_equal 4, locations.size 258 | locations = @starbucks.locations.count(:origin => @loc_a, :conditions => "distance < 3.97") 259 | assert_equal 4, locations 260 | end 261 | 262 | def test_scoped_find_within 263 | locations = @starbucks.locations.find_within(3.97, :origin => @loc_a) 264 | assert_equal 4, locations.size 265 | locations = @starbucks.locations.count_within(3.97, :origin => @loc_a) 266 | assert_equal 4, locations 267 | end 268 | 269 | def test_scoped_find_with_compound_condition 270 | locations = @starbucks.locations.find(:all, :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") 271 | assert_equal 2, locations.size 272 | locations = @starbucks.locations.count( :origin => @loc_a, :conditions => "distance < 5 and city = 'Coppell'") 273 | assert_equal 2, locations 274 | end 275 | 276 | def test_scoped_find_beyond 277 | locations = @starbucks.locations.find_beyond(3.95, :origin => @loc_a) 278 | assert_equal 1, locations.size 279 | locations = @starbucks.locations.count_beyond(3.95, :origin => @loc_a) 280 | assert_equal 1, locations 281 | end 282 | 283 | def test_scoped_find_nearest 284 | assert_equal @loc_a, @starbucks.locations.find_nearest(:origin => @loc_a) 285 | end 286 | 287 | def test_scoped_find_farthest 288 | assert_equal @loc_e, @starbucks.locations.find_farthest(:origin => @loc_a) 289 | end 290 | 291 | def test_ip_geocoded_distance_column_in_select 292 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 293 | locations = Location.find(:all, :origin => LOCATION_A_IP, :order => "distance ASC") 294 | assert_equal 6, locations.size 295 | assert_equal 0, @loc_a.distance_to(locations.first) 296 | assert_in_delta 3.97, @loc_a.distance_to(locations.last, :units => :miles, :formula => :sphere), 0.01 297 | end 298 | 299 | def test_ip_geocoded_find_with_distance_condition 300 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 301 | locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => "distance < 3.97") 302 | assert_equal 5, locations.size 303 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 304 | locations = Location.count(:origin => LOCATION_A_IP, :conditions => "distance < 3.97") 305 | assert_equal 5, locations 306 | end 307 | 308 | def test_ip_geocoded_find_within 309 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 310 | locations = Location.find_within(3.97, :origin => LOCATION_A_IP) 311 | assert_equal 5, locations.size 312 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 313 | locations = Location.count_within(3.97, :origin => LOCATION_A_IP) 314 | assert_equal 5, locations 315 | end 316 | 317 | def test_ip_geocoded_find_with_compound_condition 318 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 319 | locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => "distance < 5 and city = 'Coppell'") 320 | assert_equal 2, locations.size 321 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 322 | locations = Location.count(:origin => LOCATION_A_IP, :conditions => "distance < 5 and city = 'Coppell'") 323 | assert_equal 2, locations 324 | end 325 | 326 | def test_ip_geocoded_find_with_secure_compound_condition 327 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 328 | locations = Location.find(:all, :origin => LOCATION_A_IP, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) 329 | assert_equal 2, locations.size 330 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 331 | locations = Location.count(:origin => LOCATION_A_IP, :conditions => ["distance < ? and city = ?", 5, 'Coppell']) 332 | assert_equal 2, locations 333 | end 334 | 335 | def test_ip_geocoded_find_beyond 336 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 337 | locations = Location.find_beyond(3.95, :origin => LOCATION_A_IP) 338 | assert_equal 1, locations.size 339 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 340 | locations = Location.count_beyond(3.95, :origin => LOCATION_A_IP) 341 | assert_equal 1, locations 342 | end 343 | 344 | def test_ip_geocoded_find_nearest 345 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 346 | assert_equal @loc_a, Location.find_nearest(:origin => LOCATION_A_IP) 347 | end 348 | 349 | def test_ip_geocoded_find_farthest 350 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with(LOCATION_A_IP).returns(@location_a) 351 | assert_equal @loc_e, Location.find_farthest(:origin => LOCATION_A_IP) 352 | end 353 | 354 | def test_ip_geocoder_exception 355 | GeoKit::Geocoders::IpGeocoder.expects(:geocode).with('127.0.0.1').returns(GeoKit::GeoLoc.new) 356 | assert_raises GeoKit::Geocoders::GeocodeError do 357 | Location.find_farthest(:origin => '127.0.0.1') 358 | end 359 | end 360 | 361 | def test_address_geocode 362 | GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with('Irving, TX').returns(@location_a) 363 | locations = Location.find(:all, :origin => 'Irving, TX', :conditions => ["distance < ? and city = ?", 5, 'Coppell']) 364 | assert_equal 2, locations.size 365 | end 366 | 367 | def test_find_with_custom_distance_condition 368 | locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => "dist < 3.97") 369 | assert_equal 5, locations.size 370 | locations = CustomLocation.count(:origin => @loc_a, :conditions => "dist < 3.97") 371 | assert_equal 5, locations 372 | end 373 | 374 | def test_find_with_custom_distance_condition_using_custom_origin 375 | locations = CustomLocation.find(:all, :origin => @custom_loc_a, :conditions => "dist < 3.97") 376 | assert_equal 5, locations.size 377 | locations = CustomLocation.count(:origin => @custom_loc_a, :conditions => "dist < 3.97") 378 | assert_equal 5, locations 379 | end 380 | 381 | def test_find_within_with_custom 382 | locations = CustomLocation.find_within(3.97, :origin => @loc_a) 383 | assert_equal 5, locations.size 384 | locations = CustomLocation.count_within(3.97, :origin => @loc_a) 385 | assert_equal 5, locations 386 | end 387 | 388 | def test_find_within_with_coordinates_with_custom 389 | locations = CustomLocation.find_within(3.97, :origin =>[@loc_a.lat, @loc_a.lng]) 390 | assert_equal 5, locations.size 391 | locations = CustomLocation.count_within(3.97, :origin =>[@loc_a.lat, @loc_a.lng]) 392 | assert_equal 5, locations 393 | end 394 | 395 | def test_find_with_compound_condition_with_custom 396 | locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => "dist < 5 and city = 'Coppell'") 397 | assert_equal 1, locations.size 398 | locations = CustomLocation.count(:origin => @loc_a, :conditions => "dist < 5 and city = 'Coppell'") 399 | assert_equal 1, locations 400 | end 401 | 402 | def test_find_with_secure_compound_condition_with_custom 403 | locations = CustomLocation.find(:all, :origin => @loc_a, :conditions => ["dist < ? and city = ?", 5, 'Coppell']) 404 | assert_equal 1, locations.size 405 | locations = CustomLocation.count(:origin => @loc_a, :conditions => ["dist < ? and city = ?", 5, 'Coppell']) 406 | assert_equal 1, locations 407 | end 408 | 409 | def test_find_beyond_with_custom 410 | locations = CustomLocation.find_beyond(3.95, :origin => @loc_a) 411 | assert_equal 1, locations.size 412 | locations = CustomLocation.count_beyond(3.95, :origin => @loc_a) 413 | assert_equal 1, locations 414 | end 415 | 416 | def test_find_beyond_with_coordinates_with_custom 417 | locations = CustomLocation.find_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) 418 | assert_equal 1, locations.size 419 | locations = CustomLocation.count_beyond(3.95, :origin =>[@loc_a.lat, @loc_a.lng]) 420 | assert_equal 1, locations 421 | end 422 | 423 | def test_find_nearest_with_custom 424 | assert_equal @custom_loc_a, CustomLocation.find_nearest(:origin => @loc_a) 425 | end 426 | 427 | def test_find_nearest_with_coordinates_with_custom 428 | assert_equal @custom_loc_a, CustomLocation.find_nearest(:origin =>[@loc_a.lat, @loc_a.lng]) 429 | end 430 | 431 | def test_find_farthest_with_custom 432 | assert_equal @custom_loc_e, CustomLocation.find_farthest(:origin => @loc_a) 433 | end 434 | 435 | def test_find_farthest_with_coordinates_with_custom 436 | assert_equal @custom_loc_e, CustomLocation.find_farthest(:origin =>[@loc_a.lat, @loc_a.lng]) 437 | end 438 | 439 | def test_find_with_array_origin 440 | locations = Location.find(:all, :origin =>[@loc_a.lat,@loc_a.lng], :conditions => "distance < 3.97") 441 | assert_equal 5, locations.size 442 | locations = Location.count(:origin =>[@loc_a.lat,@loc_a.lng], :conditions => "distance < 3.97") 443 | assert_equal 5, locations 444 | end 445 | 446 | 447 | # Bounding box tests 448 | 449 | def test_find_within_bounds 450 | locations = Location.find_within_bounds([@sw,@ne]) 451 | assert_equal 2, locations.size 452 | locations = Location.count_within_bounds([@sw,@ne]) 453 | assert_equal 2, locations 454 | end 455 | 456 | def test_find_within_bounds_ordered_by_distance 457 | locations = Location.find_within_bounds([@sw,@ne], :origin=>@bounds_center, :order=>'distance asc') 458 | assert_equal locations[0], locations(:d) 459 | assert_equal locations[1], locations(:a) 460 | end 461 | 462 | def test_find_within_bounds_with_token 463 | locations = Location.find(:all, :bounds=>[@sw,@ne]) 464 | assert_equal 2, locations.size 465 | locations = Location.count(:bounds=>[@sw,@ne]) 466 | assert_equal 2, locations 467 | end 468 | 469 | def test_find_within_bounds_with_string_conditions 470 | locations = Location.find(:all, :bounds=>[@sw,@ne], :conditions=>"id !=#{locations(:a).id}") 471 | assert_equal 1, locations.size 472 | end 473 | 474 | def test_find_within_bounds_with_array_conditions 475 | locations = Location.find(:all, :bounds=>[@sw,@ne], :conditions=>["id != ?", locations(:a).id]) 476 | assert_equal 1, locations.size 477 | end 478 | 479 | def test_auto_geocode 480 | GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("Irving, TX").returns(@location_a) 481 | store=Store.new(:address=>'Irving, TX') 482 | store.save 483 | assert_equal store.lat,@location_a.lat 484 | assert_equal store.lng,@location_a.lng 485 | assert_equal 0, store.errors.size 486 | end 487 | 488 | def test_auto_geocode_failure 489 | GeoKit::Geocoders::MultiGeocoder.expects(:geocode).with("BOGUS").returns(GeoKit::GeoLoc.new) 490 | store=Store.new(:address=>'BOGUS') 491 | store.save 492 | assert store.new_record? 493 | assert_equal 1, store.errors.size 494 | end 495 | 496 | 497 | # Test :through 498 | 499 | def test_find_with_through 500 | organizations = MockOrganization.find(:all, :origin => @location_a, :order => 'distance ASC') 501 | assert_equal 2, organizations.size 502 | organizations = MockOrganization.count(:origin => @location_a, :conditions => "distance < 3.97") 503 | assert_equal 1, organizations 504 | end 505 | end 506 | -------------------------------------------------------------------------------- /vendor/plugins/geokit-rails/lib/geokit-rails/acts_as_mappable.rb: -------------------------------------------------------------------------------- 1 | module Geokit 2 | # Contains the class method acts_as_mappable targeted to be mixed into ActiveRecord. 3 | # When mixed in, augments find services such that they provide distance calculation 4 | # query services. The find method accepts additional options: 5 | # 6 | # * :origin - can be 7 | # 1. a two-element array of latititude/longitude -- :origin=>[37.792,-122.393] 8 | # 2. a geocodeable string -- :origin=>'100 Spear st, San Francisco, CA' 9 | # 3. an object which responds to lat and lng methods, or latitude and longitude methods, 10 | # or whatever methods you have specified for lng_column_name and lat_column_name 11 | # 12 | # Other finder methods are provided for specific queries. These are: 13 | # 14 | # * find_within (alias: find_inside) 15 | # * find_beyond (alias: find_outside) 16 | # * find_closest (alias: find_nearest) 17 | # * find_farthest 18 | # 19 | # Counter methods are available and work similarly to finders. 20 | # 21 | # If raw SQL is desired, the distance_sql method can be used to obtain SQL appropriate 22 | # to use in a find_by_sql call. 23 | module ActsAsMappable 24 | # Mix below class methods into ActiveRecord. 25 | def self.included(base) # :nodoc: 26 | base.extend ClassMethods 27 | end 28 | 29 | # Class method to mix into active record. 30 | module ClassMethods # :nodoc: 31 | # Class method to bring distance query support into ActiveRecord models. By default 32 | # uses :miles for distance units and performs calculations based upon the Haversine 33 | # (sphere) formula. These can be changed by setting Geokit::default_units and 34 | # Geokit::default_formula. Also, by default, uses lat, lng, and distance for respective 35 | # column names. All of these can be overridden using the :default_units, :default_formula, 36 | # :lat_column_name, :lng_column_name, and :distance_column_name hash keys. 37 | # 38 | # Can also use to auto-geocode a specific column on create. Syntax; 39 | # 40 | # acts_as_mappable :auto_geocode=>true 41 | # 42 | # By default, it tries to geocode the "address" field. Or, for more customized behavior: 43 | # 44 | # acts_as_mappable :auto_geocode=>{:field=>:address,:error_message=>'bad address'} 45 | # 46 | # In both cases, it creates a before_validation_on_create callback to geocode the given column. 47 | # For anything more customized, we recommend you forgo the auto_geocode option 48 | # and create your own AR callback to handle geocoding. 49 | def acts_as_mappable(options = {}) 50 | # Mix in the module, but ensure to do so just once. 51 | return if !defined?(Geokit::Mappable) || self.included_modules.include?(Geokit::ActsAsMappable::InstanceMethods) 52 | send :include, Geokit::ActsAsMappable::InstanceMethods 53 | # include the Mappable module. 54 | send :include, Geokit::Mappable 55 | 56 | # Handle class variables. 57 | cattr_accessor :through 58 | if self.through = options[:through] 59 | if reflection = self.reflect_on_association(self.through) 60 | (class << self; self; end).instance_eval do 61 | [ :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name ].each do |method_name| 62 | define_method method_name do 63 | reflection.klass.send(method_name) 64 | end 65 | end 66 | end 67 | end 68 | else 69 | cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name 70 | self.distance_column_name = options[:distance_column_name] || 'distance' 71 | self.default_units = options[:default_units] || Geokit::default_units 72 | self.default_formula = options[:default_formula] || Geokit::default_formula 73 | self.lat_column_name = options[:lat_column_name] || 'lat' 74 | self.lng_column_name = options[:lng_column_name] || 'lng' 75 | self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}" 76 | self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}" 77 | if options.include?(:auto_geocode) && options[:auto_geocode] 78 | # if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash 79 | options[:auto_geocode] = {} if options[:auto_geocode] == true 80 | cattr_accessor :auto_geocode_field, :auto_geocode_error_message 81 | self.auto_geocode_field = options[:auto_geocode][:field] || 'address' 82 | self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address' 83 | 84 | # set the actual callback here 85 | before_validation_on_create :auto_geocode_address 86 | end 87 | end 88 | end 89 | end 90 | 91 | # this is the callback for auto_geocoding 92 | def auto_geocode_address 93 | address=self.send(auto_geocode_field).to_s 94 | geo=Geokit::Geocoders::MultiGeocoder.geocode(address) 95 | 96 | if geo.success 97 | self.send("#{lat_column_name}=", geo.lat) 98 | self.send("#{lng_column_name}=", geo.lng) 99 | else 100 | errors.add(auto_geocode_field, auto_geocode_error_message) 101 | end 102 | 103 | geo.success 104 | end 105 | 106 | # Instance methods to mix into ActiveRecord. 107 | module InstanceMethods #:nodoc: 108 | # Mix class methods into module. 109 | def self.included(base) # :nodoc: 110 | base.extend SingletonMethods 111 | end 112 | 113 | # Class singleton methods to mix into ActiveRecord. 114 | module SingletonMethods # :nodoc: 115 | # Extends the existing find method in potentially two ways: 116 | # - If a mappable instance exists in the options, adds a distance column. 117 | # - If a mappable instance exists in the options and the distance column exists in the 118 | # conditions, substitutes the distance sql for the distance column -- this saves 119 | # having to write the gory SQL. 120 | def find(*args) 121 | prepare_for_find_or_count(:find, args) 122 | super(*args) 123 | end 124 | 125 | # Extends the existing count method by: 126 | # - If a mappable instance exists in the options and the distance column exists in the 127 | # conditions, substitutes the distance sql for the distance column -- this saves 128 | # having to write the gory SQL. 129 | def count(*args) 130 | prepare_for_find_or_count(:count, args) 131 | super(*args) 132 | end 133 | 134 | # Finds within a distance radius. 135 | def find_within(distance, options={}) 136 | options[:within] = distance 137 | find(:all, options) 138 | end 139 | alias find_inside find_within 140 | 141 | # Finds beyond a distance radius. 142 | def find_beyond(distance, options={}) 143 | options[:beyond] = distance 144 | find(:all, options) 145 | end 146 | alias find_outside find_beyond 147 | 148 | # Finds according to a range. Accepts inclusive or exclusive ranges. 149 | def find_by_range(range, options={}) 150 | options[:range] = range 151 | find(:all, options) 152 | end 153 | 154 | # Finds the closest to the origin. 155 | def find_closest(options={}) 156 | find(:nearest, options) 157 | end 158 | alias find_nearest find_closest 159 | 160 | # Finds the farthest from the origin. 161 | def find_farthest(options={}) 162 | find(:farthest, options) 163 | end 164 | 165 | # Finds within rectangular bounds (sw,ne). 166 | def find_within_bounds(bounds, options={}) 167 | options[:bounds] = bounds 168 | find(:all, options) 169 | end 170 | 171 | # counts within a distance radius. 172 | def count_within(distance, options={}) 173 | options[:within] = distance 174 | count(options) 175 | end 176 | alias count_inside count_within 177 | 178 | # Counts beyond a distance radius. 179 | def count_beyond(distance, options={}) 180 | options[:beyond] = distance 181 | count(options) 182 | end 183 | alias count_outside count_beyond 184 | 185 | # Counts according to a range. Accepts inclusive or exclusive ranges. 186 | def count_by_range(range, options={}) 187 | options[:range] = range 188 | count(options) 189 | end 190 | 191 | # Finds within rectangular bounds (sw,ne). 192 | def count_within_bounds(bounds, options={}) 193 | options[:bounds] = bounds 194 | count(options) 195 | end 196 | 197 | # Returns the distance calculation to be used as a display column or a condition. This 198 | # is provide for anyone wanting access to the raw SQL. 199 | def distance_sql(origin, units=default_units, formula=default_formula) 200 | case formula 201 | when :sphere 202 | sql = sphere_distance_sql(origin, units) 203 | when :flat 204 | sql = flat_distance_sql(origin, units) 205 | end 206 | sql 207 | end 208 | 209 | private 210 | 211 | # Prepares either a find or a count action by parsing through the options and 212 | # conditionally adding to the select clause for finders. 213 | def prepare_for_find_or_count(action, args) 214 | options = args.extract_options! 215 | #options = defined?(args.extract_options!) ? args.extract_options! : extract_options_from_args!(args) 216 | # Handle :through 217 | apply_include_for_through(options) 218 | # Obtain items affecting distance condition. 219 | origin = extract_origin_from_options(options) 220 | units = extract_units_from_options(options) 221 | formula = extract_formula_from_options(options) 222 | bounds = extract_bounds_from_options(options) 223 | # if no explicit bounds were given, try formulating them from the point and distance given 224 | bounds = formulate_bounds_from_distance(options, origin, units) unless bounds 225 | # Apply select adjustments based upon action. 226 | add_distance_to_select(options, origin, units, formula) if origin && action == :find 227 | # Apply the conditions for a bounding rectangle if applicable 228 | apply_bounds_conditions(options,bounds) if bounds 229 | # Apply distance scoping and perform substitutions. 230 | apply_distance_scope(options) 231 | substitute_distance_in_conditions(options, origin, units, formula) if origin && options.has_key?(:conditions) 232 | # Order by scoping for find action. 233 | apply_find_scope(args, options) if action == :find 234 | # Unfortunatley, we need to do extra work if you use an :include. See the method for more info. 235 | handle_order_with_include(options,origin,units,formula) if options.include?(:include) && options.include?(:order) && origin 236 | # Restore options minus the extra options that we used for the 237 | # Geokit API. 238 | args.push(options) 239 | end 240 | 241 | def apply_include_for_through(options) 242 | if self.through 243 | case options[:include] 244 | when Array 245 | options[:include] << self.through 246 | when Hash, String, Symbol 247 | options[:include] = [ self.through, options[:include] ] 248 | else 249 | options[:include] = self.through 250 | end 251 | end 252 | end 253 | 254 | # If we're here, it means that 1) an origin argument, 2) an :include, 3) an :order clause were supplied. 255 | # Now we have to sub some SQL into the :order clause. The reason is that when you do an :include, 256 | # ActiveRecord drops the psuedo-column (specificically, distance) which we supplied for :select. 257 | # So, the 'distance' column isn't available for the :order clause to reference when we use :include. 258 | def handle_order_with_include(options, origin, units, formula) 259 | # replace the distance_column_name with the distance sql in order clause 260 | options[:order].sub!(distance_column_name, distance_sql(origin, units, formula)) 261 | end 262 | 263 | # Looks for mapping-specific tokens and makes appropriate translations so that the 264 | # original finder has its expected arguments. Resets the the scope argument to 265 | # :first and ensures the limit is set to one. 266 | def apply_find_scope(args, options) 267 | case args.first 268 | when :nearest, :closest 269 | args[0] = :first 270 | options[:limit] = 1 271 | options[:order] = "#{distance_column_name} ASC" 272 | when :farthest 273 | args[0] = :first 274 | options[:limit] = 1 275 | options[:order] = "#{distance_column_name} DESC" 276 | end 277 | end 278 | 279 | # If it's a :within query, add a bounding box to improve performance. 280 | # This only gets called if a :bounds argument is not otherwise supplied. 281 | def formulate_bounds_from_distance(options, origin, units) 282 | distance = options[:within] if options.has_key?(:within) 283 | distance = options[:range].last-(options[:range].exclude_end?? 1 : 0) if options.has_key?(:range) 284 | if distance 285 | res=Geokit::Bounds.from_point_and_radius(origin,distance,:units=>units) 286 | else 287 | nil 288 | end 289 | end 290 | 291 | # Replace :within, :beyond and :range distance tokens with the appropriate distance 292 | # where clauses. Removes these tokens from the options hash. 293 | def apply_distance_scope(options) 294 | distance_condition = "#{distance_column_name} <= #{options[:within]}" if options.has_key?(:within) 295 | distance_condition = "#{distance_column_name} > #{options[:beyond]}" if options.has_key?(:beyond) 296 | distance_condition = "#{distance_column_name} >= #{options[:range].first} AND #{distance_column_name} <#{'=' unless options[:range].exclude_end?} #{options[:range].last}" if options.has_key?(:range) 297 | [:within, :beyond, :range].each { |option| options.delete(option) } if distance_condition 298 | 299 | options[:conditions]=augment_conditions(options[:conditions],distance_condition) if distance_condition 300 | end 301 | 302 | # This method lets you transparently add a new condition to a query without 303 | # worrying about whether it currently has conditions, or what kind of conditions they are 304 | # (string or array). 305 | # 306 | # Takes the current conditions (which can be an array or a string, or can be nil/false), 307 | # and a SQL string. It inserts the sql into the existing conditions, and returns new conditions 308 | # (which can be a string or an array 309 | def augment_conditions(current_conditions,sql) 310 | if current_conditions && current_conditions.is_a?(String) 311 | res="#{current_conditions} AND #{sql}" 312 | elsif current_conditions && current_conditions.is_a?(Array) 313 | current_conditions[0]="#{current_conditions[0]} AND #{sql}" 314 | res=current_conditions 315 | else 316 | res=sql 317 | end 318 | res 319 | end 320 | 321 | # Alters the conditions to include rectangular bounds conditions. 322 | def apply_bounds_conditions(options,bounds) 323 | sw,ne=bounds.sw,bounds.ne 324 | lng_sql= bounds.crosses_meridian? ? "(#{qualified_lng_column_name}<#{ne.lng} OR #{qualified_lng_column_name}>#{sw.lng})" : "#{qualified_lng_column_name}>#{sw.lng} AND #{qualified_lng_column_name}<#{ne.lng}" 325 | bounds_sql="#{qualified_lat_column_name}>#{sw.lat} AND #{qualified_lat_column_name}<#{ne.lat} AND #{lng_sql}" 326 | options[:conditions]=augment_conditions(options[:conditions],bounds_sql) 327 | end 328 | 329 | # Extracts the origin instance out of the options if it exists and returns 330 | # it. If there is no origin, looks for latitude and longitude values to 331 | # create an origin. The side-effect of the method is to remove these 332 | # option keys from the hash. 333 | def extract_origin_from_options(options) 334 | origin = options.delete(:origin) 335 | res = normalize_point_to_lat_lng(origin) if origin 336 | res 337 | end 338 | 339 | # Extract the units out of the options if it exists and returns it. If 340 | # there is no :units key, it uses the default. The side effect of the 341 | # method is to remove the :units key from the options hash. 342 | def extract_units_from_options(options) 343 | units = options[:units] || default_units 344 | options.delete(:units) 345 | units 346 | end 347 | 348 | # Extract the formula out of the options if it exists and returns it. If 349 | # there is no :formula key, it uses the default. The side effect of the 350 | # method is to remove the :formula key from the options hash. 351 | def extract_formula_from_options(options) 352 | formula = options[:formula] || default_formula 353 | options.delete(:formula) 354 | formula 355 | end 356 | 357 | def extract_bounds_from_options(options) 358 | bounds = options.delete(:bounds) 359 | bounds = Geokit::Bounds.normalize(bounds) if bounds 360 | end 361 | 362 | # Geocode IP address. 363 | def geocode_ip_address(origin) 364 | geo_location = Geokit::Geocoders::IpGeocoder.geocode(origin) 365 | return geo_location if geo_location.success 366 | raise Geokit::Geocoders::GeocodeError 367 | end 368 | 369 | 370 | # Given a point in a variety of (an address to geocode, 371 | # an array of [lat,lng], or an object with appropriate lat/lng methods, an IP addres) 372 | # this method will normalize it into a Geokit::LatLng instance. The only thing this 373 | # method adds on top of LatLng#normalize is handling of IP addresses 374 | def normalize_point_to_lat_lng(point) 375 | res = geocode_ip_address(point) if point.is_a?(String) && /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/.match(point) 376 | res = Geokit::LatLng.normalize(point) unless res 377 | res 378 | end 379 | 380 | # Augments the select with the distance SQL. 381 | def add_distance_to_select(options, origin, units=default_units, formula=default_formula) 382 | if origin 383 | distance_selector = distance_sql(origin, units, formula) + " AS #{distance_column_name}" 384 | selector = options.has_key?(:select) && options[:select] ? options[:select] : "*" 385 | options[:select] = "#{selector}, #{distance_selector}" 386 | end 387 | end 388 | 389 | # Looks for the distance column and replaces it with the distance sql. If an origin was not 390 | # passed in and the distance column exists, we leave it to be flagged as bad SQL by the database. 391 | # Conditions are either a string or an array. In the case of an array, the first entry contains 392 | # the condition. 393 | def substitute_distance_in_conditions(options, origin, units=default_units, formula=default_formula) 394 | original_conditions = options[:conditions] 395 | condition = original_conditions.is_a?(String) ? original_conditions : original_conditions.first 396 | pattern = Regexp.new("\\b#{distance_column_name}\\b") 397 | condition = condition.gsub(pattern, distance_sql(origin, units, formula)) 398 | original_conditions = condition if original_conditions.is_a?(String) 399 | original_conditions[0] = condition if original_conditions.is_a?(Array) 400 | options[:conditions] = original_conditions 401 | end 402 | 403 | # Returns the distance SQL using the spherical world formula (Haversine). The SQL is tuned 404 | # to the database in use. 405 | def sphere_distance_sql(origin, units) 406 | lat = deg2rad(origin.lat) 407 | lng = deg2rad(origin.lng) 408 | multiplier = units_sphere_multiplier(units) 409 | case connection.adapter_name.downcase 410 | when "mysql" 411 | sql=<<-SQL_END 412 | (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+ 413 | COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+ 414 | SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier}) 415 | SQL_END 416 | when "postgresql" 417 | sql=<<-SQL_END 418 | (ACOS(least(1,COS(#{lat})*COS(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*COS(RADIANS(#{qualified_lng_column_name}))+ 419 | COS(#{lat})*SIN(#{lng})*COS(RADIANS(#{qualified_lat_column_name}))*SIN(RADIANS(#{qualified_lng_column_name}))+ 420 | SIN(#{lat})*SIN(RADIANS(#{qualified_lat_column_name}))))*#{multiplier}) 421 | SQL_END 422 | else 423 | sql = "unhandled #{connection.adapter_name.downcase} adapter" 424 | end 425 | end 426 | 427 | # Returns the distance SQL using the flat-world formula (Phythagorean Theory). The SQL is tuned 428 | # to the database in use. 429 | def flat_distance_sql(origin, units) 430 | lat_degree_units = units_per_latitude_degree(units) 431 | lng_degree_units = units_per_longitude_degree(origin.lat, units) 432 | case connection.adapter_name.downcase 433 | when "mysql" 434 | sql=<<-SQL_END 435 | SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+ 436 | POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2)) 437 | SQL_END 438 | when "postgresql" 439 | sql=<<-SQL_END 440 | SQRT(POW(#{lat_degree_units}*(#{origin.lat}-#{qualified_lat_column_name}),2)+ 441 | POW(#{lng_degree_units}*(#{origin.lng}-#{qualified_lng_column_name}),2)) 442 | SQL_END 443 | else 444 | sql = "unhandled #{connection.adapter_name.downcase} adapter" 445 | end 446 | end 447 | end 448 | end 449 | end 450 | end 451 | 452 | # Extend Array with a sort_by_distance method. 453 | # This method creates a "distance" attribute on each object, 454 | # calculates the distance from the passed origin, 455 | # and finally sorts the array by the resulting distance. 456 | class Array 457 | def sort_by_distance_from(origin, opts={}) 458 | distance_attribute_name = opts.delete(:distance_attribute_name) || 'distance' 459 | self.each do |e| 460 | e.class.send(:attr_accessor, distance_attribute_name) if !e.respond_to? "#{distance_attribute_name}=" 461 | e.send("#{distance_attribute_name}=", e.distance_to(origin,opts)) 462 | end 463 | self.sort!{|a,b|a.send(distance_attribute_name) <=> b.send(distance_attribute_name)} 464 | end 465 | end 466 | -------------------------------------------------------------------------------- /public/javascripts/dragdrop.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 2 | // (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz) 3 | // 4 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 5 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 6 | 7 | if(Object.isUndefined(Effect)) 8 | throw("dragdrop.js requires including script.aculo.us' effects.js library"); 9 | 10 | var Droppables = { 11 | drops: [], 12 | 13 | remove: function(element) { 14 | this.drops = this.drops.reject(function(d) { return d.element==$(element) }); 15 | }, 16 | 17 | add: function(element) { 18 | element = $(element); 19 | var options = Object.extend({ 20 | greedy: true, 21 | hoverclass: null, 22 | tree: false 23 | }, arguments[1] || { }); 24 | 25 | // cache containers 26 | if(options.containment) { 27 | options._containers = []; 28 | var containment = options.containment; 29 | if(Object.isArray(containment)) { 30 | containment.each( function(c) { options._containers.push($(c)) }); 31 | } else { 32 | options._containers.push($(containment)); 33 | } 34 | } 35 | 36 | if(options.accept) options.accept = [options.accept].flatten(); 37 | 38 | Element.makePositioned(element); // fix IE 39 | options.element = element; 40 | 41 | this.drops.push(options); 42 | }, 43 | 44 | findDeepestChild: function(drops) { 45 | deepest = drops[0]; 46 | 47 | for (i = 1; i < drops.length; ++i) 48 | if (Element.isParent(drops[i].element, deepest.element)) 49 | deepest = drops[i]; 50 | 51 | return deepest; 52 | }, 53 | 54 | isContained: function(element, drop) { 55 | var containmentNode; 56 | if(drop.tree) { 57 | containmentNode = element.treeNode; 58 | } else { 59 | containmentNode = element.parentNode; 60 | } 61 | return drop._containers.detect(function(c) { return containmentNode == c }); 62 | }, 63 | 64 | isAffected: function(point, element, drop) { 65 | return ( 66 | (drop.element!=element) && 67 | ((!drop._containers) || 68 | this.isContained(element, drop)) && 69 | ((!drop.accept) || 70 | (Element.classNames(element).detect( 71 | function(v) { return drop.accept.include(v) } ) )) && 72 | Position.within(drop.element, point[0], point[1]) ); 73 | }, 74 | 75 | deactivate: function(drop) { 76 | if(drop.hoverclass) 77 | Element.removeClassName(drop.element, drop.hoverclass); 78 | this.last_active = null; 79 | }, 80 | 81 | activate: function(drop) { 82 | if(drop.hoverclass) 83 | Element.addClassName(drop.element, drop.hoverclass); 84 | this.last_active = drop; 85 | }, 86 | 87 | show: function(point, element) { 88 | if(!this.drops.length) return; 89 | var drop, affected = []; 90 | 91 | this.drops.each( function(drop) { 92 | if(Droppables.isAffected(point, element, drop)) 93 | affected.push(drop); 94 | }); 95 | 96 | if(affected.length>0) 97 | drop = Droppables.findDeepestChild(affected); 98 | 99 | if(this.last_active && this.last_active != drop) this.deactivate(this.last_active); 100 | if (drop) { 101 | Position.within(drop.element, point[0], point[1]); 102 | if(drop.onHover) 103 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); 104 | 105 | if (drop != this.last_active) Droppables.activate(drop); 106 | } 107 | }, 108 | 109 | fire: function(event, element) { 110 | if(!this.last_active) return; 111 | Position.prepare(); 112 | 113 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active)) 114 | if (this.last_active.onDrop) { 115 | this.last_active.onDrop(element, this.last_active.element, event); 116 | return true; 117 | } 118 | }, 119 | 120 | reset: function() { 121 | if(this.last_active) 122 | this.deactivate(this.last_active); 123 | } 124 | } 125 | 126 | var Draggables = { 127 | drags: [], 128 | observers: [], 129 | 130 | register: function(draggable) { 131 | if(this.drags.length == 0) { 132 | this.eventMouseUp = this.endDrag.bindAsEventListener(this); 133 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this); 134 | this.eventKeypress = this.keyPress.bindAsEventListener(this); 135 | 136 | Event.observe(document, "mouseup", this.eventMouseUp); 137 | Event.observe(document, "mousemove", this.eventMouseMove); 138 | Event.observe(document, "keypress", this.eventKeypress); 139 | } 140 | this.drags.push(draggable); 141 | }, 142 | 143 | unregister: function(draggable) { 144 | this.drags = this.drags.reject(function(d) { return d==draggable }); 145 | if(this.drags.length == 0) { 146 | Event.stopObserving(document, "mouseup", this.eventMouseUp); 147 | Event.stopObserving(document, "mousemove", this.eventMouseMove); 148 | Event.stopObserving(document, "keypress", this.eventKeypress); 149 | } 150 | }, 151 | 152 | activate: function(draggable) { 153 | if(draggable.options.delay) { 154 | this._timeout = setTimeout(function() { 155 | Draggables._timeout = null; 156 | window.focus(); 157 | Draggables.activeDraggable = draggable; 158 | }.bind(this), draggable.options.delay); 159 | } else { 160 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari 161 | this.activeDraggable = draggable; 162 | } 163 | }, 164 | 165 | deactivate: function() { 166 | this.activeDraggable = null; 167 | }, 168 | 169 | updateDrag: function(event) { 170 | if(!this.activeDraggable) return; 171 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 172 | // Mozilla-based browsers fire successive mousemove events with 173 | // the same coordinates, prevent needless redrawing (moz bug?) 174 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return; 175 | this._lastPointer = pointer; 176 | 177 | this.activeDraggable.updateDrag(event, pointer); 178 | }, 179 | 180 | endDrag: function(event) { 181 | if(this._timeout) { 182 | clearTimeout(this._timeout); 183 | this._timeout = null; 184 | } 185 | if(!this.activeDraggable) return; 186 | this._lastPointer = null; 187 | this.activeDraggable.endDrag(event); 188 | this.activeDraggable = null; 189 | }, 190 | 191 | keyPress: function(event) { 192 | if(this.activeDraggable) 193 | this.activeDraggable.keyPress(event); 194 | }, 195 | 196 | addObserver: function(observer) { 197 | this.observers.push(observer); 198 | this._cacheObserverCallbacks(); 199 | }, 200 | 201 | removeObserver: function(element) { // element instead of observer fixes mem leaks 202 | this.observers = this.observers.reject( function(o) { return o.element==element }); 203 | this._cacheObserverCallbacks(); 204 | }, 205 | 206 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag' 207 | if(this[eventName+'Count'] > 0) 208 | this.observers.each( function(o) { 209 | if(o[eventName]) o[eventName](eventName, draggable, event); 210 | }); 211 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event); 212 | }, 213 | 214 | _cacheObserverCallbacks: function() { 215 | ['onStart','onEnd','onDrag'].each( function(eventName) { 216 | Draggables[eventName+'Count'] = Draggables.observers.select( 217 | function(o) { return o[eventName]; } 218 | ).length; 219 | }); 220 | } 221 | } 222 | 223 | /*--------------------------------------------------------------------------*/ 224 | 225 | var Draggable = Class.create({ 226 | initialize: function(element) { 227 | var defaults = { 228 | handle: false, 229 | reverteffect: function(element, top_offset, left_offset) { 230 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; 231 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur, 232 | queue: {scope:'_draggable', position:'end'} 233 | }); 234 | }, 235 | endeffect: function(element) { 236 | var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0; 237 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 238 | queue: {scope:'_draggable', position:'end'}, 239 | afterFinish: function(){ 240 | Draggable._dragging[element] = false 241 | } 242 | }); 243 | }, 244 | zindex: 1000, 245 | revert: false, 246 | quiet: false, 247 | scroll: false, 248 | scrollSensitivity: 20, 249 | scrollSpeed: 15, 250 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] } 251 | delay: 0 252 | }; 253 | 254 | if(!arguments[1] || Object.isUndefined(arguments[1].endeffect)) 255 | Object.extend(defaults, { 256 | starteffect: function(element) { 257 | element._opacity = Element.getOpacity(element); 258 | Draggable._dragging[element] = true; 259 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 260 | } 261 | }); 262 | 263 | var options = Object.extend(defaults, arguments[1] || { }); 264 | 265 | this.element = $(element); 266 | 267 | if(options.handle && Object.isString(options.handle)) 268 | this.handle = this.element.down('.'+options.handle, 0); 269 | 270 | if(!this.handle) this.handle = $(options.handle); 271 | if(!this.handle) this.handle = this.element; 272 | 273 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) { 274 | options.scroll = $(options.scroll); 275 | this._isScrollChild = Element.childOf(this.element, options.scroll); 276 | } 277 | 278 | Element.makePositioned(this.element); // fix IE 279 | 280 | this.options = options; 281 | this.dragging = false; 282 | 283 | this.eventMouseDown = this.initDrag.bindAsEventListener(this); 284 | Event.observe(this.handle, "mousedown", this.eventMouseDown); 285 | 286 | Draggables.register(this); 287 | }, 288 | 289 | destroy: function() { 290 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); 291 | Draggables.unregister(this); 292 | }, 293 | 294 | currentDelta: function() { 295 | return([ 296 | parseInt(Element.getStyle(this.element,'left') || '0'), 297 | parseInt(Element.getStyle(this.element,'top') || '0')]); 298 | }, 299 | 300 | initDrag: function(event) { 301 | if(!Object.isUndefined(Draggable._dragging[this.element]) && 302 | Draggable._dragging[this.element]) return; 303 | if(Event.isLeftClick(event)) { 304 | // abort on form elements, fixes a Firefox issue 305 | var src = Event.element(event); 306 | if((tag_name = src.tagName.toUpperCase()) && ( 307 | tag_name=='INPUT' || 308 | tag_name=='SELECT' || 309 | tag_name=='OPTION' || 310 | tag_name=='BUTTON' || 311 | tag_name=='TEXTAREA')) return; 312 | 313 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 314 | var pos = Position.cumulativeOffset(this.element); 315 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) }); 316 | 317 | Draggables.activate(this); 318 | Event.stop(event); 319 | } 320 | }, 321 | 322 | startDrag: function(event) { 323 | this.dragging = true; 324 | if(!this.delta) 325 | this.delta = this.currentDelta(); 326 | 327 | if(this.options.zindex) { 328 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); 329 | this.element.style.zIndex = this.options.zindex; 330 | } 331 | 332 | if(this.options.ghosting) { 333 | this._clone = this.element.cloneNode(true); 334 | this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute'); 335 | if (!this.element._originallyAbsolute) 336 | Position.absolutize(this.element); 337 | this.element.parentNode.insertBefore(this._clone, this.element); 338 | } 339 | 340 | if(this.options.scroll) { 341 | if (this.options.scroll == window) { 342 | var where = this._getWindowScroll(this.options.scroll); 343 | this.originalScrollLeft = where.left; 344 | this.originalScrollTop = where.top; 345 | } else { 346 | this.originalScrollLeft = this.options.scroll.scrollLeft; 347 | this.originalScrollTop = this.options.scroll.scrollTop; 348 | } 349 | } 350 | 351 | Draggables.notify('onStart', this, event); 352 | 353 | if(this.options.starteffect) this.options.starteffect(this.element); 354 | }, 355 | 356 | updateDrag: function(event, pointer) { 357 | if(!this.dragging) this.startDrag(event); 358 | 359 | if(!this.options.quiet){ 360 | Position.prepare(); 361 | Droppables.show(pointer, this.element); 362 | } 363 | 364 | Draggables.notify('onDrag', this, event); 365 | 366 | this.draw(pointer); 367 | if(this.options.change) this.options.change(this); 368 | 369 | if(this.options.scroll) { 370 | this.stopScrolling(); 371 | 372 | var p; 373 | if (this.options.scroll == window) { 374 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } 375 | } else { 376 | p = Position.page(this.options.scroll); 377 | p[0] += this.options.scroll.scrollLeft + Position.deltaX; 378 | p[1] += this.options.scroll.scrollTop + Position.deltaY; 379 | p.push(p[0]+this.options.scroll.offsetWidth); 380 | p.push(p[1]+this.options.scroll.offsetHeight); 381 | } 382 | var speed = [0,0]; 383 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity); 384 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity); 385 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity); 386 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity); 387 | this.startScrolling(speed); 388 | } 389 | 390 | // fix AppleWebKit rendering 391 | if(Prototype.Browser.WebKit) window.scrollBy(0,0); 392 | 393 | Event.stop(event); 394 | }, 395 | 396 | finishDrag: function(event, success) { 397 | this.dragging = false; 398 | 399 | if(this.options.quiet){ 400 | Position.prepare(); 401 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 402 | Droppables.show(pointer, this.element); 403 | } 404 | 405 | if(this.options.ghosting) { 406 | if (!this.element._originallyAbsolute) 407 | Position.relativize(this.element); 408 | delete this.element._originallyAbsolute; 409 | Element.remove(this._clone); 410 | this._clone = null; 411 | } 412 | 413 | var dropped = false; 414 | if(success) { 415 | dropped = Droppables.fire(event, this.element); 416 | if (!dropped) dropped = false; 417 | } 418 | if(dropped && this.options.onDropped) this.options.onDropped(this.element); 419 | Draggables.notify('onEnd', this, event); 420 | 421 | var revert = this.options.revert; 422 | if(revert && Object.isFunction(revert)) revert = revert(this.element); 423 | 424 | var d = this.currentDelta(); 425 | if(revert && this.options.reverteffect) { 426 | if (dropped == 0 || revert != 'failure') 427 | this.options.reverteffect(this.element, 428 | d[1]-this.delta[1], d[0]-this.delta[0]); 429 | } else { 430 | this.delta = d; 431 | } 432 | 433 | if(this.options.zindex) 434 | this.element.style.zIndex = this.originalZ; 435 | 436 | if(this.options.endeffect) 437 | this.options.endeffect(this.element); 438 | 439 | Draggables.deactivate(this); 440 | Droppables.reset(); 441 | }, 442 | 443 | keyPress: function(event) { 444 | if(event.keyCode!=Event.KEY_ESC) return; 445 | this.finishDrag(event, false); 446 | Event.stop(event); 447 | }, 448 | 449 | endDrag: function(event) { 450 | if(!this.dragging) return; 451 | this.stopScrolling(); 452 | this.finishDrag(event, true); 453 | Event.stop(event); 454 | }, 455 | 456 | draw: function(point) { 457 | var pos = Position.cumulativeOffset(this.element); 458 | if(this.options.ghosting) { 459 | var r = Position.realOffset(this.element); 460 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY; 461 | } 462 | 463 | var d = this.currentDelta(); 464 | pos[0] -= d[0]; pos[1] -= d[1]; 465 | 466 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) { 467 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft; 468 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop; 469 | } 470 | 471 | var p = [0,1].map(function(i){ 472 | return (point[i]-pos[i]-this.offset[i]) 473 | }.bind(this)); 474 | 475 | if(this.options.snap) { 476 | if(Object.isFunction(this.options.snap)) { 477 | p = this.options.snap(p[0],p[1],this); 478 | } else { 479 | if(Object.isArray(this.options.snap)) { 480 | p = p.map( function(v, i) { 481 | return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this)) 482 | } else { 483 | p = p.map( function(v) { 484 | return (v/this.options.snap).round()*this.options.snap }.bind(this)) 485 | } 486 | }} 487 | 488 | var style = this.element.style; 489 | if((!this.options.constraint) || (this.options.constraint=='horizontal')) 490 | style.left = p[0] + "px"; 491 | if((!this.options.constraint) || (this.options.constraint=='vertical')) 492 | style.top = p[1] + "px"; 493 | 494 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering 495 | }, 496 | 497 | stopScrolling: function() { 498 | if(this.scrollInterval) { 499 | clearInterval(this.scrollInterval); 500 | this.scrollInterval = null; 501 | Draggables._lastScrollPointer = null; 502 | } 503 | }, 504 | 505 | startScrolling: function(speed) { 506 | if(!(speed[0] || speed[1])) return; 507 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed]; 508 | this.lastScrolled = new Date(); 509 | this.scrollInterval = setInterval(this.scroll.bind(this), 10); 510 | }, 511 | 512 | scroll: function() { 513 | var current = new Date(); 514 | var delta = current - this.lastScrolled; 515 | this.lastScrolled = current; 516 | if(this.options.scroll == window) { 517 | with (this._getWindowScroll(this.options.scroll)) { 518 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) { 519 | var d = delta / 1000; 520 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] ); 521 | } 522 | } 523 | } else { 524 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000; 525 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000; 526 | } 527 | 528 | Position.prepare(); 529 | Droppables.show(Draggables._lastPointer, this.element); 530 | Draggables.notify('onDrag', this); 531 | if (this._isScrollChild) { 532 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer); 533 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000; 534 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000; 535 | if (Draggables._lastScrollPointer[0] < 0) 536 | Draggables._lastScrollPointer[0] = 0; 537 | if (Draggables._lastScrollPointer[1] < 0) 538 | Draggables._lastScrollPointer[1] = 0; 539 | this.draw(Draggables._lastScrollPointer); 540 | } 541 | 542 | if(this.options.change) this.options.change(this); 543 | }, 544 | 545 | _getWindowScroll: function(w) { 546 | var T, L, W, H; 547 | with (w.document) { 548 | if (w.document.documentElement && documentElement.scrollTop) { 549 | T = documentElement.scrollTop; 550 | L = documentElement.scrollLeft; 551 | } else if (w.document.body) { 552 | T = body.scrollTop; 553 | L = body.scrollLeft; 554 | } 555 | if (w.innerWidth) { 556 | W = w.innerWidth; 557 | H = w.innerHeight; 558 | } else if (w.document.documentElement && documentElement.clientWidth) { 559 | W = documentElement.clientWidth; 560 | H = documentElement.clientHeight; 561 | } else { 562 | W = body.offsetWidth; 563 | H = body.offsetHeight 564 | } 565 | } 566 | return { top: T, left: L, width: W, height: H }; 567 | } 568 | }); 569 | 570 | Draggable._dragging = { }; 571 | 572 | /*--------------------------------------------------------------------------*/ 573 | 574 | var SortableObserver = Class.create({ 575 | initialize: function(element, observer) { 576 | this.element = $(element); 577 | this.observer = observer; 578 | this.lastValue = Sortable.serialize(this.element); 579 | }, 580 | 581 | onStart: function() { 582 | this.lastValue = Sortable.serialize(this.element); 583 | }, 584 | 585 | onEnd: function() { 586 | Sortable.unmark(); 587 | if(this.lastValue != Sortable.serialize(this.element)) 588 | this.observer(this.element) 589 | } 590 | }); 591 | 592 | var Sortable = { 593 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/, 594 | 595 | sortables: { }, 596 | 597 | _findRootElement: function(element) { 598 | while (element.tagName.toUpperCase() != "BODY") { 599 | if(element.id && Sortable.sortables[element.id]) return element; 600 | element = element.parentNode; 601 | } 602 | }, 603 | 604 | options: function(element) { 605 | element = Sortable._findRootElement($(element)); 606 | if(!element) return; 607 | return Sortable.sortables[element.id]; 608 | }, 609 | 610 | destroy: function(element){ 611 | var s = Sortable.options(element); 612 | 613 | if(s) { 614 | Draggables.removeObserver(s.element); 615 | s.droppables.each(function(d){ Droppables.remove(d) }); 616 | s.draggables.invoke('destroy'); 617 | 618 | delete Sortable.sortables[s.element.id]; 619 | } 620 | }, 621 | 622 | create: function(element) { 623 | element = $(element); 624 | var options = Object.extend({ 625 | element: element, 626 | tag: 'li', // assumes li children, override with tag: 'tagname' 627 | dropOnEmpty: false, 628 | tree: false, 629 | treeTag: 'ul', 630 | overlap: 'vertical', // one of 'vertical', 'horizontal' 631 | constraint: 'vertical', // one of 'vertical', 'horizontal', false 632 | containment: element, // also takes array of elements (or id's); or false 633 | handle: false, // or a CSS class 634 | only: false, 635 | delay: 0, 636 | hoverclass: null, 637 | ghosting: false, 638 | quiet: false, 639 | scroll: false, 640 | scrollSensitivity: 20, 641 | scrollSpeed: 15, 642 | format: this.SERIALIZE_RULE, 643 | 644 | // these take arrays of elements or ids and can be 645 | // used for better initialization performance 646 | elements: false, 647 | handles: false, 648 | 649 | onChange: Prototype.emptyFunction, 650 | onUpdate: Prototype.emptyFunction 651 | }, arguments[1] || { }); 652 | 653 | // clear any old sortable with same element 654 | this.destroy(element); 655 | 656 | // build options for the draggables 657 | var options_for_draggable = { 658 | revert: true, 659 | quiet: options.quiet, 660 | scroll: options.scroll, 661 | scrollSpeed: options.scrollSpeed, 662 | scrollSensitivity: options.scrollSensitivity, 663 | delay: options.delay, 664 | ghosting: options.ghosting, 665 | constraint: options.constraint, 666 | handle: options.handle }; 667 | 668 | if(options.starteffect) 669 | options_for_draggable.starteffect = options.starteffect; 670 | 671 | if(options.reverteffect) 672 | options_for_draggable.reverteffect = options.reverteffect; 673 | else 674 | if(options.ghosting) options_for_draggable.reverteffect = function(element) { 675 | element.style.top = 0; 676 | element.style.left = 0; 677 | }; 678 | 679 | if(options.endeffect) 680 | options_for_draggable.endeffect = options.endeffect; 681 | 682 | if(options.zindex) 683 | options_for_draggable.zindex = options.zindex; 684 | 685 | // build options for the droppables 686 | var options_for_droppable = { 687 | overlap: options.overlap, 688 | containment: options.containment, 689 | tree: options.tree, 690 | hoverclass: options.hoverclass, 691 | onHover: Sortable.onHover 692 | } 693 | 694 | var options_for_tree = { 695 | onHover: Sortable.onEmptyHover, 696 | overlap: options.overlap, 697 | containment: options.containment, 698 | hoverclass: options.hoverclass 699 | } 700 | 701 | // fix for gecko engine 702 | Element.cleanWhitespace(element); 703 | 704 | options.draggables = []; 705 | options.droppables = []; 706 | 707 | // drop on empty handling 708 | if(options.dropOnEmpty || options.tree) { 709 | Droppables.add(element, options_for_tree); 710 | options.droppables.push(element); 711 | } 712 | 713 | (options.elements || this.findElements(element, options) || []).each( function(e,i) { 714 | var handle = options.handles ? $(options.handles[i]) : 715 | (options.handle ? $(e).select('.' + options.handle)[0] : e); 716 | options.draggables.push( 717 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); 718 | Droppables.add(e, options_for_droppable); 719 | if(options.tree) e.treeNode = element; 720 | options.droppables.push(e); 721 | }); 722 | 723 | if(options.tree) { 724 | (Sortable.findTreeElements(element, options) || []).each( function(e) { 725 | Droppables.add(e, options_for_tree); 726 | e.treeNode = element; 727 | options.droppables.push(e); 728 | }); 729 | } 730 | 731 | // keep reference 732 | this.sortables[element.id] = options; 733 | 734 | // for onupdate 735 | Draggables.addObserver(new SortableObserver(element, options.onUpdate)); 736 | 737 | }, 738 | 739 | // return all suitable-for-sortable elements in a guaranteed order 740 | findElements: function(element, options) { 741 | return Element.findChildren( 742 | element, options.only, options.tree ? true : false, options.tag); 743 | }, 744 | 745 | findTreeElements: function(element, options) { 746 | return Element.findChildren( 747 | element, options.only, options.tree ? true : false, options.treeTag); 748 | }, 749 | 750 | onHover: function(element, dropon, overlap) { 751 | if(Element.isParent(dropon, element)) return; 752 | 753 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) { 754 | return; 755 | } else if(overlap>0.5) { 756 | Sortable.mark(dropon, 'before'); 757 | if(dropon.previousSibling != element) { 758 | var oldParentNode = element.parentNode; 759 | element.style.visibility = "hidden"; // fix gecko rendering 760 | dropon.parentNode.insertBefore(element, dropon); 761 | if(dropon.parentNode!=oldParentNode) 762 | Sortable.options(oldParentNode).onChange(element); 763 | Sortable.options(dropon.parentNode).onChange(element); 764 | } 765 | } else { 766 | Sortable.mark(dropon, 'after'); 767 | var nextElement = dropon.nextSibling || null; 768 | if(nextElement != element) { 769 | var oldParentNode = element.parentNode; 770 | element.style.visibility = "hidden"; // fix gecko rendering 771 | dropon.parentNode.insertBefore(element, nextElement); 772 | if(dropon.parentNode!=oldParentNode) 773 | Sortable.options(oldParentNode).onChange(element); 774 | Sortable.options(dropon.parentNode).onChange(element); 775 | } 776 | } 777 | }, 778 | 779 | onEmptyHover: function(element, dropon, overlap) { 780 | var oldParentNode = element.parentNode; 781 | var droponOptions = Sortable.options(dropon); 782 | 783 | if(!Element.isParent(dropon, element)) { 784 | var index; 785 | 786 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only}); 787 | var child = null; 788 | 789 | if(children) { 790 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); 791 | 792 | for (index = 0; index < children.length; index += 1) { 793 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) { 794 | offset -= Element.offsetSize (children[index], droponOptions.overlap); 795 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { 796 | child = index + 1 < children.length ? children[index + 1] : null; 797 | break; 798 | } else { 799 | child = children[index]; 800 | break; 801 | } 802 | } 803 | } 804 | 805 | dropon.insertBefore(element, child); 806 | 807 | Sortable.options(oldParentNode).onChange(element); 808 | droponOptions.onChange(element); 809 | } 810 | }, 811 | 812 | unmark: function() { 813 | if(Sortable._marker) Sortable._marker.hide(); 814 | }, 815 | 816 | mark: function(dropon, position) { 817 | // mark on ghosting only 818 | var sortable = Sortable.options(dropon.parentNode); 819 | if(sortable && !sortable.ghosting) return; 820 | 821 | if(!Sortable._marker) { 822 | Sortable._marker = 823 | ($('dropmarker') || Element.extend(document.createElement('DIV'))). 824 | hide().addClassName('dropmarker').setStyle({position:'absolute'}); 825 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); 826 | } 827 | var offsets = Position.cumulativeOffset(dropon); 828 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'}); 829 | 830 | if(position=='after') 831 | if(sortable.overlap == 'horizontal') 832 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'}); 833 | else 834 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'}); 835 | 836 | Sortable._marker.show(); 837 | }, 838 | 839 | _tree: function(element, options, parent) { 840 | var children = Sortable.findElements(element, options) || []; 841 | 842 | for (var i = 0; i < children.length; ++i) { 843 | var match = children[i].id.match(options.format); 844 | 845 | if (!match) continue; 846 | 847 | var child = { 848 | id: encodeURIComponent(match ? match[1] : null), 849 | element: element, 850 | parent: parent, 851 | children: [], 852 | position: parent.children.length, 853 | container: $(children[i]).down(options.treeTag) 854 | } 855 | 856 | /* Get the element containing the children and recurse over it */ 857 | if (child.container) 858 | this._tree(child.container, options, child) 859 | 860 | parent.children.push (child); 861 | } 862 | 863 | return parent; 864 | }, 865 | 866 | tree: function(element) { 867 | element = $(element); 868 | var sortableOptions = this.options(element); 869 | var options = Object.extend({ 870 | tag: sortableOptions.tag, 871 | treeTag: sortableOptions.treeTag, 872 | only: sortableOptions.only, 873 | name: element.id, 874 | format: sortableOptions.format 875 | }, arguments[1] || { }); 876 | 877 | var root = { 878 | id: null, 879 | parent: null, 880 | children: [], 881 | container: element, 882 | position: 0 883 | } 884 | 885 | return Sortable._tree(element, options, root); 886 | }, 887 | 888 | /* Construct a [i] index for a particular node */ 889 | _constructIndex: function(node) { 890 | var index = ''; 891 | do { 892 | if (node.id) index = '[' + node.position + ']' + index; 893 | } while ((node = node.parent) != null); 894 | return index; 895 | }, 896 | 897 | sequence: function(element) { 898 | element = $(element); 899 | var options = Object.extend(this.options(element), arguments[1] || { }); 900 | 901 | return $(this.findElements(element, options) || []).map( function(item) { 902 | return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; 903 | }); 904 | }, 905 | 906 | setSequence: function(element, new_sequence) { 907 | element = $(element); 908 | var options = Object.extend(this.options(element), arguments[2] || { }); 909 | 910 | var nodeMap = { }; 911 | this.findElements(element, options).each( function(n) { 912 | if (n.id.match(options.format)) 913 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode]; 914 | n.parentNode.removeChild(n); 915 | }); 916 | 917 | new_sequence.each(function(ident) { 918 | var n = nodeMap[ident]; 919 | if (n) { 920 | n[1].appendChild(n[0]); 921 | delete nodeMap[ident]; 922 | } 923 | }); 924 | }, 925 | 926 | serialize: function(element) { 927 | element = $(element); 928 | var options = Object.extend(Sortable.options(element), arguments[1] || { }); 929 | var name = encodeURIComponent( 930 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id); 931 | 932 | if (options.tree) { 933 | return Sortable.tree(element, arguments[1]).children.map( function (item) { 934 | return [name + Sortable._constructIndex(item) + "[id]=" + 935 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); 936 | }).flatten().join('&'); 937 | } else { 938 | return Sortable.sequence(element, arguments[1]).map( function(item) { 939 | return name + "[]=" + encodeURIComponent(item); 940 | }).join('&'); 941 | } 942 | } 943 | } 944 | 945 | // Returns true if child is contained within element 946 | Element.isParent = function(child, element) { 947 | if (!child.parentNode || child == element) return false; 948 | if (child.parentNode == element) return true; 949 | return Element.isParent(child.parentNode, element); 950 | } 951 | 952 | Element.findChildren = function(element, only, recursive, tagName) { 953 | if(!element.hasChildNodes()) return null; 954 | tagName = tagName.toUpperCase(); 955 | if(only) only = [only].flatten(); 956 | var elements = []; 957 | $A(element.childNodes).each( function(e) { 958 | if(e.tagName && e.tagName.toUpperCase()==tagName && 959 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) })))) 960 | elements.push(e); 961 | if(recursive) { 962 | var grandchildren = Element.findChildren(e, only, recursive, tagName); 963 | if(grandchildren) elements.push(grandchildren); 964 | } 965 | }); 966 | 967 | return (elements.length>0 ? elements.flatten() : []); 968 | } 969 | 970 | Element.offsetSize = function (element, type) { 971 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')]; 972 | } 973 | --------------------------------------------------------------------------------