├── .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 |
96 |
?
97 |
98 |
?
99 |
?
100 |
?
101 |
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 | 
23 |
24 | ### Running full screen on a cheap ($50) Android tablet
25 |
26 | 
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.
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).
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).
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.
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' .
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.
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));
--------------------------------------------------------------------------------