From dc822017834b915323c03c68529721a91b5ae995 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Fri, 23 Mar 2012 14:50:09 +0100 Subject: [PATCH 01/25] fixed linked to extreme startup servers --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d00f8b..c69ab43 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Notes for facilitators $ WARMUP=1 ruby web_server.rb ```` -* In the warmup round, just make sure that everyone has something technologically working, you just get the same request repeatedly. @bodil has provided some [nice sample players in different languages](https://github.com/bodil/extreme_startup_servers). +* In the warmup round, just make sure that everyone has something technologically working, you just get the same request repeatedly. @bodil has provided some [nice sample players in different languages](https://github.com/steria/extreme_startup_servers). * Real game: revert to using the full QuizMaster, and restart the server. This will clear any registered players, but that's ok. * As the game progresses, you can introduce new question types by moving to the next round. Visit /controlpanel and press the "Advance round" button. Do this when you feel like some of the teams are making good progress in the current round. Typically we've found this to be about every 10 mins. But you can go faster/slower as you like. There are 6 rounds available. From 56e452dce1c0140ddef95331889a5bad446d9016 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 17 Apr 2012 22:12:02 +0200 Subject: [PATCH 02/25] fixed failing specs for squares/cubes due to rounding errors --- lib/extreme_startup/question_factory.rb | 8 ++++++-- spec/extreme_startup/square_and_cube_spec.rb | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 304b0dc..013aae6 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -258,7 +258,7 @@ def is_cube(x) if (x ==0) return true end - (x % Math.cbrt(x)) == 0 + (x % Math.cbrt(x).truncate) == 0 end end @@ -304,7 +304,11 @@ def question_bank ["which city is the Eiffel tower in", "Paris"], ["what currency did Spain use before the Euro", "peseta"], ["what colour is a banana", "yellow"], - ["who played James Bond in the film Dr No", "Sean Connery"] + ["who played James Bond in the film Dr No", "Sean Connery"], + ["""what is the name of the narrator in Proust's 'La recherche du temps perdu'""", ""], + ["what is the capital city of Botswana", "Gaborone"], + ["what is the date of Bloom's Day", "16 June"], + ["what is the date of Bloom's Day in Dublin", "16 June"] ] end end diff --git a/spec/extreme_startup/square_and_cube_spec.rb b/spec/extreme_startup/square_and_cube_spec.rb index 5b0b860..938d082 100644 --- a/spec/extreme_startup/square_and_cube_spec.rb +++ b/spec/extreme_startup/square_and_cube_spec.rb @@ -43,7 +43,7 @@ module ExtremeStartup end it "identifies an incorrect answer" do - question.answered_correctly?("64").should be_false + question.answered_correctly?("65").should be_false end end From 6494418e87cf5466261674d9026409e732eaf960 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 17 Apr 2012 22:23:48 +0200 Subject: [PATCH 03/25] added more questions, just in case... --- lib/extreme_startup/anagrams.yaml | 24 ++++++++++++++++++++++++ lib/extreme_startup/question_factory.rb | 5 ++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/extreme_startup/anagrams.yaml b/lib/extreme_startup/anagrams.yaml index 177a593..3bd4c1b 100644 --- a/lib/extreme_startup/anagrams.yaml +++ b/lib/extreme_startup/anagrams.yaml @@ -6,3 +6,27 @@ - enlists - google - banana + +- + anagram: war + correct: raw + incorrect: + - wharf + - wary + - rave + +- + anagram: admirer + correct: married + incorrect: + - mirror + - admiter + +- + anagram: master + correct: + - stream + - tamers + incorrect: + - raster + - sternum diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 013aae6..bb7dcfd 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -305,10 +305,9 @@ def question_bank ["what currency did Spain use before the Euro", "peseta"], ["what colour is a banana", "yellow"], ["who played James Bond in the film Dr No", "Sean Connery"], - ["""what is the name of the narrator in Proust's 'La recherche du temps perdu'""", ""], + ["what is the name of the narrator in Proust's 'La recherche du temps perdu'", ""], ["what is the capital city of Botswana", "Gaborone"], - ["what is the date of Bloom's Day", "16 June"], - ["what is the date of Bloom's Day in Dublin", "16 June"] + ["what is the date of BloomsDay", "16 June"] ] end end From 09d49218183c4233ea91d5caa076e81d8fe5daa5 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly and Willem van den Ende Date: Tue, 4 Jun 2013 21:03:26 +0000 Subject: [PATCH 04/25] updated away from .rvmrc --- .ruby-gemset | 1 + .ruby-version | 1 + .rvmrc | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .ruby-gemset create mode 100644 .ruby-version delete mode 100644 .rvmrc diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000..c6dbe2c --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +extreme_startup diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..8fdcf38 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +1.9.2 diff --git a/.rvmrc b/.rvmrc deleted file mode 100644 index df2e21b..0000000 --- a/.rvmrc +++ /dev/null @@ -1 +0,0 @@ -rvm use 1.9.2@extreme_startup --create \ No newline at end of file From fee9cc1873eb61d00ef6034c0086b3e5285af353 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 5 Jun 2013 11:15:55 +0200 Subject: [PATCH 05/25] increase timeout on cucumber features --- features/basic_scoring.feature | 6 +++--- features/penalty_rules.feature | 2 +- features/warm_up.feature | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/features/basic_scoring.feature b/features/basic_scoring.feature index 2c4548a..28bfa28 100644 --- a/features/basic_scoring.feature +++ b/features/basic_scoring.feature @@ -9,7 +9,7 @@ Feature: Basic Scoring """ And the correct answer to every question is '4' worth 10 points When the player is entered - And the game is played for 1 second + And the game is played for 2 second Then the scores should be: | player | score | | bob | 10 | @@ -23,7 +23,7 @@ Feature: Basic Scoring """ And the correct answer to every question is '4' worth 10 points When the player is entered - And the game is played for 1 second + And the game is played for 2 second Then the scores should be: | player | score | | charlie | -10 | @@ -37,7 +37,7 @@ Feature: Basic Scoring """ And the correct answer to every question is '4' When the player is entered - And the game is played for 1 second + And the game is played for 2 second Then the scores should be: | player | score | | ernie | -50 | \ No newline at end of file diff --git a/features/penalty_rules.feature b/features/penalty_rules.feature index 6af4634..0076272 100644 --- a/features/penalty_rules.feature +++ b/features/penalty_rules.feature @@ -22,7 +22,7 @@ Feature: Penalty Rules """ And the correct answer to every question is '0' worth 10 points When the players are entered - And the game is played for 1 second + And the game is played for 2 second Then the scores should be: | player | score | | always-right | 10 | diff --git a/features/warm_up.feature b/features/warm_up.feature index d27dce0..21f1fb4 100644 --- a/features/warm_up.feature +++ b/features/warm_up.feature @@ -11,7 +11,7 @@ Feature: Warm up end """ When the player is entered - And the game is played for 1 second + And the game is played for 2 second Then the scores should be: | player | score | | bob | 10 | @@ -26,7 +26,7 @@ Feature: Warm up end """ When the player is entered - And the game is played for 1 second + And the game is played for 2 second Then the scores should be: | player | score | | Sébastian | 10 | From 1fc27a23fdcd3282e1da1cbe05432f53452ef927 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 5 Jun 2013 11:16:37 +0200 Subject: [PATCH 06/25] added questions about the weather * these questions request current weather information for a bunch of cities in the world from users * this implies players need to connect to the internet to provide the correct answer * players are awarded a variable number of points depending on the accuracy of their answer * it relies on yahoo-weather interface (and gem) * random weather can be generated when not connected to internet --- Gemfile | 4 + Gemfile.lock | 105 +++++----- lib/extreme_startup/locations.yaml | 41 ++++ lib/extreme_startup/question_factory.rb | 187 +++++++++++++++++- spec/extreme_startup/weather_question_spec.rb | 168 ++++++++++++++++ 5 files changed, 451 insertions(+), 54 deletions(-) create mode 100644 lib/extreme_startup/locations.yaml create mode 100644 spec/extreme_startup/weather_question_spec.rb diff --git a/Gemfile b/Gemfile index 5422d67..7aa5b81 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,6 @@ source :rubygems +gem 'rake' gem 'eventmachine', '1.0.0.beta.3' gem 'thin' gem 'sinatra' @@ -10,5 +11,8 @@ gem 'rspec' gem 'cucumber' gem 'rack-test' gem 'capybara' +gem 'launchy' gem 'json' +gem 'yahoo-weather', '1.2.1' + diff --git a/Gemfile.lock b/Gemfile.lock index 9f1dc1f..18cf14e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,67 +1,72 @@ GEM remote: http://rubygems.org/ specs: - builder (3.0.0) - capybara (1.0.0) + addressable (2.3.4) + builder (3.2.2) + capybara (2.1.0) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - selenium-webdriver (~> 0.2.0) - xpath (~> 0.1.4) - childprocess (0.1.9) - ffi (~> 1.0.6) - crack (0.1.8) - cucumber (0.10.6) + xpath (~> 2.0) + cucumber (1.3.2) builder (>= 2.1.2) - diff-lcs (>= 1.1.2) - gherkin (~> 2.4.0) - json (>= 1.4.6) - term-ansicolor (>= 1.0.5) - daemons (1.1.3) - diff-lcs (1.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.12.0) + multi_json (~> 1.3) + daemons (1.1.9) + diff-lcs (1.2.4) eventmachine (1.0.0.beta.3) - ffi (1.0.9) - gherkin (2.4.0) - json (>= 1.4.6) - haml (3.1.2) - httparty (0.7.8) - crack (= 0.1.8) - json (1.5.2) - json_pure (1.5.2) - macaddr (1.0.0) - mime-types (1.16) - nokogiri (1.4.5) - rack (1.3.0) - rack-test (0.6.0) + eventmachine (1.0.0.beta.3-x86-mingw32) + gherkin (2.12.0) + multi_json (~> 1.3) + gherkin (2.12.0-x86-mingw32) + multi_json (~> 1.3) + haml (4.0.3) + tilt + httparty (0.11.0) + multi_json (~> 1.0) + multi_xml (>= 0.5.2) + json (1.8.0) + launchy (2.3.0) + addressable (~> 2.3) + macaddr (1.6.1) + systemu (~> 2.5.0) + mime-types (1.23) + multi_json (1.7.6) + multi_xml (0.5.4) + nokogiri (1.5.9) + nokogiri (1.5.9-x86-mingw32) + rack (1.5.2) + rack-protection (1.5.0) + rack + rack-test (0.6.2) rack (>= 1.0) - rspec (2.6.0) - rspec-core (~> 2.6.0) - rspec-expectations (~> 2.6.0) - rspec-mocks (~> 2.6.0) - rspec-core (2.6.4) - rspec-expectations (2.6.0) - diff-lcs (~> 1.1.2) - rspec-mocks (2.6.0) - rubyzip (0.9.4) - selenium-webdriver (0.2.1) - childprocess (>= 0.1.7) - ffi (>= 1.0.7) - json_pure - rubyzip - sinatra (1.2.6) - rack (~> 1.1) - tilt (>= 1.2.2, < 2.0) - term-ansicolor (1.0.5) - thin (1.2.11) + rake (10.0.4) + rspec (2.13.0) + rspec-core (~> 2.13.0) + rspec-expectations (~> 2.13.0) + rspec-mocks (~> 2.13.0) + rspec-core (2.13.1) + rspec-expectations (2.13.0) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.13.1) + sinatra (1.4.2) + rack (~> 1.5, >= 1.5.2) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) + systemu (2.5.2) + thin (1.5.1) daemons (>= 1.0.9) eventmachine (>= 0.12.6) rack (>= 1.0.0) - tilt (1.3.2) - uuid (2.3.2) + tilt (1.4.1) + uuid (2.3.7) macaddr (~> 1.0) - xpath (0.1.4) + xpath (2.0.0) nokogiri (~> 1.3) + yahoo-weather (1.2.1) + nokogiri (>= 1.4.1) PLATFORMS ruby @@ -74,7 +79,9 @@ DEPENDENCIES httparty json rack-test + rake rspec sinatra thin uuid + yahoo-weather (= 1.2.1) diff --git a/lib/extreme_startup/locations.yaml b/lib/extreme_startup/locations.yaml new file mode 100644 index 0000000..ec25ec2 --- /dev/null +++ b/lib/extreme_startup/locations.yaml @@ -0,0 +1,41 @@ +--- +- + city: Paris + code: 12597155 +- + city: Toronto + code: 4118 +- + city: New-York + code: 2459115 +- + city: London + code: 44418 +- + city: Berlin + code: 638242 +- + city: Stockholm + code: 906057 +- + city: Brazzaville + code: 1279685 +- + city: Moskva + code: 2122265 +- + city: Beijing + code: 2151330 +- + city: Tokyo + code: 1118370 +- + city: Auckland + code: 2348079 +- + city: Kuala Lumpur + code: 1154781 +- + city: Cape Town + code: 1591691 + \ No newline at end of file diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 7bebd24..8076158 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -15,7 +15,7 @@ def ask(player) puts "GET: " + url begin response = get(url) - if (response.success?) then + if response.success? then self.answer = response.to_s else @problem = "error_response" @@ -255,7 +255,7 @@ def is_square(x) end def is_cube(x) - if (x ==0) + if x ==0 return true end (x % Math.cbrt(x).truncate) == 0 @@ -282,13 +282,13 @@ def candidate_numbers class FibonacciQuestion < BinaryMathsQuestion def as_text n = @n1 + 4 - if (n > 20 && n % 10 == 1) + if n > 20 && n % 10 == 1 return "what is the #{n}st number in the Fibonacci sequence" end - if (n > 20 && n % 10 == 2) + if n > 20 && n % 10 == 2 return "what is the #{n}nd number in the Fibonacci sequence" end - return "what is the #{n}th number in the Fibonacci sequence" + "what is the #{n}th number in the Fibonacci sequence" end def points 50 @@ -389,6 +389,180 @@ def scrabble_scores end end + class Location + def initialize(name, weather) + @name = name + @weather = weather + end + + def name + @name + end + + def weather + @weather + end + + end + + require 'yahoo-weather' + + # Superclass for all weather related questions + # + # Initializes globally the weather conditions for all cities so that they get initialized only once + # + # * Site with various APIs: http://www.meteorologic.net/donnees-meteo.php + # * details of Yahoo weather API: http://developer.yahoo.com/weather/ + # * API for synop: http://blogdev.meteorologic.net/index.php?2008/03/19/10-synop-api-d-acces-aux-donnees + # * Stations list for synop: http://www.uradio.ku.dk/~ct/eurostationer.txt + # * One can also use yahoo-weather gem (code at https://github.com/shaper/yahoo-weather) + class WeatherQuestion < Question + + Sample_yahoo_response = <<-EOS + + + + Yahoo! Weather - Sunnyvale, CA + http://us.rd.yahoo.com/dailynews/rss/weather/Sunnyvale__CA/*http://weather.yahoo.com/forecast/USCA1116_f.html + Yahoo! Weather for Sunnyvale, CA + en-us + Fri, 18 Dec 2009 9:38 am PST + 60 + + + + + + + Yahoo! Weather + 142 + 18 + http://weather.yahoo.com + http://l.yimg.com/a/i/us/nws/th/main_142b.gif + + + Conditions for Sunnyvale, CA at 9:38 am PST + 37.37 + -122.04 + http://us.rd.yahoo.com/dailynews/rss/weather/Sunnyvale__CA/*http://weather.yahoo.com/forecast/USCA1116_f.html + Fri, 18 Dec 2009 9:38 am PST + +
+Current Conditions:
+Mostly Cloudy, 50 F
+
Forecast:
+Fri - Partly Cloudy. High: 62 Low: 49
+Sat - Partly Cloudy. High: 65 Low: 49
+
+Full Forecast at Yahoo! Weather

+(provided by The Weather Channel)
+]]>
+ + + USCA1116_2009_12_18_9_38_PST +
+
+
+ EOS + + def WeatherQuestion.random_weather + doc = Nokogiri::XML.parse(Sample_yahoo_response % [ rand * 100.0, 800.0 + (rand * 400.0), rand * 30]) + YahooWeather::Response.new("12345", "http://some.host/", doc) + end + + def WeatherQuestion.weather_of(location) + begin + client = YahooWeather::Client.new + weather = client.lookup_by_woeid(location['code'],'c') + + print("weather of %s is %s\n" % [ location['city'], weather.to_s]) + + Location.new(location['city'],weather) + rescue => exception + weather = WeatherQuestion.random_weather + print("got error %s trying to get weather for %s, setting to random weather %s\n" % [exception, location, weather.to_yaml]) + Location.new(location['city'],weather) + end + end + + # A list of known cities is loaded from the current directory and their current weather + # is looked up using Yahoo! Weather service + # see http://developer.yahoo.com/weather/ + @@weather_in_cities = YAML.load_file(File.join(File.dirname(__FILE__), "locations.yaml")) + .map { |location| weather_of(location) } + #.map { |location| Location.new(location['city'],nil) } + + def WeatherQuestion.weather_in_cities + @@weather_in_cities + end + + # Initializes weather question for a given player + # + # +player+:: A +Player+ instance + # +location+:: a +City+ object containing name of a location and its current weather (see YahooWeather) + # If +nil+, a sample city is selected from WeatherQuestion.weather_in_cities + def initialize(player, location=nil, error_margin = 0.10) + @error_margin = error_margin + if location + @location = location + else + @location = WeatherQuestion.weather_in_cities.sample + end + end + + # A weather answer may be correct even if not exactly the required weather + # This question accepts a tolerance of +- 10% around the exact answer with an exponentially decaying + # number of points earned for approximate answers. + # + # +answer+:: The answer, a string representing a decimal number + def answered_correctly?(answer) + answer.to_f > (correct_answer * (1 - @error_margin)) && + answer.to_f < (correct_answer * (1 + @error_margin)) + end + + def points + 100 / (1 + Math.exp(2 * (correct_answer - answer.to_f).abs)) + end + + end + + class TemperatureQuestion < WeatherQuestion + + def as_text + "what is the current (Celsius) temperature in #{@location.name}?" + end + + def correct_answer + @location.weather.condition.temp + end + + end + + class PressureQuestion < WeatherQuestion + + def as_text + "what is the current (Millibar) pressure in #{@location.name}?" + end + + def correct_answer + @location.weather.atmosphere.pressure + end + + end + + class WindQuestion < WeatherQuestion + + def as_text + "what is the current (kph) speed of wind in #{@location.name}?" + end + + def correct_answer + @location.weather.wind.speed + end + + end + class QuestionFactory attr_reader :round @@ -404,10 +578,13 @@ def initialize SubtractionQuestion, FibonacciQuestion, PowerQuestion, + TemperatureQuestion, AdditionAdditionQuestion, AdditionMultiplicationQuestion, + PressureQuestion, MultiplicationAdditionQuestion, AnagramQuestion, + WindQuestion, ScrabbleQuestion ] end diff --git a/spec/extreme_startup/weather_question_spec.rb b/spec/extreme_startup/weather_question_spec.rb new file mode 100644 index 0000000..7b13f6f --- /dev/null +++ b/spec/extreme_startup/weather_question_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' +require 'extreme_startup/question_factory' +require 'extreme_startup/player' + + +module ExtremeStartup + + Sample_yahoo_response = <<-EOS + + + + Yahoo! Weather - Sunnyvale, CA + http://us.rd.yahoo.com/dailynews/rss/weather/Sunnyvale__CA/*http://weather.yahoo.com/forecast/USCA1116_f.html + Yahoo! Weather for Sunnyvale, CA + en-us + Fri, 18 Dec 2009 9:38 am PST + 60 + + + + + + + Yahoo! Weather + 142 + 18 + http://weather.yahoo.com + http://l.yimg.com/a/i/us/nws/th/main_142b.gif + + + Conditions for Sunnyvale, CA at 9:38 am PST + 37.37 + -122.04 + http://us.rd.yahoo.com/dailynews/rss/weather/Sunnyvale__CA/*http://weather.yahoo.com/forecast/USCA1116_f.html + Fri, 18 Dec 2009 9:38 am PST + +
+Current Conditions:
+Mostly Cloudy, 50 F
+
Forecast:
+Fri - Partly Cloudy. High: 62 Low: 49
+Sat - Partly Cloudy. High: 65 Low: 49
+
+Full Forecast at Yahoo! Weather

