├── 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 | -
254 |
Use script/generate to create your models and controllers
255 | To see all available options, run it without parameters.
256 |
257 |
258 | -
259 |
Set up a default route and remove or rename this file
260 | Routes are set up in config/routes.rb.
261 |
262 |
263 | -
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 |
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 |
--------------------------------------------------------------------------------