├── .gitignore ├── weather ├── fog.png ├── clear.png ├── hazy.png ├── rain.png ├── sleet.png ├── snow.png ├── sunny.png ├── cloudy.png ├── flurries.png ├── nt_clear.png ├── nt_fog.png ├── nt_hazy.png ├── nt_rain.png ├── nt_sleet.png ├── nt_snow.png ├── nt_sunny.png ├── tstorms.png ├── chancerain.png ├── chancesnow.png ├── nt_cloudy.png ├── nt_tstorms.png ├── chancesleet.png ├── chancetstorms.png ├── mostlycloudy.png ├── mostlysunny.png ├── nt_chancerain.png ├── nt_chancesnow.png ├── nt_flurries.png ├── partlycloudy.png ├── partlysunny.png ├── chanceflurries.png ├── nt_chancesleet.png ├── nt_mostlycloudy.png ├── nt_mostlysunny.png ├── nt_partlycloudy.png ├── nt_partlysunny.png ├── nt_chanceflurries.png └── nt_chancetstorms.png ├── launcher-icon-1x.png ├── launcher-icon-2x.png ├── launcher-icon-3x.png ├── launcher-icon-4x.png ├── fonts ├── FontAwesome.otf ├── fontawesome-webfont.eot ├── fontawesome-webfont.ttf └── fontawesome-webfont.woff ├── launcher-icon-1-5x.png ├── launcher-icon-0-75x.png ├── screenshots ├── screenshot1.png └── screenshot2.png ├── manifest.json ├── LICENSE ├── data.php ├── config.js ├── css ├── style.css └── font-awesome.min.css ├── index.html ├── README.md └── js ├── dashboard.js └── jquery.atmosphere.js /.gitignore: -------------------------------------------------------------------------------- 1 | /config.local.js 2 | -------------------------------------------------------------------------------- /weather/fog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/fog.png -------------------------------------------------------------------------------- /weather/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/clear.png -------------------------------------------------------------------------------- /weather/hazy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/hazy.png -------------------------------------------------------------------------------- /weather/rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/rain.png -------------------------------------------------------------------------------- /weather/sleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/sleet.png -------------------------------------------------------------------------------- /weather/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/snow.png -------------------------------------------------------------------------------- /weather/sunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/sunny.png -------------------------------------------------------------------------------- /launcher-icon-1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/launcher-icon-1x.png -------------------------------------------------------------------------------- /launcher-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/launcher-icon-2x.png -------------------------------------------------------------------------------- /launcher-icon-3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/launcher-icon-3x.png -------------------------------------------------------------------------------- /launcher-icon-4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/launcher-icon-4x.png -------------------------------------------------------------------------------- /weather/cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/cloudy.png -------------------------------------------------------------------------------- /weather/flurries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/flurries.png -------------------------------------------------------------------------------- /weather/nt_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_clear.png -------------------------------------------------------------------------------- /weather/nt_fog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_fog.png -------------------------------------------------------------------------------- /weather/nt_hazy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_hazy.png -------------------------------------------------------------------------------- /weather/nt_rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_rain.png -------------------------------------------------------------------------------- /weather/nt_sleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_sleet.png -------------------------------------------------------------------------------- /weather/nt_snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_snow.png -------------------------------------------------------------------------------- /weather/nt_sunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_sunny.png -------------------------------------------------------------------------------- /weather/tstorms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/tstorms.png -------------------------------------------------------------------------------- /fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /launcher-icon-1-5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/launcher-icon-1-5x.png -------------------------------------------------------------------------------- /weather/chancerain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/chancerain.png -------------------------------------------------------------------------------- /weather/chancesnow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/chancesnow.png -------------------------------------------------------------------------------- /weather/nt_cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_cloudy.png -------------------------------------------------------------------------------- /weather/nt_tstorms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_tstorms.png -------------------------------------------------------------------------------- /launcher-icon-0-75x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/launcher-icon-0-75x.png -------------------------------------------------------------------------------- /weather/chancesleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/chancesleet.png -------------------------------------------------------------------------------- /weather/chancetstorms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/chancetstorms.png -------------------------------------------------------------------------------- /weather/mostlycloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/mostlycloudy.png -------------------------------------------------------------------------------- /weather/mostlysunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/mostlysunny.png -------------------------------------------------------------------------------- /weather/nt_chancerain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_chancerain.png -------------------------------------------------------------------------------- /weather/nt_chancesnow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_chancesnow.png -------------------------------------------------------------------------------- /weather/nt_flurries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_flurries.png -------------------------------------------------------------------------------- /weather/partlycloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/partlycloudy.png -------------------------------------------------------------------------------- /weather/partlysunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/partlysunny.png -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/screenshots/screenshot2.png -------------------------------------------------------------------------------- /weather/chanceflurries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/chanceflurries.png -------------------------------------------------------------------------------- /weather/nt_chancesleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_chancesleet.png -------------------------------------------------------------------------------- /weather/nt_mostlycloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_mostlycloudy.png -------------------------------------------------------------------------------- /weather/nt_mostlysunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_mostlysunny.png -------------------------------------------------------------------------------- /weather/nt_partlycloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_partlycloudy.png -------------------------------------------------------------------------------- /weather/nt_partlysunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_partlysunny.png -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /weather/nt_chanceflurries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_chanceflurries.png -------------------------------------------------------------------------------- /weather/nt_chancetstorms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HomeAutomationForGeeks/openhab-controlpanel-advanced/HEAD/weather/nt_chancetstorms.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HA Dashboard", 3 | "icons": [ 4 | { 5 | "src": "launcher-icon-0-75x.png", 6 | "sizes": "36x36", 7 | "type": "image/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "launcher-icon-1x.png", 12 | "sizes": "48x48", 13 | "type": "image/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "launcher-icon-1-5x.png", 18 | "sizes": "72x72", 19 | "type": "image/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "launcher-icon-2x.png", 24 | "sizes": "96x96", 25 | "type": "image/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "launcher-icon-3x.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "launcher-icon-4x.png", 36 | "sizes": "192x192", 37 | "type": "image/png", 38 | "density": "4.0" 39 | } 40 | ], 41 | "start_url": "index.html", 42 | "display": "standalone", 43 | "orientation": "landscape" 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 HomeAutomationForGeeks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /data.php: -------------------------------------------------------------------------------- 1 | connect_error) { 16 | die("Connection failed: " . $conn->connect_error); 17 | } 18 | 19 | class LogEntry 20 | { 21 | public $timestamp; 22 | public $value; 23 | } 24 | 25 | $response = array(); 26 | 27 | if (isset($_GET["item"])) 28 | { 29 | $itemname = $conn->real_escape_string($_GET["item"]); 30 | 31 | $sql = "SELECT ItemId FROM openhab.Items WHERE ItemName='$itemname' limit 1;"; 32 | $result = $conn->query($sql); 33 | $itemid = $result->fetch_assoc()["ItemId"]; 34 | 35 | $sql = "SELECT Time, Value FROM openhab.Item".$itemid." ORDER BY Time DESC LIMIT 6;"; 36 | $result = $conn->query($sql); 37 | 38 | if ($result->num_rows > 0) { 39 | // output data of each row 40 | while($row = $result->fetch_assoc()) { 41 | $entry = new LogEntry(); 42 | // some DateTime parsing to drop the "seconds" part (space is precious on our widgets) 43 | $new_date = DateTime::createFromFormat('Y-m-d H:i:s', $row["Time"]); 44 | $entry->timestamp = $new_date->format('Y-m-d H:i'); 45 | $entry->value = $row["Value"]; 46 | $response[] = $entry; 47 | } 48 | } 49 | } 50 | 51 | header('Content-Type: application/json'); 52 | echo json_encode($response, JSON_PRETTY_PRINT); 53 | 54 | $conn->close(); 55 | ?> -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | // 2 | // Overwrite these with your own settings in config.local.js! 3 | // 4 | 5 | // OpenHAB URL (without the "http://" part; note that you can't use "localhost" or "127.0.0.1" or OpenHAB will send "Connection Refused") 6 | var openhabURL = "192.168.1.80:8080/rest/items/"; // TODO: change this to your Pi's IP address 7 | // Log URL 8 | var logURL = "http://192.168.1.80/data.php?item="; // TODO: change this to your Pi's IP address 9 | 10 | // Wunderground API 11 | // TODO: insert your API key and location code (example location code: pws:KNYNEWYO118) 12 | var weatherURL = "http://api.wunderground.com/api/YOUR_API_KEY_HERE/conditions/q/YOUR_LOCATION_CODE_HERE.json"; 13 | var forecastURL = "http://api.wunderground.com/api/YOUR_API_KEY_HERE/forecast/q/YOUR_LOCATION_CODE_HERE.json"; 14 | 15 | // Logging level (0 = off, 1 = ajax errors only, 2 = some logging, 3 = full logging) 16 | // This logs to the developer console: in Google Chrome, press F12 and make sure the "Console" tab is selected 17 | var loggingLevel = 3; 18 | // How often to check weather (in milliseconds) 19 | var weatherFrequency = 600000; // every 600 seconds (= 10 minutes) 20 | var forecastFrequency = 3600000; // every 3600 seconds (= 1 hour) 21 | 22 | var reloadHour = 4; // time at which to reload the entire page. Helps keep things fresh (Chrome tends to time out the websockets). 24 hour format (0 = midnight, 23 = 11 PM) 23 | 24 | var weatherUnit = "fc"; //c for celsius, f for farenheit, cf for celsius (farenheit), fc for farenheit (celsius) 25 | var tempRound = false; //rounds all temperatures to whole numbers (so today and forecast match) 26 | 27 | var twelveHour = false; //if true, time is displayed as 12-hour rather than 24-hour 28 | var twelveHourAMPM = true; //if true, 'am' or 'pm' is appended to the time. Probably doesn't make sense unless twelveHour is also true. 29 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | @import url(font-awesome.min.css); 2 | @import url("http://fonts.googleapis.com/css?family=Roboto:100,300,100italic,300italic"); 3 | 4 | /* Widgets */ 5 | 6 | widget { 7 | float: left; 8 | width: 200px; 9 | height: 180px; 10 | padding: 10px; 11 | margin: 8px; 12 | text-align: center; 13 | vertical-align: middle; 14 | border-radius: 4px; 15 | border: solid 1px rgba(255, 255, 255, 0.3); 16 | background-color: #21232D; 17 | } 18 | 19 | .icon { 20 | text-decoration: none; 21 | border-bottom: none; 22 | position: relative; 23 | background: #272833; 24 | border-radius: 100%; 25 | display: inline-block; 26 | text-align: center; 27 | line-height: 6em; 28 | width: 6em; 29 | margin: 0.4em; 30 | } 31 | 32 | .icon:before { 33 | -moz-osx-font-smoothing: grayscale; 34 | -webkit-font-smoothing: antialiased; 35 | font-family: FontAwesome; 36 | font-style: normal; 37 | font-weight: normal; 38 | text-transform: none !important; 39 | font-size: 2.25em; 40 | /* the next two lines make it so the outline of the icon is shown instead of the icon itself */ 41 | color: #272833 !important; 42 | text-shadow: 1px 0 0 #ffffff, -1px 0 0 #ffffff, 0 1px 0 #ffffff, 0 -1px 0 #ffffff; 43 | } 44 | 45 | .sensor { 46 | font-size: 2.5em; 47 | font-weight: bold; 48 | } 49 | 50 | .switchon { 51 | background-color: #267A00 !important; 52 | } 53 | 54 | .trash { 55 | background-color: #AF9200 !important; 56 | } 57 | 58 | .dooropen { 59 | background-color: #BC1D1A !important; 60 | } 61 | 62 | .logrow { 63 | font-size: 0.3em; 64 | text-align: left; 65 | font-weight: normal; 66 | } 67 | 68 | /* Top bar */ 69 | 70 | .topbar{ 71 | position: fixed; 72 | top: 0; 73 | width: 100%; 74 | height: 37px; 75 | background-color: #21232D; 76 | border-bottom: solid 1px rgba(255, 255, 255, 0.3); 77 | clear: both; 78 | font-size: 1.5em; 79 | } 80 | 81 | .clock{ 82 | width: 120px; 83 | text-align: center; 84 | font-weight: bold; 85 | margin: 0 auto; 86 | font-family: consolas; 87 | padding-top: 1px; 88 | } 89 | 90 | .date{ 91 | float: left; 92 | line-height: 1em; 93 | margin: 8px 0 0 5px; 94 | font-size: 0.75em; 95 | } 96 | 97 | .refresh{ 98 | width: 30px; 99 | float: right; 100 | line-height: 1em; 101 | margin: 6px 13px 0 0; 102 | cursor: pointer; 103 | font-size: 0.8em; 104 | } 105 | 106 | /* Bottom bar */ 107 | 108 | .weatherbar{ 109 | position: fixed; 110 | bottom: 0; 111 | width: 100%; 112 | height: 100px; 113 | background-color: #21232D; 114 | border-top: solid 1px rgba(255, 255, 255, 0.3); 115 | clear: both; 116 | } 117 | 118 | .weather_observation{ 119 | position: absolute; 120 | bottom: 0; 121 | left: 0; 122 | width: 50%; 123 | height: 100px; 124 | } 125 | 126 | .weather_forecast{ 127 | position: absolute; 128 | bottom: 0; 129 | left: 50%; 130 | width: 50%; 131 | height: 100px; 132 | border-left: solid 1px rgba(255, 255, 255, 0.3); 133 | } 134 | 135 | /* General stuff */ 136 | 137 | html, body { 138 | background-color: #1c1d26; 139 | color: rgba(255, 255, 255, 0.75); 140 | font-family: "Roboto", Helvetica, sans-serif; 141 | font-size: 15pt; 142 | font-weight: 100; 143 | line-height: 1.75em; 144 | } 145 | 146 | h3 { 147 | color: #ffffff; 148 | font-weight: 300; 149 | font-size: 1.35em; 150 | line-height: 1.5em; 151 | margin: 0 0 1em 0; 152 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | HA Dashboard 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 |
38 |
2015-01-01
39 |
40 |
12:00
41 |
42 | 43 |
44 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 | 79 | 80 |
81 |
82 |
NOW
83 | 84 |
?
85 |
?
86 |
?
87 |
88 |
89 |
?
90 | 91 |
?
92 |
?
93 |
?
94 |
95 | 102 |
?
103 |
?
104 |
105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openhab-controlpanel 2 | A simple dashboard using the OpenHAB REST services. 3 | 4 | ### See also 5 | 6 | http://www.homeautomationforgeeks.com/dashboard.shtml 7 | 8 | ## Table of Contents 9 | 10 | * [Screenshots](#screenshots) 11 | * [Features](#features) 12 | * [Installing Prerequisitesn](#installing-prerequisites) 13 | * [The Weather Underground API](#the-weather-underground-api) 14 | * [Installation](#installation) 15 | * [Included Dependencies](#included-dependencies) 16 | * [Full Screen Mode On Your Android Tablet](#full-screen-mode-on-your-android-tablet) 17 | 18 | ## Screenshots 19 | 20 | ### Example setup 21 | 22 | ![Example setup](https://github.com/HomeAutomationForGeeks/openhab-controlpanel/raw/master/screenshots/screenshot1.png) 23 | 24 | ### Running full screen on a cheap ($50) Android tablet 25 | 26 | ![Running on an Android tablet](https://github.com/HomeAutomationForGeeks/openhab-controlpanel/raw/master/screenshots/screenshot2.png) 27 | 28 | ## Features 29 | 30 | * **Switches**: display the on/off status of switches (green = on) + tap to toggle them 31 | * **Open/close sensors**: display the open/close status of eg. a door sensor (red = open) + tap to view a log of recent entries 32 | * **Data sensors**: display sensor data + tap to view a log of recent data 33 | * **Weather data**: get up-to-date weather info from a nearby weather station 34 | * **Trash reminder**: on trash days, a trash icon appears as a reminder. On recycle days, a recycle icon appears 35 | 36 | ## Installing Prerequisites 37 | 38 | ### Raspberry Pi / Linux 39 | 40 | This dashboard was created on a Raspberry Pi 2 but can probably run on most systems with very little modification. 41 | 42 | #### Apache 43 | 44 | To install Apache on your Raspberry Pi: 45 | 46 | `sudo apt-get install apache2` 47 | 48 | To check if Apache works, just browse to your Pi's IP address (eg. http://192.168.1.80). If Apache installed correctly, you'll see a page saying *"It works!"*. 49 | 50 | #### PHP 51 | 52 | If you enabled *persistence* in OpenHAB and want to be able to view logs, you also need to install PHP, and the MySQL PHP libraries: 53 | 54 | `sudo apt-get install php5 libapache2-mod-php5` 55 | 56 | `sudo apt-get install php5-mysql` 57 | 58 | ### The Weather Underground API 59 | 60 | The weather bar uses the [Weather Underground API](http://www.wunderground.com/weather/api/) to get weather data. You can remove it if you don't want this. If you keep it, you'll need: 61 | 62 | * A (free) Weather Underground API key 63 | * A weather station ID 64 | 65 | #### Start by requesting an API key: 66 | 67 | * Go to the [Weather Underground API](http://www.wunderground.com/weather/api/) and click the Sign Up button 68 | * Create an account, and activate it using the confirmation e-mail 69 | * Once you're logged in, you need to [request an API key](http://www.wunderground.com/weather/api/d/pricing.html): you can select the biggest (Anvil) plan and the history add-on: as long as you select the Developer option it will be $0. 70 | * Make a note of your API key. 71 | * Also note that the free key allows **500 calls per day**. By default, the weather refresh interval on the sample dashboard is every 10 minutes, which would make 144 calls per day. If you change it, make sure you don't go over your quota - or buy an upgrade plan. 72 | 73 | #### The next step is to find a weather station ID: 74 | 75 | * Go to the [Weather Underground home page](http://www.wunderground.com/). 76 | * Search for your location - it will take you to the weather detail page for a nearby station. 77 | * Optional: change the default station to one closer to you by clicking on "Change Station". 78 | * Click on the weather station name - it's right under your location name. 79 | * On this page the station's ID will be right next to the station name - make a note of that ID. 80 | * **Example:** if you search for "New York, New York", the default station is "Flatiron". If you click on Flatiron, the station's page lists the ID as "KNYNEWYO118". 81 | * The full URL is then *(including the "pws:" prefix for weather station codes)*: http://api.wunderground.com/api/YOUR_API_KEY_HERE/conditions/q/pws:KNYNEWYO118.json 82 | 83 | ## Installation 84 | 85 | ### Download the project files 86 | 87 | [Download the dashboard](https://github.com/HomeAutomationForGeeks/openhab-controlpanel/archive/master.zip) and unzip it to /var/www on your Pi. It won't work out of the box: you'll need to open **index.html** and look for all the places where it says **TODO**. That's where you need to change things like insert your weather API key and configure widgets. 88 | 89 | You'll probably want to look through the code to understand what's going on. There are comments explaining things throughout. 90 | The main files are: 91 | 92 | * **index.html**: set up general configuration + add, remove and configure widgets ([HTML](http://www.w3schools.com/html/)) 93 | * **js/dashboard.js**: main logic ([javascript](http://www.w3schools.com/js/)/[jQuery](https://jquery.com/)) 94 | * **css/style.css**: widget style ([CSS](http://www.w3schools.com/css/)) 95 | * **data.php**: used to query the openhab logs in MySQL and return the last 6 entries as JSON ([PHP](http://www.w3schools.com/php/)) 96 | 97 | There's also logging: you configure the amount of logging by changing the loggingLevel value in index.html. **To see the log output**, press F12 while in your browser and make sure the "console" tab is selected. 98 | 99 | ## Included Dependencies 100 | 101 | * [JQuery](https://jquery.com/) v1.11.1 102 | * [JQuery Atmosphere](https://github.com/Atmosphere/atmosphere-javascript) 103 | * [Font Awesome](https://fortawesome.github.io/Font-Awesome/) 104 | 105 | ## Full Screen Mode on your Android tablet 106 | 107 | If you're using an Android tablet as your screen, you'll want the browser to work in full screen mode so the various tool bars don't take up precious screen space. 108 | 109 | There are probably several ways to do it - here's one way: 110 | 111 | 1. Install the [Chrome For Android browser](https://play.google.com/store/apps/details?id=com.android.chrome), or update it to the latest version (you need at least version 39). 112 | 2. Browse to to your dashboard site, and once it's loaded open the Settings menu and click **"Add to homescreen"**. This will create a launcher icon, and allow it to start full-screen. If you're curious, [see here](https://developer.chrome.com/multidevice/android/installtohomescreen) for how that works. 113 | 3. Install the [Immersive Mode](https://play.google.com/store/apps/details?id=com.gmd.immersive) app. Once installed open the app and have it start on boot. 114 | 4. Open your dashboard site using the launcher icon you created earlier. The Chrome interface (address bar etc) should be hidden, but you can still see the Android notifications bar at the top and actions bar at the bottom. 115 | 5. Slide down the Android notifications from the top, and select the right-most icon in the Immersive Mode notification. This should hide both bars - your dashboard is now the only thing visible on the tablet. 116 | -------------------------------------------------------------------------------- /js/dashboard.js: -------------------------------------------------------------------------------- 1 | var socket = $.atmosphere; 2 | var secondsSinceLoad = 0; 3 | 4 | function Start() 5 | { 6 | BuildWidgets(); 7 | 8 | CheckStates(); 9 | 10 | GetWeather(); 11 | 12 | CheckTrashDay(); 13 | 14 | StartTime(); 15 | } 16 | 17 | function StartTime() { 18 | var today = new Date(); 19 | 20 | $("div.clock").html((twelveHour == true ? (today.getHours() % 12 || 12) : today.getHours())+ ":" + ToDoubleDigits(today.getMinutes()) + (twelveHourAMPM == true ? (today.getHours() >= 12 ? 'pm' : 'am') : "")); // + " " + (today.getHours() >= 12 ? 'pm' : 'am') 21 | $("div.date").html(today.toDateString()); 22 | 23 | if (today.getHours() == reloadHour && today.getMinutes() == 0 && secondsSinceLoad > 600) { // The extra checks besides reloadHour are to make sure we don't reload every second when we hit reloadHour 24 | window.location.reload(true); 25 | } 26 | 27 | secondsSinceLoad++; 28 | 29 | setTimeout(function(){ StartTime() }, 1000); 30 | } 31 | 32 | // Called once when the page is first loaded 33 | // Creates the HTML for the various widgets depending on type 34 | function BuildWidgets() 35 | { 36 | $("widget").each(function() { 37 | var widget = $(this); 38 | 39 | Log("[BuildWidgets] " + widget.data("title"), 2); 40 | 41 | var icon = widget.data("icon") || "lightbulb-o"; 42 | 43 | switch (widget.data("type")) { 44 | case "switch": 45 | widget.html("

" + widget.data("title") + "

"); 46 | break; 47 | case "openclose": 48 | widget.html("

" + widget.data("title") + "

"); 49 | break; 50 | case "sensor": 51 | widget.html("?

" + widget.data("title") + "

"); 52 | break; 53 | case "rollershutter": 54 | widget.html("?

 " + widget.data("title") + " 

"); 55 | break; 56 | } 57 | }); 58 | } 59 | 60 | function CheckStates() 61 | { 62 | $("widget").each(function() { 63 | try 64 | { 65 | var widget = $(this); 66 | 67 | // Skip widgets in history mode - logic is in GetHistory 68 | var widgetmode = widget.data("mode") || "main"; 69 | if (widgetmode === "history") { 70 | return; 71 | } 72 | 73 | // Skip trash widgets - OpenHAB has nothing to do with those 74 | if (widget.data("type") === "trash") { 75 | return; 76 | } 77 | 78 | // Send a simple GET request to OpenHAB to get the item's CURRENT state 79 | $.ajax({ 80 | type: "GET", 81 | url: "http://" + openhabURL + widget.data("item") + "/state" 82 | }) 83 | .done(function(data) { 84 | Log("[CheckStates] Initial state of " + widget.data("item") + ": " + data, 3); 85 | DisplayItemState(widget, data); 86 | }).fail( function(jqXHR, data) { 87 | Log("Error getting state: " + data, 1); 88 | }); 89 | 90 | // Now open a socket to be alerted of FUTURE state updates 91 | Log('[socket] Opening socket...', 3) 92 | 93 | var request = { 94 | url: "ws://" + openhabURL + widget.data("item"), 95 | maxRequest : 256, 96 | timeout: 59000, 97 | attachHeadersAsQueryString : true, 98 | executeCallbackBeforeReconnect : false, 99 | transport : 'websocket' , 100 | fallbackTransport: 'long-polling', 101 | headers: { 'Accept': 'application/json' } 102 | }; 103 | 104 | request.onOpen = function(response) { 105 | Log('[socket] Socket opened (' + response.transport + ')', 2); 106 | }; 107 | 108 | request.onClose = function(response) { 109 | Log('[socket] Socket closed', 2); 110 | }; 111 | 112 | request.onError = function(response) { 113 | Log('[socket] Something went wrong', 1); 114 | }; 115 | 116 | request.onMessage = function (response) { 117 | if (response.status == 200) { 118 | // response.responseBody looks like this: 119 | // { "type": "SwitchItem", "name": "ItemName", "state": "ON", "link": "http://192.168.1.80:8080/rest/items/ItemName" } 120 | var message = $.parseJSON(response.responseBody); 121 | DisplayItemState(widget, message.state); 122 | } 123 | } 124 | 125 | socket.subscribe(request); 126 | } 127 | catch (exception) { 128 | Log('[socket] Error:' + exception, 1); 129 | } 130 | }); // end each 131 | } 132 | 133 | function DisplayItemState(widget, state) 134 | { 135 | // How to display state information depends on the widget type 136 | switch (widget.data("type")) { 137 | case "switch": 138 | if (state === "ON") { 139 | widget.children("span:first").addClass("switchon"); 140 | widget.data("onclick", "OFF"); 141 | } else { 142 | widget.children("span:first").removeClass("switchon"); 143 | widget.data("onclick", "ON"); 144 | } 145 | break; 146 | case "sensor": 147 | case "rollershutter": 148 | if (isNaN(state)) { 149 | state = "-"; 150 | } 151 | if (widget.data("format")) { 152 | state = widget.data("format").format(state); 153 | } 154 | widget.children("span:first").html("" + state + ""); 155 | break; 156 | case "openclose": 157 | if (state === "OPEN") { 158 | widget.children("span:first").addClass("dooropen"); 159 | } else { 160 | widget.children("span:first").removeClass("dooropen"); 161 | } 162 | break; 163 | } 164 | } 165 | 166 | function CheckTrashDay() 167 | { 168 | // Look for a widget defined as type "trash" 169 | var trashWidget = $("widget[data-type='trash']"); 170 | if (!trashWidget.length) return; // no trash widget found 171 | 172 | // Get the days defined as trash and recycle days 173 | // These variables will have value NaN if the data attribute is not found, which is fine 174 | var trashDay = parseInt(trashWidget.data("trashday")); 175 | var recycleDay = parseInt(trashWidget.data("recycleday")); 176 | 177 | // See if we need to show a trash icon, recycle icon, or hide the widget 178 | var now = new Date(); 179 | switch (now.getDay()) { 180 | case trashDay: 181 | trashWidget.show(); 182 | trashWidget.html("

Trash Day

"); 183 | break; 184 | case recycleDay: 185 | trashWidget.show(); 186 | trashWidget.html("

Recycle Day

"); 187 | break; 188 | default: 189 | // No trash today 190 | trashWidget.hide(); 191 | break; 192 | } 193 | 194 | // Check again at 4 AM (forever, every day) 195 | var millisTo4AM = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 4, 0, 0, 0) - now; 196 | if (millisTo4AM < 0) { 197 | millisTo4AM += 86400000; // it's after 4 AM, try 4 AM tomorrow. 198 | } 199 | setTimeout("CheckTrashDay()", millisTo4AM); 200 | } 201 | 202 | function GetHistory(widget) 203 | { 204 | // Query the page that gives us history data for the widget 205 | $.ajax({ 206 | type: "GET", 207 | url: logURL + widget.data("item"), 208 | dataType: "json" 209 | }) 210 | .done(function(data) { 211 | Log("Response: " + data, 3); 212 | // We're showing ALL the data returned - we assume the data service has limited the data for us 213 | var logdata = ""; 214 | $.each(data, function (i, value) { 215 | logdata += "
" + data[i].timestamp + ": " + data[i].value + "
"; 216 | }); 217 | widget.html("" + logdata + ""); 218 | }).fail( function(jqXHR, data) { 219 | Log("Error getting state: " + data, 1); 220 | }); 221 | } 222 | 223 | function GetWeather() 224 | { 225 | if ($("div.weatherbar").length) { 226 | GetWeatherConditions(); 227 | GetWeatherForecast() 228 | } 229 | } 230 | 231 | function GetWeatherConditions() 232 | { 233 | Log("Updating weather conditions", 2); 234 | 235 | // Send a request to the weather service for this location 236 | $.ajax({ 237 | type: "GET", 238 | url: weatherURL, 239 | dataType: "jsonp" 240 | }) 241 | .done(function(data) { 242 | Log(data, 4); 243 | // The weather service sends back a lot of data, but we'll only use some 244 | $("#observation_icon").attr("src", "weather/" + data.current_observation.icon + ".png"); 245 | $("#observation_weather").html(ParseWeatherConditions(data.current_observation.weather)); 246 | $("#observation_temperature").html(formatTemperature(data.current_observation.temp_c,data.current_observation.temp_f)); 247 | $("#observation_feelslike").html("feels like: " + formatTemperature(data.current_observation.feelslike_c,data.current_observation.feelslike_f)); 248 | $("#observation_time").html(data.current_observation.observation_time); 249 | }).fail( function(jqXHR, data) { 250 | Log("Error getting weather: " + data, 1); 251 | }); 252 | 253 | setTimeout(GetWeatherConditions, weatherFrequency); 254 | } 255 | 256 | function GetWeatherForecast() 257 | { 258 | Log("Updating weather forecast", 2); 259 | 260 | // Send a request to the weather service for this location 261 | $.ajax({ 262 | type: "GET", 263 | url: forecastURL, 264 | dataType: "jsonp" 265 | }) 266 | .done(function(data) { 267 | Log(data, 4); 268 | 269 | // The weather service sends back a lot of data, but we'll only use some 270 | $("#forecast_title", "#weather_today").html(data.forecast.simpleforecast.forecastday[0].date.weekday); 271 | $("#forecast_icon", "#weather_today").attr("src", "weather/" + data.forecast.simpleforecast.forecastday[0].icon + ".png"); 272 | $("#forecast_conditions", "#weather_today").html(ParseWeatherConditions(data.forecast.simpleforecast.forecastday[0].conditions)); 273 | $("#forecast_high", "#weather_today").html(formatTemperature(data.forecast.simpleforecast.forecastday[0].high.celsius,data.forecast.simpleforecast.forecastday[0].high.fahrenheit)); 274 | $("#forecast_low", "#weather_today").html(formatTemperature(data.forecast.simpleforecast.forecastday[0].low.celsius,data.forecast.simpleforecast.forecastday[0].low.fahrenheit)); 275 | 276 | $("#forecast_title", "#weather_tomorrow").html(data.forecast.simpleforecast.forecastday[1].date.weekday); 277 | $("#forecast_icon", "#weather_tomorrow").attr("src", "weather/" + data.forecast.simpleforecast.forecastday[1].icon + ".png"); 278 | $("#forecast_conditions", "#weather_tomorrow").html(ParseWeatherConditions(data.forecast.simpleforecast.forecastday[1].conditions)); 279 | $("#forecast_high", "#weather_tomorrow").html(formatTemperature(data.forecast.simpleforecast.forecastday[1].high.celsius,data.forecast.simpleforecast.forecastday[1].high.fahrenheit)); 280 | $("#forecast_low", "#weather_tomorrow").html(formatTemperature(data.forecast.simpleforecast.forecastday[1].low.celsius,data.forecast.simpleforecast.forecastday[1].low.fahrenheit)); 281 | 282 | $("#forecast_time").html("Forecast Time: " + data.forecast.txt_forecast.date); 283 | 284 | // After 5 PM: show tomorrow's forecast. Before 5 PM: show today's. 285 | var today = new Date(); 286 | if (today.getHours() >= 17){ 287 | $("#weather_today").hide(); 288 | $("#weather_tomorrow").show(); 289 | } else { 290 | $("#weather_today").show(); 291 | $("#weather_tomorrow").hide(); 292 | } 293 | }).fail( function(jqXHR, data) { 294 | Log("Error getting weather: " + data, 1); 295 | }); 296 | 297 | setTimeout(GetWeatherForecast, forecastFrequency); 298 | } 299 | 300 | // Some descriptions are a bit too long, so we replace them with something a little shorter 301 | function ParseWeatherConditions(conditions) { 302 | switch (conditions.toLowerCase()) { 303 | case "chance of a thunderstorm": 304 | return "Chance of Storm" 305 | default: 306 | return conditions; 307 | } 308 | } 309 | 310 | // Capture clicks: Weather Bar 311 | $(document).on("mousedown", "div.weather_forecast", function(){ 312 | // Switch between today & tomorrow 313 | if ($("#weather_today").is(":visible")) { 314 | $("#weather_today").hide(); 315 | $("#weather_tomorrow").show(); 316 | } else { 317 | $("#weather_today").show(); 318 | $("#weather_tomorrow").hide(); 319 | } 320 | }); 321 | 322 | // Capture clicks: Widgets 323 | $(document).on("mousedown", "widget", function(event){ 324 | var widget = $(this); 325 | 326 | Log("Clicked widget: " + widget.data("title") + " of type '" + widget.data("type") + "'", 2); 327 | 328 | if (widget.data("type") === "switch") { 329 | // Clicked on a switch: send command to OpenHAB 330 | $.ajax({ 331 | type: "POST", 332 | url: "http://" + openhabURL + widget.data("item"), 333 | data: widget.data("onclick"), 334 | headers: { "Content-Type": "text/plain" } 335 | }) 336 | .done(function(data) { 337 | Log("Command sent", 2); 338 | }).fail( function(jqXHR, data) { 339 | Log("Error on tap: " + data, 1); 340 | }); 341 | } else if(widget.data("type") === "rollershutter") { 342 | var midx=widget.prop("offsetLeft")+widget.prop("offsetWidth")/2; 343 | var midy=widget.prop("offsetTop")+widget.prop("offsetHeight")/2; 344 | 345 | if(event.clientY>midy) 346 | { 347 | var cmd; 348 | if(event.clientX>midx) 349 | cmd="DOWN"; 350 | else 351 | cmd="UP"; 352 | Log(cmd+" client.x="+event.clientX+" midx="+midx+ " client.y="+event.clientY+" , midy="+ midy,2); 353 | $.ajax({ 354 | type: "POST", 355 | url: "http://" + openhabURL + widget.data("item"), 356 | data: cmd, 357 | headers: { "Content-Type": "text/plain" } 358 | }) 359 | .done(function(data) { 360 | Log("Command sent", 2); 361 | }).fail( function(jqXHR, data) { 362 | Log("Error on tap: " + data, 1); 363 | }); 364 | } 365 | 366 | } else if (widget.data("history")) { 367 | // Clicked on a widget that has history data: toggle between main mode and history mode 368 | var widgetmode = widget.data("mode") || "main"; 369 | 370 | if (widgetmode === "main") { 371 | // Switch to history mode 372 | widget.data("mode", "history"); 373 | GetHistory(widget); 374 | } 375 | else if (widgetmode === "history") { 376 | // Switch to main mode: rebuild the original widget 377 | switch (widget.data("type")) { 378 | case "sensor": 379 | widget.html("?

" + widget.data("title") + "

"); 380 | break; 381 | case "openclose": 382 | widget.html("

" + widget.data("title") + "

"); 383 | break; 384 | } 385 | widget.data("mode", "main"); 386 | } 387 | } 388 | } ); 389 | 390 | 391 | // Helper Functions 392 | 393 | // Add a zero in front of numbers < 10 394 | function ToDoubleDigits(i) { 395 | if (i < 10) {i = "0" + i}; 396 | return i; 397 | } 398 | 399 | // Write to the console, but only when logging at a level below our loggingLevel 400 | function Log(text, level) { 401 | if (loggingLevel >= level) { 402 | console.log(text); 403 | } 404 | } 405 | 406 | // Very simple Format function - just replaces {0}, {1} etc in the given string 407 | String.prototype.format = function() { 408 | var s = this, 409 | i = arguments.length; 410 | 411 | while (i--) { 412 | s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]); 413 | } 414 | return s; 415 | }; 416 | 417 | function formatTemperature(c, f) { 418 | tempC_suffix = "C"; 419 | tempF_suffix = "F"; 420 | 421 | if (tempRound == true) { 422 | c = Math.round(c).toString(); 423 | f = Math.round(f).toString(); 424 | } 425 | 426 | if (weatherUnit == "c"){ 427 | return (c + tempC_suffix); 428 | } else if (weatherUnit == "f") { 429 | return (f + tempF_suffix); 430 | } else if (weatherUnit == "cf") { 431 | return (c + tempC_suffix + " (" + f + tempF_suffix + ")"); 432 | } else { 433 | return (f + tempF_suffix + " (" + c + tempC_suffix + ")"); 434 | } 435 | } -------------------------------------------------------------------------------- /css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.2.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"} -------------------------------------------------------------------------------- /js/jquery.atmosphere.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | /* 15 | * Part of this code has been taked from 16 | * 17 | * jQuery Stream @VERSION 18 | * Comet Streaming JavaScript Library 19 | * http://code.google.com/p/jquery-stream/ 20 | * 21 | * Copyright 2011, Donghwan Kim 22 | * Licensed under the Apache License, Version 2.0 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Compatible with jQuery 1.5+ 26 | */ 27 | jQuery.atmosphere = function() { 28 | jQuery(window).unload(function() { 29 | jQuery.atmosphere.unsubscribe(); 30 | }); 31 | 32 | var parseHeaders = function(headerString) { 33 | var match, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, headers = {}; 34 | while (match = rheaders.exec(headerString)) { 35 | headers[match[1]] = match[2]; 36 | } 37 | return headers; 38 | }; 39 | 40 | return { 41 | version : 0.9, 42 | requests : [], 43 | callbacks : [], 44 | 45 | onError : function(response) {}, 46 | onClose : function(response) {}, 47 | onOpen : function(response) {}, 48 | onMessage : function(response) {}, 49 | onReconnect : function(request, response) {}, 50 | onMessagePublished : function(response) {}, 51 | 52 | AtmosphereRequest : function(options) { 53 | 54 | /** 55 | * {Object} Request parameters. 56 | * @private 57 | */ 58 | var _request = { 59 | timeout: 300000, 60 | method: 'GET', 61 | headers: {}, 62 | contentType : '', 63 | cache: true, 64 | async: true, 65 | ifModified: false, 66 | callback: null, 67 | dataType: '', 68 | url : '', 69 | data : '', 70 | suspend : true, 71 | maxRequest : 60, 72 | maxStreamingLength : 10000000, 73 | lastIndex : 0, 74 | logLevel : 'info', 75 | requestCount : 0, 76 | fallbackMethod: 'GET', 77 | fallbackTransport : 'streaming', 78 | transport : 'long-polling', 79 | webSocketImpl: null, 80 | webSocketUrl: null, 81 | webSocketPathDelimiter: "@@", 82 | enableXDR : false, 83 | rewriteURL : false, 84 | attachHeadersAsQueryString : true, 85 | executeCallbackBeforeReconnect : false, 86 | readyState : 0, 87 | lastTimestamp : 0, 88 | withCredentials : false, 89 | trackMessageLength : false , 90 | messageDelimiter : '|', 91 | connectTimeout : -1, 92 | onError : function(response) {}, 93 | onClose : function(response) {}, 94 | onOpen : function(response) {}, 95 | onMessage : function(response) {}, 96 | onReconnect : function(request, response) {}, 97 | onMessagePublished : function(response) {} 98 | }; 99 | 100 | /** 101 | * {Object} Request's last response. 102 | * @private 103 | */ 104 | var _response = { 105 | status: 200, 106 | responseBody : '', 107 | expectedBodySize : -1, 108 | headers : [], 109 | state : "messageReceived", 110 | transport : "polling", 111 | error: null, 112 | id : 0 113 | }; 114 | 115 | /** 116 | * {number} Request id. 117 | * 118 | * @private 119 | */ 120 | var _uuid = 0; 121 | 122 | /** 123 | * {websocket} Opened web socket. 124 | * 125 | * @private 126 | */ 127 | var _websocket = null; 128 | 129 | /** 130 | * {XMLHttpRequest, ActiveXObject} Opened ajax request (in case of 131 | * http-streaming or long-polling) 132 | * 133 | * @private 134 | */ 135 | var _activeRequest = null; 136 | 137 | /** 138 | * {Object} Object use for streaming with IE. 139 | * 140 | * @private 141 | */ 142 | var _ieStream = null; 143 | 144 | /** 145 | * {Object} Object use for jsonp transport. 146 | * 147 | * @private 148 | */ 149 | var _jqxhr = null; 150 | 151 | /** 152 | * {boolean} If request has been subscribed or not. 153 | * 154 | * @private 155 | */ 156 | var _subscribed = true; 157 | 158 | /** 159 | * {number} Number of test reconnection. 160 | * 161 | * @private 162 | */ 163 | var _requestCount = 0; 164 | 165 | /** 166 | * {boolean} If request is currently aborded. 167 | * 168 | * @private 169 | */ 170 | var _abordingConnection = false; 171 | 172 | // Automatic call to subscribe 173 | _subscribe(options); 174 | 175 | /** 176 | * Initialize atmosphere request object. 177 | * 178 | * @private 179 | */ 180 | function _init() { 181 | _uuid = 0; 182 | _subscribed = true; 183 | _abordingConnection = false; 184 | _requestCount = 0; 185 | 186 | _websocket = null; 187 | _activeRequest = null; 188 | _ieStream = null; 189 | } 190 | 191 | /** 192 | * Re-initialize atmosphere object. 193 | * @private 194 | */ 195 | function _reinit() { 196 | _close(); 197 | _init(); 198 | } 199 | 200 | /** 201 | * Subscribe request using request transport.
202 | * If request is currently opened, this one will be closed. 203 | * 204 | * @param {Object} 205 | * Request parameters. 206 | * @private 207 | */ 208 | function _subscribe(options) { 209 | _reinit(); 210 | 211 | _request = jQuery.extend(_request, options); 212 | _uuid = jQuery.atmosphere.guid(); 213 | 214 | _execute(); 215 | } 216 | 217 | /** 218 | * Check if web socket is supported (check for custom implementation 219 | * provided by request object or browser implementation). 220 | * 221 | * @returns {boolean} True if web socket is supported, false 222 | * otherwise. 223 | * @private 224 | */ 225 | function _supportWebsocket() { 226 | return _request.webSocketImpl != null || window.WebSocket || window.MozWebSocket; 227 | } 228 | 229 | /** 230 | * Open request using request transport.
231 | * If request transport is 'websocket' but websocket can't be 232 | * opened, request will automatically reconnect using fallback 233 | * transport. 234 | * 235 | * @private 236 | */ 237 | function _execute() { 238 | if (_request.transport != 'websocket') { 239 | _open('opening',_request.transport); 240 | _executeRequest(); 241 | 242 | } else if (_request.transport == 'websocket') { 243 | if (!_supportWebsocket()) { 244 | jQuery.atmosphere.log(_request.logLevel, ["Websocket is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"]); 245 | _open('opening', _request.fallbackTransport); 246 | _reconnectWithFallbackTransport(); 247 | } else { 248 | _executeWebSocket(false); 249 | } 250 | } 251 | } 252 | 253 | /** 254 | * @private 255 | */ 256 | function _open(state, transport) { 257 | var prevState = _response.state; 258 | _response.state = state; 259 | _response.status = 200; 260 | var prevTransport = _response.transport; 261 | _response.transport = transport; 262 | _response.responseBody = ""; 263 | _invokeCallback(); 264 | _response.state = prevState; 265 | _response.transport = prevTransport; 266 | } 267 | 268 | /** 269 | * Execute request using jsonp transport. 270 | * 271 | * @param request 272 | * {Object} request Request parameters, if 273 | * undefined _request object will be used. 274 | * @private 275 | */ 276 | function _jsonp(request) { 277 | var rq = _request; 278 | if ((request != null) && (typeof(request) != 'undefined')) { 279 | rq = request; 280 | } 281 | 282 | var url = rq.url; 283 | var data = rq.data; 284 | if (rq.attachHeadersAsQueryString) { 285 | url = _attachHeaders(rq); 286 | if (data != '') { 287 | url += "&X-Atmosphere-Post-Body=" + data; 288 | } 289 | data = ''; 290 | } 291 | 292 | _jqxhr = jQuery.ajax({ 293 | url : url, 294 | type : rq.method, 295 | dataType: "jsonp", 296 | error : function(jqXHR, textStatus, errorThrown) { 297 | if (jqXHR.status < 300) { 298 | _reconnect(_jqxhr, rq); 299 | } else { 300 | _prepareCallback(textStatus, "error", jqXHR.status, rq.transport); 301 | } 302 | }, 303 | jsonp : "jsonpTransport", 304 | success: function(json) { 305 | if (rq.executeCallbackBeforeReconnect) { 306 | _reconnect(_jqxhr, rq); 307 | } 308 | 309 | var msg = json.message; 310 | if (msg != null && typeof msg != 'string') { 311 | try { 312 | msg = jQuery.stringifyJSON(msg); 313 | } catch (err) { 314 | // The message was partial 315 | } 316 | } 317 | 318 | _prepareCallback(msg, "messageReceived", 200, rq.transport); 319 | 320 | if (!rq.executeCallbackBeforeReconnect) { 321 | _reconnect(_jqxhr, rq); 322 | } 323 | }, 324 | data : rq.data, 325 | beforeSend : function(jqXHR) { 326 | _doRequest(jqXHR, rq, false); 327 | } 328 | }); 329 | } 330 | 331 | /** 332 | * Build websocket object. 333 | * 334 | * @param location 335 | * {string} Web socket url. 336 | * @returns {websocket} Web socket object. 337 | * @private 338 | */ 339 | function _getWebSocket(location) { 340 | if (_request.webSocketImpl != null) { 341 | return _request.webSocketImpl; 342 | } else { 343 | if (window.WebSocket) { 344 | return new WebSocket(location); 345 | } else { 346 | return new MozWebSocket(location); 347 | } 348 | } 349 | } 350 | 351 | /** 352 | * Build web socket url from request url. 353 | * 354 | * @return {string} Web socket url (start with "ws" or "wss" for 355 | * secure web socket). 356 | * @private 357 | */ 358 | function _buildWebSocketUrl() { 359 | var url = _request.url; 360 | url = _attachHeaders(); 361 | if (url.indexOf("http") == -1 && url.indexOf("ws") == -1) { 362 | url = jQuery.atmosphere.parseUri(document.location, url); 363 | } 364 | return url.replace('http:', 'ws:').replace('https:', 'wss:'); 365 | } 366 | 367 | /** 368 | * Open web socket.
369 | * Automatically use fallback transport if web socket can't be 370 | * opened. 371 | * 372 | * @private 373 | */ 374 | function _executeWebSocket(webSocketOpened) { 375 | 376 | _response.transport = "websocket"; 377 | 378 | var location = _buildWebSocketUrl(_request.url); 379 | 380 | jQuery.atmosphere.log(_request.logLevel, ["Invoking executeWebSocket"]); 381 | if (_request.logLevel == 'debug') { 382 | jQuery.atmosphere.debug("Using URL: " + location); 383 | } 384 | 385 | _websocket = _getWebSocket(location); 386 | 387 | if (_request.connectTimeout > 0) { 388 | _request.id = setTimeout(function() { 389 | if (!webSocketOpened) { 390 | var _message = { 391 | code : 1002, 392 | reason : "", 393 | wasClean : false 394 | }; 395 | _websocket.onclose(_message); 396 | } 397 | }, _request.connectTimeout); 398 | } 399 | 400 | _websocket.onopen = function(message) { 401 | if (_request.logLevel == 'debug') { 402 | jQuery.atmosphere.debug("Websocket successfully opened"); 403 | } 404 | 405 | _subscribed = true; 406 | _open(webSocketOpened ? 're-opening' : 'opening', "websocket"); 407 | 408 | webSocketOpened = true; 409 | 410 | if (_request.method == 'POST') { 411 | _response.state = "messageReceived"; 412 | _websocket.send(_request.data); 413 | } 414 | }; 415 | 416 | _websocket.onmessage = function(message) { 417 | if (message.data.indexOf("parent.callback") != -1) { 418 | jQuery.atmosphere.log(_request.logLevel, ["parent.callback no longer supported with 0.8 version and up. Please upgrade"]); 419 | } 420 | 421 | _response.state = 'messageReceived'; 422 | _response.status = 200; 423 | 424 | var message = message.data; 425 | var skipCallbackInvocation = _trackMessageSize(message, _request, _response); 426 | 427 | if (!skipCallbackInvocation) { 428 | _invokeCallback(); 429 | _response.responseBody = ''; 430 | } 431 | }; 432 | 433 | _websocket.onerror = function(message) { 434 | jQuery.atmosphere.warn("Websocket error, reason: " + message.reason); 435 | 436 | _response.state = 'error'; 437 | _response.responseBody = ""; 438 | _response.status = 500; 439 | _invokeCallback(); 440 | }; 441 | 442 | _websocket.onclose = function(message) { 443 | var reason = message.reason; 444 | if (reason === "") { 445 | switch (message.code) { 446 | case 1000: 447 | reason = "Normal closure; the connection successfully completed whatever purpose for which " + 448 | "it was created."; 449 | break; 450 | case 1001: 451 | reason = "The endpoint is going away, either because of a server failure or because the " + 452 | "browser is navigating away from the page that opened the connection."; 453 | break; 454 | case 1002: 455 | reason = "The endpoint is terminating the connection due to a protocol error."; 456 | break; 457 | case 1003: 458 | reason = "The connection is being terminated because the endpoint received data of a type it " + 459 | "cannot accept (for example, a text-only endpoint received binary data)."; 460 | break; 461 | case 1004: 462 | reason = "The endpoint is terminating the connection because a data frame was received that " + 463 | "is too large."; 464 | break; 465 | case 1005: 466 | reason = "Unknown: no status code was provided even though one was expected."; 467 | break; 468 | case 1006: 469 | reason = "Connection was closed abnormally (that is, with no close frame being sent)."; 470 | break; 471 | } 472 | } 473 | 474 | jQuery.atmosphere.warn("Websocket closed, reason: " + reason); 475 | jQuery.atmosphere.warn("Websocket closed, wasClean: " + message.wasClean); 476 | 477 | _response.state = 'closed'; 478 | _response.responseBody = ""; 479 | _response.status = 200; 480 | _invokeCallback(); 481 | 482 | if (_abordingConnection) { 483 | _abordingConnection = false; 484 | jQuery.atmosphere.log(_request.logLevel, ["Websocket closed normally"]); 485 | 486 | } else if (!webSocketOpened) { 487 | jQuery.atmosphere.log(_request.logLevel, ["Websocket failed. Downgrading to Comet and resending"]); 488 | _open('opening', _request.fallbackTransport); 489 | _reconnectWithFallbackTransport(); 490 | 491 | } else if ((_subscribed) && (_response.transport == 'websocket')) { 492 | if (_requestCount++ < _request.maxRequest) { 493 | _request.requestCount = _requestCount; 494 | _response.responseBody = ""; 495 | _executeWebSocket(true); 496 | } else { 497 | jQuery.atmosphere.log(_request.logLevel, ["Websocket reconnect maximum try reached " + _request.requestCount]); 498 | } 499 | } 500 | }; 501 | } 502 | 503 | /** 504 | * Track received message and make sure callbacks/functions are only invoked when the complete message 505 | * has been received. 506 | * 507 | * @param message 508 | * @param request 509 | * @param response 510 | */ 511 | function _trackMessageSize(message, request, response) { 512 | if (request.trackMessageLength) { 513 | // The message length is the included within the message 514 | var messageStart = message.indexOf(request.messageDelimiter); 515 | 516 | var length = response.expectedBodySize; 517 | if (messageStart != -1) { 518 | length = message.substring(0, messageStart); 519 | message = message.substring(messageStart + 1); 520 | response.expectedBodySize = length; 521 | } 522 | 523 | if (messageStart != -1) { 524 | response.responseBody = message; 525 | } else { 526 | response.responseBody += message; 527 | } 528 | 529 | if (response.responseBody.length != length) { 530 | return true; 531 | } 532 | } else { 533 | response.responseBody = message; 534 | } 535 | return false; 536 | } 537 | 538 | /** 539 | * Reconnect request with fallback transport.
540 | * Used in case websocket can't be opened. 541 | * 542 | * @private 543 | */ 544 | function _reconnectWithFallbackTransport() { 545 | _request.transport = _request.fallbackTransport; 546 | _request.method = _request.fallbackMethod; 547 | _response.transport = _request.fallbackTransport; 548 | _executeRequest(); 549 | } 550 | 551 | /** 552 | * Get url from request and attach headers to it. 553 | * 554 | * @param request 555 | * {Object} request Request parameters, if 556 | * undefined _request object will be used. 557 | * 558 | * @returns {Object} Request object, if undefined, 559 | * _request object will be used. 560 | * @private 561 | */ 562 | function _attachHeaders(request) { 563 | var rq = _request; 564 | if ((request != null) && (typeof(request) != 'undefined')) { 565 | rq = request; 566 | } 567 | 568 | var url = rq.url; 569 | 570 | // If not enabled 571 | if (!rq.attachHeadersAsQueryString) return url; 572 | 573 | // If already added 574 | if (url.indexOf("X-Atmosphere-Framework") != -1) { 575 | return url; 576 | } 577 | 578 | url += (url.indexOf('?') != -1) ? '&' : '?'; 579 | url += "X-Atmosphere-tracking-id=" + _uuid; 580 | url += "&X-Atmosphere-Framework=" + jQuery.atmosphere.version; 581 | url += "&X-Atmosphere-Transport=" + rq.transport; 582 | 583 | if (rq.trackMessageLength) { 584 | url += "&X-Atmosphere-TrackMessageSize=" + "true"; 585 | } 586 | 587 | if (rq.lastTimestamp != undefined) { 588 | url += "&X-Cache-Date=" + rq.lastTimestamp; 589 | } else { 590 | url += "&X-Cache-Date=" + 0; 591 | } 592 | 593 | if (rq.contentType != '') { 594 | url += "&Content-Type=" + rq.contentType; 595 | } 596 | 597 | jQuery.each(rq.headers, function(name, value) { 598 | var h = jQuery.isFunction(value) ? value.call(this, ajaxRequest, request, create) : value; 599 | if (h) { 600 | url += "&" + encodeURIComponent(name) + "=" + encodeURIComponent(h); 601 | } 602 | }); 603 | 604 | return url; 605 | } 606 | 607 | /** 608 | * Build ajax request.
609 | * Ajax Request is an XMLHttpRequest object, except for IE6 where 610 | * ajax request is an ActiveXObject. 611 | * 612 | * @return {XMLHttpRequest, ActiveXObject} Ajax request. 613 | * @private 614 | */ 615 | function _buildAjaxRequest() { 616 | var ajaxRequest; 617 | if (jQuery.browser.msie) { 618 | var activexmodes = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"]; 619 | for (var i = 0; i < activexmodes.length; i++) { 620 | try { 621 | ajaxRequest = new ActiveXObject(activexmodes[i]); 622 | } catch(e) { } 623 | } 624 | 625 | } else if (window.XMLHttpRequest) { 626 | ajaxRequest = new XMLHttpRequest(); 627 | } 628 | return ajaxRequest; 629 | } 630 | 631 | /** 632 | * Execute ajax request.
633 | * 634 | * @param request 635 | * {Object} request Request parameters, if 636 | * undefined _request object will be used. 637 | * @private 638 | */ 639 | function _executeRequest(request) { 640 | var rq = _request; 641 | if ((request != null) || (typeof(request) != 'undefined')) { 642 | rq = request; 643 | } 644 | 645 | // CORS fake using JSONP 646 | if ((rq.transport == 'jsonp') || ((rq.enableXDR) && (jQuery.atmosphere.checkCORSSupport()))) { 647 | _jsonp(rq); 648 | return; 649 | } 650 | 651 | if ((rq.transport == 'streaming') && (jQuery.browser.msie)) { 652 | rq.enableXDR && window.XDomainRequest ? _ieXDR(rq) : _ieStreaming(rq); 653 | return; 654 | } 655 | 656 | if ((rq.enableXDR) && (window.XDomainRequest)) { 657 | _ieXDR(rq); 658 | return; 659 | } 660 | 661 | if (rq.requestCount++ < rq.maxRequest) { 662 | var ajaxRequest = _buildAjaxRequest(); 663 | _doRequest(ajaxRequest, rq, true); 664 | 665 | if (rq.suspend) { 666 | _activeRequest = ajaxRequest; 667 | } 668 | 669 | if (rq.transport != 'polling') { 670 | _response.transport = rq.transport; 671 | } 672 | 673 | var error = false; 674 | if (!jQuery.browser.msie) { 675 | ajaxRequest.onerror = function() { 676 | error = true; 677 | try { 678 | _response.status = XMLHttpRequest.status; 679 | } catch(e) { 680 | _response.status = 404; 681 | } 682 | 683 | _response.state = "error"; 684 | _invokeCallback(); 685 | ajaxRequest.abort(); 686 | _activeRequest = null; 687 | }; 688 | } 689 | 690 | ajaxRequest.onreadystatechange = function() { 691 | if (_abordingConnection) { 692 | return; 693 | } 694 | 695 | var skipCallbackInvocation = false; 696 | var update = false; 697 | 698 | // Remote server disconnected us, reconnect. 699 | if (rq.transport == 'streaming' 700 | && (rq.readyState > 2 701 | && ajaxRequest.readyState == 4)) { 702 | 703 | rq.readyState = 0; 704 | rq.lastIndex = 0; 705 | 706 | _reconnect(ajaxRequest, rq, true); 707 | return; 708 | } 709 | 710 | rq.readyState = ajaxRequest.readyState; 711 | 712 | if (ajaxRequest.readyState == 4) { 713 | if (jQuery.browser.msie) { 714 | update = true; 715 | } else if (rq.transport == 'streaming') { 716 | update = true; 717 | } else if (rq.transport == 'long-polling') { 718 | update = true; 719 | clearTimeout(rq.id); 720 | } 721 | 722 | } else if (!jQuery.browser.msie && ajaxRequest.readyState == 3 && ajaxRequest.status == 200 && rq.transport != 'long-polling') { 723 | update = true; 724 | } else { 725 | clearTimeout(rq.id); 726 | } 727 | 728 | if (update) { 729 | 730 | var tempDate = ajaxRequest.getResponseHeader('X-Cache-Date'); 731 | if (tempDate != null || tempDate != undefined) { 732 | _request.lastTimestamp = tempDate.split(" ").pop(); 733 | } 734 | 735 | var responseText = ajaxRequest.responseText; 736 | this.previousLastIndex = rq.lastIndex; 737 | if (rq.transport == 'streaming') { 738 | var text = responseText.substring(rq.lastIndex, responseText.length); 739 | _response.isJunkEnded = true; 740 | 741 | if (rq.lastIndex == 0 && text.indexOf(""; 747 | var endOfJunkLenght = endOfJunk.length; 748 | var junkEnd = text.indexOf(endOfJunk) + endOfJunkLenght; 749 | 750 | if (junkEnd > endOfJunkLenght && junkEnd != text.length) { 751 | _response.responseBody = text.substring(junkEnd); 752 | } else { 753 | skipCallbackInvocation = true; 754 | } 755 | } else { 756 | var message = responseText.substring(rq.lastIndex, responseText.length); 757 | skipCallbackInvocation = _trackMessageSize(message, rq, _response); 758 | } 759 | rq.lastIndex = responseText.length; 760 | 761 | if (jQuery.browser.opera) { 762 | jQuery.atmosphere.iterate(function() { 763 | if (ajaxRequest.responseText.length > rq.lastIndex) { 764 | try { 765 | _response.status = ajaxRequest.status; 766 | _response.headers = parseHeaders(ajaxRequest.getAllResponseHeaders()); 767 | 768 | // HOTFIX for firefox bug: https://bugzilla.mozilla.org/show_bug.cgi?id=608735 769 | if (_request.headers) { 770 | jQuery.each(_request.headers, function(name) { 771 | var v = ajaxRequest.getResponseHeader(name); 772 | if (v) { 773 | _response.headers[name] = v; 774 | } 775 | }); 776 | } 777 | } 778 | catch(e) { 779 | _response.status = 404; 780 | } 781 | _response.state = "messageReceived"; 782 | _response.responseBody = ajaxRequest.responseText.substring(rq.lastIndex); 783 | rq.lastIndex = ajaxRequest.responseText.length; 784 | 785 | _invokeCallback(); 786 | if ((rq.transport == 'streaming') && (ajaxRequest.responseText.length > rq.maxStreamingLength)) { 787 | // Close and reopen connection on large data received 788 | ajaxRequest.abort(); 789 | _doRequest(ajaxRequest, rq, true); 790 | } 791 | } 792 | }, 0); 793 | } 794 | 795 | if (skipCallbackInvocation) { 796 | return; 797 | } 798 | } else { 799 | skipCallbackInvocation = _trackMessageSize(responseText, rq, _response); 800 | rq.lastIndex = responseText.length; 801 | } 802 | 803 | try { 804 | _response.status = ajaxRequest.status; 805 | _response.headers = parseHeaders(ajaxRequest.getAllResponseHeaders()); 806 | } catch(e) { 807 | _response.status = 404; 808 | } 809 | 810 | if (rq.suspend) { 811 | _response.state = _response.status == 0 ? "closed" : "messageReceived"; 812 | } else { 813 | _response.state = "messagePublished"; 814 | } 815 | 816 | if (!rq.executeCallbackBeforeReconnect) { 817 | _reconnect(ajaxRequest, rq, false); 818 | } 819 | 820 | // For backward compatibility with Atmosphere < 0.8 821 | if (_response.responseBody.indexOf("parent.callback") != -1) { 822 | jQuery.atmosphere.log(rq.logLevel, ["parent.callback no longer supported with 0.8 version and up. Please upgrade"]); 823 | } 824 | _invokeCallback(); 825 | 826 | if (rq.executeCallbackBeforeReconnect) { 827 | _reconnect(ajaxRequest, rq, false); 828 | } 829 | 830 | if ((rq.transport == 'streaming') && (responseText.length > rq.maxStreamingLength)) { 831 | // Close and reopen connection on large data received 832 | ajaxRequest.abort(); 833 | _doRequest(ajaxRequest, rq, true); 834 | } else { 835 | _open('re-opening', rq.transport); 836 | } 837 | } 838 | }; 839 | ajaxRequest.send(rq.data); 840 | 841 | if (rq.suspend) { 842 | rq.id = setTimeout(function() { 843 | if (_subscribed) { 844 | ajaxRequest.abort(); 845 | _subscribe(rq); 846 | } 847 | }, rq.timeout); 848 | } 849 | _subscribed = true; 850 | 851 | } else { 852 | jQuery.atmosphere.log(rq.logLevel, ["Max re-connection reached."]); 853 | } 854 | } 855 | 856 | /** 857 | * Do ajax request. 858 | * @param ajaxRequest Ajax request. 859 | * @param request Request parameters. 860 | * @param create If ajax request has to be open. 861 | */ 862 | function _doRequest(ajaxRequest, request, create) { 863 | // Prevent Android to cache request 864 | var url = jQuery.atmosphere.prepareURL(request.url); 865 | 866 | if (create) { 867 | ajaxRequest.open(request.method, url, true); 868 | if (request.connectTimeout > -1) { 869 | request.id = setTimeout(function() { 870 | if (request.requestCount == 0) { 871 | ajaxRequest.abort(); 872 | _prepareCallback("Connect timeout", "closed", 200, request.transport); 873 | } 874 | }, request.connectTimeout); 875 | } 876 | } 877 | 878 | if (_request.withCredentials) { 879 | if ("withCredentials" in ajaxRequest) { 880 | ajaxRequest.withCredentials = true; 881 | } 882 | } 883 | 884 | ajaxRequest.setRequestHeader("X-Atmosphere-Framework", jQuery.atmosphere.version); 885 | ajaxRequest.setRequestHeader("X-Atmosphere-Transport", request.transport); 886 | if (request.lastTimestamp != undefined) { 887 | ajaxRequest.setRequestHeader("X-Cache-Date", request.lastTimestamp); 888 | } else { 889 | ajaxRequest.setRequestHeader("X-Cache-Date", 0); 890 | } 891 | 892 | if (request.trackMessageLength) { 893 | ajaxRequest.setRequestHeader("X-Atmosphere-TrackMessageSize", "true") 894 | } 895 | 896 | if (request.contentType != '') { 897 | ajaxRequest.setRequestHeader("Content-Type", request.contentType); 898 | } 899 | ajaxRequest.setRequestHeader("X-Atmosphere-tracking-id", _uuid); 900 | 901 | jQuery.each(request.headers, function(name, value) { 902 | var h = jQuery.isFunction(value) ? value.call(this, ajaxRequest, request, create, _response) : value; 903 | if (h) { 904 | ajaxRequest.setRequestHeader(name, h); 905 | } 906 | }); 907 | } 908 | 909 | function _reconnect(ajaxRequest, request, force) { 910 | if (force || (request.suspend && ajaxRequest.status == 200 && request.transport != 'streaming' && _subscribed)) { 911 | _executeRequest(); 912 | } 913 | } 914 | 915 | // From jquery-stream, which is APL2 licensed as well. 916 | function _ieXDR(request) { 917 | _ieStream = _configureXDR(request); 918 | _ieStream.open(); 919 | } 920 | 921 | // From jquery-stream 922 | function _configureXDR(request) { 923 | var rq = _request; 924 | if ((request != null) && (typeof(request) != 'undefined')) { 925 | rq = request; 926 | } 927 | 928 | var lastMessage = ""; 929 | var transport = rq.transport; 930 | var lastIndex = 0; 931 | 932 | var xdrCallback = function (xdr) { 933 | var responseBody = xdr.responseText; 934 | var isJunkEnded = false; 935 | 936 | if (responseBody.indexOf(""; 942 | var endOfJunkLenght = endOfJunk.length; 943 | var junkEnd = responseBody.indexOf(endOfJunk) + endOfJunkLenght; 944 | 945 | responseBody = responseBody.substring(junkEnd + lastIndex); 946 | lastIndex += responseBody.length; 947 | } 948 | 949 | _prepareCallback(responseBody, "messageReceived", 200, transport); 950 | }; 951 | 952 | var xdr = new window.XDomainRequest(); 953 | var rewriteURL = rq.rewriteURL || function(url) { 954 | // Maintaining session by rewriting URL 955 | // http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url 956 | var rewriters = { 957 | JSESSIONID: function(sid) { 958 | return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + sid + "$1"); 959 | }, 960 | PHPSESSID: function(sid) { 961 | return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + sid + "&").replace(/&$/, ""); 962 | } 963 | }; 964 | 965 | for (var name in rewriters) { 966 | // Finds session id from cookie 967 | var matcher = new RegExp("(?:^|;\\s*)" + encodeURIComponent(name) + "=([^;]*)").exec(document.cookie); 968 | if (matcher) { 969 | return rewriters[name](matcher[1]); 970 | } 971 | } 972 | 973 | return url; 974 | }; 975 | 976 | // Handles open and message event 977 | xdr.onprogress = function() { 978 | xdrCallback(xdr); 979 | }; 980 | // Handles error event 981 | xdr.onerror = function() { 982 | _prepareCallback(xdr.responseText, "error", 500, transport); 983 | }; 984 | // Handles close event 985 | xdr.onload = function() { 986 | if (lastMessage != xdr.responseText) { 987 | xdrCallback(xdr); 988 | } 989 | if (rq.transport == "long-polling") { 990 | _executeRequest(); 991 | } 992 | }; 993 | 994 | return { 995 | open: function() { 996 | if (rq.method == 'POST') { 997 | rq.attachHeadersAsQueryString = true; 998 | } 999 | var url = _attachHeaders(rq); 1000 | if (rq.method == 'POST') { 1001 | url += "&X-Atmosphere-Post-Body=" + rq.data; 1002 | } 1003 | xdr.open(rq.method, rewriteURL(url)); 1004 | xdr.send(); 1005 | if (rq.connectTimeout > -1) { 1006 | rq.id = setTimeout(function() { 1007 | if (rq.requestCount == 0) { 1008 | xdr.abort(); 1009 | _prepareCallback("Connect timeout", "closed", 200, rq.transport); 1010 | } 1011 | }, rq.connectTimeout); 1012 | } 1013 | }, 1014 | close: function() { 1015 | xdr.abort(); 1016 | _prepareCallback(xdr.responseText, "closed", 200, transport); 1017 | } 1018 | }; 1019 | } 1020 | 1021 | // From jquery-stream, which is APL2 licensed as well. 1022 | function _ieStreaming(request) { 1023 | _ieStream = _configureIE(request); 1024 | _ieStream.open(); 1025 | } 1026 | 1027 | function _configureIE(request) { 1028 | var rq = _request; 1029 | if ((request != null) && (typeof(request) != 'undefined')) { 1030 | rq = request; 1031 | } 1032 | 1033 | var stop; 1034 | var doc = new window.ActiveXObject("htmlfile"); 1035 | 1036 | doc.open(); 1037 | doc.close(); 1038 | 1039 | var url = rq.url; 1040 | 1041 | if (rq.transport != 'polling') { 1042 | _response.transport = rq.transport; 1043 | } 1044 | 1045 | return { 1046 | open: function() { 1047 | var iframe = doc.createElement("iframe"); 1048 | 1049 | url = _attachHeaders(rq); 1050 | if (rq.data != '') { 1051 | url += "&X-Atmosphere-Post-Body=" + rq.data; 1052 | } 1053 | 1054 | // Finally attach a timestamp to prevent Android and IE caching. 1055 | url = jQuery.atmosphere.prepareURL(url); 1056 | 1057 | iframe.src = url; 1058 | doc.body.appendChild(iframe); 1059 | 1060 | // For the server to respond in a consistent format regardless of user agent, we polls response text 1061 | var cdoc = iframe.contentDocument || iframe.contentWindow.document; 1062 | 1063 | stop = jQuery.atmosphere.iterate(function() { 1064 | if (!cdoc.firstChild) { 1065 | return; 1066 | } 1067 | 1068 | // Detects connection failure 1069 | if (cdoc.readyState === "complete") { 1070 | try { 1071 | jQuery.noop(cdoc.fileSize); 1072 | } catch(e) { 1073 | _prepareCallback("Connection Failure", "error", 500, rq.transport); 1074 | return false; 1075 | } 1076 | } 1077 | 1078 | var res = cdoc.body ? cdoc.body.lastChild : cdoc; 1079 | var readResponse = function() { 1080 | // Clones the element not to disturb the original one 1081 | var clone = res.cloneNode(true); 1082 | 1083 | // If the last character is a carriage return or a line feed, IE ignores it in the innerText property 1084 | // therefore, we add another non-newline character to preserve it 1085 | clone.appendChild(cdoc.createTextNode(".")); 1086 | 1087 | var text = clone.innerText; 1088 | var isJunkEnded = true; 1089 | 1090 | if (text.indexOf(""; 1096 | var endOfJunkLenght = endOfJunk.length; 1097 | var junkEnd = text.indexOf(endOfJunk) + endOfJunkLenght; 1098 | 1099 | text = text.substring(junkEnd); 1100 | } 1101 | return text.substring(0, text.length - 1); 1102 | }; 1103 | 1104 | //To support text/html content type 1105 | if (!jQuery.nodeName(res, "pre")) { 1106 | // Injects a plaintext element which renders text without interpreting the HTML and cannot be stopped 1107 | // it is deprecated in HTML5, but still works 1108 | var head = cdoc.head || cdoc.getElementsByTagName("head")[0] || cdoc.documentElement || cdoc; 1109 | var script = cdoc.createElement("script"); 1110 | 1111 | script.text = "document.write('')"; 1112 | 1113 | head.insertBefore(script, head.firstChild); 1114 | head.removeChild(script); 1115 | 1116 | // The plaintext element will be the response container 1117 | res = cdoc.body.lastChild; 1118 | } 1119 | 1120 | // Handles open event 1121 | _prepareCallback(readResponse(), "opening", 200, rq.transport); 1122 | 1123 | // Handles message and close event 1124 | stop = jQuery.atmosphere.iterate(function() { 1125 | var text = readResponse(); 1126 | if (text.length > rq.lastIndex) { 1127 | _response.status = 200; 1128 | _prepareCallback(text, "messageReceived", 200, rq.transport); 1129 | 1130 | // Empties response every time that it is handled 1131 | res.innerText = ""; 1132 | rq.lastIndex = 0; 1133 | } 1134 | 1135 | if (cdoc.readyState === "complete") { 1136 | _prepareCallback("", "re-opening", 200, rq.transport); 1137 | _ieStreaming(rq); 1138 | return false; 1139 | } 1140 | }, null); 1141 | 1142 | return false; 1143 | }); 1144 | }, 1145 | 1146 | close: function() { 1147 | if (stop) { 1148 | stop(); 1149 | } 1150 | 1151 | doc.execCommand("Stop"); 1152 | _prepareCallback("", "closed", 200, rq.transport); 1153 | } 1154 | }; 1155 | } 1156 | 1157 | /** 1158 | * Send message. <br> 1159 | * Will be automatically dispatch to other connected. 1160 | * 1161 | * @param {Object, 1162 | * string} Message to send. 1163 | * @private 1164 | */ 1165 | function _push(message) { 1166 | if (_activeRequest != null) { 1167 | _pushAjaxMessage(message); 1168 | } else if (_ieStream != null) { 1169 | _pushIE(message); 1170 | } else if (_jqxhr != null) { 1171 | _pushJsonp(message); 1172 | } else if (_websocket != null) { 1173 | _pushWebSocket(message); 1174 | } 1175 | } 1176 | 1177 | /** 1178 | * Send a message using currently opened ajax request (using 1179 | * http-streaming or long-polling). <br> 1180 | * 1181 | * @param {string, Object} Message to send. This is an object, string 1182 | * message is saved in data member. 1183 | * @private 1184 | */ 1185 | function _pushAjaxMessage(message) { 1186 | var rq = _getPushRequest(message); 1187 | _executeRequest(rq); 1188 | } 1189 | 1190 | /** 1191 | * Send a message using currently opened ie streaming (using 1192 | * http-streaming or long-polling). <br> 1193 | * 1194 | * @param {string, Object} Message to send. This is an object, string 1195 | * message is saved in data member. 1196 | * @private 1197 | */ 1198 | function _pushIE(message) { 1199 | _pushAjaxMessage(message); 1200 | } 1201 | 1202 | /** 1203 | * Send a message using jsonp transport. <br> 1204 | * 1205 | * @param {string, Object} Message to send. This is an object, string 1206 | * message is saved in data member. 1207 | * @private 1208 | */ 1209 | function _pushJsonp(message) { 1210 | _pushAjaxMessage(message); 1211 | } 1212 | 1213 | function _getStringMessage(message) { 1214 | var msg = message; 1215 | if (typeof(msg) == 'object') { 1216 | msg = message.data; 1217 | } 1218 | return msg; 1219 | } 1220 | 1221 | /** 1222 | * Build request use to push message using method 'POST' <br>. 1223 | * Transport is defined as 'polling' and 'suspend' is set to false. 1224 | * 1225 | * @return {Object} Request object use to push message. 1226 | * @private 1227 | */ 1228 | function _getPushRequest(message) { 1229 | var msg = _getStringMessage(message); 1230 | 1231 | var rq = { 1232 | connected: false, 1233 | timeout: 60000, 1234 | method: 'POST', 1235 | url: _request.url, 1236 | contentType : _request.contentType, 1237 | headers: {}, 1238 | cache: true, 1239 | async: true, 1240 | ifModified: false, 1241 | callback: null, 1242 | dataType: '', 1243 | data : msg, 1244 | suspend : false, 1245 | maxRequest : 60, 1246 | logLevel : 'info', 1247 | requestCount : 0, 1248 | transport: 'polling' 1249 | }; 1250 | 1251 | if (typeof(message) == 'object') { 1252 | rq = $.extend(rq, message); 1253 | } 1254 | 1255 | return rq; 1256 | } 1257 | 1258 | /** 1259 | * Send a message using currently opened websocket. <br> 1260 | * 1261 | * @param {string, Object} 1262 | * Message to send. This is an object, string message is 1263 | * saved in data member. 1264 | */ 1265 | function _pushWebSocket(message) { 1266 | var msg = _getStringMessage(message); 1267 | var data; 1268 | try { 1269 | if (_request.webSocketUrl != null) { 1270 | data = _request.webSocketPathDelimiter 1271 | + _request.webSocketUrl 1272 | + _request.webSocketPathDelimiter 1273 | + msg; 1274 | } else { 1275 | data = msg; 1276 | } 1277 | 1278 | _websocket.send(data); 1279 | 1280 | } catch (e) { 1281 | jQuery.atmosphere.log(_request.logLevel, ["Websocket failed. Downgrading to Comet and resending " + data]); 1282 | 1283 | _websocket.onclose = function(message) { 1284 | }; 1285 | _websocket.close(); 1286 | 1287 | _reconnectWithFallbackTransport(); 1288 | _pushAjaxMessage(message); 1289 | } 1290 | } 1291 | 1292 | function _prepareCallback(messageBody, state, errorCode, transport) { 1293 | 1294 | if (state == "messageReceived") { 1295 | if (_trackMessageSize(messageBody, _request, _response)) return; 1296 | } 1297 | 1298 | _response.transport = transport; 1299 | _response.status = errorCode; 1300 | 1301 | // If not -1, we have buffered the message. 1302 | if (_response.expectedBodySize == -1) { 1303 | _response.responseBody = messageBody; 1304 | } 1305 | _response.state = state; 1306 | 1307 | _invokeCallback(); 1308 | } 1309 | 1310 | function _invokeFunction(response) { 1311 | _f(response, _request); 1312 | // Global 1313 | _f(response, jQuery.atmosphere); 1314 | } 1315 | 1316 | function _f(response, f) { 1317 | switch (response.state) { 1318 | case "messageReceived" : 1319 | if (typeof(f.onMessage) != 'undefined') f.onMessage(response); 1320 | break; 1321 | case "error" : 1322 | if (typeof(f.onError) != 'undefined') f.onError(response); 1323 | break; 1324 | case "opening" : 1325 | if (typeof(f.onOpen) != 'undefined') f.onOpen(response); 1326 | break; 1327 | case "messagePublished" : 1328 | if (typeof(f.onMessagePublished) != 'undefined') f.onMessagePublished(response); 1329 | break; 1330 | case "re-opening" : 1331 | if (typeof(f.onReconnect) != 'undefined') f.onReconnect(_request, response); 1332 | break; 1333 | case "closed" : 1334 | if (typeof(f.onClose) != 'undefined') f.onClose(response); 1335 | break; 1336 | } 1337 | } 1338 | 1339 | /** 1340 | * Invoke request callbacks. 1341 | * 1342 | * @private 1343 | */ 1344 | function _invokeCallback() { 1345 | var call = function (index, func) { 1346 | func(_response); 1347 | }; 1348 | 1349 | _invokeFunction(_response); 1350 | 1351 | // Invoke global callbacks 1352 | if (jQuery.atmosphere.callbacks.length > 0) { 1353 | jQuery.atmosphere.debug("Invoking " + jQuery.atmosphere.callbacks.length + " global callbacks: " + _response.state); 1354 | try { 1355 | jQuery.each(jQuery.atmosphere.callbacks, call); 1356 | } catch (e) { 1357 | jQuery.atmosphere.log(_request.logLevel, ["Callback exception" + e]); 1358 | } 1359 | } 1360 | 1361 | // Invoke request callback 1362 | if (typeof(_request.callback) == 'function') { 1363 | if (_request.logLevel == 'debug') { 1364 | jQuery.atmosphere.debug("Invoking request callbacks"); 1365 | } 1366 | try { 1367 | _request.callback(_response); 1368 | } catch (e) { 1369 | jQuery.atmosphere.log(_request.logLevel, ["Callback exception" + e]); 1370 | } 1371 | } 1372 | } 1373 | 1374 | /** 1375 | * Close request. 1376 | * 1377 | * @private 1378 | */ 1379 | function _close() { 1380 | _subscribed = false; 1381 | _abordingConnection = true; 1382 | _response.state = 'unsubscribe'; 1383 | _response.responseBody = ""; 1384 | _response.status = 408; 1385 | _invokeCallback(); 1386 | 1387 | if (_ieStream != null) { 1388 | _ieStream.close(); 1389 | _ieStream = null; 1390 | _abordingConnection = false; 1391 | } 1392 | if (_jqxhr != null) { 1393 | _jqxhr.abort(); 1394 | _jqxhr = null; 1395 | _abordingConnection = false; 1396 | } 1397 | if (_activeRequest != null) { 1398 | _activeRequest.abort(); 1399 | _activeRequest = null; 1400 | _abordingConnection = false; 1401 | } 1402 | if (_websocket != null) { 1403 | _closingWebSocket = true; 1404 | _websocket.close(); 1405 | _websocket = null; 1406 | } 1407 | } 1408 | 1409 | this.subscribe = function(options) { 1410 | _subscribe(options); 1411 | }; 1412 | 1413 | this.execute = function() { 1414 | _execute(); 1415 | }; 1416 | 1417 | this.invokeCallback = function() { 1418 | _invokeCallback(); 1419 | }; 1420 | 1421 | this.close = function() { 1422 | _close(); 1423 | }; 1424 | 1425 | this.getUrl = function() { 1426 | return _request.url; 1427 | }; 1428 | 1429 | this.push = function(message) { 1430 | _push(message); 1431 | } 1432 | 1433 | this.response = _response; 1434 | }, 1435 | 1436 | subscribe: function(url, callback, request) { 1437 | if (typeof(callback) == 'function') { 1438 | jQuery.atmosphere.addCallback(callback); 1439 | } 1440 | 1441 | if (typeof(url) != "string") { 1442 | request = url; 1443 | } else { 1444 | request.url = url; 1445 | } 1446 | 1447 | var rq = new jQuery.atmosphere.AtmosphereRequest(request); 1448 | jQuery.atmosphere.requests[jQuery.atmosphere.requests.length] = rq; 1449 | return rq; 1450 | }, 1451 | 1452 | addCallback: function(func) { 1453 | if (jQuery.inArray(func, jQuery.atmosphere.callbacks) == -1) { 1454 | jQuery.atmosphere.callbacks.push(func); 1455 | } 1456 | }, 1457 | 1458 | removeCallback: function(func) { 1459 | var index = jQuery.inArray(func, jQuery.atmosphere.callbacks); 1460 | if (index != -1) { 1461 | jQuery.atmosphere.callbacks.splice(index, 1); 1462 | } 1463 | }, 1464 | 1465 | unsubscribe : function() { 1466 | if (jQuery.atmosphere.requests.length > 0) { 1467 | for (var i = 0; i < jQuery.atmosphere.requests.length; i++) { 1468 | jQuery.atmosphere.requests[i].close(); 1469 | clearTimeout(jQuery.atmosphere.requests[i].id); 1470 | } 1471 | } 1472 | jQuery.atmosphere.requests = []; 1473 | jQuery.atmosphere.callbacks = []; 1474 | }, 1475 | 1476 | unsubscribeUrl: function(url) { 1477 | var idx = -1; 1478 | if (jQuery.atmosphere.requests.length > 0) { 1479 | for (var i = 0; i < jQuery.atmosphere.requests.length; i++) { 1480 | var rq = jQuery.atmosphere.requests[i]; 1481 | 1482 | // Suppose you can subscribe once to an url 1483 | if (rq.getUrl() == url) { 1484 | rq.close(); 1485 | clearTimeout(rq.id); 1486 | idx = i; 1487 | break; 1488 | } 1489 | } 1490 | } 1491 | if (idx >= 0) { 1492 | jQuery.atmosphere.requests.splice(idx, 1); 1493 | } 1494 | }, 1495 | 1496 | publish: function(request) { 1497 | if (typeof(request.callback) == 'function') { 1498 | jQuery.atmosphere.addCallback(callback); 1499 | } 1500 | request.transport = "polling"; 1501 | 1502 | var rq = new jQuery.atmosphere.AtmosphereRequest(request); 1503 | jQuery.atmosphere.requests[jQuery.atmosphere.requests.length] = rq; 1504 | return rq; 1505 | }, 1506 | 1507 | checkCORSSupport : function() { 1508 | if (jQuery.browser.msie && !window.XDomainRequest) { 1509 | return true; 1510 | } else if (jQuery.browser.opera) { 1511 | return true; 1512 | } 1513 | 1514 | // Force Android to use CORS as some version like 2.2.3 fail otherwise 1515 | var ua = navigator.userAgent.toLowerCase(); 1516 | var isAndroid = ua.indexOf("android") > -1; 1517 | if (isAndroid) { 1518 | return true; 1519 | } 1520 | return false; 1521 | }, 1522 | 1523 | S4 : function() { 1524 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 1525 | }, 1526 | 1527 | guid : function() { 1528 | return (jQuery.atmosphere.S4() + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + jQuery.atmosphere.S4() + jQuery.atmosphere.S4()); 1529 | }, 1530 | 1531 | // From jQuery-Stream 1532 | prepareURL: function(url) { 1533 | // Attaches a time stamp to prevent caching 1534 | var ts = jQuery.now(); 1535 | var ret = url.replace(/([?&])_=[^&]*/, "$1_=" + ts); 1536 | 1537 | return ret + (ret === url ? (/\?/.test(url) ? "&" : "?") + "_=" + ts : ""); 1538 | }, 1539 | 1540 | // From jQuery-Stream 1541 | param : function(data) { 1542 | return jQuery.param(data, jQuery.ajaxSettings.traditional); 1543 | }, 1544 | 1545 | iterate : function (fn, interval) { 1546 | var timeoutId; 1547 | 1548 | // Though the interval is 0 for real-time application, there is a delay between setTimeout calls 1549 | // For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting 1550 | interval = interval || 0; 1551 | 1552 | (function loop() { 1553 | timeoutId = setTimeout(function() { 1554 | if (fn() === false) { 1555 | return; 1556 | } 1557 | 1558 | loop(); 1559 | }, interval); 1560 | })(); 1561 | 1562 | return function() { 1563 | clearTimeout(timeoutId); 1564 | }; 1565 | }, 1566 | 1567 | parseUri : function(baseUrl, uri) { 1568 | var protocol = window.location.protocol; 1569 | var host = window.location.host; 1570 | var path = window.location.pathname; 1571 | var parameters = {}; 1572 | var anchor = ''; 1573 | var pos; 1574 | 1575 | if ((pos = uri.search(/\:/)) >= 0) { 1576 | protocol = uri.substring(0, pos + 1); 1577 | uri = uri.substring(pos + 1); 1578 | } 1579 | 1580 | if ((pos = uri.search(/\#/)) >= 0) { 1581 | anchor = uri.substring(pos + 1); 1582 | uri = uri.substring(0, pos); 1583 | } 1584 | 1585 | if ((pos = uri.search(/\?/)) >= 0) { 1586 | var paramsStr = uri.substring(pos + 1) + '&;'; 1587 | uri = uri.substring(0, pos); 1588 | while ((pos = paramsStr.search(/\&/)) >= 0) { 1589 | var paramStr = paramsStr.substring(0, pos); 1590 | paramsStr = paramsStr.substring(pos + 1); 1591 | 1592 | if (paramStr.length) { 1593 | var equPos = paramStr.search(/\=/); 1594 | if (equPos < 0) { 1595 | parameters[paramStr] = ''; 1596 | } else { 1597 | parameters[paramStr.substring(0, equPos)] = 1598 | decodeURIComponent(paramStr.substring(equPos + 1)); 1599 | } 1600 | } 1601 | } 1602 | } 1603 | 1604 | if (uri.search(/\/\//) == 0) { 1605 | uri = uri.substring(2); 1606 | if ((pos = uri.search(/\//)) >= 0) { 1607 | host = uri.substring(0, pos); 1608 | path = uri.substring(pos); 1609 | } else { 1610 | host = uri; 1611 | path = '/'; 1612 | } 1613 | } else if (uri.search(/\//) == 0) { 1614 | path = uri; 1615 | } 1616 | 1617 | else // relative to directory 1618 | { 1619 | var p = path.lastIndexOf('/'); 1620 | if (p < 0) { 1621 | path = '/'; 1622 | } else if (p < path.length - 1) { 1623 | path = path.substring(0, p + 1); 1624 | } 1625 | 1626 | while (uri.search(/\.\.\//) == 0) { 1627 | p = path.lastIndexOf('/', path.lastIndexOf('/') - 1); 1628 | if (p >= 0) { 1629 | path = path.substring(0, p + 1); 1630 | } 1631 | uri = uri.substring(3); 1632 | } 1633 | path = path + uri; 1634 | } 1635 | 1636 | var formattedUri = protocol + '//' + host + path; 1637 | var div = '?'; 1638 | for (var key in parameters) { 1639 | formattedUri += div + key + '=' + encodeURIComponent(parameters[key]); 1640 | div = '&'; 1641 | } 1642 | return formattedUri; 1643 | }, 1644 | 1645 | log: function (level, args) { 1646 | if (window.console) { 1647 | var logger = window.console[level]; 1648 | if (typeof logger == 'function') { 1649 | logger.apply(window.console, args); 1650 | } 1651 | } 1652 | }, 1653 | 1654 | warn: function() { 1655 | jQuery.atmosphere.log('warn', arguments); 1656 | }, 1657 | 1658 | info :function() { 1659 | jQuery.atmosphere.log('info', arguments); 1660 | }, 1661 | 1662 | debug: function() { 1663 | jQuery.atmosphere.log('debug', arguments); 1664 | } 1665 | }; 1666 | }(); 1667 | 1668 | /* 1669 | * jQuery stringifyJSON 1670 | * http://github.com/flowersinthesand/jquery-stringifyJSON 1671 | * 1672 | * Copyright 2011, Donghwan Kim 1673 | * Licensed under the Apache License, Version 2.0 1674 | * http://www.apache.org/licenses/LICENSE-2.0 1675 | */ 1676 | // This plugin is heavily based on Douglas Crockford's reference implementation 1677 | (function($) { 1678 | 1679 | var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, meta = { 1680 | '\b' : '\\b', 1681 | '\t' : '\\t', 1682 | '\n' : '\\n', 1683 | '\f' : '\\f', 1684 | '\r' : '\\r', 1685 | '"' : '\\"', 1686 | '\\' : '\\\\' 1687 | }; 1688 | 1689 | function quote(string) { 1690 | return '"' + string.replace(escapable, function(a) { 1691 | var c = meta[a]; 1692 | return typeof c === "string" ? c : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); 1693 | }) + '"'; 1694 | } 1695 | 1696 | function f(n) { 1697 | return n < 10 ? "0" + n : n; 1698 | } 1699 | 1700 | function str(key, holder) { 1701 | var i, v, len, partial, value = holder[key], type = typeof value; 1702 | 1703 | if (value && typeof value === "object" && typeof value.toJSON === "function") { 1704 | value = value.toJSON(key); 1705 | type = typeof value; 1706 | } 1707 | 1708 | switch (type) { 1709 | case "string": 1710 | return quote(value); 1711 | case "number": 1712 | return isFinite(value) ? String(value) : "null"; 1713 | case "boolean": 1714 | return String(value); 1715 | case "object": 1716 | if (!value) { 1717 | return "null"; 1718 | } 1719 | 1720 | switch (Object.prototype.toString.call(value)) { 1721 | case "[object Date]": 1722 | return isFinite(value.valueOf()) ? '"' + value.getUTCFullYear() + "-" + f(value.getUTCMonth() + 1) + "-" + f(value.getUTCDate()) + "T" + 1723 | f(value.getUTCHours()) + ":" + f(value.getUTCMinutes()) + ":" + f(value.getUTCSeconds()) + "Z" + '"' : "null"; 1724 | case "[object Array]": 1725 | len = value.length; 1726 | partial = []; 1727 | for (i = 0; i < len; i++) { 1728 | partial.push(str(i, value) || "null"); 1729 | } 1730 | 1731 | return "[" + partial.join(",") + "]"; 1732 | default: 1733 | partial = []; 1734 | for (i in value) { 1735 | if (Object.prototype.hasOwnProperty.call(value, i)) { 1736 | v = str(i, value); 1737 | if (v) { 1738 | partial.push(quote(i) + ":" + v); 1739 | } 1740 | } 1741 | } 1742 | 1743 | return "{" + partial.join(",") + "}"; 1744 | } 1745 | } 1746 | } 1747 | 1748 | $.stringifyJSON = function(value) { 1749 | if (window.JSON && window.JSON.stringify) { 1750 | return window.JSON.stringify(value); 1751 | } 1752 | 1753 | return str("", {"": value}); 1754 | }; 1755 | 1756 | }(jQuery)); --------------------------------------------------------------------------------