+(provided by The Weather Channel)
+]]>
+ + + USCA1116_2009_12_18_9_38_PST +
+
+
+ EOS + + describe TemperatureQuestion do + let(:question) { TemperatureQuestion.new(Player.new) } + + let(:sample_weather_data) { + doc = Nokogiri::XML.parse(Sample_yahoo_response) + YahooWeather::Response.new("12345", "http://some.host/", doc) + } + + it "converts to a string" do + question.as_text.should =~ /what is the current \(Celsius\) temperature in .+\?/i + end + + context "when temperature is known" do + let(:question) { TemperatureQuestion.new(Player.new, Location.new("Tours",sample_weather_data)) } + + it "identifies '50' as correct (exact) answer" do + question.answered_correctly?("50").should be_true + end + + it "identifies '48' as correct (lower approximate) answer" do + question.answered_correctly?("48").should be_true + end + + it "rejects '53' as incorrect (over upper bound approximate) answer" do + question.answered_correctly?("56").should be_false + end + end + + context "when temperature is known and player has answered" do + let(:question) { TemperatureQuestion.new(Player.new, Location.new("Tours",sample_weather_data)) } + + it "credit 50 points for exact answer" do + question.answer = "50" + question.points.should == 50 + end + + it "credit less than 50 points for approximate lower bound correct answer" do + question.answer = "48" + question.points.should < 50 + end + + it "credit less than 50 points for approximate upper bound correct answer" do + question.answer = "53" + question.points.should < 50 + end + + end + end + + describe PressureQuestion do + let(:question) { PressureQuestion.new(Player.new) } + + let(:sample_weather_data) { + doc = Nokogiri::XML.parse(Sample_yahoo_response) + YahooWeather::Response.new("12345", "http://some.host/", doc) + } + + it "converts to a string" do + question.as_text.should =~ /what is the current \(Millibar\) pressure in .+\?/i + end + + context "when pressure is known and player has answered" do + let(:question) { PressureQuestion.new(Player.new, Location.new("Tours",sample_weather_data)) } + + it "credit 50 points for exact answer" do + question.answer = "30.27" + question.points.should == 50 + end + + it "credit less than 50 points for approximate lower bound correct answer" do + question.answer = "28" + question.points.should < 50 + end + + it "credit less than 50 points for approximate upper bound correct answer" do + question.answer = "32" + question.points.should < 50 + end + + end + + end + + describe WindQuestion do + let(:question) { WindQuestion.new(Player.new) } + + let(:sample_weather_data) { + doc = Nokogiri::XML.parse(Sample_yahoo_response) + YahooWeather::Response.new("12345", "http://some.host/", doc) + } + + it "converts to a string" do + question.as_text.should =~ /what is the current \(kph\) speed of wind in .+\?/i + end + + context "when wind speed is known and player has answered" do + let(:question) { WindQuestion.new(Player.new, Location.new("Tours",sample_weather_data)) } + + it "credit 50 points for exact answer" do + question.answer = "10" + question.points.should == 50 + end + + it "credit less than 50 points for approximate upper bound correct answer" do + question.answer = "9.5" + question.points.should < 50 + end + + end + + end + +end \ No newline at end of file From 07b41c846ebfbb6d934a5ff25f7c4df320a62f7f Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 5 Jun 2013 23:02:30 +0200 Subject: [PATCH 07/25] updated .gitignore with IDEA project files --- .gitignore | 2 ++ Gemfile.lock | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 3d747ba..46f7703 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ *.swp .project .buildpath +.idea +*.iml diff --git a/Gemfile.lock b/Gemfile.lock index 18cf14e..2fb1248 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,6 +70,7 @@ GEM PLATFORMS ruby + x86-mingw32 DEPENDENCIES capybara @@ -78,6 +79,7 @@ DEPENDENCIES haml httparty json + launchy rack-test rake rspec From 20f9f142e718d26a12b9671ebd8cf4201d5ebfca Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Sun, 9 Jun 2013 09:42:31 +0200 Subject: [PATCH 08/25] some notes to prepare geocoding questions --- lib/extreme_startup/question_factory.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 8076158..b390d6a 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -563,6 +563,13 @@ def correct_answer end + # Some notes for geolocation questions + # + # * http://www.ncolomer.net/2011/06/use-openstreetmap-web-api/ + # * http://services.gisgraphy.com/public/geocoding_worldwide.html + # * http://www.gisgraphy.com/documentation/user-guide.htm#geocodingservice + # * https://github.com/homelight/ruby-geocoder + class QuestionFactory attr_reader :round From 7d3302595819b47451ff2468582f61ba40164a5b Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Sun, 9 Jun 2013 22:12:12 +0200 Subject: [PATCH 09/25] introduced an event log which records all events in a game * event log is saved to *.log file in current directory * it records all major events in a game: player start/withdraw, questions asked, answers, awarded points * (not implemented) it can be used later on to reload a game at some state or provide game's analysis --- features/support/env.rb | 6 +- lib/extreme_startup/events.rb | 122 ++++++++++++++++++++++++ lib/extreme_startup/question_factory.rb | 6 +- lib/extreme_startup/quiz_master.rb | 7 +- lib/extreme_startup/scoreboard.rb | 7 +- lib/extreme_startup/web_server.rb | 23 ++++- spec/extreme_startup/scoreboard_spec.rb | 63 +++++++++++- 7 files changed, 220 insertions(+), 14 deletions(-) create mode 100644 lib/extreme_startup/events.rb diff --git a/features/support/env.rb b/features/support/env.rb index dafe369..f1c3946 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -17,14 +17,14 @@ def app $silence_logging = true Before do - app.players = Hash.new - app.players_threads = Hash.new - app.scoreboard = ExtremeStartup::Scoreboard.new(false) end After do app.players_threads.each do |uuid, thread| thread.exit end + app.players.clear + app.players_threads.clear + app.scoreboard = ExtremeStartup::Scoreboard.new(false, ExtremeStartup::Events.new($stdout)) app.question_factory = ExtremeStartup::QuestionFactory.new end diff --git a/lib/extreme_startup/events.rb b/lib/extreme_startup/events.rb new file mode 100644 index 0000000..cc6dc54 --- /dev/null +++ b/lib/extreme_startup/events.rb @@ -0,0 +1,122 @@ +require 'uuid' +require 'json' +require 'thread' + +# provide timestamp as milliseconds since epoch +class Time + def to_ms + (self.to_f * 1000.0).to_i + end +end + +module ExtremeStartup + + # An object exposing all the registrable and replayable events for the game as methods + # + # An instance of Events is intended to be used for generating a replayable trace of an ExtremeSTartup session, in + # order to be able to save, pause and restore a game at a known state. + class Events + + class << self + + def generate_uuid + @uuid_generator ||= UUID.new + @uuid_generator.generate.to_s + end + + def timestamp + now = Time.now.to_ms + end + + end + + def initialize(out = nil) + now = Time.now + if out.nil? + @out = File.new("extreme_startup_%d_%d_%d_%d_%d.log" % [now.year, now.month, now.day, now.hour, now.min] , "w+") + else + @out = out + end + @semaphore = Mutex.new + end + + # A player has just registered for the game and has started playing + # + # +player+ :: a hash containing a unique id and timestamp for the event, the name, url and pid of the player + def player_started(player) + output({ + :event => "player_started", + :id => Events.generate_uuid, + :timestamp => Events.timestamp, + :name => player.name, + :url => player.url, + :pid => player.uuid + }) + end + + # A player has just withdrawn from the game + # + # +player+ :: the Player object + # return a hash containing a unique id and timestamp for the event, the name, url and pid of the player + def player_withdraw(player) + output({ + :event => "player_withdraw", + :id => Events.generate_uuid, + :timestamp => Events.timestamp, + :name => player.name, + :url => player.url, + :pid => player.uuid + }) + end + + # Round has been advanced + # + # +question_factory+ :: Question factory object advancing round + # return a hash containing a unique id and timestamp for the event, the name, url and pid of the player + def advance_round(question_factory) + output({ + :event => "advance_round", + :id => Events.generate_uuid, + :timestamp => Events.timestamp, + :round => question_factory.round + }) + end + + # A question has been asked and answered, possibly with an error + # + # +question+ :: Question asked + def question(player, question) + output({ + :event => "question", + :id => Events.generate_uuid, + :timestamp => Events.timestamp, + :playerid => player.uuid, + :question => question.to_s, + :answer => question.answer, + :result => question.result, + :duration => question.duration + }) + end + + def increment_score(player, increment, new_score) + output({ + :event => "score", + :id => Events.generate_uuid, + :timestamp => Events.timestamp, + :playerid => player.uuid, + :increment => increment, + :score => new_score + }) + end + + private + + def output(event) + @semaphore.synchronize { + @out.print("%s\n" % JSON.generate(event)) + @out.fsync + } + end + end + +end \ No newline at end of file diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index b390d6a..0141b61 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -2,7 +2,9 @@ require 'prime' module ExtremeStartup - class Question + class Question + attr_reader :duration + class << self def generate_uuid @uuid_generator ||= UUID.new @@ -23,6 +25,8 @@ def ask(player) rescue => exception puts exception @problem = "no_server_response" + ensure + @duration = Time.now - start end end diff --git a/lib/extreme_startup/quiz_master.rb b/lib/extreme_startup/quiz_master.rb index dfeeb93..8929429 100644 --- a/lib/extreme_startup/quiz_master.rb +++ b/lib/extreme_startup/quiz_master.rb @@ -73,12 +73,13 @@ def update_algorithm_based_on_score(score) end class QuizMaster - def initialize(player, scoreboard, question_factory, game_state) + def initialize(player, scoreboard, question_factory, game_state, events) @player = player @scoreboard = scoreboard @question_factory = question_factory @game_state = game_state @rate_controller = RateController.new + @events = events end def player_passed?(response) @@ -90,7 +91,9 @@ def start if (@game_state.is_running?) question = @question_factory.next_question(@player) question.ask(@player) - puts "For player #{@player}\n#{question.display_result}" + + @events.question(@player, question) + @scoreboard.record_request_for(@player) @scoreboard.increment_score_for(@player, question) @rate_controller.wait_for_next_request(question) diff --git a/lib/extreme_startup/scoreboard.rb b/lib/extreme_startup/scoreboard.rb index 8969cb7..b19ea46 100644 --- a/lib/extreme_startup/scoreboard.rb +++ b/lib/extreme_startup/scoreboard.rb @@ -2,8 +2,9 @@ module ExtremeStartup class Scoreboard attr_reader :scores - def initialize(lenient) + def initialize(lenient, events) @lenient = lenient + @events = events @scores = Hash.new { 0 } @correct_tally = Hash.new { 0 } @incorrect_tally = Hash.new { 0 } @@ -18,7 +19,9 @@ def increment_score_for(player, question) elsif (increment < 0) @incorrect_tally[player.uuid] += 1 end - puts "added #{increment} to player #{player.name}'s score. It is now #{@scores[player.uuid]}" + + @events.increment_score(player,increment, @scores[player.uuid]) + player.log_result(question.id, question.result, increment) end diff --git a/lib/extreme_startup/web_server.rb b/lib/extreme_startup/web_server.rb index e97981c..fb31149 100644 --- a/lib/extreme_startup/web_server.rb +++ b/lib/extreme_startup/web_server.rb @@ -8,6 +8,7 @@ require_relative 'scoreboard' require_relative 'player' require_relative 'quiz_master' +require_relative 'events' Thread.abort_on_exception = true @@ -17,13 +18,16 @@ class WebServer < Sinatra::Base set :port, 3000 set :static, true - set :public, 'public' + set :logging, true + set :events, Events.new + + set :public_dir, 'public' set :players, Hash.new set :players_threads, Hash.new - set :scoreboard, Scoreboard.new(ENV['LENIENT']) + set :scoreboard, Scoreboard.new(ENV['LENIENT'], events) set :question_factory, ENV['WARMUP'] ? WarmupQuestionFactory.new : QuestionFactory.new set :game_state, GameState.new - + get '/' do haml :leaderboard, :locals => { :leaderboard => LeaderBoard.new(scoreboard, players, game_state), @@ -134,6 +138,8 @@ def to_json(*a) post '/advance_round' do question_factory.advance_round + + events.advance_round(question_factory) end post '/pause' do @@ -145,10 +151,14 @@ def to_json(*a) end get %r{/withdraw/([\w]+)} do |uuid| + player = players[uuid] + events.player_withdraw(player) + scoreboard.delete_player(players[uuid]) players.delete(uuid) players_threads[uuid].kill players_threads.delete(uuid) + redirect '/' end @@ -158,8 +168,11 @@ def to_json(*a) players[player.uuid] = player player_thread = Thread.new do - QuizMaster.new(player, scoreboard, question_factory, game_state).start + QuizMaster.new(player, scoreboard, question_factory, game_state, events).start end + + events.player_started(player) + players_threads[player.uuid] = player_thread haml :player_added, :locals => { :playerid => player.uuid } @@ -171,7 +184,7 @@ def local_ip UDPSocket.open {|s| s.connect("64.233.187.99", 1); s.addr.last} end - [:players, :players_threads, :scoreboard, :question_factory, :game_state].each do |setting| + [:players, :players_threads, :scoreboard, :question_factory, :game_state, :events].each do |setting| define_method(setting) do settings.send(setting) end diff --git a/spec/extreme_startup/scoreboard_spec.rb b/spec/extreme_startup/scoreboard_spec.rb index b2fa8b5..986fe7d 100644 --- a/spec/extreme_startup/scoreboard_spec.rb +++ b/spec/extreme_startup/scoreboard_spec.rb @@ -19,5 +19,66 @@ module ExtremeStartup it "it sorts by the number of points" do end end + + it "credits question's points when answer is correct" do + scoreboard.score(StubAnswer.new("correct"), nil).should == 12 + end + + it "scores flat penalty when server responded with error" do + scoreboard.score(StubAnswer.new("error_response"), nil).should == -50 + end + + it "scores flat penalty when no response from server" do + scoreboard.score(StubAnswer.new("no_server_response"), nil).should == -20 + end + + describe "lenient scoreboard" do + let(:scoreboard) { Scoreboard.new(true, nil) } + + it "scores 0 points when no answer provided" do + scoreboard.score(StubAnswer.new("wrong",""), nil).should == 0 + end + + it "scores penalty inversely proportional to ranking when wrong answer provided" do + scoreboard.score(StubAnswer.new("wrong","bar"), 1).should == -12 + scoreboard.score(StubAnswer.new("wrong","bar"), 2).should == -6 + end + end + + describe "strict scoreboard" do + let(:scoreboard) { Scoreboard.new(false, nil) } + + it "scores penalty inversely proportional to ranking when no answer provided" do + scoreboard.score(StubAnswer.new("wrong",""), 1).should == -12 + end + + it "scores penalty inversely proportional to ranking when wrong answer provided" do + scoreboard.score(StubAnswer.new("wrong","bar"), 1).should == -12 + scoreboard.score(StubAnswer.new("wrong","bar"), 2).should == -6 + end + end + + class StubAnswer + attr_reader :result, :answer + + def initialize(result, answer = "foo") + @result = result + @answer = answer + end + def was_answered_correctly + @result == "correct" + end + def was_answered_wrongly + @result == "wrong" + end + def points + 12 + end + def base_points + 12 + end + end + + end -end \ No newline at end of file +end From dc23c0511f8b4a37d5f6a40da310ba1a718598fb Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 11 Jun 2013 23:03:30 +0200 Subject: [PATCH 10/25] ensure correct penalty for questions providing variable number of points --- lib/extreme_startup/question_factory.rb | 14 +++++++++++++- lib/extreme_startup/scoreboard.rb | 2 +- spec/extreme_startup/scoreboard_spec.rb | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 0141b61..a25f554 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -87,6 +87,12 @@ def answered_correctly?(answer) def points 10 end + + # Provides base value of this question disregarding actual answer if defined + # This method defaults to +points+ + def base_points + points + end end class BinaryMathsQuestion < Question @@ -525,10 +531,16 @@ def answered_correctly?(answer) answer.to_f < (correct_answer * (1 + @error_margin)) end + # Score of weather questions follows sigmoid function around the base_points value. + # + # Approximate answers within the error_margin still get exponentially decaying points. def points - 100 / (1 + Math.exp(2 * (correct_answer - answer.to_f).abs)) + ((2 * base_points) / (1 + Math.exp(2 * (correct_answer - answer.to_f).abs))).to_i end + def base_points + 50 + end end class TemperatureQuestion < WeatherQuestion diff --git a/lib/extreme_startup/scoreboard.rb b/lib/extreme_startup/scoreboard.rb index b19ea46..38d64c5 100644 --- a/lib/extreme_startup/scoreboard.rb +++ b/lib/extreme_startup/scoreboard.rb @@ -78,7 +78,7 @@ def allow_passes(question, leaderboard_position) end def penalty(question, leaderboard_position) - -1 * question.points / leaderboard_position + -1 * question.base_points / leaderboard_position end end diff --git a/spec/extreme_startup/scoreboard_spec.rb b/spec/extreme_startup/scoreboard_spec.rb index 986fe7d..e511e27 100644 --- a/spec/extreme_startup/scoreboard_spec.rb +++ b/spec/extreme_startup/scoreboard_spec.rb @@ -3,7 +3,7 @@ module ExtremeStartup describe Scoreboard do - let(:scoreboard) { Scoreboard.new(false) } + let(:scoreboard) { Scoreboard.new(false, nil) } describe "#leaderboard_position" do let(:player_a) { stub(:uuid => 'a') } From 07e764f7e747778aa209ba08a41774ff503cf314 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Sun, 9 Jun 2013 22:12:41 +0200 Subject: [PATCH 11/25] move views following upgrade to dependencies * looks like this change is accidental following a `gem upgrade` --- {views => lib/extreme_startup/views}/add_player.haml | 0 {views => lib/extreme_startup/views}/controlpanel.haml | 0 {views => lib/extreme_startup/views}/leaderboard.haml | 0 {views => lib/extreme_startup/views}/metrics_index.haml | 0 {views => lib/extreme_startup/views}/no_such_player.haml | 0 {views => lib/extreme_startup/views}/personal_page.haml | 0 {views => lib/extreme_startup/views}/player_added.haml | 0 {views => lib/extreme_startup/views}/scores.haml | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {views => lib/extreme_startup/views}/add_player.haml (100%) rename {views => lib/extreme_startup/views}/controlpanel.haml (100%) rename {views => lib/extreme_startup/views}/leaderboard.haml (100%) rename {views => lib/extreme_startup/views}/metrics_index.haml (100%) rename {views => lib/extreme_startup/views}/no_such_player.haml (100%) rename {views => lib/extreme_startup/views}/personal_page.haml (100%) rename {views => lib/extreme_startup/views}/player_added.haml (100%) rename {views => lib/extreme_startup/views}/scores.haml (100%) diff --git a/views/add_player.haml b/lib/extreme_startup/views/add_player.haml similarity index 100% rename from views/add_player.haml rename to lib/extreme_startup/views/add_player.haml diff --git a/views/controlpanel.haml b/lib/extreme_startup/views/controlpanel.haml similarity index 100% rename from views/controlpanel.haml rename to lib/extreme_startup/views/controlpanel.haml diff --git a/views/leaderboard.haml b/lib/extreme_startup/views/leaderboard.haml similarity index 100% rename from views/leaderboard.haml rename to lib/extreme_startup/views/leaderboard.haml diff --git a/views/metrics_index.haml b/lib/extreme_startup/views/metrics_index.haml similarity index 100% rename from views/metrics_index.haml rename to lib/extreme_startup/views/metrics_index.haml diff --git a/views/no_such_player.haml b/lib/extreme_startup/views/no_such_player.haml similarity index 100% rename from views/no_such_player.haml rename to lib/extreme_startup/views/no_such_player.haml diff --git a/views/personal_page.haml b/lib/extreme_startup/views/personal_page.haml similarity index 100% rename from views/personal_page.haml rename to lib/extreme_startup/views/personal_page.haml diff --git a/views/player_added.haml b/lib/extreme_startup/views/player_added.haml similarity index 100% rename from views/player_added.haml rename to lib/extreme_startup/views/player_added.haml diff --git a/views/scores.haml b/lib/extreme_startup/views/scores.haml similarity index 100% rename from views/scores.haml rename to lib/extreme_startup/views/scores.haml From f2b87c754672aca9459d2f878b5fe5e181212034 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 11 Jun 2013 21:46:47 +0200 Subject: [PATCH 12/25] introduced fixed timeout of 10 seconds --- lib/extreme_startup/question_factory.rb | 10 +++++----- lib/extreme_startup/quiz_master.rb | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index a25f554..7549ada 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -12,11 +12,11 @@ def generate_uuid end end - def ask(player) + def ask(player, timeout = 10) url = player.url + '?q=' + URI.escape(self.to_s) - puts "GET: " + url + start = Time.now begin - response = get(url) + response = get(url, timeout) if response.success? then self.answer = response.to_s else @@ -30,8 +30,8 @@ def ask(player) end end - def get(url) - HTTParty.get(url) + def get(url, timeout = 10) + HTTParty.get(url, :timeout => timeout) end def result diff --git a/lib/extreme_startup/quiz_master.rb b/lib/extreme_startup/quiz_master.rb index 8929429..c5cb024 100644 --- a/lib/extreme_startup/quiz_master.rb +++ b/lib/extreme_startup/quiz_master.rb @@ -90,7 +90,8 @@ def start while true if (@game_state.is_running?) question = @question_factory.next_question(@player) - question.ask(@player) + + question.ask(@player, 10) @events.question(@player, question) From 40ebb5b244f62d42ebe39c04e3968abced8efb47 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 12 Jun 2013 10:47:38 +0200 Subject: [PATCH 13/25] replace smoothie with flot for graphing on /graph page --- lib/extreme_startup/views/scores.haml | 4 +- public/css/style.css | 91 +- public/js/jquery.flot.js | 2599 +++++++++++++++++++++++++ public/js/leaderboard.js | 133 +- public/js/smoothie.js | 290 --- 5 files changed, 2747 insertions(+), 370 deletions(-) create mode 100644 public/js/jquery.flot.js delete mode 100644 public/js/smoothie.js diff --git a/lib/extreme_startup/views/scores.haml b/lib/extreme_startup/views/scores.haml index 36898b3..c435c4b 100644 --- a/lib/extreme_startup/views/scores.haml +++ b/lib/extreme_startup/views/scores.haml @@ -2,13 +2,13 @@ %head %title Extreme Startup - Score Graph %link(rel="stylesheet" href="/css/style.css") - %script{:type => "text/javascript", :src => "/js/smoothie.js"} %script{:type => "text/javascript", :src => "/js/jquery-1.6.2.min.js"} + %script{:type => "text/javascript", :src => "/js/jquery.flot.js"} %script{:type => "text/javascript", :src => "/js/leaderboard.js"} %body .left - %canvas{:id => "mycanvas", :width => 500, :height => 500 } + %div{:id => "mycanvas" } .right %h1 Leader Board %ul{:id => "scoreboard" } diff --git a/public/css/style.css b/public/css/style.css index 592f675..5831f5d 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,22 +1,79 @@ -h1 {color:blue} +h1 { + color: blue +} -.score {color:blue} +.score { + color: blue +} -.left { float: left; width: 500px; } -.right{ float: right; width: 500px; } +.left { + float: left; + width: 500px; +} -li { display: block; } +.right { + float: right; + width: 500px; +} -.logline {margin:2px; padding:3px; display:inline-block} -.logline.id {width:100} -.logline.result {width:200} -.logline.points {width:200} -.logline.correct {background-color:#7cfc00} -.logline.pass {background-color:#eed5b7} -.logline.wrong {background-color:#ffaa00} -.logline.no_server_response {background-color:#ff4500} -.logline.error_response {background-color:#ff4500} +li { + display: block; +} -.ranking {margin:2px; padding:3px; display:inline-block; background-color:#eed5b7} -.ranking.name {width:200} -.ranking.points {width:200} +.logline { + margin: 2px; + padding: 3px; + display: inline-block +} + +.logline.id { + width: 100 +} + +.logline.result { + width: 200 +} + +.logline.points { + width: 200 +} + +.logline.correct { + background-color: #7cfc00 +} + +.logline.pass { + background-color: #eed5b7 +} + +.logline.wrong { + background-color: #ffaa00 +} + +.logline.no_server_response { + background-color: #ff4500 +} + +.logline.error_response { + background-color: #ff4500 +} + +.ranking { + margin: 2px; + padding: 3px; + display: inline-block; + background-color: #eed5b7 +} + +.ranking.name { + width: 200 +} + +.ranking.points { + width: 200 +} + +#mycanvas { + width: 650px; + height: 500px; +} \ No newline at end of file diff --git a/public/js/jquery.flot.js b/public/js/jquery.flot.js new file mode 100644 index 0000000..aabc544 --- /dev/null +++ b/public/js/jquery.flot.js @@ -0,0 +1,2599 @@ +/*! Javascript plotting library for jQuery, v. 0.7. + * + * Released under the MIT license by IOLA, December 2007. + * + */ + +// first an inline dependency, jquery.colorhelpers.js, we inline it here +// for convenience + +/* Plugin for jQuery for working with colors. + * + * Version 1.1. + * + * Inspiration from jQuery color animation plugin by John Resig. + * + * Released under the MIT license by Ole Laursen, October 2009. + * + * Examples: + * + * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString() + * var c = $.color.extract($("#mydiv"), 'background-color'); + * console.log(c.r, c.g, c.b, c.a); + * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)" + * + * Note that .scale() and .add() return the same modified object + * instead of making a new one. + * + * V. 1.1: Fix error handling so e.g. parsing an empty string does + * produce a color rather than just crashing. + */ +(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return KI?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery); + +// the actual Flot code +(function($) { + function Plot(placeholder, data_, options_, plugins) { + // data is on the form: + // [ series1, series2 ... ] + // where series is either just the data as [ [x1, y1], [x2, y2], ... ] + // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... } + + var series = [], + options = { + // the color theme used for graphs + colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"], + legend: { + show: true, + noColumns: 1, // number of colums in legend table + labelFormatter: null, // fn: string -> string + labelBoxBorderColor: "#ccc", // border color for the little label boxes + container: null, // container (as jQuery object) to put legend in, null means default on top of graph + position: "ne", // position of default legend container within plot + margin: 5, // distance from grid edge to default legend container within plot + backgroundColor: null, // null means auto-detect + backgroundOpacity: 0.85 // set to 0 to avoid background + }, + xaxis: { + show: null, // null = auto-detect, true = always, false = never + position: "bottom", // or "top" + mode: null, // null or "time" + color: null, // base color, labels, ticks + tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" + transform: null, // null or f: number -> number to transform axis + inverseTransform: null, // if transform is set, this should be the inverse function + min: null, // min. value to show, null means set automatically + max: null, // max. value to show, null means set automatically + autoscaleMargin: null, // margin in % to add if auto-setting min/max + ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks + tickFormatter: null, // fn: number -> string + labelWidth: null, // size of tick labels in pixels + labelHeight: null, + reserveSpace: null, // whether to reserve space even if axis isn't shown + tickLength: null, // size in pixels of ticks, or "full" for whole line + alignTicksWithAxis: null, // axis number or null for no sync + + // mode specific options + tickDecimals: null, // no. of decimals, null means auto + tickSize: null, // number or [number, "unit"] + minTickSize: null, // number or [number, "unit"] + monthNames: null, // list of names of months + timeformat: null, // format string to use + twelveHourClock: false // 12 or 24 time in time mode + }, + yaxis: { + autoscaleMargin: 0.02, + position: "left" // or "right" + }, + xaxes: [], + yaxes: [], + series: { + points: { + show: false, + radius: 3, + lineWidth: 2, // in pixels + fill: true, + fillColor: "#ffffff", + symbol: "circle" // or callback + }, + lines: { + // we don't put in show: false so we can see + // whether lines were actively disabled + lineWidth: 2, // in pixels + fill: false, + fillColor: null, + steps: false + }, + bars: { + show: false, + lineWidth: 2, // in pixels + barWidth: 1, // in units of the x axis + fill: true, + fillColor: null, + align: "left", // or "center" + horizontal: false + }, + shadowSize: 3 + }, + grid: { + show: true, + aboveData: false, + color: "#545454", // primary color used for outline and labels + backgroundColor: null, // null for transparent, else color + borderColor: null, // set if different from the grid color + tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)" + labelMargin: 5, // in pixels + axisMargin: 8, // in pixels + borderWidth: 2, // in pixels + minBorderMargin: null, // in pixels, null means taken from points radius + markings: null, // array of ranges or fn: axes -> array of ranges + markingsColor: "#f4f4f4", + markingsLineWidth: 2, + // interactive stuff + clickable: false, + hoverable: false, + autoHighlight: true, // highlight in case mouse is near + mouseActiveRadius: 10 // how far the mouse can be away to activate an item + }, + hooks: {} + }, + canvas = null, // the canvas for the plot itself + overlay = null, // canvas for interactive stuff on top of plot + eventHolder = null, // jQuery object that events should be bound to + ctx = null, octx = null, + xaxes = [], yaxes = [], + plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, + canvasWidth = 0, canvasHeight = 0, + plotWidth = 0, plotHeight = 0, + hooks = { + processOptions: [], + processRawData: [], + processDatapoints: [], + drawSeries: [], + draw: [], + bindEvents: [], + drawOverlay: [], + shutdown: [] + }, + plot = this; + + // public functions + plot.setData = setData; + plot.setupGrid = setupGrid; + plot.draw = draw; + plot.getPlaceholder = function() { return placeholder; }; + plot.getCanvas = function() { return canvas; }; + plot.getPlotOffset = function() { return plotOffset; }; + plot.width = function () { return plotWidth; }; + plot.height = function () { return plotHeight; }; + plot.offset = function () { + var o = eventHolder.offset(); + o.left += plotOffset.left; + o.top += plotOffset.top; + return o; + }; + plot.getData = function () { return series; }; + plot.getAxes = function () { + var res = {}, i; + $.each(xaxes.concat(yaxes), function (_, axis) { + if (axis) + res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis; + }); + return res; + }; + plot.getXAxes = function () { return xaxes; }; + plot.getYAxes = function () { return yaxes; }; + plot.c2p = canvasToAxisCoords; + plot.p2c = axisToCanvasCoords; + plot.getOptions = function () { return options; }; + plot.highlight = highlight; + plot.unhighlight = unhighlight; + plot.triggerRedrawOverlay = triggerRedrawOverlay; + plot.pointOffset = function(point) { + return { + left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left), + top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top) + }; + }; + plot.shutdown = shutdown; + plot.resize = function () { + getCanvasDimensions(); + resizeCanvas(canvas); + resizeCanvas(overlay); + }; + + // public attributes + plot.hooks = hooks; + + // initialize + initPlugins(plot); + parseOptions(options_); + setupCanvases(); + setData(data_); + setupGrid(); + draw(); + bindEvents(); + + + function executeHooks(hook, args) { + args = [plot].concat(args); + for (var i = 0; i < hook.length; ++i) + hook[i].apply(this, args); + } + + function initPlugins() { + for (var i = 0; i < plugins.length; ++i) { + var p = plugins[i]; + p.init(plot); + if (p.options) + $.extend(true, options, p.options); + } + } + + function parseOptions(opts) { + var i; + + $.extend(true, options, opts); + + if (options.xaxis.color == null) + options.xaxis.color = options.grid.color; + if (options.yaxis.color == null) + options.yaxis.color = options.grid.color; + + if (options.xaxis.tickColor == null) // backwards-compatibility + options.xaxis.tickColor = options.grid.tickColor; + if (options.yaxis.tickColor == null) // backwards-compatibility + options.yaxis.tickColor = options.grid.tickColor; + + if (options.grid.borderColor == null) + options.grid.borderColor = options.grid.color; + if (options.grid.tickColor == null) + options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + // fill in defaults in axes, copy at least always the + // first as the rest of the code assumes it'll be there + for (i = 0; i < Math.max(1, options.xaxes.length); ++i) + options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]); + for (i = 0; i < Math.max(1, options.yaxes.length); ++i) + options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]); + + // backwards compatibility, to be removed in future + if (options.xaxis.noTicks && options.xaxis.ticks == null) + options.xaxis.ticks = options.xaxis.noTicks; + if (options.yaxis.noTicks && options.yaxis.ticks == null) + options.yaxis.ticks = options.yaxis.noTicks; + if (options.x2axis) { + options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis); + options.xaxes[1].position = "top"; + } + if (options.y2axis) { + options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis); + options.yaxes[1].position = "right"; + } + if (options.grid.coloredAreas) + options.grid.markings = options.grid.coloredAreas; + if (options.grid.coloredAreasColor) + options.grid.markingsColor = options.grid.coloredAreasColor; + if (options.lines) + $.extend(true, options.series.lines, options.lines); + if (options.points) + $.extend(true, options.series.points, options.points); + if (options.bars) + $.extend(true, options.series.bars, options.bars); + if (options.shadowSize != null) + options.series.shadowSize = options.shadowSize; + + // save options on axes for future reference + for (i = 0; i < options.xaxes.length; ++i) + getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i]; + for (i = 0; i < options.yaxes.length; ++i) + getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i]; + + // add hooks from options + for (var n in hooks) + if (options.hooks[n] && options.hooks[n].length) + hooks[n] = hooks[n].concat(options.hooks[n]); + + executeHooks(hooks.processOptions, [options]); + } + + function setData(d) { + series = parseData(d); + fillInSeriesOptions(); + processData(); + } + + function parseData(d) { + var res = []; + for (var i = 0; i < d.length; ++i) { + var s = $.extend(true, {}, options.series); + + if (d[i].data != null) { + s.data = d[i].data; // move the data instead of deep-copy + delete d[i].data; + + $.extend(true, s, d[i]); + + d[i].data = s.data; + } + else + s.data = d[i]; + res.push(s); + } + + return res; + } + + function axisNumber(obj, coord) { + var a = obj[coord + "axis"]; + if (typeof a == "object") // if we got a real axis, extract number + a = a.n; + if (typeof a != "number") + a = 1; // default to first axis + return a; + } + + function allAxes() { + // return flat array without annoying null entries + return $.grep(xaxes.concat(yaxes), function (a) { return a; }); + } + + function canvasToAxisCoords(pos) { + // return an object with x/y corresponding to all used axes + var res = {}, i, axis; + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) + res["x" + axis.n] = axis.c2p(pos.left); + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) + res["y" + axis.n] = axis.c2p(pos.top); + } + + if (res.x1 !== undefined) + res.x = res.x1; + if (res.y1 !== undefined) + res.y = res.y1; + + return res; + } + + function axisToCanvasCoords(pos) { + // get canvas coords from the first pair of x/y found in pos + var res = {}, i, axis, key; + + for (i = 0; i < xaxes.length; ++i) { + axis = xaxes[i]; + if (axis && axis.used) { + key = "x" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "x"; + + if (pos[key] != null) { + res.left = axis.p2c(pos[key]); + break; + } + } + } + + for (i = 0; i < yaxes.length; ++i) { + axis = yaxes[i]; + if (axis && axis.used) { + key = "y" + axis.n; + if (pos[key] == null && axis.n == 1) + key = "y"; + + if (pos[key] != null) { + res.top = axis.p2c(pos[key]); + break; + } + } + } + + return res; + } + + function getOrCreateAxis(axes, number) { + if (!axes[number - 1]) + axes[number - 1] = { + n: number, // save the number for future reference + direction: axes == xaxes ? "x" : "y", + options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis) + }; + + return axes[number - 1]; + } + + function fillInSeriesOptions() { + var i; + + // collect what we already got of colors + var neededColors = series.length, + usedColors = [], + assignedColors = []; + for (i = 0; i < series.length; ++i) { + var sc = series[i].color; + if (sc != null) { + --neededColors; + if (typeof sc == "number") + assignedColors.push(sc); + else + usedColors.push($.color.parse(series[i].color)); + } + } + + // we might need to generate more colors if higher indices + // are assigned + for (i = 0; i < assignedColors.length; ++i) { + neededColors = Math.max(neededColors, assignedColors[i] + 1); + } + + // produce colors as needed + var colors = [], variation = 0; + i = 0; + while (colors.length < neededColors) { + var c; + if (options.colors.length == i) // check degenerate case + c = $.color.make(100, 100, 100); + else + c = $.color.parse(options.colors[i]); + + // vary color if needed + var sign = variation % 2 == 1 ? -1 : 1; + c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2) + + // FIXME: if we're getting to close to something else, + // we should probably skip this one + colors.push(c); + + ++i; + if (i >= options.colors.length) { + i = 0; + ++variation; + } + } + + // fill in the options + var colori = 0, s; + for (i = 0; i < series.length; ++i) { + s = series[i]; + + // assign colors + if (s.color == null) { + s.color = colors[colori].toString(); + ++colori; + } + else if (typeof s.color == "number") + s.color = colors[s.color].toString(); + + // turn on lines automatically in case nothing is set + if (s.lines.show == null) { + var v, show = true; + for (v in s) + if (s[v] && s[v].show) { + show = false; + break; + } + if (show) + s.lines.show = true; + } + + // setup axes + s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); + s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); + } + } + + function processData() { + var topSentry = Number.POSITIVE_INFINITY, + bottomSentry = Number.NEGATIVE_INFINITY, + fakeInfinity = Number.MAX_VALUE, + i, j, k, m, length, + s, points, ps, x, y, axis, val, f, p; + + function updateAxis(axis, min, max) { + if (min < axis.datamin && min != -fakeInfinity) + axis.datamin = min; + if (max > axis.datamax && max != fakeInfinity) + axis.datamax = max; + } + + $.each(allAxes(), function (_, axis) { + // init axis + axis.datamin = topSentry; + axis.datamax = bottomSentry; + axis.used = false; + }); + + for (i = 0; i < series.length; ++i) { + s = series[i]; + s.datapoints = { points: [] }; + + executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]); + } + + // first pass: clean and copy data + for (i = 0; i < series.length; ++i) { + s = series[i]; + + var data = s.data, format = s.datapoints.format; + + if (!format) { + format = []; + // find out how to copy + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + format.push({ y: true, number: true, required: false, defaultValue: 0 }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + s.datapoints.format = format; + } + + if (s.datapoints.pointsize != null) + continue; // already filled in + + s.datapoints.pointsize = format.length; + + ps = s.datapoints.pointsize; + points = s.datapoints.points; + + insertSteps = s.lines.show && s.lines.steps; + s.xaxis.used = s.yaxis.used = true; + + for (j = k = 0; j < data.length; ++j, k += ps) { + p = data[j]; + + var nullify = p == null; + if (!nullify) { + for (m = 0; m < ps; ++m) { + val = p[m]; + f = format[m]; + + if (f) { + if (f.number && val != null) { + val = +val; // convert to number + if (isNaN(val)) + val = null; + else if (val == Infinity) + val = fakeInfinity; + else if (val == -Infinity) + val = -fakeInfinity; + } + + if (val == null) { + if (f.required) + nullify = true; + + if (f.defaultValue != null) + val = f.defaultValue; + } + } + + points[k + m] = val; + } + } + + if (nullify) { + for (m = 0; m < ps; ++m) { + val = points[k + m]; + if (val != null) { + f = format[m]; + // extract min/max info + if (f.x) + updateAxis(s.xaxis, val, val); + if (f.y) + updateAxis(s.yaxis, val, val); + } + points[k + m] = null; + } + } + else { + // a little bit of line specific stuff that + // perhaps shouldn't be here, but lacking + // better means... + if (insertSteps && k > 0 + && points[k - ps] != null + && points[k - ps] != points[k] + && points[k - ps + 1] != points[k + 1]) { + // copy the point to make room for a middle point + for (m = 0; m < ps; ++m) + points[k + ps + m] = points[k + m]; + + // middle point has same y + points[k + 1] = points[k - ps + 1]; + + // we've added a point, better reflect that + k += ps; + } + } + } + } + + // give the hooks a chance to run + for (i = 0; i < series.length; ++i) { + s = series[i]; + + executeHooks(hooks.processDatapoints, [ s, s.datapoints]); + } + + // second pass: find datamax/datamin for auto-scaling + for (i = 0; i < series.length; ++i) { + s = series[i]; + points = s.datapoints.points, + ps = s.datapoints.pointsize; + + var xmin = topSentry, ymin = topSentry, + xmax = bottomSentry, ymax = bottomSentry; + + for (j = 0; j < points.length; j += ps) { + if (points[j] == null) + continue; + + for (m = 0; m < ps; ++m) { + val = points[j + m]; + f = format[m]; + if (!f || val == fakeInfinity || val == -fakeInfinity) + continue; + + if (f.x) { + if (val < xmin) + xmin = val; + if (val > xmax) + xmax = val; + } + if (f.y) { + if (val < ymin) + ymin = val; + if (val > ymax) + ymax = val; + } + } + } + + if (s.bars.show) { + // make sure we got room for the bar on the dancing floor + var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2; + if (s.bars.horizontal) { + ymin += delta; + ymax += delta + s.bars.barWidth; + } + else { + xmin += delta; + xmax += delta + s.bars.barWidth; + } + } + + updateAxis(s.xaxis, xmin, xmax); + updateAxis(s.yaxis, ymin, ymax); + } + + $.each(allAxes(), function (_, axis) { + if (axis.datamin == topSentry) + axis.datamin = null; + if (axis.datamax == bottomSentry) + axis.datamax = null; + }); + } + + function makeCanvas(skipPositioning, cls) { + var c = document.createElement('canvas'); + c.className = cls; + c.width = canvasWidth; + c.height = canvasHeight; + + if (!skipPositioning) + $(c).css({ position: 'absolute', left: 0, top: 0 }); + + $(c).appendTo(placeholder); + + if (!c.getContext) // excanvas hack + c = window.G_vmlCanvasManager.initElement(c); + + // used for resetting in case we get replotted + c.getContext("2d").save(); + + return c; + } + + function getCanvasDimensions() { + canvasWidth = placeholder.width(); + canvasHeight = placeholder.height(); + + if (canvasWidth <= 0 || canvasHeight <= 0) + throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight; + } + + function resizeCanvas(c) { + // resizing should reset the state (excanvas seems to be + // buggy though) + if (c.width != canvasWidth) + c.width = canvasWidth; + + if (c.height != canvasHeight) + c.height = canvasHeight; + + // so try to get back to the initial state (even if it's + // gone now, this should be safe according to the spec) + var cctx = c.getContext("2d"); + cctx.restore(); + + // and save again + cctx.save(); + } + + function setupCanvases() { + var reused, + existingCanvas = placeholder.children("canvas.base"), + existingOverlay = placeholder.children("canvas.overlay"); + + if (existingCanvas.length == 0 || existingOverlay == 0) { + // init everything + + placeholder.html(""); // make sure placeholder is clear + + placeholder.css({ padding: 0 }); // padding messes up the positioning + + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay + + getCanvasDimensions(); + + canvas = makeCanvas(true, "base"); + overlay = makeCanvas(false, "overlay"); // overlay canvas for interactive features + + reused = false; + } + else { + // reuse existing elements + + canvas = existingCanvas.get(0); + overlay = existingOverlay.get(0); + + reused = true; + } + + ctx = canvas.getContext("2d"); + octx = overlay.getContext("2d"); + + // we include the canvas in the event holder too, because IE 7 + // sometimes has trouble with the stacking order + eventHolder = $([overlay, canvas]); + + if (reused) { + // run shutdown in the old plot object + placeholder.data("plot").shutdown(); + + // reset reused canvases + plot.resize(); + + // make sure overlay pixels are cleared (canvas is cleared when we redraw) + octx.clearRect(0, 0, canvasWidth, canvasHeight); + + // then whack any remaining obvious garbage left + eventHolder.unbind(); + placeholder.children().not([canvas, overlay]).remove(); + } + + // save in case we get replotted + placeholder.data("plot", plot); + } + + function bindEvents() { + // bind events + if (options.grid.hoverable) { + eventHolder.mousemove(onMouseMove); + eventHolder.mouseleave(onMouseLeave); + } + + if (options.grid.clickable) + eventHolder.click(onClick); + + executeHooks(hooks.bindEvents, [eventHolder]); + } + + function shutdown() { + if (redrawTimeout) + clearTimeout(redrawTimeout); + + eventHolder.unbind("mousemove", onMouseMove); + eventHolder.unbind("mouseleave", onMouseLeave); + eventHolder.unbind("click", onClick); + + executeHooks(hooks.shutdown, [eventHolder]); + } + + function setTransformationHelpers(axis) { + // set helper functions on the axis, assumes plot area + // has been computed already + + function identity(x) { return x; } + + var s, m, t = axis.options.transform || identity, + it = axis.options.inverseTransform; + + // precompute how much the axis is scaling a point + // in canvas space + if (axis.direction == "x") { + s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min)); + m = Math.min(t(axis.max), t(axis.min)); + } + else { + s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min)); + s = -s; + m = Math.max(t(axis.max), t(axis.min)); + } + + // data point to canvas coordinate + if (t == identity) // slight optimization + axis.p2c = function (p) { return (p - m) * s; }; + else + axis.p2c = function (p) { return (t(p) - m) * s; }; + // canvas coordinate to data point + if (!it) + axis.c2p = function (c) { return m + c / s; }; + else + axis.c2p = function (c) { return it(m + c / s); }; + } + + function measureTickLabels(axis) { + var opts = axis.options, i, ticks = axis.ticks || [], labels = [], + l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv; + + function makeDummyDiv(labels, width) { + return $('
' + + '
' + + labels.join("") + '
') + .appendTo(placeholder); + } + + if (axis.direction == "x") { + // to avoid measuring the widths of the labels (it's slow), we + // construct fixed-size boxes and put the labels inside + // them, we don't need the exact figures and the + // fixed-size box content is easy to center + if (w == null) + w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1)); + + // measure x label heights + if (h == null) { + labels = []; + for (i = 0; i < ticks.length; ++i) { + l = ticks[i].label; + if (l) + labels.push('
' + l + '
'); + } + + if (labels.length > 0) { + // stick them all in the same div and measure + // collective height + labels.push('
'); + dummyDiv = makeDummyDiv(labels, "width:10000px;"); + h = dummyDiv.height(); + dummyDiv.remove(); + } + } + } + else if (w == null || h == null) { + // calculate y label dimensions + for (i = 0; i < ticks.length; ++i) { + l = ticks[i].label; + if (l) + labels.push('
' + l + '
'); + } + + if (labels.length > 0) { + dummyDiv = makeDummyDiv(labels, ""); + if (w == null) + w = dummyDiv.children().width(); + if (h == null) + h = dummyDiv.find("div.tickLabel").height(); + dummyDiv.remove(); + } + } + + if (w == null) + w = 0; + if (h == null) + h = 0; + + axis.labelWidth = w; + axis.labelHeight = h; + } + + function allocateAxisBoxFirstPhase(axis) { + // find the bounding box of the axis by looking at label + // widths/heights and ticks, make room by diminishing the + // plotOffset + + var lw = axis.labelWidth, + lh = axis.labelHeight, + pos = axis.options.position, + tickLength = axis.options.tickLength, + axismargin = options.grid.axisMargin, + padding = options.grid.labelMargin, + all = axis.direction == "x" ? xaxes : yaxes, + index; + + // determine axis margin + var samePosition = $.grep(all, function (a) { + return a && a.options.position == pos && a.reserveSpace; + }); + if ($.inArray(axis, samePosition) == samePosition.length - 1) + axismargin = 0; // outermost + + // determine tick length - if we're innermost, we can use "full" + if (tickLength == null) + tickLength = "full"; + + var sameDirection = $.grep(all, function (a) { + return a && a.reserveSpace; + }); + + var innermost = $.inArray(axis, sameDirection) == 0; + if (!innermost && tickLength == "full") + tickLength = 5; + + if (!isNaN(+tickLength)) + padding += +tickLength; + + // compute box + if (axis.direction == "x") { + lh += padding; + + if (pos == "bottom") { + plotOffset.bottom += lh + axismargin; + axis.box = { top: canvasHeight - plotOffset.bottom, height: lh }; + } + else { + axis.box = { top: plotOffset.top + axismargin, height: lh }; + plotOffset.top += lh + axismargin; + } + } + else { + lw += padding; + + if (pos == "left") { + axis.box = { left: plotOffset.left + axismargin, width: lw }; + plotOffset.left += lw + axismargin; + } + else { + plotOffset.right += lw + axismargin; + axis.box = { left: canvasWidth - plotOffset.right, width: lw }; + } + } + + // save for future reference + axis.position = pos; + axis.tickLength = tickLength; + axis.box.padding = padding; + axis.innermost = innermost; + } + + function allocateAxisBoxSecondPhase(axis) { + // set remaining bounding box coordinates + if (axis.direction == "x") { + axis.box.left = plotOffset.left; + axis.box.width = plotWidth; + } + else { + axis.box.top = plotOffset.top; + axis.box.height = plotHeight; + } + } + + function setupGrid() { + var i, axes = allAxes(); + + // first calculate the plot and axis box dimensions + + $.each(axes, function (_, axis) { + axis.show = axis.options.show; + if (axis.show == null) + axis.show = axis.used; // by default an axis is visible if it's got data + + axis.reserveSpace = axis.show || axis.options.reserveSpace; + + setRange(axis); + }); + + allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); + + plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0; + if (options.grid.show) { + $.each(allocatedAxes, function (_, axis) { + // make the ticks + setupTickGeneration(axis); + setTicks(axis); + snapRangeToTicks(axis, axis.ticks); + + // find labelWidth/Height for axis + measureTickLabels(axis); + }); + + // with all dimensions in house, we can compute the + // axis boxes, start from the outside (reverse order) + for (i = allocatedAxes.length - 1; i >= 0; --i) + allocateAxisBoxFirstPhase(allocatedAxes[i]); + + // make sure we've got enough space for things that + // might stick out + var minMargin = options.grid.minBorderMargin; + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2); + } + + for (var a in plotOffset) { + plotOffset[a] += options.grid.borderWidth; + plotOffset[a] = Math.max(minMargin, plotOffset[a]); + } + } + + plotWidth = canvasWidth - plotOffset.left - plotOffset.right; + plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; + + // now we got the proper plotWidth/Height, we can compute the scaling + $.each(axes, function (_, axis) { + setTransformationHelpers(axis); + }); + + if (options.grid.show) { + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); + + insertAxisLabels(); + } + + insertLegend(); + } + + function setRange(axis) { + var opts = axis.options, + min = +(opts.min != null ? opts.min : axis.datamin), + max = +(opts.max != null ? opts.max : axis.datamax), + delta = max - min; + + if (delta == 0.0) { + // degenerate case + var widen = max == 0 ? 1 : 0.01; + + if (opts.min == null) + min -= widen; + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (opts.max == null || opts.min != null) + max += widen; + } + else { + // consider autoscaling + var margin = opts.autoscaleMargin; + if (margin != null) { + if (opts.min == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && axis.datamin != null && axis.datamin >= 0) + min = 0; + } + if (opts.max == null) { + max += delta * margin; + if (max > 0 && axis.datamax != null && axis.datamax <= 0) + max = 0; + } + } + } + axis.min = min; + axis.max = max; + } + + function setupTickGeneration(axis) { + var opts = axis.options; + + // estimate number of ticks + var noTicks; + if (typeof opts.ticks == "number" && opts.ticks > 0) + noTicks = opts.ticks; + else + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight); + + var delta = (axis.max - axis.min) / noTicks, + size, generator, unit, formatter, i, magn, norm; + + if (opts.mode == "time") { + // pretty handling of time + + // map of app. size of time units in milliseconds + var timeUnitSize = { + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + var spec = [ + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"], [3, "month"], [6, "month"], + [1, "year"] + ]; + + var minSize = 0; + if (opts.minTickSize != null) { + if (typeof opts.tickSize == "number") + minSize = opts.tickSize; + else + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; + } + + for (var i = 0; i < spec.length - 1; ++i) + if (delta < (spec[i][0] * timeUnitSize[spec[i][1]] + + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 + && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) + break; + size = spec[i][0]; + unit = spec[i][1]; + + // special-case the possibility of several years + if (unit == "year") { + magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10)); + norm = (delta / timeUnitSize.year) / magn; + if (norm < 1.5) + size = 1; + else if (norm < 3) + size = 2; + else if (norm < 7.5) + size = 5; + else + size = 10; + + size *= magn; + } + + axis.tickSize = opts.tickSize || [size, unit]; + + generator = function(axis) { + var ticks = [], + tickSize = axis.tickSize[0], unit = axis.tickSize[1], + d = new Date(axis.min); + + var step = tickSize * timeUnitSize[unit]; + + if (unit == "second") + d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize)); + if (unit == "minute") + d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize)); + if (unit == "hour") + d.setUTCHours(floorInBase(d.getUTCHours(), tickSize)); + if (unit == "month") + d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize)); + if (unit == "year") + d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize)); + + // reset smaller components + d.setUTCMilliseconds(0); + if (step >= timeUnitSize.minute) + d.setUTCSeconds(0); + if (step >= timeUnitSize.hour) + d.setUTCMinutes(0); + if (step >= timeUnitSize.day) + d.setUTCHours(0); + if (step >= timeUnitSize.day * 4) + d.setUTCDate(1); + if (step >= timeUnitSize.year) + d.setUTCMonth(0); + + + var carry = 0, v = Number.NaN, prev; + do { + prev = v; + v = d.getTime(); + ticks.push(v); + if (unit == "month") { + if (tickSize < 1) { + // a bit complicated - we'll divide the month + // up but we need to take care of fractions + // so we don't end up in the middle of a day + d.setUTCDate(1); + var start = d.getTime(); + d.setUTCMonth(d.getUTCMonth() + 1); + var end = d.getTime(); + d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); + carry = d.getUTCHours(); + d.setUTCHours(0); + } + else + d.setUTCMonth(d.getUTCMonth() + tickSize); + } + else if (unit == "year") { + d.setUTCFullYear(d.getUTCFullYear() + tickSize); + } + else + d.setTime(v + step); + } while (v < axis.max && v != prev); + + return ticks; + }; + + formatter = function (v, axis) { + var d = new Date(v); + + // first check global format + if (opts.timeformat != null) + return $.plot.formatDate(d, opts.timeformat, opts.monthNames); + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + var suffix = (opts.twelveHourClock) ? " %p" : ""; + + if (t < timeUnitSize.minute) + fmt = "%h:%M:%S" + suffix; + else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) + fmt = "%h:%M" + suffix; + else + fmt = "%b %d %h:%M" + suffix; + } + else if (t < timeUnitSize.month) + fmt = "%b %d"; + else if (t < timeUnitSize.year) { + if (span < timeUnitSize.year) + fmt = "%b"; + else + fmt = "%b %y"; + } + else + fmt = "%y"; + + return $.plot.formatDate(d, fmt, opts.monthNames); + }; + } + else { + // pretty rounding of base-10 numbers + var maxDec = opts.tickDecimals; + var dec = -Math.floor(Math.log(delta) / Math.LN10); + if (maxDec != null && dec > maxDec) + dec = maxDec; + + magn = Math.pow(10, -dec); + norm = delta / magn; // norm is between 1.0 and 10.0 + + if (norm < 1.5) + size = 1; + else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } + else if (norm < 7.5) + size = 5; + else + size = 10; + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) + size = opts.minTickSize; + + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; + + generator = function (axis) { + var ticks = []; + + // spew out all possible ticks + var start = floorInBase(axis.min, axis.tickSize), + i = 0, v = Number.NaN, prev; + do { + prev = v; + v = start + i * axis.tickSize; + ticks.push(v); + ++i; + } while (v < axis.max && v != prev); + return ticks; + }; + + formatter = function (v, axis) { + return v.toFixed(axis.tickDecimals); + }; + } + + if (opts.alignTicksWithAxis != null) { + var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1]; + if (otherAxis && otherAxis.used && otherAxis != axis) { + // consider snapping min/max to outermost nice ticks + var niceTicks = generator(axis); + if (niceTicks.length > 0) { + if (opts.min == null) + axis.min = Math.min(axis.min, niceTicks[0]); + if (opts.max == null && niceTicks.length > 1) + axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]); + } + + generator = function (axis) { + // copy ticks, scaled to this axis + var ticks = [], v, i; + for (i = 0; i < otherAxis.ticks.length; ++i) { + v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min); + v = axis.min + v * (axis.max - axis.min); + ticks.push(v); + } + return ticks; + }; + + // we might need an extra decimal since forced + // ticks don't necessarily fit naturally + if (axis.mode != "time" && opts.tickDecimals == null) { + var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1), + ts = generator(axis); + + // only proceed if the tick interval rounded + // with an extra decimal doesn't give us a + // zero at end + if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec)))) + axis.tickDecimals = extraDec; + } + } + } + + axis.tickGenerator = generator; + if ($.isFunction(opts.tickFormatter)) + axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); }; + else + axis.tickFormatter = formatter; + } + + function setTicks(axis) { + var oticks = axis.options.ticks, ticks = []; + if (oticks == null || (typeof oticks == "number" && oticks > 0)) + ticks = axis.tickGenerator(axis); + else if (oticks) { + if ($.isFunction(oticks)) + // generate the ticks + ticks = oticks({ min: axis.min, max: axis.max }); + else + ticks = oticks; + } + + // clean up/labelify the supplied ticks, copy them over + var i, v; + axis.ticks = []; + for (i = 0; i < ticks.length; ++i) { + var label = null; + var t = ticks[i]; + if (typeof t == "object") { + v = +t[0]; + if (t.length > 1) + label = t[1]; + } + else + v = +t; + if (label == null) + label = axis.tickFormatter(v, axis); + if (!isNaN(v)) + axis.ticks.push({ v: v, label: label }); + } + } + + function snapRangeToTicks(axis, ticks) { + if (axis.options.autoscaleMargin && ticks.length > 0) { + // snap to ticks + if (axis.options.min == null) + axis.min = Math.min(axis.min, ticks[0].v); + if (axis.options.max == null && ticks.length > 1) + axis.max = Math.max(axis.max, ticks[ticks.length - 1].v); + } + } + + function draw() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + var grid = options.grid; + + // draw background, if any + if (grid.show && grid.backgroundColor) + drawBackground(); + + if (grid.show && !grid.aboveData) + drawGrid(); + + for (var i = 0; i < series.length; ++i) { + executeHooks(hooks.drawSeries, [ctx, series[i]]); + drawSeries(series[i]); + } + + executeHooks(hooks.draw, [ctx]); + + if (grid.show && grid.aboveData) + drawGrid(); + } + + function extractRange(ranges, coord) { + var axis, from, to, key, axes = allAxes(); + + for (i = 0; i < axes.length; ++i) { + axis = axes[i]; + if (axis.direction == coord) { + key = coord + axis.n + "axis"; + if (!ranges[key] && axis.n == 1) + key = coord + "axis"; // support x1axis as xaxis + if (ranges[key]) { + from = ranges[key].from; + to = ranges[key].to; + break; + } + } + } + + // backwards-compat stuff - to be removed in future + if (!ranges[key]) { + axis = coord == "x" ? xaxes[0] : yaxes[0]; + from = ranges[coord + "1"]; + to = ranges[coord + "2"]; + } + + // auto-reverse as an added bonus + if (from != null && to != null && from > to) { + var tmp = from; + from = to; + to = tmp; + } + + return { from: from, to: to, axis: axis }; + } + + function drawBackground() { + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)"); + ctx.fillRect(0, 0, plotWidth, plotHeight); + ctx.restore(); + } + + function drawGrid() { + var i; + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // draw markings + var markings = options.grid.markings; + if (markings) { + if ($.isFunction(markings)) { + var axes = plot.getAxes(); + // xmin etc. is backwards compatibility, to be + // removed in the future + axes.xmin = axes.xaxis.min; + axes.xmax = axes.xaxis.max; + axes.ymin = axes.yaxis.min; + axes.ymax = axes.yaxis.max; + + markings = markings(axes); + } + + for (i = 0; i < markings.length; ++i) { + var m = markings[i], + xrange = extractRange(m, "x"), + yrange = extractRange(m, "y"); + + // fill in missing + if (xrange.from == null) + xrange.from = xrange.axis.min; + if (xrange.to == null) + xrange.to = xrange.axis.max; + if (yrange.from == null) + yrange.from = yrange.axis.min; + if (yrange.to == null) + yrange.to = yrange.axis.max; + + // clip + if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max || + yrange.to < yrange.axis.min || yrange.from > yrange.axis.max) + continue; + + xrange.from = Math.max(xrange.from, xrange.axis.min); + xrange.to = Math.min(xrange.to, xrange.axis.max); + yrange.from = Math.max(yrange.from, yrange.axis.min); + yrange.to = Math.min(yrange.to, yrange.axis.max); + + if (xrange.from == xrange.to && yrange.from == yrange.to) + continue; + + // then draw + xrange.from = xrange.axis.p2c(xrange.from); + xrange.to = xrange.axis.p2c(xrange.to); + yrange.from = yrange.axis.p2c(yrange.from); + yrange.to = yrange.axis.p2c(yrange.to); + + if (xrange.from == xrange.to || yrange.from == yrange.to) { + // draw line + ctx.beginPath(); + ctx.strokeStyle = m.color || options.grid.markingsColor; + ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth; + ctx.moveTo(xrange.from, yrange.from); + ctx.lineTo(xrange.to, yrange.to); + ctx.stroke(); + } + else { + // fill area + ctx.fillStyle = m.color || options.grid.markingsColor; + ctx.fillRect(xrange.from, yrange.to, + xrange.to - xrange.from, + yrange.from - yrange.to); + } + } + } + + // draw the ticks + var axes = allAxes(), bw = options.grid.borderWidth; + + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box, + t = axis.tickLength, x, y, xoff, yoff; + if (!axis.show || axis.ticks.length == 0) + continue + + ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString(); + ctx.lineWidth = 1; + + // find the edges + if (axis.direction == "x") { + x = 0; + if (t == "full") + y = (axis.position == "top" ? 0 : plotHeight); + else + y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0); + } + else { + y = 0; + if (t == "full") + x = (axis.position == "left" ? 0 : plotWidth); + else + x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0); + } + + // draw tick bar + if (!axis.innermost) { + ctx.beginPath(); + xoff = yoff = 0; + if (axis.direction == "x") + xoff = plotWidth; + else + yoff = plotHeight; + + if (ctx.lineWidth == 1) { + x = Math.floor(x) + 0.5; + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + ctx.stroke(); + } + + // draw ticks + ctx.beginPath(); + for (i = 0; i < axis.ticks.length; ++i) { + var v = axis.ticks[i].v; + + xoff = yoff = 0; + + if (v < axis.min || v > axis.max + // skip those lying on the axes if we got a border + || (t == "full" && bw > 0 + && (v == axis.min || v == axis.max))) + continue; + + if (axis.direction == "x") { + x = axis.p2c(v); + yoff = t == "full" ? -plotHeight : t; + + if (axis.position == "top") + yoff = -yoff; + } + else { + y = axis.p2c(v); + xoff = t == "full" ? -plotWidth : t; + + if (axis.position == "left") + xoff = -xoff; + } + + if (ctx.lineWidth == 1) { + if (axis.direction == "x") + x = Math.floor(x) + 0.5; + else + y = Math.floor(y) + 0.5; + } + + ctx.moveTo(x, y); + ctx.lineTo(x + xoff, y + yoff); + } + + ctx.stroke(); + } + + + // draw border + if (bw) { + ctx.lineWidth = bw; + ctx.strokeStyle = options.grid.borderColor; + ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw); + } + + ctx.restore(); + } + + function insertAxisLabels() { + placeholder.find(".tickLabels").remove(); + + var html = ['
']; + + var axes = allAxes(); + for (var j = 0; j < axes.length; ++j) { + var axis = axes[j], box = axis.box; + if (!axis.show) + continue; + //debug: html.push('
') + html.push('
'); + for (var i = 0; i < axis.ticks.length; ++i) { + var tick = axis.ticks[i]; + if (!tick.label || tick.v < axis.min || tick.v > axis.max) + continue; + + var pos = {}, align; + + if (axis.direction == "x") { + align = "center"; + pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2); + if (axis.position == "bottom") + pos.top = box.top + box.padding; + else + pos.bottom = canvasHeight - (box.top + box.height - box.padding); + } + else { + pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2); + if (axis.position == "left") { + pos.right = canvasWidth - (box.left + box.width - box.padding) + align = "right"; + } + else { + pos.left = box.left + box.padding; + align = "left"; + } + } + + pos.width = axis.labelWidth; + + var style = ["position:absolute", "text-align:" + align ]; + for (var a in pos) + style.push(a + ":" + pos[a] + "px") + + html.push('
' + tick.label + '
'); + } + html.push('
'); + } + + html.push('
'); + + placeholder.append(html.join("")); + } + + function drawSeries(series) { + if (series.lines.show) + drawSeriesLines(series); + if (series.bars.show) + drawSeriesBars(series); + if (series.points.show) + drawSeriesPoints(series); + } + + function drawSeriesLines(series) { + function plotLine(datapoints, xoffset, yoffset, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + prevx = null, prevy = null; + + ctx.beginPath(); + for (var i = ps; i < points.length; i += ps) { + var x1 = points[i - ps], y1 = points[i - ps + 1], + x2 = points[i], y2 = points[i + 1]; + + if (x1 == null || x2 == null) + continue; + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) + continue; // line segment is outside + // compute new intersection point + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) + continue; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) + continue; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) + continue; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (x1 != prevx || y1 != prevy) + ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset); + + prevx = x2; + prevy = y2; + ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset); + } + ctx.stroke(); + } + + function plotLineArea(datapoints, axisx, axisy) { + var points = datapoints.points, + ps = datapoints.pointsize, + bottom = Math.min(Math.max(0, axisy.min), axisy.max), + i = 0, top, areaOpen = false, + ypos = 1, segmentStart = 0, segmentEnd = 0; + + // we process each segment in two turns, first forward + // direction to sketch out top, then once we hit the + // end we go backwards to sketch the bottom + while (true) { + if (ps > 0 && i > points.length + ps) + break; + + i += ps; // ps is negative if going backwards + + var x1 = points[i - ps], + y1 = points[i - ps + ypos], + x2 = points[i], y2 = points[i + ypos]; + + if (areaOpen) { + if (ps > 0 && x1 != null && x2 == null) { + // at turning point + segmentEnd = i; + ps = -ps; + ypos = 2; + continue; + } + + if (ps < 0 && i == segmentStart + ps) { + // done with the reverse sweep + ctx.fill(); + areaOpen = false; + ps = -ps; + ypos = 1; + i = segmentStart = segmentEnd + ps; + continue; + } + } + + if (x1 == null || x2 == null) + continue; + + // clip x values + + // clip with xmin + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) + continue; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; + } + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) + continue; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; + } + + // clip with xmax + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) + continue; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; + } + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) + continue; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; + } + + if (!areaOpen) { + // open area + ctx.beginPath(); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); + areaOpen = true; + } + + // now first check the case where both is outside + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); + continue; + } + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); + continue; + } + + // else it's a bit more complicated, there might + // be a flat maxed out rectangle first, then a + // triangular cutout or reverse; to find these + // keep track of the current x values + var x1old = x1, x2old = x2; + + // clip the y values, without shortcutting, we + // go through all cases in turn + + // clip with ymin + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; + } + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; + } + + // clip with ymax + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; + } + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; + } + + // if the x value was changed we got a rectangle + // to fill + if (x1 != x1old) { + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1)); + // it goes to (x1, y1), but we fill that below + } + + // fill triangular section, this sometimes result + // in redundant points if (x1, y1) hasn't changed + // from previous line to, but we just ignore that + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + + // fill the other rectangle if it's there + if (x2 != x2old) { + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2)); + } + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + ctx.lineJoin = "round"; + + var lw = series.lines.lineWidth, + sw = series.shadowSize; + // FIXME: consider another form of shadow when filling is turned on + if (lw > 0 && sw > 0) { + // draw shadow as a thick and thin line with transparency + ctx.lineWidth = sw; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + // position shadow at angle from the mid of line + var angle = Math.PI/18; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis); + ctx.lineWidth = sw/2; + plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight); + if (fillStyle) { + ctx.fillStyle = fillStyle; + plotLineArea(series.datapoints, series.xaxis, series.yaxis); + } + + if (lw > 0) + plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis); + ctx.restore(); + } + + function drawSeriesPoints(series) { + function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + var x = points[i], y = points[i + 1]; + if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + continue; + + ctx.beginPath(); + x = axisx.p2c(x); + y = axisy.p2c(y) + offset; + if (symbol == "circle") + ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); + else + symbol(ctx, x, y, radius, shadow); + ctx.closePath(); + + if (fillStyle) { + ctx.fillStyle = fillStyle; + ctx.fill(); + } + ctx.stroke(); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + var lw = series.points.lineWidth, + sw = series.shadowSize, + radius = series.points.radius, + symbol = series.points.symbol; + if (lw > 0 && sw > 0) { + // draw shadow in two steps + var w = sw / 2; + ctx.lineWidth = w; + ctx.strokeStyle = "rgba(0,0,0,0.1)"; + plotPoints(series.datapoints, radius, null, w + w/2, true, + series.xaxis, series.yaxis, symbol); + + ctx.strokeStyle = "rgba(0,0,0,0.2)"; + plotPoints(series.datapoints, radius, null, w/2, true, + series.xaxis, series.yaxis, symbol); + } + + ctx.lineWidth = lw; + ctx.strokeStyle = series.color; + plotPoints(series.datapoints, radius, + getFillStyle(series.points, series.color), 0, false, + series.xaxis, series.yaxis, symbol); + ctx.restore(); + } + + function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) { + var left, right, bottom, top, + drawLeft, drawRight, drawTop, drawBottom, + tmp; + + // in horizontal mode, we start the bar from the left + // instead of from the bottom so it appears to be + // horizontal rather than vertical + if (horizontal) { + drawBottom = drawRight = drawTop = true; + drawLeft = false; + left = b; + right = x; + top = y + barLeft; + bottom = y + barRight; + + // account for negative bars + if (right < left) { + tmp = right; + right = left; + left = tmp; + drawLeft = true; + drawRight = false; + } + } + else { + drawLeft = drawRight = drawTop = true; + drawBottom = false; + left = x + barLeft; + right = x + barRight; + bottom = b; + top = y; + + // account for negative bars + if (top < bottom) { + tmp = top; + top = bottom; + bottom = tmp; + drawBottom = true; + drawTop = false; + } + } + + // clip + if (right < axisx.min || left > axisx.max || + top < axisy.min || bottom > axisy.max) + return; + + if (left < axisx.min) { + left = axisx.min; + drawLeft = false; + } + + if (right > axisx.max) { + right = axisx.max; + drawRight = false; + } + + if (bottom < axisy.min) { + bottom = axisy.min; + drawBottom = false; + } + + if (top > axisy.max) { + top = axisy.max; + drawTop = false; + } + + left = axisx.p2c(left); + bottom = axisy.p2c(bottom); + right = axisx.p2c(right); + top = axisy.p2c(top); + + // fill the bar + if (fillStyleCallback) { + c.beginPath(); + c.moveTo(left, bottom); + c.lineTo(left, top); + c.lineTo(right, top); + c.lineTo(right, bottom); + c.fillStyle = fillStyleCallback(bottom, top); + c.fill(); + } + + // draw outline + if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) { + c.beginPath(); + + // FIXME: inline moveTo is buggy with excanvas + c.moveTo(left, bottom + offset); + if (drawLeft) + c.lineTo(left, top + offset); + else + c.moveTo(left, top + offset); + if (drawTop) + c.lineTo(right, top + offset); + else + c.moveTo(right, top + offset); + if (drawRight) + c.lineTo(right, bottom + offset); + else + c.moveTo(right, bottom + offset); + if (drawBottom) + c.lineTo(left, bottom + offset); + else + c.moveTo(left, bottom + offset); + c.stroke(); + } + } + + function drawSeriesBars(series) { + function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) { + var points = datapoints.points, ps = datapoints.pointsize; + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth); + } + } + + ctx.save(); + ctx.translate(plotOffset.left, plotOffset.top); + + // FIXME: figure out a way to add shadows (for instance along the right edge) + ctx.lineWidth = series.bars.lineWidth; + ctx.strokeStyle = series.color; + var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; + var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null; + plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis); + ctx.restore(); + } + + function getFillStyle(filloptions, seriesColor, bottom, top) { + var fill = filloptions.fill; + if (!fill) + return null; + + if (filloptions.fillColor) + return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor); + + var c = $.color.parse(seriesColor); + c.a = typeof fill == "number" ? fill : 0.4; + c.normalize(); + return c.toString(); + } + + function insertLegend() { + placeholder.find(".legend").remove(); + + if (!options.legend.show) + return; + + var fragments = [], rowStarted = false, + lf = options.legend.labelFormatter, s, label; + for (var i = 0; i < series.length; ++i) { + s = series[i]; + label = s.label; + if (!label) + continue; + + if (i % options.legend.noColumns == 0) { + if (rowStarted) + fragments.push(''); + fragments.push(''); + rowStarted = true; + } + + if (lf) + label = lf(label, s); + + fragments.push( + '
' + + '' + label + ''); + } + if (rowStarted) + fragments.push(''); + + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
'; + if (options.legend.container != null) + $(options.legend.container).html(table); + else { + var pos = "", + p = options.legend.position, + m = options.legend.margin; + if (m[0] == null) + m = [m, m]; + if (p.charAt(0) == "n") + pos += 'top:' + (m[1] + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m[0] + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m[0] + plotOffset.left) + 'px;'; + var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(placeholder); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + c = options.grid.backgroundColor; + if (c && typeof c == "string") + c = $.color.parse(c); + else + c = $.color.extract(legend, 'background-color'); + c.a = 1; + c = c.toString(); + } + var div = legend.children(); + $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } + } + } + + + // interactive features + + var highlights = [], + redrawTimeout = null; + + // returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY, seriesFilter) { + var maxDistance = options.grid.mouseActiveRadius, + smallestDistance = maxDistance * maxDistance + 1, + item = null, foundPoint = false, i, j; + + for (i = series.length - 1; i >= 0; --i) { + if (!seriesFilter(series[i])) + continue; + + var s = series[i], + axisx = s.xaxis, + axisy = s.yaxis, + points = s.datapoints.points, + ps = s.datapoints.pointsize, + mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster + my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + + // with inverse transforms, we can't use the maxx/maxy + // optimization, sadly + if (axisx.options.inverseTransform) + maxx = Number.MAX_VALUE; + if (axisy.options.inverseTransform) + maxy = Number.MAX_VALUE; + + if (s.lines.show || s.points.show) { + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1]; + if (x == null) + continue; + + // For points and lines, the cursor must be within a + // certain distance to the data point + if (x - mx > maxx || x - mx < -maxx || + y - my > maxy || y - my < -maxy) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scales of the axes may be different + var dx = Math.abs(axisx.p2c(x) - mouseX), + dy = Math.abs(axisy.p2c(y) - mouseY), + dist = dx * dx + dy * dy; // we save the sqrt + + // use <= to ensure last point takes precedence + // (last generally means on top of) + if (dist < smallestDistance) { + smallestDistance = dist; + item = [i, j / ps]; + } + } + } + + if (s.bars.show && !item) { // no other point can be nearby + var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2, + barRight = barLeft + s.bars.barWidth; + + for (j = 0; j < points.length; j += ps) { + var x = points[j], y = points[j + 1], b = points[j + 2]; + if (x == null) + continue; + + // for a bar graph, the cursor must be inside the bar + if (series[i].bars.horizontal ? + (mx <= Math.max(b, x) && mx >= Math.min(b, x) && + my >= y + barLeft && my <= y + barRight) : + (mx >= x + barLeft && mx <= x + barRight && + my >= Math.min(b, y) && my <= Math.max(b, y))) + item = [i, j / ps]; + } + } + } + + if (item) { + i = item[0]; + j = item[1]; + ps = series[i].datapoints.pointsize; + + return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps), + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + + return null; + } + + function onMouseMove(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return s["hoverable"] != false; }); + } + + function onMouseLeave(e) { + if (options.grid.hoverable) + triggerClickHoverEvent("plothover", e, + function (s) { return false; }); + } + + function onClick(e) { + triggerClickHoverEvent("plotclick", e, + function (s) { return s["clickable"] != false; }); + } + + // trigger click or hover event (they send the same parameters + // so we share their code) + function triggerClickHoverEvent(eventname, event, seriesFilter) { + var offset = eventHolder.offset(), + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top, + pos = canvasToAxisCoords({ left: canvasX, top: canvasY }); + + pos.pageX = event.pageX; + pos.pageY = event.pageY; + + var item = findNearbyItem(canvasX, canvasY, seriesFilter); + + if (item) { + // fill in mouse pos for any listeners out there + item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left); + item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top); + } + + if (options.grid.autoHighlight) { + // clear auto-highlights + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.auto == eventname && + !(item && h.series == item.series && + h.point[0] == item.datapoint[0] && + h.point[1] == item.datapoint[1])) + unhighlight(h.series, h.point); + } + + if (item) + highlight(item.series, item.datapoint, eventname); + } + + placeholder.trigger(eventname, [ pos, item ]); + } + + function triggerRedrawOverlay() { + if (!redrawTimeout) + redrawTimeout = setTimeout(drawOverlay, 30); + } + + function drawOverlay() { + redrawTimeout = null; + + // draw highlights + octx.save(); + octx.clearRect(0, 0, canvasWidth, canvasHeight); + octx.translate(plotOffset.left, plotOffset.top); + + var i, hi; + for (i = 0; i < highlights.length; ++i) { + hi = highlights[i]; + + if (hi.series.bars.show) + drawBarHighlight(hi.series, hi.point); + else + drawPointHighlight(hi.series, hi.point); + } + octx.restore(); + + executeHooks(hooks.drawOverlay, [octx]); + } + + function highlight(s, point, auto) { + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } + + var i = indexOfHighlight(s, point); + if (i == -1) { + highlights.push({ series: s, point: point, auto: auto }); + + triggerRedrawOverlay(); + } + else if (!auto) + highlights[i].auto = false; + } + + function unhighlight(s, point) { + if (s == null && point == null) { + highlights = []; + triggerRedrawOverlay(); + } + + if (typeof s == "number") + s = series[s]; + + if (typeof point == "number") + point = s.data[point]; + + var i = indexOfHighlight(s, point); + if (i != -1) { + highlights.splice(i, 1); + + triggerRedrawOverlay(); + } + } + + function indexOfHighlight(s, p) { + for (var i = 0; i < highlights.length; ++i) { + var h = highlights[i]; + if (h.series == s && h.point[0] == p[0] + && h.point[1] == p[1]) + return i; + } + return -1; + } + + function drawPointHighlight(series, point) { + var x = point[0], y = point[1], + axisx = series.xaxis, axisy = series.yaxis; + + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) + return; + + var pointRadius = series.points.radius + series.points.lineWidth / 2; + octx.lineWidth = pointRadius; + octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); + var radius = 1.5 * pointRadius, + x = axisx.p2c(x), + y = axisy.p2c(y); + + octx.beginPath(); + if (series.points.symbol == "circle") + octx.arc(x, y, radius, 0, 2 * Math.PI, false); + else + series.points.symbol(octx, x, y, radius, false); + octx.closePath(); + octx.stroke(); + } + + function drawBarHighlight(series, point) { + octx.lineWidth = series.bars.lineWidth; + octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString(); + var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString(); + var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2; + drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth, + 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth); + } + + function getColorOrGradient(spec, bottom, top, defaultColor) { + if (typeof spec == "string") + return spec; + else { + // assume this is a gradient spec; IE currently only + // supports a simple vertical gradient properly, so that's + // what we support too + var gradient = ctx.createLinearGradient(0, top, 0, bottom); + + for (var i = 0, l = spec.colors.length; i < l; ++i) { + var c = spec.colors[i]; + if (typeof c != "string") { + var co = $.color.parse(defaultColor); + if (c.brightness != null) + co = co.scale('rgb', c.brightness) + if (c.opacity != null) + co.a *= c.opacity; + c = co.toString(); + } + gradient.addColorStop(i / (l - 1), c); + } + + return gradient; + } + } + } + + $.plot = function(placeholder, data, options) { + //var t0 = new Date(); + var plot = new Plot($(placeholder), data, options, $.plot.plugins); + //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime())); + return plot; + }; + + $.plot.version = "0.7"; + + $.plot.plugins = []; + + // returns a string with the date d formatted according to fmt + $.plot.formatDate = function(d, fmt, monthNames) { + var leftPad = function(n) { + n = "" + n; + return n.length == 1 ? "0" + n : n; + }; + + var r = []; + var escape = false, padNext = false; + var hours = d.getUTCHours(); + var isAM = hours < 12; + if (monthNames == null) + monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + + if (fmt.search(/%p|%P/) != -1) { + if (hours > 12) { + hours = hours - 12; + } else if (hours == 0) { + hours = 12; + } + } + for (var i = 0; i < fmt.length; ++i) { + var c = fmt.charAt(i); + + if (escape) { + switch (c) { + case 'h': c = "" + hours; break; + case 'H': c = leftPad(hours); break; + case 'M': c = leftPad(d.getUTCMinutes()); break; + case 'S': c = leftPad(d.getUTCSeconds()); break; + case 'd': c = "" + d.getUTCDate(); break; + case 'm': c = "" + (d.getUTCMonth() + 1); break; + case 'y': c = "" + d.getUTCFullYear(); break; + case 'b': c = "" + monthNames[d.getUTCMonth()]; break; + case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; + case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case '0': c = ""; padNext = true; break; + } + if (c && padNext) { + c = leftPad(c); + padNext = false; + } + r.push(c); + if (!padNext) + escape = false; + } + else { + if (c == "%") + escape = true; + else + r.push(c); + } + } + return r.join(""); + }; + + // round to nearby lower multiple of base + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + +})(jQuery); diff --git a/public/js/leaderboard.js b/public/js/leaderboard.js index e71bfca..d5117f3 100644 --- a/public/js/leaderboard.js +++ b/public/js/leaderboard.js @@ -1,66 +1,77 @@ -$(document).ready(function() { - var colourTable = {} +$(document).ready(function () { + var MAX_LENGTH = 100; + var colourTable = {} - var Graph = function(canvas) { - var timeSeries = {}; - var smoothie = new SmoothieChart({millisPerPixel: 500}); - smoothie.streamTo(canvas, 1000); - var randomRgbValue = function () { - return Math.floor(Math.random() * 156 + 99); - } - var randomColour = function () { - return 'rgb(' + [randomRgbValue(), randomRgbValue(), randomRgbValue()].join(',') + ')'; - } - this.updateWith = function (leaderboard) { - for (var i=0; i < leaderboard.length; i += 1) { - var entry = leaderboard[i]; - var series = timeSeries[entry.playerid]; - if (!series) { - series = timeSeries[entry.playerid] = new TimeSeries(); - colourTable[entry.playerid] = randomColour(); - smoothie.addTimeSeries(series, { strokeStyle:colourTable[entry.playerid], lineWidth:3 }); - } - series.append(new Date().getTime(), entry.score); - smoothie.start(); - } - }; - this.pause = function() { - smoothie.stop(); - } - }; + var Graph = function () { + var timeSeries = {}; + var randomRgbValue = function () { + return Math.floor(Math.random() * 156 + 99); + } + var randomColour = function () { + return 'rgb(' + [randomRgbValue(), randomRgbValue(), randomRgbValue()].join(',') + ')'; + } + var getSeries = function (pid) { + return timeSeries[pid]; + } + this.updateWith = function (leaderboard) { + for (var i = 0; i < leaderboard.length; i += 1) { + var entry = leaderboard[i]; + var series = timeSeries[entry.playerid]; + if (!series) { + colourTable[entry.playerid] = randomColour(); + series = timeSeries[entry.playerid] = { + label: entry.playername, + data: [], + lines: { lineWidth: 0.8}, + color: colourTable[entry.playerid]}; + } + if(series.data.length > MAX_LENGTH) { + series.data.shift(); + } + series.data.push([new Date().getTime(), entry.score]); + } + var elems = []; + for (var p in timeSeries) { + if (timeSeries.hasOwnProperty) { + elems.push(timeSeries[p]); + } + } + $.plot($('#mycanvas'), elems, + {xaxis: {mode: "time"}, + legend: {show: false}}); + }; + }; - var ScoreBoard = function(div) { - this.updateWith = function (leaderboard) { - var list = $('
    '); - for (var i=0; i < leaderboard.length; i += 1) { - var entry = leaderboard[i]; - list.append( - $('
    ').append( - $('
  • ', {class: "player"}) - .append($('
    ' + entry.playername + '
    ').addClass("ranking name").css("background-color", colourTable[entry.playerid])) - .append($('
    ' + entry.score + '
    ').addClass("ranking points").css("background-color", colourTable[entry.playerid])) - .append($('Withdraw').attr("href", "/withdraw/" + entry.playerid)))); - } - $("#scoreboard").replaceWith(list); - } - }; + var ScoreBoard = function (div) { + this.updateWith = function (leaderboard) { + var list = $('
      '); + for (var i = 0; i < leaderboard.length; i += 1) { + var entry = leaderboard[i]; + list.append( + $('
      ').append( + $('
    • ', {class: "player"}) + .append($('
      ' + entry.playername + '
      ').addClass("ranking name").css("background-color", colourTable[entry.playerid])) + .append($('
      ' + entry.score + '
      ').addClass("ranking points").css("background-color", colourTable[entry.playerid])) + .append($('Withdraw').attr("href", "/withdraw/" + entry.playerid)))); + } + $("#scoreboard").replaceWith(list); + } + }; - var graph = new Graph($('#mycanvas')[0]); // get DOM object from jQuery object - var scoreboard = new ScoreBoard($('#scoreboard')); + var graph = new Graph(); // get DOM object from jQuery object + var scoreboard = new ScoreBoard($('#scoreboard')); - setInterval(function() { - $.ajax({ - url: '/scores', - success: function( data ) { - var leaderboard = JSON.parse(data); - if (leaderboard.inplay) { - graph.updateWith(leaderboard.entries); - scoreboard.updateWith(leaderboard.entries); - } else { - graph.pause(); - } - } - }); - }, 1000); - } + setInterval(function () { + $.ajax({ + url: '/scores', + success: function (data) { + var leaderboard = JSON.parse(data); + if (leaderboard.inplay) { + graph.updateWith(leaderboard.entries); + scoreboard.updateWith(leaderboard.entries); + } + } + }); + }, 10000); + } ); \ No newline at end of file diff --git a/public/js/smoothie.js b/public/js/smoothie.js deleted file mode 100644 index 6aad177..0000000 --- a/public/js/smoothie.js +++ /dev/null @@ -1,290 +0,0 @@ -// MIT License: -// -// Copyright (c) 2010-2011, Joe Walnes -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -/** - * Smoothie Charts - http://smoothiecharts.org/ - * (c) 2010, Joe Walnes - * - * v1.0: Main charting library, by Joe Walnes - * v1.1: Auto scaling of axis, by Neil Dunn - * v1.2: fps (frames per second) option, by Mathias Petterson - * v1.3: Fix for divide by zero, by Paul Nikitochkin - * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds - * v1.5: Set default frames per second to 50... smoother. - * .start(), .stop() methods for conserving CPU, by Dmitry Vyal - * options.iterpolation = 'bezier' or 'line', by Dmitry Vyal - * options.maxValue to fix scale, by Dmitry Vyal - */ - -function TimeSeries(options) { - options = options || {}; - options.resetBoundsInterval = options.resetBoundsInterval || 3000; // Reset the max/min bounds after this many milliseconds - options.resetBounds = options.resetBounds || true; // Enable or disable the resetBounds timer - this.options = options; - this.data = []; - - this.maxValue = Number.NaN; // The maximum value ever seen in this time series. - this.minValue = Number.NaN; // The minimum value ever seen in this time series. - - // Start a resetBounds Interval timer desired - if (options.resetBounds) { - this.boundsTimer = setInterval(function(thisObj) { thisObj.resetBounds(); }, options.resetBoundsInterval, this); - } -} - -// Reset the min and max for this timeseries so the graph rescales itself -TimeSeries.prototype.resetBounds = function() { - this.maxValue = Number.NaN; - this.minValue = Number.NaN; - for (var i = 0; i < this.data.length; i++) { - this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, this.data[i][1]) : this.data[i][1]; - this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, this.data[i][1]) : this.data[i][1]; - } -}; - -TimeSeries.prototype.append = function(timestamp, value) { - this.data.push([timestamp, value]); - this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, value) : value; - this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, value) : value; -}; - -function SmoothieChart(options) { - // Defaults - options = options || {}; - options.grid = options.grid || { fillStyle:'#000000', strokeStyle: '#777777', lineWidth: 1, millisPerLine: 1000, verticalSections: 2 }; - options.millisPerPixel = options.millisPerPixel || 20; - options.fps = options.fps || 50; - options.maxValueScale = options.maxValueScale || 1; - options.minValue = options.minValue; - options.maxValue = options.maxValue; - options.labels = options.labels || { fillStyle:'#ffffff' }; - options.interpolation = options.interpolation || "bezier"; - this.options = options; - this.seriesSet = []; -} - -SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { - this.seriesSet.push({timeSeries: timeSeries, options: options || {}}); -}; - -SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { - this.seriesSet.splice(this.seriesSet.indexOf(timeSeries), 1); -}; - -SmoothieChart.prototype.streamTo = function(canvas, delay) { - var self = this; - this.render_on_tick = function() { - self.render(canvas, new Date().getTime() - (delay || 0)); - }; - - this.start(); -}; - -SmoothieChart.prototype.start = function() { - if (!this.timer) - this.timer = setInterval(this.render_on_tick, 1000/this.options.fps); -}; - -SmoothieChart.prototype.stop = function() { - if (this.timer) { - clearInterval(this.timer); - this.timer = undefined; - } -}; - -SmoothieChart.prototype.render = function(canvas, time) { - var canvasContext = canvas.getContext("2d"); - var options = this.options; - var dimensions = {top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight}; - - // Save the state of the canvas context, any transformations applied in this method - // will get removed from the stack at the end of this method when .restore() is called. - canvasContext.save(); - - // Round time down to pixel granularity, so motion appears smoother. - time = time - time % options.millisPerPixel; - - // Move the origin. - canvasContext.translate(dimensions.left, dimensions.top); - - // Create a clipped rectangle - anything we draw will be constrained to this rectangle. - // This prevents the occasional pixels from curves near the edges overrunning and creating - // screen cheese (that phrase should neeed no explanation). - canvasContext.beginPath(); - canvasContext.rect(0, 0, dimensions.width, dimensions.height); - canvasContext.clip(); - - // Clear the working area. - canvasContext.save(); - canvasContext.fillStyle = options.grid.fillStyle; - canvasContext.fillRect(0, 0, dimensions.width, dimensions.height); - canvasContext.restore(); - - // Grid lines.... - canvasContext.save(); - canvasContext.lineWidth = options.grid.lineWidth || 1; - canvasContext.strokeStyle = options.grid.strokeStyle || '#ffffff'; - // Vertical (time) dividers. - if (options.grid.millisPerLine > 0) { - for (var t = time - (time % options.grid.millisPerLine); t >= time - (dimensions.width * options.millisPerPixel); t -= options.grid.millisPerLine) { - canvasContext.beginPath(); - var gx = Math.round(dimensions.width - ((time - t) / options.millisPerPixel)); - canvasContext.moveTo(gx, 0); - canvasContext.lineTo(gx, dimensions.height); - canvasContext.stroke(); - canvasContext.closePath(); - } - } - - // Horizontal (value) dividers. - for (var v = 1; v < options.grid.verticalSections; v++) { - var gy = Math.round(v * dimensions.height / options.grid.verticalSections); - canvasContext.beginPath(); - canvasContext.moveTo(0, gy); - canvasContext.lineTo(dimensions.width, gy); - canvasContext.stroke(); - canvasContext.closePath(); - } - // Bounding rectangle. - canvasContext.beginPath(); - canvasContext.strokeRect(0, 0, dimensions.width, dimensions.height); - canvasContext.closePath(); - canvasContext.restore(); - - // Calculate the current scale of the chart, from all time series. - var maxValue = Number.NaN; - var minValue = Number.NaN; - - for (var d = 0; d < this.seriesSet.length; d++) { - // TODO(ndunn): We could calculate / track these values as they stream in. - var timeSeries = this.seriesSet[d].timeSeries; - if (!isNaN(timeSeries.maxValue)) { - maxValue = !isNaN(maxValue) ? Math.max(maxValue, timeSeries.maxValue) : timeSeries.maxValue; - } - - if (!isNaN(timeSeries.minValue)) { - minValue = !isNaN(minValue) ? Math.min(minValue, timeSeries.minValue) : timeSeries.minValue; - } - } - - if (isNaN(maxValue) && isNaN(minValue)) { - return; - } - - // Scale the maxValue to add padding at the top if required - if (options.maxValue != null) - maxValue = options.maxValue; - else - maxValue = maxValue * options.maxValueScale; - // Set the minimum if we've specified one - if (options.minValue != null) - minValue = options.minValue; - var valueRange = maxValue - minValue; - - // For each data set... - for (var d = 0; d < this.seriesSet.length; d++) { - canvasContext.save(); - var timeSeries = this.seriesSet[d].timeSeries; - var dataSet = timeSeries.data; - var seriesOptions = this.seriesSet[d].options; - - // Delete old data that's moved off the left of the chart. - // We must always keep the last expired data point as we need this to draw the - // line that comes into the chart, but any points prior to that can be removed. - while (dataSet.length >= 2 && dataSet[1][0] < time - (dimensions.width * options.millisPerPixel)) { - dataSet.splice(0, 1); - } - - // Set style for this dataSet. - canvasContext.lineWidth = seriesOptions.lineWidth || 1; - canvasContext.fillStyle = seriesOptions.fillStyle; - canvasContext.strokeStyle = seriesOptions.strokeStyle || '#ffffff'; - // Draw the line... - canvasContext.beginPath(); - // Retain lastX, lastY for calculating the control points of bezier curves. - var firstX = 0, lastX = 0, lastY = 0; - for (var i = 0; i < dataSet.length; i++) { - // TODO: Deal with dataSet.length < 2. - var x = Math.round(dimensions.width - ((time - dataSet[i][0]) / options.millisPerPixel)); - var value = dataSet[i][1]; - var offset = maxValue - value; - var scaledValue = valueRange ? Math.round((offset / valueRange) * dimensions.height) : 0; - var y = Math.max(Math.min(scaledValue, dimensions.height - 1), 1); // Ensure line is always on chart. - - if (i == 0) { - firstX = x; - canvasContext.moveTo(x, y); - } - // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/B�zier_curve#Quadratic_curves - // - // Assuming A was the last point in the line plotted and B is the new point, - // we draw a curve with control points P and Q as below. - // - // A---P - // | - // | - // | - // Q---B - // - // Importantly, A and P are at the same y coordinate, as are B and Q. This is - // so adjacent curves appear to flow as one. - // - else { - switch (options.interpolation) { - case "line": - canvasContext.lineTo(x,y); - break; - case "bezier": - default: - canvasContext.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop - Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) - Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) - x, y); // endPoint (B) - break; - } - } - - lastX = x, lastY = y; - } - if (dataSet.length > 0 && seriesOptions.fillStyle) { - // Close up the fill region. - canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); - canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); - canvasContext.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); - canvasContext.fill(); - } - canvasContext.stroke(); - canvasContext.closePath(); - canvasContext.restore(); - } - - // Draw the axis values on the chart. - if (!options.labels.disabled) { - canvasContext.fillStyle = options.labels.fillStyle; - var maxValueString = maxValue.toFixed(2); - var minValueString = minValue.toFixed(2); - canvasContext.fillText(maxValueString, dimensions.width - canvasContext.measureText(maxValueString).width - 2, 10); - canvasContext.fillText(minValueString, dimensions.width - canvasContext.measureText(minValueString).width - 2, dimensions.height - 2); - } - - canvasContext.restore(); // See .save() above. -} \ No newline at end of file From c6a9ae35785ae72a31bd7f41b4332be87aae60c0 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Thu, 13 Jun 2013 14:42:58 +0200 Subject: [PATCH 14/25] improve layout of more screens using bootstrap --- lib/extreme_startup/views/leaderboard.haml | 29 ++++++++++------- lib/extreme_startup/views/personal_page.haml | 33 +++++++++++--------- lib/extreme_startup/views/scores.haml | 8 +++-- public/css/bootstrap.min.css | 9 ++++++ public/js/leaderboard.js | 6 ++-- 5 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 public/css/bootstrap.min.css diff --git a/lib/extreme_startup/views/leaderboard.haml b/lib/extreme_startup/views/leaderboard.haml index 09e4621..65b5f9c 100644 --- a/lib/extreme_startup/views/leaderboard.haml +++ b/lib/extreme_startup/views/leaderboard.haml @@ -1,18 +1,23 @@ %html %head %title Extreme Startup - Leaderboard + %link(rel="stylesheet" href="/css/bootstrap.min.css") %link(rel="stylesheet" href="/css/style.css") %body - %a{ :href => '/players' } - I want to play! - %h1 Leaderboard - %ul - - leaderboard.each do |entry| - %li.player - %div{:class => ["ranking", "name"]} #{html_escape(entry.playername)} - %div{:class => ["ranking", "points"]} #{entry.score} - - :javascript - refresh = function() { window.location.reload(); }; - setTimeout(refresh, 10000); + %div{:class => ["hero-unit"]} + %h1 Extreme Startup + %div{:class => ["span3 offset1"]} + %a{ :href => '/players', :class => ["btn btn-large btn-primary"] } + I want to play! + %div{:class => ["span7"]} + %h1 Leaderboard + %ul + - leaderboard.each do |entry| + %li.player + %div{:class => ["ranking", "name"]} #{html_escape(entry.playername)} + %div{:class => ["ranking", "points"]} #{entry.score} + + :javascript + refresh = function() { window.location.reload(); }; + setTimeout(refresh, 10000); diff --git a/lib/extreme_startup/views/personal_page.haml b/lib/extreme_startup/views/personal_page.haml index dbd11f1..a9af634 100644 --- a/lib/extreme_startup/views/personal_page.haml +++ b/lib/extreme_startup/views/personal_page.haml @@ -1,22 +1,27 @@ %html %head %title Extreme Startup - #{name} + %link(rel="stylesheet" href="/css/bootstrap.min.css") %link(rel="stylesheet" href="/css/style.css") %body - %h1 Hello, #{name}! + %div{:class => ["hero-unit"]} + %h1 Extreme Startup + %h2 Hello, #{name}! - %h2 - Your score is: - %span.score #{score} - - %a{ :href => "/withdraw/#{playerid}" } - Withdraw + %div{:class => ["span5"]} + %h2 + Your score is: + %span.score #{score} + + %a{ :href => "/withdraw/#{playerid}", :class => ["btn btn-large btn-primary"] } + Withdraw - %h2 Log: - %ul - - log.each do |logline| - %li - %div{:class => ["logline", "id", logline.result]} #{logline.id} - %div{:class => ["logline", "result", logline.result]} result: #{logline.result} - %div{:class => ["logline", "points", logline.result]} points awarded: #{logline.points} + %div{:class => ["span10"]} + %h2 Log: + %ul + - log.each do |logline| + %li + %div{:class => ["logline", "id", logline.result]} #{logline.id} + %div{:class => ["logline", "result", logline.result]} result: #{logline.result} + %div{:class => ["logline", "points", logline.result]} points awarded: #{logline.points} \ No newline at end of file diff --git a/lib/extreme_startup/views/scores.haml b/lib/extreme_startup/views/scores.haml index c435c4b..35c8dd0 100644 --- a/lib/extreme_startup/views/scores.haml +++ b/lib/extreme_startup/views/scores.haml @@ -1,14 +1,16 @@ %html %head %title Extreme Startup - Score Graph + %link(rel="stylesheet" href="/css/bootstrap.min.css") %link(rel="stylesheet" href="/css/style.css") %script{:type => "text/javascript", :src => "/js/jquery-1.6.2.min.js"} %script{:type => "text/javascript", :src => "/js/jquery.flot.js"} %script{:type => "text/javascript", :src => "/js/leaderboard.js"} %body - .left - %div{:id => "mycanvas" } - .right + %div{:class => ["hero-unit"]} + %h1 Extreme Startup + %div{:id => "mycanvas", :class => ["span7"]} + %div{:class => ["span7"]} %h1 Leader Board %ul{:id => "scoreboard" } diff --git a/public/css/bootstrap.min.css b/public/css/bootstrap.min.css new file mode 100644 index 0000000..c10c7f4 --- /dev/null +++ b/public/css/bootstrap.min.css @@ -0,0 +1,9 @@ +/*! + * Bootstrap v2.3.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}a:hover,a:active{outline:0}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{width:auto\9;height:auto;max-width:100%;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic}#map_canvas img,.google-maps img{max-width:none}button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle}button,input{*overflow:visible;line-height:normal}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}label,select,button,input[type="button"],input[type="reset"],input[type="submit"],input[type="radio"],input[type="checkbox"]{cursor:pointer}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}textarea{overflow:auto;vertical-align:top}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333;background-color:#fff}a{color:#08c;text-decoration:none}a:hover,a:focus{color:#005580;text-decoration:underline}.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.1);box-shadow:0 1px 3px rgba(0,0,0,0.1)}.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px}.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.span12{width:940px}.span11{width:860px}.span10{width:780px}.span9{width:700px}.span8{width:620px}.span7{width:540px}.span6{width:460px}.span5{width:380px}.span4{width:300px}.span3{width:220px}.span2{width:140px}.span1{width:60px}.offset12{margin-left:980px}.offset11{margin-left:900px}.offset10{margin-left:820px}.offset9{margin-left:740px}.offset8{margin-left:660px}.offset7{margin-left:580px}.offset6{margin-left:500px}.offset5{margin-left:420px}.offset4{margin-left:340px}.offset3{margin-left:260px}.offset2{margin-left:180px}.offset1{margin-left:100px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.127659574468085%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%}.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%}.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%}.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%}.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%}.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%}.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%}.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%}.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%}.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%}.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%}.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%}.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%}.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%}.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%}.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%}.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%}.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%}.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%}.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%}.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%}.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%}.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%}.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%}.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%}.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%}.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%}.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%}.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%}.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%}.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%}.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%}.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%}.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%}.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%}[class*="span"].hide,.row-fluid [class*="span"].hide{display:none}[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right}.container{margin-right:auto;margin-left:auto;*zoom:1}.container:before,.container:after{display:table;line-height:0;content:""}.container:after{clear:both}.container-fluid{padding-right:20px;padding-left:20px;*zoom:1}.container-fluid:before,.container-fluid:after{display:table;line-height:0;content:""}.container-fluid:after{clear:both}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px}small{font-size:85%}strong{font-weight:bold}em{font-style:italic}cite{font-style:normal}.muted{color:#999}a.muted:hover,a.muted:focus{color:#808080}.text-warning{color:#c09853}a.text-warning:hover,a.text-warning:focus{color:#a47e3c}.text-error{color:#b94a48}a.text-error:hover,a.text-error:focus{color:#953b39}.text-info{color:#3a87ad}a.text-info:hover,a.text-info:focus{color:#2d6987}.text-success{color:#468847}a.text-success:hover,a.text-success:focus{color:#356635}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:20px;color:inherit;text-rendering:optimizelegibility}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{line-height:40px}h1{font-size:38.5px}h2{font-size:31.5px}h3{font-size:24.5px}h4{font-size:17.5px}h5{font-size:14px}h6{font-size:11.9px}h1 small{font-size:24.5px}h2 small{font-size:17.5px}h3 small{font-size:14px}h4 small{font-size:14px}.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eee}ul,ol{padding:0;margin:0 0 10px 25px}ul ul,ul ol,ol ol,ol ul{margin-bottom:0}li{line-height:20px}ul.unstyled,ol.unstyled{margin-left:0;list-style:none}ul.inline,ol.inline{margin-left:0;list-style:none}ul.inline>li,ol.inline>li{display:inline-block;*display:inline;padding-right:5px;padding-left:5px;*zoom:1}dl{margin-bottom:20px}dt,dd{line-height:20px}dt{font-weight:bold}dd{margin-left:10px}.dl-horizontal{*zoom:1}.dl-horizontal:before,.dl-horizontal:after{display:table;line-height:0;content:""}.dl-horizontal:after{clear:both}.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}hr{margin:20px 0;border:0;border-top:1px solid #eee;border-bottom:1px solid #fff}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}abbr.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{margin-bottom:0;font-size:17.5px;font-weight:300;line-height:1.25}blockquote small{display:block;line-height:20px;color:#999}blockquote small:before{content:'\2014 \00A0'}blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small{text-align:right}blockquote.pull-right small:before{content:''}blockquote.pull-right small:after{content:'\00A0 \2014'}q:before,q:after,blockquote:before,blockquote:after{content:""}address{display:block;margin-bottom:20px;font-style:normal;line-height:20px}code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}code{padding:2px 4px;color:#d14;white-space:nowrap;background-color:#f7f7f9;border:1px solid #e1e1e8}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}pre.prettyprint{margin-bottom:20px}pre code{padding:0;color:inherit;white-space:pre;white-space:pre-wrap;background-color:transparent;border:0}.pre-scrollable{max-height:340px;overflow-y:scroll}form{margin:0 0 20px}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333;border:0;border-bottom:1px solid #e5e5e5}legend small{font-size:15px;color:#999}label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px}input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}label{display:block;margin-bottom:5px}select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:10px;font-size:14px;line-height:20px;color:#555;vertical-align:middle;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}input,textarea,.uneditable-input{width:206px}textarea{height:auto}textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#fff;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border linear .2s,box-shadow linear .2s;-moz-transition:border linear .2s,box-shadow linear .2s;-o-transition:border linear .2s,box-shadow linear .2s;transition:border linear .2s,box-shadow linear .2s}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82,168,236,0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(82,168,236,0.6)}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;*margin-top:0;line-height:normal}input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto}select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px}select{width:220px;background-color:#fff;border:1px solid #ccc}select[multiple],select[size]{height:auto}select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.uneditable-input,.uneditable-textarea{color:#999;cursor:not-allowed;background-color:#fcfcfc;border-color:#ccc;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.025);box-shadow:inset 0 1px 2px rgba(0,0,0,0.025)}.uneditable-input{overflow:hidden;white-space:nowrap}.uneditable-textarea{width:auto;height:auto}input:-moz-placeholder,textarea:-moz-placeholder{color:#999}input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999}.radio,.checkbox{min-height:20px;padding-left:20px}.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-20px}.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px}.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle}.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px}.input-mini{width:60px}.input-small{width:90px}.input-medium{width:150px}.input-large{width:210px}.input-xlarge{width:270px}.input-xxlarge{width:530px}input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0}.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:926px}input.span11,textarea.span11,.uneditable-input.span11{width:846px}input.span10,textarea.span10,.uneditable-input.span10{width:766px}input.span9,textarea.span9,.uneditable-input.span9{width:686px}input.span8,textarea.span8,.uneditable-input.span8{width:606px}input.span7,textarea.span7,.uneditable-input.span7{width:526px}input.span6,textarea.span6,.uneditable-input.span6{width:446px}input.span5,textarea.span5,.uneditable-input.span5{width:366px}input.span4,textarea.span4,.uneditable-input.span4{width:286px}input.span3,textarea.span3,.uneditable-input.span3{width:206px}input.span2,textarea.span2,.uneditable-input.span2{width:126px}input.span1,textarea.span1,.uneditable-input.span1{width:46px}.controls-row{*zoom:1}.controls-row:before,.controls-row:after{display:table;line-height:0;content:""}.controls-row:after{clear:both}.controls-row [class*="span"],.row-fluid .controls-row [class*="span"]{float:left}.controls-row .checkbox[class*="span"],.controls-row .radio[class*="span"]{padding-top:5px}input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eee}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent}.control-group.warning .control-label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853}.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853}.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #dbc59e}.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853}.control-group.error .control-label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48}.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48}.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #d59392}.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48}.control-group.success .control-label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847}.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847}.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7aba7b}.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847}.control-group.info .control-label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad}.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad}.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #7ab5d3}.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#b94a48;border-color:#ee5f5b}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7}.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1}.form-actions:before,.form-actions:after{display:table;line-height:0;content:""}.form-actions:after{clear:both}.help-block,.help-inline{color:#595959}.help-block{display:block;margin-bottom:10px}.help-inline{display:inline-block;*display:inline;padding-left:5px;vertical-align:middle;*zoom:1}.input-append,.input-prepend{display:inline-block;margin-bottom:10px;font-size:0;white-space:nowrap;vertical-align:middle}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input,.input-append .dropdown-menu,.input-prepend .dropdown-menu,.input-append .popover,.input-prepend .popover{font-size:14px}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;vertical-align:top;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2}.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #fff;background-color:#eee;border:1px solid #ccc}.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn,.input-append .btn-group>.dropdown-toggle,.input-prepend .btn-group>.dropdown-toggle{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546}.input-prepend .add-on,.input-prepend .btn{margin-right:-1px}.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-append input+.btn-group .btn:last-child,.input-append select+.btn-group .btn:last-child,.input-append .uneditable-input+.btn-group .btn:last-child{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-append .add-on,.input-append .btn,.input-append .btn-group{margin-left:-1px}.input-append .add-on:last-child,.input-append .btn:last-child,.input-append .btn-group:last-child>.dropdown-toggle{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.input-prepend.input-append input+.btn-group .btn,.input-prepend.input-append select+.btn-group .btn,.input-prepend.input-append .uneditable-input+.btn-group .btn{-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.input-prepend.input-append .btn-group:first-child{margin-left:0}input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0}.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px}.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;margin-bottom:0;vertical-align:middle;*zoom:1}.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none}.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block}.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0}.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle}.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0}.control-group{margin-bottom:10px}legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate}.form-horizontal .control-group{margin-bottom:20px;*zoom:1}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;line-height:0;content:""}.form-horizontal .control-group:after{clear:both}.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right}.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0}.form-horizontal .controls:first-child{*padding-left:180px}.form-horizontal .help-block{margin-bottom:0}.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block,.form-horizontal .uneditable-input+.help-block,.form-horizontal .input-prepend+.help-block,.form-horizontal .input-append+.help-block{margin-top:10px}.form-horizontal .form-actions{padding-left:180px}table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0}.table{width:100%;margin-bottom:20px}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #ddd}.table th{font-weight:bold}.table thead th{vertical-align:bottom}.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0}.table tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed th,.table-condensed td{padding:4px 5px}.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.table-bordered th,.table-bordered td{border-left:1px solid #ddd}.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0}.table-bordered thead:first-child tr:first-child>th:first-child,.table-bordered tbody:first-child tr:first-child>td:first-child,.table-bordered tbody:first-child tr:first-child>th:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered thead:first-child tr:first-child>th:last-child,.table-bordered tbody:first-child tr:first-child>td:last-child,.table-bordered tbody:first-child tr:first-child>th:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-bordered thead:last-child tr:last-child>th:first-child,.table-bordered tbody:last-child tr:last-child>td:first-child,.table-bordered tbody:last-child tr:last-child>th:first-child,.table-bordered tfoot:last-child tr:last-child>td:first-child,.table-bordered tfoot:last-child tr:last-child>th:first-child{-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px}.table-bordered thead:last-child tr:last-child>th:last-child,.table-bordered tbody:last-child tr:last-child>td:last-child,.table-bordered tbody:last-child tr:last-child>th:last-child,.table-bordered tfoot:last-child tr:last-child>td:last-child,.table-bordered tfoot:last-child tr:last-child>th:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px}.table-bordered tfoot+tbody:last-child tr:last-child td:first-child{-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-moz-border-radius-bottomleft:0}.table-bordered tfoot+tbody:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0;-moz-border-radius-bottomright:0}.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px}.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px}.table-striped tbody>tr:nth-child(odd)>td,.table-striped tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover tbody tr:hover>td,.table-hover tbody tr:hover>th{background-color:#f5f5f5}table td[class*="span"],table th[class*="span"],.row-fluid table td[class*="span"],.row-fluid table th[class*="span"]{display:table-cell;float:none;margin-left:0}.table td.span1,.table th.span1{float:none;width:44px;margin-left:0}.table td.span2,.table th.span2{float:none;width:124px;margin-left:0}.table td.span3,.table th.span3{float:none;width:204px;margin-left:0}.table td.span4,.table th.span4{float:none;width:284px;margin-left:0}.table td.span5,.table th.span5{float:none;width:364px;margin-left:0}.table td.span6,.table th.span6{float:none;width:444px;margin-left:0}.table td.span7,.table th.span7{float:none;width:524px;margin-left:0}.table td.span8,.table th.span8{float:none;width:604px;margin-left:0}.table td.span9,.table th.span9{float:none;width:684px;margin-left:0}.table td.span10,.table th.span10{float:none;width:764px;margin-left:0}.table td.span11,.table th.span11{float:none;width:844px;margin-left:0}.table td.span12,.table th.span12{float:none;width:924px;margin-left:0}.table tbody tr.success>td{background-color:#dff0d8}.table tbody tr.error>td{background-color:#f2dede}.table tbody tr.warning>td{background-color:#fcf8e3}.table tbody tr.info>td{background-color:#d9edf7}.table-hover tbody tr.success:hover>td{background-color:#d0e9c6}.table-hover tbody tr.error:hover>td{background-color:#ebcccc}.table-hover tbody tr.warning:hover>td{background-color:#faf2cc}.table-hover tbody tr.info:hover>td{background-color:#c4e3f3}[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;margin-top:1px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:focus>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>li>a:focus>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:focus>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"],.dropdown-submenu:focus>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png")}.icon-glass{background-position:0 0}.icon-music{background-position:-24px 0}.icon-search{background-position:-48px 0}.icon-envelope{background-position:-72px 0}.icon-heart{background-position:-96px 0}.icon-star{background-position:-120px 0}.icon-star-empty{background-position:-144px 0}.icon-user{background-position:-168px 0}.icon-film{background-position:-192px 0}.icon-th-large{background-position:-216px 0}.icon-th{background-position:-240px 0}.icon-th-list{background-position:-264px 0}.icon-ok{background-position:-288px 0}.icon-remove{background-position:-312px 0}.icon-zoom-in{background-position:-336px 0}.icon-zoom-out{background-position:-360px 0}.icon-off{background-position:-384px 0}.icon-signal{background-position:-408px 0}.icon-cog{background-position:-432px 0}.icon-trash{background-position:-456px 0}.icon-home{background-position:0 -24px}.icon-file{background-position:-24px -24px}.icon-time{background-position:-48px -24px}.icon-road{background-position:-72px -24px}.icon-download-alt{background-position:-96px -24px}.icon-download{background-position:-120px -24px}.icon-upload{background-position:-144px -24px}.icon-inbox{background-position:-168px -24px}.icon-play-circle{background-position:-192px -24px}.icon-repeat{background-position:-216px -24px}.icon-refresh{background-position:-240px -24px}.icon-list-alt{background-position:-264px -24px}.icon-lock{background-position:-287px -24px}.icon-flag{background-position:-312px -24px}.icon-headphones{background-position:-336px -24px}.icon-volume-off{background-position:-360px -24px}.icon-volume-down{background-position:-384px -24px}.icon-volume-up{background-position:-408px -24px}.icon-qrcode{background-position:-432px -24px}.icon-barcode{background-position:-456px -24px}.icon-tag{background-position:0 -48px}.icon-tags{background-position:-25px -48px}.icon-book{background-position:-48px -48px}.icon-bookmark{background-position:-72px -48px}.icon-print{background-position:-96px -48px}.icon-camera{background-position:-120px -48px}.icon-font{background-position:-144px -48px}.icon-bold{background-position:-167px -48px}.icon-italic{background-position:-192px -48px}.icon-text-height{background-position:-216px -48px}.icon-text-width{background-position:-240px -48px}.icon-align-left{background-position:-264px -48px}.icon-align-center{background-position:-288px -48px}.icon-align-right{background-position:-312px -48px}.icon-align-justify{background-position:-336px -48px}.icon-list{background-position:-360px -48px}.icon-indent-left{background-position:-384px -48px}.icon-indent-right{background-position:-408px -48px}.icon-facetime-video{background-position:-432px -48px}.icon-picture{background-position:-456px -48px}.icon-pencil{background-position:0 -72px}.icon-map-marker{background-position:-24px -72px}.icon-adjust{background-position:-48px -72px}.icon-tint{background-position:-72px -72px}.icon-edit{background-position:-96px -72px}.icon-share{background-position:-120px -72px}.icon-check{background-position:-144px -72px}.icon-move{background-position:-168px -72px}.icon-step-backward{background-position:-192px -72px}.icon-fast-backward{background-position:-216px -72px}.icon-backward{background-position:-240px -72px}.icon-play{background-position:-264px -72px}.icon-pause{background-position:-288px -72px}.icon-stop{background-position:-312px -72px}.icon-forward{background-position:-336px -72px}.icon-fast-forward{background-position:-360px -72px}.icon-step-forward{background-position:-384px -72px}.icon-eject{background-position:-408px -72px}.icon-chevron-left{background-position:-432px -72px}.icon-chevron-right{background-position:-456px -72px}.icon-plus-sign{background-position:0 -96px}.icon-minus-sign{background-position:-24px -96px}.icon-remove-sign{background-position:-48px -96px}.icon-ok-sign{background-position:-72px -96px}.icon-question-sign{background-position:-96px -96px}.icon-info-sign{background-position:-120px -96px}.icon-screenshot{background-position:-144px -96px}.icon-remove-circle{background-position:-168px -96px}.icon-ok-circle{background-position:-192px -96px}.icon-ban-circle{background-position:-216px -96px}.icon-arrow-left{background-position:-240px -96px}.icon-arrow-right{background-position:-264px -96px}.icon-arrow-up{background-position:-289px -96px}.icon-arrow-down{background-position:-312px -96px}.icon-share-alt{background-position:-336px -96px}.icon-resize-full{background-position:-360px -96px}.icon-resize-small{background-position:-384px -96px}.icon-plus{background-position:-408px -96px}.icon-minus{background-position:-433px -96px}.icon-asterisk{background-position:-456px -96px}.icon-exclamation-sign{background-position:0 -120px}.icon-gift{background-position:-24px -120px}.icon-leaf{background-position:-48px -120px}.icon-fire{background-position:-72px -120px}.icon-eye-open{background-position:-96px -120px}.icon-eye-close{background-position:-120px -120px}.icon-warning-sign{background-position:-144px -120px}.icon-plane{background-position:-168px -120px}.icon-calendar{background-position:-192px -120px}.icon-random{width:16px;background-position:-216px -120px}.icon-comment{background-position:-240px -120px}.icon-magnet{background-position:-264px -120px}.icon-chevron-up{background-position:-288px -120px}.icon-chevron-down{background-position:-313px -119px}.icon-retweet{background-position:-336px -120px}.icon-shopping-cart{background-position:-360px -120px}.icon-folder-close{width:16px;background-position:-384px -120px}.icon-folder-open{width:16px;background-position:-408px -120px}.icon-resize-vertical{background-position:-432px -119px}.icon-resize-horizontal{background-position:-456px -118px}.icon-hdd{background-position:0 -144px}.icon-bullhorn{background-position:-24px -144px}.icon-bell{background-position:-48px -144px}.icon-certificate{background-position:-72px -144px}.icon-thumbs-up{background-position:-96px -144px}.icon-thumbs-down{background-position:-120px -144px}.icon-hand-right{background-position:-144px -144px}.icon-hand-left{background-position:-168px -144px}.icon-hand-up{background-position:-192px -144px}.icon-hand-down{background-position:-216px -144px}.icon-circle-arrow-right{background-position:-240px -144px}.icon-circle-arrow-left{background-position:-264px -144px}.icon-circle-arrow-up{background-position:-288px -144px}.icon-circle-arrow-down{background-position:-312px -144px}.icon-globe{background-position:-336px -144px}.icon-wrench{background-position:-360px -144px}.icon-tasks{background-position:-384px -144px}.icon-filter{background-position:-408px -144px}.icon-briefcase{background-position:-432px -144px}.icon-fullscreen{background-position:-456px -144px}.dropup,.dropdown{position:relative}.dropdown-toggle{*margin-bottom:-3px}.dropdown-toggle:active,.open .dropdown-toggle{outline:0}.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000;border-right:4px solid transparent;border-left:4px solid transparent;content:""}.dropdown .caret{margin-top:8px;margin-left:2px}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-submenu:hover>a,.dropdown-submenu:focus>a{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#0081c2;background-image:-moz-linear-gradient(top,#08c,#0077b3);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));background-image:-webkit-linear-gradient(top,#08c,#0077b3);background-image:-o-linear-gradient(top,#08c,#0077b3);background-image:linear-gradient(to bottom,#08c,#0077b3);background-repeat:repeat-x;outline:0;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0)}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open{*z-index:1000}.open>.dropdown-menu{display:block}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}.dropdown-submenu{position:relative}.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px}.dropdown-submenu:hover>.dropdown-menu{display:block}.dropup .dropdown-submenu>.dropdown-menu{top:auto;bottom:0;margin-top:0;margin-bottom:-2px;-webkit-border-radius:5px 5px 5px 0;-moz-border-radius:5px 5px 5px 0;border-radius:5px 5px 5px 0}.dropdown-submenu>a:after{display:block;float:right;width:0;height:0;margin-top:5px;margin-right:-10px;border-color:transparent;border-left-color:#ccc;border-style:solid;border-width:5px 0 5px 5px;content:" "}.dropdown-submenu:hover>a:after{border-left-color:#fff}.dropdown-submenu.pull-left{float:none}.dropdown-submenu.pull-left>.dropdown-menu{left:-100%;margin-left:10px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.dropdown .dropdown-menu .nav-header{padding-right:20px;padding-left:20px}.typeahead{z-index:1051;margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.fade{opacity:0;-webkit-transition:opacity .15s linear;-moz-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-moz-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.collapse.in{height:auto}.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.btn{display:inline-block;*display:inline;padding:4px 12px;margin-bottom:0;*margin-left:.3em;font-size:14px;line-height:20px;color:#333;text-align:center;text-shadow:0 1px 1px rgba(255,255,255,0.75);vertical-align:middle;cursor:pointer;background-color:#f5f5f5;*background-color:#e6e6e6;background-image:-moz-linear-gradient(top,#fff,#e6e6e6);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,#e6e6e6);background-image:-o-linear-gradient(top,#fff,#e6e6e6);background-image:linear-gradient(to bottom,#fff,#e6e6e6);background-repeat:repeat-x;border:1px solid #ccc;*border:0;border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);*zoom:1;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn:hover,.btn:focus,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333;background-color:#e6e6e6;*background-color:#d9d9d9}.btn:active,.btn.active{background-color:#ccc \9}.btn:first-child{*margin-left:0}.btn:hover,.btn:focus{color:#333;text-decoration:none;background-position:0 -15px;-webkit-transition:background-position .1s linear;-moz-transition:background-position .1s linear;-o-transition:background-position .1s linear;transition:background-position .1s linear}.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn.disabled,.btn[disabled]{cursor:default;background-image:none;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-large{padding:11px 19px;font-size:17.5px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.btn-large [class^="icon-"],.btn-large [class*=" icon-"]{margin-top:4px}.btn-small{padding:2px 10px;font-size:11.9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-small [class^="icon-"],.btn-small [class*=" icon-"]{margin-top:0}.btn-mini [class^="icon-"],.btn-mini [class*=" icon-"]{margin-top:-1px}.btn-mini{padding:0 6px;font-size:10.5px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255,255,255,0.75)}.btn-primary{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#006dcc;*background-color:#04c;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.btn-primary:active,.btn-primary.active{background-color:#039 \9}.btn-warning{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#faa732;*background-color:#f89406;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;border-color:#f89406 #f89406 #ad6704;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#fff;background-color:#f89406;*background-color:#df8505}.btn-warning:active,.btn-warning.active{background-color:#c67605 \9}.btn-danger{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#da4f49;*background-color:#bd362f;background-image:-moz-linear-gradient(top,#ee5f5b,#bd362f);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#bd362f));background-image:-webkit-linear-gradient(top,#ee5f5b,#bd362f);background-image:-o-linear-gradient(top,#ee5f5b,#bd362f);background-image:linear-gradient(to bottom,#ee5f5b,#bd362f);background-repeat:repeat-x;border-color:#bd362f #bd362f #802420;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffbd362f',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#fff;background-color:#bd362f;*background-color:#a9302a}.btn-danger:active,.btn-danger.active{background-color:#942a25 \9}.btn-success{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#5bb75b;*background-color:#51a351;background-image:-moz-linear-gradient(top,#62c462,#51a351);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#51a351));background-image:-webkit-linear-gradient(top,#62c462,#51a351);background-image:-o-linear-gradient(top,#62c462,#51a351);background-image:linear-gradient(to bottom,#62c462,#51a351);background-repeat:repeat-x;border-color:#51a351 #51a351 #387038;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff51a351',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#fff;background-color:#51a351;*background-color:#499249}.btn-success:active,.btn-success.active{background-color:#408140 \9}.btn-info{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#49afcd;*background-color:#2f96b4;background-image:-moz-linear-gradient(top,#5bc0de,#2f96b4);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#2f96b4));background-image:-webkit-linear-gradient(top,#5bc0de,#2f96b4);background-image:-o-linear-gradient(top,#5bc0de,#2f96b4);background-image:linear-gradient(to bottom,#5bc0de,#2f96b4);background-repeat:repeat-x;border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff2f96b4',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#fff;background-color:#2f96b4;*background-color:#2a85a0}.btn-info:active,.btn-info.active{background-color:#24748c \9}.btn-inverse{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#363636;*background-color:#222;background-image:-moz-linear-gradient(top,#444,#222);background-image:-webkit-gradient(linear,0 0,0 100%,from(#444),to(#222));background-image:-webkit-linear-gradient(top,#444,#222);background-image:-o-linear-gradient(top,#444,#222);background-image:linear-gradient(to bottom,#444,#222);background-repeat:repeat-x;border-color:#222 #222 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444',endColorstr='#ff222222',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.btn-inverse:hover,.btn-inverse:focus,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#fff;background-color:#222;*background-color:#151515}.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9}button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0}button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px}button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px}button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px}.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.btn-link{color:#08c;cursor:pointer;border-color:transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-link:hover,.btn-link:focus{color:#005580;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,.btn-link[disabled]:focus{color:#333;text-decoration:none}.btn-group{position:relative;display:inline-block;*display:inline;*margin-left:.3em;font-size:0;white-space:nowrap;vertical-align:middle;*zoom:1}.btn-group:first-child{*margin-left:0}.btn-group+.btn-group{margin-left:5px}.btn-toolbar{margin-top:10px;margin-bottom:10px;font-size:0}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group{margin-left:5px}.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group>.btn+.btn{margin-left:-1px}.btn-group>.btn,.btn-group>.dropdown-menu,.btn-group>.popover{font-size:14px}.btn-group>.btn-mini{font-size:10.5px}.btn-group>.btn-small{font-size:11.9px}.btn-group>.btn-large{font-size:17.5px}.btn-group>.btn:first-child{margin-left:0;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{*padding-top:5px;padding-right:8px;*padding-bottom:5px;padding-left:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 1px 0 0 rgba(255,255,255,0.125),inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05)}.btn-group>.btn-mini+.dropdown-toggle{*padding-top:2px;padding-right:5px;*padding-bottom:2px;padding-left:5px}.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px}.btn-group>.btn-large+.dropdown-toggle{*padding-top:7px;padding-right:12px;*padding-bottom:7px;padding-left:12px}.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05)}.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6}.btn-group.open .btn-primary.dropdown-toggle{background-color:#04c}.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406}.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f}.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351}.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4}.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222}.btn .caret{margin-top:8px;margin-left:0}.btn-large .caret{margin-top:6px}.btn-large .caret{border-top-width:5px;border-right-width:5px;border-left-width:5px}.btn-mini .caret,.btn-small .caret{margin-top:8px}.dropup .btn-large .caret{border-bottom-width:5px}.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#fff;border-bottom-color:#fff}.btn-group-vertical{display:inline-block;*display:inline;*zoom:1}.btn-group-vertical>.btn{display:block;float:none;max-width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.btn-group-vertical>.btn+.btn{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.btn-group-vertical>.btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0}.btn-group-vertical>.btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255,255,255,0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.alert,.alert h4{color:#c09853}.alert h4{margin:0}.alert .close{position:relative;top:-2px;right:-21px;line-height:20px}.alert-success{color:#468847;background-color:#dff0d8;border-color:#d6e9c6}.alert-success h4{color:#468847}.alert-danger,.alert-error{color:#b94a48;background-color:#f2dede;border-color:#eed3d7}.alert-danger h4,.alert-error h4{color:#b94a48}.alert-info{color:#3a87ad;background-color:#d9edf7;border-color:#bce8f1}.alert-info h4{color:#3a87ad}.alert-block{padding-top:14px;padding-bottom:14px}.alert-block>p,.alert-block>ul{margin-bottom:0}.alert-block p+p{margin-top:5px}.nav{margin-bottom:20px;margin-left:0;list-style:none}.nav>li>a{display:block}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li>a>img{max-width:none}.nav>.pull-right{float:right}.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999;text-shadow:0 1px 0 rgba(255,255,255,0.5);text-transform:uppercase}.nav li+.nav-header{margin-top:9px}.nav-list{padding-right:15px;padding-left:15px;margin-bottom:0}.nav-list>li>a,.nav-list .nav-header{margin-right:-15px;margin-left:-15px;text-shadow:0 1px 0 rgba(255,255,255,0.5)}.nav-list>li>a{padding:3px 15px}.nav-list>.active>a,.nav-list>.active>a:hover,.nav-list>.active>a:focus{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.2);background-color:#08c}.nav-list [class^="icon-"],.nav-list [class*=" icon-"]{margin-right:2px}.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #fff}.nav-tabs,.nav-pills{*zoom:1}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;line-height:0;content:""}.nav-tabs:after,.nav-pills:after{clear:both}.nav-tabs>li,.nav-pills>li{float:left}.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{margin-bottom:-1px}.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover,.nav-tabs>li>a:focus{border-color:#eee #eee #ddd}.nav-tabs>.active>a,.nav-tabs>.active>a:hover,.nav-tabs>.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.nav-pills>.active>a,.nav-pills>.active>a:hover,.nav-pills>.active>a:focus{color:#fff;background-color:#08c}.nav-stacked>li{float:none}.nav-stacked>li>a{margin-right:0}.nav-tabs.nav-stacked{border-bottom:0}.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-topleft:4px}.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomright:4px;-moz-border-radius-bottomleft:4px}.nav-tabs.nav-stacked>li>a:hover,.nav-tabs.nav-stacked>li>a:focus{z-index:2;border-color:#ddd}.nav-pills.nav-stacked>li>a{margin-bottom:3px}.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px}.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px}.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.nav .dropdown-toggle .caret{margin-top:6px;border-top-color:#08c;border-bottom-color:#08c}.nav .dropdown-toggle:hover .caret,.nav .dropdown-toggle:focus .caret{border-top-color:#005580;border-bottom-color:#005580}.nav-tabs .dropdown-toggle .caret{margin-top:8px}.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.nav>.dropdown.active>a:hover,.nav>.dropdown.active>a:focus{cursor:pointer}.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover,.nav>li.dropdown.open.active>a:focus{color:#fff;background-color:#999;border-color:#999}.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret,.nav li.dropdown.open a:focus .caret{border-top-color:#fff;border-bottom-color:#fff;opacity:1;filter:alpha(opacity=100)}.tabs-stacked .open>a:hover,.tabs-stacked .open>a:focus{border-color:#999}.tabbable{*zoom:1}.tabbable:before,.tabbable:after{display:table;line-height:0;content:""}.tabbable:after{clear:both}.tab-content{overflow:auto}.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0}.tab-content>.tab-pane,.pill-content>.pill-pane{display:none}.tab-content>.active,.pill-content>.active{display:block}.tabs-below>.nav-tabs{border-top:1px solid #ddd}.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0}.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px}.tabs-below>.nav-tabs>li>a:hover,.tabs-below>.nav-tabs>li>a:focus{border-top-color:#ddd;border-bottom-color:transparent}.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover,.tabs-below>.nav-tabs>.active>a:focus{border-color:transparent #ddd #ddd #ddd}.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none}.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px}.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd}.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.tabs-left>.nav-tabs>li>a:hover,.tabs-left>.nav-tabs>li>a:focus{border-color:#eee #ddd #eee #eee}.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover,.tabs-left>.nav-tabs .active>a:focus{border-color:#ddd transparent #ddd #ddd;*border-right-color:#fff}.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd}.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.tabs-right>.nav-tabs>li>a:hover,.tabs-right>.nav-tabs>li>a:focus{border-color:#eee #eee #eee #ddd}.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover,.tabs-right>.nav-tabs .active>a:focus{border-color:#ddd #ddd #ddd transparent;*border-left-color:#fff}.nav>.disabled>a{color:#999}.nav>.disabled>a:hover,.nav>.disabled>a:focus{text-decoration:none;cursor:default;background-color:transparent}.navbar{*position:relative;*z-index:2;margin-bottom:20px;overflow:visible}.navbar-inner{min-height:40px;padding-right:20px;padding-left:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top,#fff,#f2f2f2);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fff),to(#f2f2f2));background-image:-webkit-linear-gradient(top,#fff,#f2f2f2);background-image:-o-linear-gradient(top,#fff,#f2f2f2);background-image:linear-gradient(to bottom,#fff,#f2f2f2);background-repeat:repeat-x;border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff2f2f2',GradientType=0);*zoom:1;-webkit-box-shadow:0 1px 4px rgba(0,0,0,0.065);-moz-box-shadow:0 1px 4px rgba(0,0,0,0.065);box-shadow:0 1px 4px rgba(0,0,0,0.065)}.navbar-inner:before,.navbar-inner:after{display:table;line-height:0;content:""}.navbar-inner:after{clear:both}.navbar .container{width:auto}.nav-collapse.collapse{height:auto;overflow:visible}.navbar .brand{display:block;float:left;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777;text-shadow:0 1px 0 #fff}.navbar .brand:hover,.navbar .brand:focus{text-decoration:none}.navbar-text{margin-bottom:0;line-height:40px;color:#777}.navbar-link{color:#777}.navbar-link:hover,.navbar-link:focus{color:#333}.navbar .divider-vertical{height:40px;margin:0 9px;border-right:1px solid #fff;border-left:1px solid #f2f2f2}.navbar .btn,.navbar .btn-group{margin-top:5px}.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn,.navbar .input-prepend .btn-group,.navbar .input-append .btn-group{margin-top:0}.navbar-form{margin-bottom:0;*zoom:1}.navbar-form:before,.navbar-form:after{display:table;line-height:0;content:""}.navbar-form:after{clear:both}.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px}.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0}.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px}.navbar-form .input-append,.navbar-form .input-prepend{margin-top:5px;white-space:nowrap}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0}.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0}.navbar-search .search-query{padding:4px 14px;margin-bottom:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.navbar-static-top{position:static;margin-bottom:0}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px}.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-right:0;padding-left:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0}.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px}.navbar-fixed-top{top:0}.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:0 1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 1px 10px rgba(0,0,0,0.1);box-shadow:0 1px 10px rgba(0,0,0,0.1)}.navbar-fixed-bottom{bottom:0}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:0 -1px 10px rgba(0,0,0,0.1);-moz-box-shadow:0 -1px 10px rgba(0,0,0,0.1);box-shadow:0 -1px 10px rgba(0,0,0,0.1)}.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0}.navbar .nav.pull-right{float:right;margin-right:0}.navbar .nav>li{float:left}.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777;text-decoration:none;text-shadow:0 1px 0 #fff}.navbar .nav .dropdown-toggle .caret{margin-top:8px}.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{color:#333;text-decoration:none;background-color:transparent}.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 8px rgba(0,0,0,0.125);box-shadow:inset 0 3px 8px rgba(0,0,0,0.125)}.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-right:5px;margin-left:5px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#ededed;*background-color:#e5e5e5;background-image:-moz-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f2f2f2),to(#e5e5e5));background-image:-webkit-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:-o-linear-gradient(top,#f2f2f2,#e5e5e5);background-image:linear-gradient(to bottom,#f2f2f2,#e5e5e5);background-repeat:repeat-x;border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2',endColorstr='#ffe5e5e5',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.075)}.navbar .btn-navbar:hover,.navbar .btn-navbar:focus,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#fff;background-color:#e5e5e5;*background-color:#d9d9d9}.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#ccc \9}.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,0.25);-moz-box-shadow:0 1px 0 rgba(0,0,0,0.25);box-shadow:0 1px 0 rgba(0,0,0,0.25)}.btn-navbar .icon-bar+.icon-bar{margin-top:3px}.navbar .nav>li>.dropdown-menu:before{position:absolute;top:-7px;left:9px;display:inline-block;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-left:7px solid transparent;border-bottom-color:rgba(0,0,0,0.2);content:''}.navbar .nav>li>.dropdown-menu:after{position:absolute;top:-6px;left:10px;display:inline-block;border-right:6px solid transparent;border-bottom:6px solid #fff;border-left:6px solid transparent;content:''}.navbar-fixed-bottom .nav>li>.dropdown-menu:before{top:auto;bottom:-7px;border-top:7px solid #ccc;border-bottom:0;border-top-color:rgba(0,0,0,0.2)}.navbar-fixed-bottom .nav>li>.dropdown-menu:after{top:auto;bottom:-6px;border-top:6px solid #fff;border-bottom:0}.navbar .nav li.dropdown>a:hover .caret,.navbar .nav li.dropdown>a:focus .caret{border-top-color:#333;border-bottom-color:#333}.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{color:#555;background-color:#e5e5e5}.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777;border-bottom-color:#777}.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555;border-bottom-color:#555}.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{right:12px;left:auto}.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{right:13px;left:auto}.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{right:100%;left:auto;margin-right:-1px;margin-left:0;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top,#222,#111);background-image:-webkit-gradient(linear,0 0,0 100%,from(#222),to(#111));background-image:-webkit-linear-gradient(top,#222,#111);background-image:-o-linear-gradient(top,#222,#111);background-image:linear-gradient(to bottom,#222,#111);background-repeat:repeat-x;border-color:#252525;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222',endColorstr='#ff111111',GradientType=0)}.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover,.navbar-inverse .brand:focus,.navbar-inverse .nav>li>a:focus{color:#fff}.navbar-inverse .brand{color:#999}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#fff;background-color:#111}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover,.navbar-inverse .navbar-link:focus{color:#fff}.navbar-inverse .divider-vertical{border-right-color:#222;border-left-color:#111}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{color:#fff;background-color:#111}.navbar-inverse .nav li.dropdown>a:hover .caret,.navbar-inverse .nav li.dropdown>a:focus .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999;border-bottom-color:#999}.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff}.navbar-inverse .navbar-search .search-query{color:#fff;background-color:#515151;border-color:#111;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1),0 1px 0 rgba(255,255,255,0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#ccc}.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333;text-shadow:0 1px 0 #fff;background-color:#fff;border:0;outline:0;-webkit-box-shadow:0 0 3px rgba(0,0,0,0.15);-moz-box-shadow:0 0 3px rgba(0,0,0,0.15);box-shadow:0 0 3px rgba(0,0,0,0.15)}.navbar-inverse .btn-navbar{color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e0e0e;*background-color:#040404;background-image:-moz-linear-gradient(top,#151515,#040404);background-image:-webkit-gradient(linear,0 0,0 100%,from(#151515),to(#040404));background-image:-webkit-linear-gradient(top,#151515,#040404);background-image:-o-linear-gradient(top,#151515,#040404);background-image:linear-gradient(to bottom,#151515,#040404);background-repeat:repeat-x;border-color:#040404 #040404 #000;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515',endColorstr='#ff040404',GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:focus,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#fff;background-color:#040404;*background-color:#000}.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000 \9}.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.breadcrumb>li{display:inline-block;*display:inline;text-shadow:0 1px 0 #fff;*zoom:1}.breadcrumb>li>.divider{padding:0 5px;color:#ccc}.breadcrumb>.active{color:#999}.pagination{margin:20px 0}.pagination ul{display:inline-block;*display:inline;margin-bottom:0;margin-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*zoom:1;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.pagination ul>li{display:inline}.pagination ul>li>a,.pagination ul>li>span{float:left;padding:4px 12px;line-height:20px;text-decoration:none;background-color:#fff;border:1px solid #ddd;border-left-width:0}.pagination ul>li>a:hover,.pagination ul>li>a:focus,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5}.pagination ul>.active>a,.pagination ul>.active>span{color:#999;cursor:default}.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover,.pagination ul>.disabled>a:focus{color:#999;cursor:default;background-color:transparent}.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-bottomleft:4px;-moz-border-radius-topleft:4px}.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-topright:4px;-moz-border-radius-bottomright:4px}.pagination-centered{text-align:center}.pagination-right{text-align:right}.pagination-large ul>li>a,.pagination-large ul>li>span{padding:11px 19px;font-size:17.5px}.pagination-large ul>li:first-child>a,.pagination-large ul>li:first-child>span{-webkit-border-bottom-left-radius:6px;border-bottom-left-radius:6px;-webkit-border-top-left-radius:6px;border-top-left-radius:6px;-moz-border-radius-bottomleft:6px;-moz-border-radius-topleft:6px}.pagination-large ul>li:last-child>a,.pagination-large ul>li:last-child>span{-webkit-border-top-right-radius:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;border-bottom-right-radius:6px;-moz-border-radius-topright:6px;-moz-border-radius-bottomright:6px}.pagination-mini ul>li:first-child>a,.pagination-small ul>li:first-child>a,.pagination-mini ul>li:first-child>span,.pagination-small ul>li:first-child>span{-webkit-border-bottom-left-radius:3px;border-bottom-left-radius:3px;-webkit-border-top-left-radius:3px;border-top-left-radius:3px;-moz-border-radius-bottomleft:3px;-moz-border-radius-topleft:3px}.pagination-mini ul>li:last-child>a,.pagination-small ul>li:last-child>a,.pagination-mini ul>li:last-child>span,.pagination-small ul>li:last-child>span{-webkit-border-top-right-radius:3px;border-top-right-radius:3px;-webkit-border-bottom-right-radius:3px;border-bottom-right-radius:3px;-moz-border-radius-topright:3px;-moz-border-radius-bottomright:3px}.pagination-small ul>li>a,.pagination-small ul>li>span{padding:2px 10px;font-size:11.9px}.pagination-mini ul>li>a,.pagination-mini ul>li>span{padding:0 6px;font-size:10.5px}.pager{margin:20px 0;text-align:center;list-style:none;*zoom:1}.pager:before,.pager:after{display:table;line-height:0;content:""}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#f5f5f5}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:default;background-color:#fff}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop,.modal-backdrop.fade.in{opacity:.8;filter:alpha(opacity=80)}.modal{position:fixed;top:10%;left:50%;z-index:1050;width:560px;margin-left:-280px;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;outline:0;-webkit-box-shadow:0 3px 7px rgba(0,0,0,0.3);-moz-box-shadow:0 3px 7px rgba(0,0,0,0.3);box-shadow:0 3px 7px rgba(0,0,0,0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box}.modal.fade{top:-25%;-webkit-transition:opacity .3s linear,top .3s ease-out;-moz-transition:opacity .3s linear,top .3s ease-out;-o-transition:opacity .3s linear,top .3s ease-out;transition:opacity .3s linear,top .3s ease-out}.modal.fade.in{top:10%}.modal-header{padding:9px 15px;border-bottom:1px solid #eee}.modal-header .close{margin-top:2px}.modal-header h3{margin:0;line-height:30px}.modal-body{position:relative;max-height:400px;padding:15px;overflow-y:auto}.modal-form{margin-bottom:0}.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;*zoom:1;-webkit-box-shadow:inset 0 1px 0 #fff;-moz-box-shadow:inset 0 1px 0 #fff;box-shadow:inset 0 1px 0 #fff}.modal-footer:before,.modal-footer:after{display:table;line-height:0;content:""}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.tooltip{position:absolute;z-index:1030;display:block;font-size:11px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.8;filter:alpha(opacity=80)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0}.thumbnails{margin-left:-20px;list-style:none;*zoom:1}.thumbnails:before,.thumbnails:after{display:table;line-height:0;content:""}.thumbnails:after{clear:both}.row-fluid .thumbnails{margin-left:0}.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px}.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.055);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.055);box-shadow:0 1px 3px rgba(0,0,0,0.055);-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}a.thumbnail:hover,a.thumbnail:focus{border-color:#08c;-webkit-box-shadow:0 1px 4px rgba(0,105,214,0.25);-moz-box-shadow:0 1px 4px rgba(0,105,214,0.25);box-shadow:0 1px 4px rgba(0,105,214,0.25)}.thumbnail>img{display:block;max-width:100%;margin-right:auto;margin-left:auto}.thumbnail .caption{padding:9px;color:#555}.media,.media-body{overflow:hidden;*overflow:visible;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{margin-left:0;list-style:none}.label,.badge{display:inline-block;padding:2px 4px;font-size:11.844px;font-weight:bold;line-height:14px;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25);white-space:nowrap;vertical-align:baseline;background-color:#999}.label{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.badge{padding-right:9px;padding-left:9px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px}.label:empty,.badge:empty{display:none}a.label:hover,a.label:focus,a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.label-important,.badge-important{background-color:#b94a48}.label-important[href],.badge-important[href]{background-color:#953b39}.label-warning,.badge-warning{background-color:#f89406}.label-warning[href],.badge-warning[href]{background-color:#c67605}.label-success,.badge-success{background-color:#468847}.label-success[href],.badge-success[href]{background-color:#356635}.label-info,.badge-info{background-color:#3a87ad}.label-info[href],.badge-info[href]{background-color:#2d6987}.label-inverse,.badge-inverse{background-color:#333}.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a}.btn .label,.btn .badge{position:relative;top:-1px}.btn-mini .label,.btn-mini .badge{top:0}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:0 0}to{background-position:40px 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f7f7f7;background-image:-moz-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#f5f5f5),to(#f9f9f9));background-image:-webkit-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:-o-linear-gradient(top,#f5f5f5,#f9f9f9);background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);background-repeat:repeat-x;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);-moz-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress .bar{float:left;width:0;height:100%;font-size:12px;color:#fff;text-align:center;text-shadow:0 -1px 0 rgba(0,0,0,0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top,#149bdf,#0480be);background-image:-webkit-gradient(linear,0 0,0 100%,from(#149bdf),to(#0480be));background-image:-webkit-linear-gradient(top,#149bdf,#0480be);background-image:-o-linear-gradient(top,#149bdf,#0480be);background-image:linear-gradient(to bottom,#149bdf,#0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width .6s ease;-moz-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);-moz-box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 1px 0 0 rgba(0,0,0,0.15),inset 0 -1px 0 rgba(0,0,0,0.15)}.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px}.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top,#ee5f5b,#c43c35);background-image:-webkit-gradient(linear,0 0,0 100%,from(#ee5f5b),to(#c43c35));background-image:-webkit-linear-gradient(top,#ee5f5b,#c43c35);background-image:-o-linear-gradient(top,#ee5f5b,#c43c35);background-image:linear-gradient(to bottom,#ee5f5b,#c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b',endColorstr='#ffc43c35',GradientType=0)}.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top,#62c462,#57a957);background-image:-webkit-gradient(linear,0 0,0 100%,from(#62c462),to(#57a957));background-image:-webkit-linear-gradient(top,#62c462,#57a957);background-image:-o-linear-gradient(top,#62c462,#57a957);background-image:linear-gradient(to bottom,#62c462,#57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462',endColorstr='#ff57a957',GradientType=0)}.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top,#5bc0de,#339bb9);background-image:-webkit-gradient(linear,0 0,0 100%,from(#5bc0de),to(#339bb9));background-image:-webkit-linear-gradient(top,#5bc0de,#339bb9);background-image:-o-linear-gradient(top,#5bc0de,#339bb9);background-image:linear-gradient(to bottom,#5bc0de,#339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff339bb9',GradientType=0)}.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top,#fbb450,#f89406);background-image:-webkit-gradient(linear,0 0,0 100%,from(#fbb450),to(#f89406));background-image:-webkit-linear-gradient(top,#fbb450,#f89406);background-image:-o-linear-gradient(top,#fbb450,#f89406);background-image:linear-gradient(to bottom,#fbb450,#f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450',endColorstr='#fff89406',GradientType=0)}.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255,255,255,0.15)),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255,255,255,0.15)),color-stop(0.75,rgba(255,255,255,0.15)),color-stop(0.75,transparent),to(transparent));background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-moz-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.accordion{margin-bottom:20px}.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.accordion-heading{border-bottom:0}.accordion-heading .accordion-toggle{display:block;padding:8px 15px}.accordion-toggle{cursor:pointer}.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5}.carousel{position:relative;margin-bottom:20px;line-height:1}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-moz-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#fff;text-align:center;background:#222;border:3px solid #fff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:.5;filter:alpha(opacity=50)}.carousel-control.right{right:15px;left:auto}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-indicators{position:absolute;top:15px;right:15px;z-index:5;margin:0;list-style:none}.carousel-indicators li{display:block;float:left;width:10px;height:10px;margin-left:5px;text-indent:-999px;background-color:#ccc;background-color:rgba(255,255,255,0.25);border-radius:5px}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:0;bottom:0;left:0;padding:15px;background:#333;background:rgba(0,0,0,0.75)}.carousel-caption h4,.carousel-caption p{line-height:20px;color:#fff}.carousel-caption h4{margin:0 0 5px}.carousel-caption p{margin-bottom:0}.hero-unit{padding:60px;margin-bottom:30px;font-size:18px;font-weight:200;line-height:30px;color:inherit;background-color:#eee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;color:inherit}.hero-unit li{line-height:30px}.pull-right{float:right}.pull-left{float:left}.hide{display:none}.show{display:block}.invisible{visibility:hidden}.affix{position:fixed} diff --git a/public/js/leaderboard.js b/public/js/leaderboard.js index d5117f3..d524a7a 100644 --- a/public/js/leaderboard.js +++ b/public/js/leaderboard.js @@ -50,9 +50,9 @@ $(document).ready(function () { list.append( $('
      ').append( $('
    • ', {class: "player"}) - .append($('
      ' + entry.playername + '
      ').addClass("ranking name").css("background-color", colourTable[entry.playerid])) - .append($('
      ' + entry.score + '
      ').addClass("ranking points").css("background-color", colourTable[entry.playerid])) - .append($('Withdraw').attr("href", "/withdraw/" + entry.playerid)))); + .append($('' + entry.playername + '').addClass("ranking name").css("background-color", colourTable[entry.playerid])) + .append($('' + entry.score + '').addClass("ranking points").css("background-color", colourTable[entry.playerid])) + .append($('Withdraw').attr("href", "/withdraw/" + entry.playerid).addClass("btn btn-warning")))); } $("#scoreboard").replaceWith(list); } From e83efbfc13b3324906eceb199a41cff0923a73b6 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Thu, 13 Jun 2013 14:43:17 +0200 Subject: [PATCH 15/25] bind server to all available interfaces --- lib/extreme_startup/web_server.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/extreme_startup/web_server.rb b/lib/extreme_startup/web_server.rb index fb31149..85d6849 100644 --- a/lib/extreme_startup/web_server.rb +++ b/lib/extreme_startup/web_server.rb @@ -17,6 +17,7 @@ module ExtremeStartup class WebServer < Sinatra::Base set :port, 3000 + set :bind, "0.0.0.0" set :static, true set :logging, true set :events, Events.new From e08d947d88ba88580c1d6ace5eb61d40ceb14344 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 19 Jun 2013 22:05:52 +0200 Subject: [PATCH 16/25] provide the ability to save/load the list of questions to ask at startup * questions' class list are stored in a `questions_factory.yaml` file * this list is looked up at startup and loaded if it exists --- lib/extreme_startup/question_factory.rb | 64 ++++++++++++------- spec/extreme_startup/question_factory_spec.rb | 54 +++++++++++++++- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 7549ada..7bb5799 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -500,8 +500,8 @@ def WeatherQuestion.weather_of(location) # is looked up using Yahoo! Weather service # see http://developer.yahoo.com/weather/ @@weather_in_cities = YAML.load_file(File.join(File.dirname(__FILE__), "locations.yaml")) - .map { |location| weather_of(location) } - #.map { |location| Location.new(location['city'],nil) } + .map { |location| Location.new(location['city'],nil) } + #.map { |location| weather_of(location) } def WeatherQuestion.weather_in_cities @@weather_in_cities @@ -589,27 +589,33 @@ def correct_answer class QuestionFactory attr_reader :round - def initialize + def initialize(profile = nil) @round = 1 - @question_types = [ - AdditionQuestion, - MaximumQuestion, - MultiplicationQuestion, - SquareCubeQuestion, - GeneralKnowledgeQuestion, - PrimesQuestion, - SubtractionQuestion, - FibonacciQuestion, - PowerQuestion, - TemperatureQuestion, - AdditionAdditionQuestion, - AdditionMultiplicationQuestion, - PressureQuestion, - MultiplicationAdditionQuestion, - AnagramQuestion, - WindQuestion, - ScrabbleQuestion - ] + if profile.nil? + @question_types = [ + AdditionQuestion, + MaximumQuestion, + MultiplicationQuestion, + SquareCubeQuestion, + GeneralKnowledgeQuestion, + PrimesQuestion, + SubtractionQuestion, + FibonacciQuestion, + PowerQuestion, + TemperatureQuestion, + AdditionAdditionQuestion, + AdditionMultiplicationQuestion, + PressureQuestion, + MultiplicationAdditionQuestion, + AnagramQuestion, + WindQuestion, + ScrabbleQuestion + ] + else + File.open(profile,'r') do |file| + @question_types = load_profile(file) + end + end end def next_question(player) @@ -623,6 +629,20 @@ def advance_round @round += 1 end + def save_profile (stream = nil) + if stream.nil? + stream = File.open("question_factory.yaml", "w") + end + + stream.write(@question_types.to_yaml) + ensure + stream.close + end + + def load_profile (stream) + YAML.load(stream) + end + end class WarmupQuestion < Question diff --git a/spec/extreme_startup/question_factory_spec.rb b/spec/extreme_startup/question_factory_spec.rb index 5512ff2..6aa526b 100644 --- a/spec/extreme_startup/question_factory_spec.rb +++ b/spec/extreme_startup/question_factory_spec.rb @@ -2,11 +2,63 @@ require 'extreme_startup/question_factory' require 'extreme_startup/player' +class File + def File.remove (filename) + File.delete(filename) if File.exist?(filename) + end +end + module ExtremeStartup describe QuestionFactory do let(:player) { Player.new("player one") } let(:factory) { QuestionFactory.new } - + + context 'when created' do + + after(:each) do + File.remove('question_factory.yaml') + File.remove('question_factory_test.yaml') + end + + it 'can send yaml profile listing question classes to stream' do + io = StringIO.new + factory.save_profile(io) + + io.string.should match '.*class.*ExtremeStartup::AdditionQuestion.*' + end + + it "default streaming profile to 'question_factory.yaml' file" do + factory.save_profile() + + File.file?('question_factory.yaml').should be_true + end + + it 'loads questions list from given file if it exists' do + + File.open('question_factory_test.yaml','w') do |file| + file.puts '---' + file.puts "- !ruby/class 'ExtremeStartup::AdditionQuestion'" + end + + factory = QuestionFactory.new('question_factory_test.yaml') + questions = 20.times.map { factory.next_question(player) } + questions.all? { |q| q.class == AdditionQuestion } + end + + it 'loads questions list from default file "question_factory.yaml" if it exists' do + + File.open('question_factory.yaml','w') do |file| + file.puts '---' + file.puts "- !ruby/class 'ExtremeStartup::MultiplicationQuestion'" + end + + factory = QuestionFactory.new + questions = 20.times.map { factory.next_question(player) } + questions.all? { |q| q.class == MultiplicationQuestion } + end + + end + context "in the first round" do it "creates both AdditionQuestions and SquareCubeQuestion" do questions = 10.times.map { factory.next_question(player) } From 33cd4aed7d4d39bd533ac45b5f433e61d9083b65 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 26 Jun 2013 10:51:56 +0200 Subject: [PATCH 17/25] merge rchatley/master --- README.md | 8 +++- lib/extreme_startup/question_factory.rb | 40 ++++++++++---------- lib/extreme_startup/views/leaderboard.haml | 4 +- lib/extreme_startup/views/personal_page.haml | 5 +-- 4 files changed, 31 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 79854b4..0daa419 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,19 @@ Getting started * ruby dk.rb init * Edit the file config.yml (Add the locations where ruby is installed e.g. c:\Ruby192) * ruby dk.rb install +* (For Ubuntu 12.04 onwards) + * Remove existing installation of Ruby and ruby related packages (do not use sudo or Ubuntu Software centre or any other Ubuntu package manager to install Ruby or any of its components) + * Remove rvm and related package from Ubuntu + * Install RVM using the instructions on https://rvm.io/ + * In case RVM is broken it can be fixed by going to http://stackoverflow.com/questions/9056008/installed-ruby-1-9-3-with-rvm-but-command-line-doesnt-show-ruby-v/9056395#9056395 + * Install Ruby and Rubygems using RVM only (for Rubygems use: 'rvm rubygems current' or 'rvm rubygems latest') * Install dependencies: ```` cd ../ gem install bundler -bundle +bundle install ```` * Start the game server diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 7bb5799..ae67748 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -2,7 +2,7 @@ require 'prime' module ExtremeStartup - class Question + class Question attr_reader :duration class << self @@ -261,14 +261,14 @@ def is_square(x) if (x ==0) return true end - (x % Math.sqrt(x)) == 0 + (x % (Math.sqrt(x).round(4))) == 0 end def is_cube(x) if x ==0 return true end - (x % Math.cbrt(x).truncate) == 0 + (x % (Math.cbrt(x).round(4))) == 0 end end @@ -592,30 +592,30 @@ class QuestionFactory def initialize(profile = nil) @round = 1 if profile.nil? - @question_types = [ - AdditionQuestion, - MaximumQuestion, - MultiplicationQuestion, - SquareCubeQuestion, - GeneralKnowledgeQuestion, - PrimesQuestion, - SubtractionQuestion, - FibonacciQuestion, - PowerQuestion, + @question_types = [ + AdditionQuestion, + MaximumQuestion, + MultiplicationQuestion, + SquareCubeQuestion, + GeneralKnowledgeQuestion, + PrimesQuestion, + SubtractionQuestion, + FibonacciQuestion, + PowerQuestion, TemperatureQuestion, - AdditionAdditionQuestion, - AdditionMultiplicationQuestion, + AdditionAdditionQuestion, + AdditionMultiplicationQuestion, PressureQuestion, - MultiplicationAdditionQuestion, - AnagramQuestion, + MultiplicationAdditionQuestion, + AnagramQuestion, WindQuestion, - ScrabbleQuestion - ] + ScrabbleQuestion + ] else File.open(profile,'r') do |file| @question_types = load_profile(file) end - end + end end def next_question(player) diff --git a/lib/extreme_startup/views/leaderboard.haml b/lib/extreme_startup/views/leaderboard.haml index 65b5f9c..0acbd16 100644 --- a/lib/extreme_startup/views/leaderboard.haml +++ b/lib/extreme_startup/views/leaderboard.haml @@ -14,10 +14,10 @@ %ul - leaderboard.each do |entry| %li.player - %div{:class => ["ranking", "name"]} #{html_escape(entry.playername)} + %div{:class => ["ranking", "name"]} + &= entry.playername %div{:class => ["ranking", "points"]} #{entry.score} :javascript refresh = function() { window.location.reload(); }; setTimeout(refresh, 10000); - diff --git a/lib/extreme_startup/views/personal_page.haml b/lib/extreme_startup/views/personal_page.haml index a9af634..03762c2 100644 --- a/lib/extreme_startup/views/personal_page.haml +++ b/lib/extreme_startup/views/personal_page.haml @@ -4,9 +4,9 @@ %link(rel="stylesheet" href="/css/bootstrap.min.css") %link(rel="stylesheet" href="/css/style.css") %body - %div{:class => ["hero-unit"]} + %div{:class => ["hero-unit"]} %h1 Extreme Startup - %h2 Hello, #{name}! + %h2 &= "Hello, #{name}!" %div{:class => ["span5"]} %h2 @@ -24,4 +24,3 @@ %div{:class => ["logline", "id", logline.result]} #{logline.id} %div{:class => ["logline", "result", logline.result]} result: #{logline.result} %div{:class => ["logline", "points", logline.result]} points awarded: #{logline.points} - \ No newline at end of file From 5569c69092591bdf11930a729b57470cc7ff7adf Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 26 Jun 2013 10:56:34 +0200 Subject: [PATCH 18/25] remove yahoo-weather version from gemfile --- Gemfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 7aa5b81..6f79b9d 100644 --- a/Gemfile +++ b/Gemfile @@ -14,5 +14,6 @@ gem 'capybara' gem 'launchy' gem 'json' -gem 'yahoo-weather', '1.2.1' +# needed for weather questions +gem 'yahoo-weather' From 86c437445272b559f2231fed48b6108c2e044951 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 1 Oct 2013 18:08:50 +0200 Subject: [PATCH 19/25] added question using minus symbol --- Gemfile.lock | 2 +- lib/extreme_startup/question_factory.rb | 10 +++++++ spec/extreme_startup/arithmetic_question.rb | 30 +++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 spec/extreme_startup/arithmetic_question.rb diff --git a/Gemfile.lock b/Gemfile.lock index 2fb1248..25ad1e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -86,4 +86,4 @@ DEPENDENCIES sinatra thin uuid - yahoo-weather (= 1.2.1) + yahoo-weather diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index ae67748..f826e60 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -177,6 +177,16 @@ def correct_answer end end + class MinusQuestion < BinaryMathsQuestion + def as_text + "what is #{@n1} - #{@n2}" + end + private + def correct_answer + @n1 - @n2 + end + end + class MultiplicationQuestion < BinaryMathsQuestion def as_text "what is #{@n1} multiplied by #{@n2}" diff --git a/spec/extreme_startup/arithmetic_question.rb b/spec/extreme_startup/arithmetic_question.rb new file mode 100644 index 0000000..4a17bc0 --- /dev/null +++ b/spec/extreme_startup/arithmetic_question.rb @@ -0,0 +1,30 @@ +require 'spec_helper' +require 'extreme_startup/question_factory' +require 'extreme_startup/player' + +module ExtremeStartup + describe MinusQuestion do + let(:question) { MinusQuestion.new(Player.new) } + + it "converts to a string" do + question.as_text.should =~ /what is \d+ - \d+/i + end + + context "when the numbers are known" do + let(:question) { MinusQuestion.new(Player.new, 2,3) } + + it "converts to the right string" do + question.as_text.should =~ /what is 2 - 3/i + end + + it "identifies a correct answer" do + question.answered_correctly?("-1").should be_true + end + + it "identifies an incorrect answer" do + question.answered_correctly?("77").should be_false + end + end + + end +end From 87458d8fa271e4115987b3815da1350cee57ef35 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 1 Oct 2013 18:15:30 +0200 Subject: [PATCH 20/25] added plus with symbol question --- lib/extreme_startup/question_factory.rb | 10 ++++++++ spec/extreme_startup/arithmetic_question.rb | 26 +++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index f826e60..357287f 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -167,6 +167,16 @@ def correct_answer end end + class PlusQuestion < BinaryMathsQuestion + def as_text + "what is #{@n1} + #{@n2}" + end + private + def correct_answer + @n1 + @n2 + end + end + class SubtractionQuestion < BinaryMathsQuestion def as_text "what is #{@n1} minus #{@n2}" diff --git a/spec/extreme_startup/arithmetic_question.rb b/spec/extreme_startup/arithmetic_question.rb index 4a17bc0..cef3fa4 100644 --- a/spec/extreme_startup/arithmetic_question.rb +++ b/spec/extreme_startup/arithmetic_question.rb @@ -3,6 +3,7 @@ require 'extreme_startup/player' module ExtremeStartup + describe MinusQuestion do let(:question) { MinusQuestion.new(Player.new) } @@ -27,4 +28,29 @@ module ExtremeStartup end end + + describe PlusQuestion do + let(:question) { PlusQuestion.new(Player.new) } + + it "converts to a string" do + question.as_text.should =~ /what is \d+ + \d+/i + end + + context "when the numbers are known" do + let(:question) { PlusQuestion.new(Player.new, 2,3) } + + it "converts to the right string" do + question.as_text.should =~ /what is 2 + 3/i + end + + it "identifies a correct answer" do + question.answered_correctly?("5").should be_true + end + + it "identifies an incorrect answer" do + question.answered_correctly?("77").should be_false + end + end + + end end From 7f149f53242d994280fded024616a056b997f6ba Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 1 Oct 2013 18:22:24 +0200 Subject: [PATCH 21/25] added mult question with symbol --- lib/extreme_startup/question_factory.rb | 10 +++++++ ...uestion.rb => arithmetic_question_spec.rb} | 29 +++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) rename spec/extreme_startup/{arithmetic_question.rb => arithmetic_question_spec.rb} (63%) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 357287f..c79ee51 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -207,6 +207,16 @@ def correct_answer end end + class MultQuestion < BinaryMathsQuestion + def as_text + "what is #{@n1} * #{@n2}" + end + private + def correct_answer + @n1 * @n2 + end + end + class AdditionAdditionQuestion < TernaryMathsQuestion def as_text "what is #{@n1} plus #{@n2} plus #{@n3}" diff --git a/spec/extreme_startup/arithmetic_question.rb b/spec/extreme_startup/arithmetic_question_spec.rb similarity index 63% rename from spec/extreme_startup/arithmetic_question.rb rename to spec/extreme_startup/arithmetic_question_spec.rb index cef3fa4..3bfb8d8 100644 --- a/spec/extreme_startup/arithmetic_question.rb +++ b/spec/extreme_startup/arithmetic_question_spec.rb @@ -33,14 +33,14 @@ module ExtremeStartup let(:question) { PlusQuestion.new(Player.new) } it "converts to a string" do - question.as_text.should =~ /what is \d+ + \d+/i + question.as_text.should =~ /what is \d+ \+ \d+/i end context "when the numbers are known" do let(:question) { PlusQuestion.new(Player.new, 2,3) } it "converts to the right string" do - question.as_text.should =~ /what is 2 + 3/i + question.as_text.should =~ /what is 2 \+ 3/i end it "identifies a correct answer" do @@ -53,4 +53,29 @@ module ExtremeStartup end end + + describe MultQuestion do + let(:question) { MultQuestion.new(Player.new) } + + it "converts to a string" do + question.as_text.should =~ /what is \d+ \* \d+/i + end + + context "when the numbers are known" do + let(:question) { MultQuestion.new(Player.new, 2,3) } + + it "converts to the right string" do + question.as_text.should =~ /what is 2 \* 3/i + end + + it "identifies a correct answer" do + question.answered_correctly?("6").should be_true + end + + it "identifies an incorrect answer" do + question.answered_correctly?("77").should be_false + end + end + + end end From 2aa3b0b0ee2fad0370efed6857bae351a3bac6e6 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 1 Oct 2013 18:46:28 +0200 Subject: [PATCH 22/25] added generalized arithmetic questions --- lib/extreme_startup/question_factory.rb | 34 +++++++++++++++++++ .../arithmetic_question_spec.rb | 21 ++++++++++++ 2 files changed, 55 insertions(+) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index c79ee51..43dea95 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -217,6 +217,40 @@ def correct_answer end end + # Generates arbitrary arithmetic questions with +, * and - symbols + # + # Generated expression should be evaluated from left to right respecting usual precedence of + # operators: * binds tighter than +. This means the expression + # 12 + 3 * 4 + 5 + # + # evaluates to 29 and not 65. + # + class GeneralArithmeticQuestion < Question + def initialize(player, *tokens) + @tokens = tokens if tokens.any? + end + + def points + 60 + end + + def as_text + "what is " + expression + end + + private + + def expression + @tokens.inject('') do |txt, tok| + txt + ("%s " % tok) + end + end + + def correct_answer + eval(expression) + end + end + class AdditionAdditionQuestion < TernaryMathsQuestion def as_text "what is #{@n1} plus #{@n2} plus #{@n3}" diff --git a/spec/extreme_startup/arithmetic_question_spec.rb b/spec/extreme_startup/arithmetic_question_spec.rb index 3bfb8d8..c48fda7 100644 --- a/spec/extreme_startup/arithmetic_question_spec.rb +++ b/spec/extreme_startup/arithmetic_question_spec.rb @@ -78,4 +78,25 @@ module ExtremeStartup end end + + describe GeneralArithmeticQuestion do + let(:question) { GeneralArithmeticQuestion.new(Player.new) } + + it "yields 60 points when answered correctly" do + question.points.should == 60 + end + + context "when numbers and operators are known" do + let(:question) { GeneralArithmeticQuestion.new(Player.new, 12, '+', 3, '*', 4, '+', 5) } + + it "converts to a string" do + question.as_text.should =~ /what is 12 \+ 3 \* 4 \+ 5/ + end + + it "identifies a correct answer" do + question.answered_correctly?("29").should be_true + end + + end + end end From 251acf12ef977379cc09a42ea2b3747b088a870f Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 2 Oct 2013 16:30:26 +0200 Subject: [PATCH 23/25] name displayed in personal page had bad layout --- lib/extreme_startup/views/personal_page.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/extreme_startup/views/personal_page.haml b/lib/extreme_startup/views/personal_page.haml index 03762c2..d9b6c25 100644 --- a/lib/extreme_startup/views/personal_page.haml +++ b/lib/extreme_startup/views/personal_page.haml @@ -6,7 +6,7 @@ %body %div{:class => ["hero-unit"]} %h1 Extreme Startup - %h2 &= "Hello, #{name}!" + %h2 Hello, #{name}! %div{:class => ["span5"]} %h2 From 253988f806f3c71b62f75a88cd587a2721c280cb Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Wed, 2 Oct 2013 16:31:56 +0200 Subject: [PATCH 24/25] include general arithmetic in questions list --- lib/extreme_startup/question_factory.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 43dea95..5219f7c 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -2,6 +2,7 @@ require 'prime' module ExtremeStartup + class Question attr_reader :duration @@ -671,6 +672,7 @@ def initialize(profile = nil) AdditionMultiplicationQuestion, PressureQuestion, MultiplicationAdditionQuestion, + GeneralArithmeticQuestion, AnagramQuestion, WindQuestion, ScrabbleQuestion From 41249e26173c3532adc210734c814390629087ad Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 15 Oct 2013 18:50:59 +0200 Subject: [PATCH 25/25] fix general arithmetic questions --- lib/extreme_startup/question_factory.rb | 49 +++++++++++++++---- .../arithmetic_question_spec.rb | 13 ++++- 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/lib/extreme_startup/question_factory.rb b/lib/extreme_startup/question_factory.rb index 5219f7c..aee7302 100644 --- a/lib/extreme_startup/question_factory.rb +++ b/lib/extreme_startup/question_factory.rb @@ -218,7 +218,7 @@ def correct_answer end end - # Generates arbitrary arithmetic questions with +, * and - symbols + # Generates arbitrary arithmetic questions with 'plus', 'times' and 'minus' operators. # # Generated expression should be evaluated from left to right respecting usual precedence of # operators: * binds tighter than +. This means the expression @@ -227,8 +227,15 @@ def correct_answer # evaluates to 29 and not 65. # class GeneralArithmeticQuestion < Question + + @@operators = ['plus', 'minus', 'times'] + def initialize(player, *tokens) - @tokens = tokens if tokens.any? + if tokens.any? + @tokens = tokens + else + @tokens = generate_expression() + end end def points @@ -241,14 +248,36 @@ def as_text private - def expression + def generate_expression + exp = [] + (rand(8) + 1).times { exp += [ rand(100), @@operators.sample ] } + exp + [ rand(100) ] + end + + def expression @tokens.inject('') do |txt, tok| txt + ("%s " % tok) end end + def evaluable_expression + @tokens.map do |tok| + if tok == "plus" then + "+" + elsif tok == "times" then + "*" + elsif tok == "minus" then + "-" + else + tok + end + end.inject('') do |txt, tok| + txt + ("%s " % tok) + end + end + def correct_answer - eval(expression) + eval(evaluable_expression) end end @@ -664,17 +693,19 @@ def initialize(profile = nil) SquareCubeQuestion, GeneralKnowledgeQuestion, PrimesQuestion, + PlusQuestion, SubtractionQuestion, FibonacciQuestion, PowerQuestion, - TemperatureQuestion, + MinusQuestion, +# TemperatureQuestion, AdditionAdditionQuestion, + MultQuestion, AdditionMultiplicationQuestion, - PressureQuestion, - MultiplicationAdditionQuestion, - GeneralArithmeticQuestion, +# PressureQuestion, AnagramQuestion, - WindQuestion, + GeneralArithmeticQuestion, +# WindQuestion, ScrabbleQuestion ] else diff --git a/spec/extreme_startup/arithmetic_question_spec.rb b/spec/extreme_startup/arithmetic_question_spec.rb index c48fda7..88d5d3f 100644 --- a/spec/extreme_startup/arithmetic_question_spec.rb +++ b/spec/extreme_startup/arithmetic_question_spec.rb @@ -87,10 +87,10 @@ module ExtremeStartup end context "when numbers and operators are known" do - let(:question) { GeneralArithmeticQuestion.new(Player.new, 12, '+', 3, '*', 4, '+', 5) } + let(:question) { GeneralArithmeticQuestion.new(Player.new, 12, 'plus', 3, 'times', 4, 'plus', 5) } it "converts to a string" do - question.as_text.should =~ /what is 12 \+ 3 \* 4 \+ 5/ + question.as_text.should =~ /what is 12 plus 3 times 4 plus 5/ end it "identifies a correct answer" do @@ -98,5 +98,14 @@ module ExtremeStartup end end + + context "when numbers and operators are not known" do + let(:question) { GeneralArithmeticQuestion.new(Player.new) } + + it "generates a random expression" do + question.as_text.should =~ /what is (\d+ (plus|minus|times))+ \d+/ + end + + end end end