├── .gitignore
├── example-plane-data-empty.json
├── process.json
├── client
├── images
│ ├── thumb.jpg
│ ├── reload.png
│ ├── routes
│ │ └── lyon--fr.jpg
│ └── placeholder
│ │ ├── bosnia.jpg
│ │ ├── moon.jpg
│ │ ├── norway.jpg
│ │ ├── rome.jpg
│ │ ├── serbia.jpg
│ │ ├── sussex.jpg
│ │ ├── sydney.jpg
│ │ ├── wales.jpg
│ │ ├── bulgaria.jpg
│ │ ├── cambodia.jpg
│ │ ├── finland.jpg
│ │ ├── hamburg.jpg
│ │ ├── iceland.jpg
│ │ ├── ireland.jpg
│ │ ├── new-york.jpg
│ │ ├── scotland.jpg
│ │ ├── thailand.jpg
│ │ ├── yosemite.jpg
│ │ ├── barcelona.jpg
│ │ ├── hong-kong.jpg
│ │ ├── milky-way.jpg
│ │ ├── montenegro.jpg
│ │ ├── new-zealand.jpg
│ │ ├── stockholm.jpg
│ │ └── vancouver.jpg
├── manifest.json
├── index.html
├── js
│ ├── plane-spotter.js
│ └── jquery-3.1.1.min.js
└── css
│ └── plane-spotter.css
├── deploy.sh
├── lib
├── debug.js
├── get-ip.js
├── update-csv-files.js
├── find-closest-plane.js
├── find-plane-route.js
└── find-image.js
├── package.json
├── config.sample.js
├── readme.md
├── example-plane-data.json
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | config.js
2 | node_modules
--------------------------------------------------------------------------------
/example-plane-data-empty.json:
--------------------------------------------------------------------------------
1 | {
2 | "aircraft": [
3 | ]
4 | }
5 |
--------------------------------------------------------------------------------
/process.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_watch" : ["node_modules", "client/images/routes", "data"]
3 | }
--------------------------------------------------------------------------------
/client/images/thumb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/thumb.jpg
--------------------------------------------------------------------------------
/client/images/reload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/reload.png
--------------------------------------------------------------------------------
/client/images/routes/lyon--fr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/routes/lyon--fr.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/bosnia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/bosnia.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/moon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/moon.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/norway.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/norway.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/rome.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/rome.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/serbia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/serbia.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/sussex.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/sussex.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/sydney.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/sydney.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/wales.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/wales.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/bulgaria.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/bulgaria.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/cambodia.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/cambodia.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/finland.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/finland.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/hamburg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/hamburg.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/iceland.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/iceland.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/ireland.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/ireland.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/new-york.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/new-york.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/scotland.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/scotland.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/thailand.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/thailand.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/yosemite.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/yosemite.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/barcelona.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/barcelona.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/hong-kong.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/hong-kong.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/milky-way.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/milky-way.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/montenegro.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/montenegro.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/new-zealand.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/new-zealand.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/stockholm.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/stockholm.jpg
--------------------------------------------------------------------------------
/client/images/placeholder/vancouver.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orangespaceman/plane-spotter/master/client/images/placeholder/vancouver.jpg
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | rsync ./ pi@pi:~/plane-spotter --exclude=.DS_Store --exclude=deploy.sh --exclude=node_modules --exclude=.git --progress --rsh=ssh --recursive --verbose --delete --links
--------------------------------------------------------------------------------
/client/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Plane Spotter",
3 | "name": "Plane Spotter",
4 | "icons": [
5 | {
6 | "src": "images/thumb.jpg",
7 | "sizes": "192x192",
8 | "type": "image/jpeg"
9 | }
10 | ],
11 | "start_url": "/",
12 | "display": "fullscreen",
13 | "orientation": "landscape"
14 | }
15 |
--------------------------------------------------------------------------------
/lib/debug.js:
--------------------------------------------------------------------------------
1 | const config = require('../config')
2 | let io = null
3 |
4 | function log () {
5 | if (config.debug) {
6 | console.log.apply(console, arguments)
7 | io.emit.call(io, 'debug', Array.prototype.slice.call(arguments))
8 | }
9 | }
10 |
11 | function storeSocketReference (ioReference) {
12 | io = ioReference
13 | }
14 |
15 | module.exports = {
16 | log: log,
17 | storeSocketReference: storeSocketReference
18 | }
--------------------------------------------------------------------------------
/lib/get-ip.js:
--------------------------------------------------------------------------------
1 | const debug = require('./debug')
2 | const os = require('os')
3 |
4 | function getIp () {
5 | const interfaces = os.networkInterfaces()
6 | const addresses = []
7 |
8 | for (let k in interfaces) {
9 | for (let k2 in interfaces[k]) {
10 | let address = interfaces[k][k2]
11 | if (address.family === 'IPv4' && !address.internal) {
12 | addresses.push(address.address)
13 | }
14 | }
15 | }
16 |
17 | return addresses[0]
18 | }
19 |
20 | module.exports = getIp
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "plane-spotter",
3 | "version": "0.0.1",
4 | "description": "A Node app to identify nearby aeroplanes with a Raspberry Pi, Dump1090, Node, an LCD touchscreen and some magic",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "csv": "^1.1.0",
13 | "express": "^4.14.0",
14 | "google-images": "^1.0.0",
15 | "haversine": "^1.0.1",
16 | "lodash": "^4.17.0",
17 | "request": "^2.78.0",
18 | "smartcrop-gm": "^1.0.2",
19 | "socket.io": "^1.5.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/config.sample.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | // planeData: './example-plane-data.json',
3 | // planeData: './example-plane-data-empty.json',
4 | planeData: '/run/dump1090-mutability/aircraft.json',
5 | debug: false,
6 | port: 3000,
7 | placeholderPath: 'client/images/placeholder/',
8 | position: {
9 | latitude: 50.829,
10 | longitude: -0.141
11 | },
12 | screen: {
13 | width: 480,
14 | height: 320
15 | },
16 | maxAge: new Date().getTime() - (1000 * 60 * 60 * 24 * 30),
17 | timeout: 10000,
18 | launch: {
19 | application: 'chromium-browser',
20 | flags: '--noerrdialogs --kiosk --incognito --display=:0 &'
21 | },
22 | googleCseId: '',
23 | googleApiKey: ''
24 | }
25 |
26 | module.exports = config
27 |
--------------------------------------------------------------------------------
/lib/update-csv-files.js:
--------------------------------------------------------------------------------
1 | const debug = require('./debug')
2 | const config = require('../config')
3 | const request = require('request')
4 | const fs = require('fs')
5 |
6 | const files = [
7 | {
8 | path: 'data/airports.csv',
9 | location: 'http://ourairports.com/data/airports.csv'
10 | },
11 | {
12 | path: 'data/flights.csv',
13 | location: 'http://www.virtualradarserver.co.uk/Files/FlightNumbers.csv'
14 | }
15 | ]
16 |
17 | function updateCsvFiles () {
18 | files.forEach(file => {
19 | fs.stat(file.path, (err, stat) => {
20 | if (err) {
21 | debug.log('ERROR: updateCsvFiles', err)
22 | return
23 | }
24 | const fileAge = new Date(stat.ctime).getTime()
25 | if (fileAge < config.maxAge) {
26 | debug.log(`${file.path} is older than one month, updating`)
27 | request(file.location).pipe(fs.createWriteStream(file.path))
28 | }
29 | })
30 | })
31 | }
32 |
33 | module.exports = updateCsvFiles
--------------------------------------------------------------------------------
/lib/find-closest-plane.js:
--------------------------------------------------------------------------------
1 | const config = require ('../config')
2 | const debug = require('./debug')
3 | const haversine = require('haversine')
4 |
5 | function findClosestPlane (planes) {
6 | let closestPlane
7 | let closestDistance
8 | let mostRecentlySeenPlane
9 | let mostRecentlySeenPlaneTime
10 |
11 | planes.forEach(plane => {
12 | if (!plane.flight || !'seen' in plane) return
13 | if (!mostRecentlySeenPlaneTime || plane.seen < mostRecentlySeenPlaneTime) {
14 | mostRecentlySeenPlane = plane
15 | mostRecentlySeenPlaneTime = plane.seen
16 | }
17 | if (plane.lat && plane.lon) {
18 | let distance = haversine(config.position, {
19 | latitude: plane.lat,
20 | longitude: plane.lon
21 | })
22 | debug.log(`Plane: ${plane.flight.trim()}, Distance: ${distance.toFixed(2)}mi`)
23 | if (!closestDistance || distance < closestDistance) {
24 | closestDistance = distance
25 | closestPlane = plane
26 | }
27 | }
28 | })
29 | if (closestPlane) {
30 | return {plane: closestPlane, distance: closestDistance}
31 | } else if (mostRecentlySeenPlane) {
32 | return {plane: mostRecentlySeenPlane}
33 | } else {
34 | return {}
35 | }
36 | }
37 |
38 | module.exports = findClosestPlane
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Plane Spotter
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | | Route |
17 | |
18 |
19 |
20 | | Flight number |
21 | |
22 |
23 |
24 | | Altitude |
25 | |
26 |
27 |
28 | | Latitude |
29 | |
30 |
31 |
32 | | Longitude |
33 | |
34 |
35 |
36 | | Speed |
37 | |
38 |
39 |
40 | | Distance |
41 | |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/lib/find-plane-route.js:
--------------------------------------------------------------------------------
1 | const config = require ('../config')
2 | const debug = require('./debug')
3 | const fs = require('fs')
4 | const parse = require('csv-parse/lib/sync')
5 | const _ = require('lodash')
6 | const haversine = require('haversine')
7 |
8 | debug.log(`parsing flight data`)
9 | const flightData = parse(fs.readFileSync('data/flights.csv'), {columns: true})
10 | debug.log(`parsing airport data`)
11 | const airportData = parse(fs.readFileSync('data/airports.csv'), { columns: true})
12 | debug.log(`parsing data complete`)
13 |
14 | function findPlaneRoute ({plane}) {
15 | const routeName = findRoute(plane)
16 | if (!routeName) return
17 |
18 | const [origin, destination] = routeName.Route.split('-')
19 | const route = {}
20 |
21 | route.origin = findAirport(origin)
22 | route.destination = findAirport(destination)
23 |
24 | const distanceFromOrigin = haversine(config.position, {latitude: route.origin.latitude_deg, longitude: route.origin.longitude_deg})
25 | const distanceFromDestination = haversine(config.position, {latitude: route.destination.latitude_deg, longitude: route.destination.longitude_deg})
26 |
27 | debug.log(`Origin distance: ${distanceFromOrigin.toFixed(2)}mi, destination distance: ${distanceFromDestination.toFixed(2)}mi`)
28 |
29 | if (distanceFromDestination < distanceFromOrigin) {
30 | route.feature = route.origin
31 | } else {
32 | route.feature = route.destination
33 | }
34 |
35 | route.description = `${route.origin.municipality}, ${route.origin.iso_country} - ${route.destination.municipality}, ${route.destination.iso_country}`
36 |
37 | return route
38 | }
39 |
40 | function findRoute (plane) {
41 | const airlineCode = plane.flight.trim().slice(0, 3)
42 | const flightNumber = plane.flight.trim().slice(3)
43 |
44 | return _.find(flightData, {'AirlineCode': airlineCode, 'FlightNumber': flightNumber})
45 | }
46 |
47 | function findAirport (ident) {
48 | return _.find(airportData, {'ident': ident})
49 | }
50 |
51 | module.exports = findPlaneRoute
52 |
--------------------------------------------------------------------------------
/client/js/plane-spotter.js:
--------------------------------------------------------------------------------
1 | // cache els
2 |
3 | var $body = $('body');
4 | var $bg = $('.bg');
5 | var $detail = $('.detail');
6 | var $flightNumber = $('.flight-number');
7 | var $altitude = $('.altitude');
8 | var $latitude = $('.latitude');
9 | var $longitude = $('.longitude');
10 | var $speed = $('.speed');
11 | var $distance = $('.distance');
12 | var $route = $('.route');
13 | var $reload = $('.reload');
14 | var $url = $('.url');
15 |
16 | // sockets
17 |
18 | var socket = io();
19 | var lastState;
20 |
21 | socket.on('state', function (state) {
22 | if (lastState && lastState.imagePath === state.imagePath) {
23 | updateText(state);
24 | } else {
25 | var showDetail = $body.hasClass('show');
26 | $body.removeClass('show');
27 | $bg.fadeOut(1000, function () {
28 | updateText(state);
29 | $bg
30 | .css('background-image', 'url('+state.imagePath+')')
31 | .fadeIn(1000);
32 | if (showDetail) {
33 | $body.addClass('show');
34 | }
35 | });
36 | }
37 | lastState = state;
38 | });
39 |
40 | function updateText (state) {
41 | $url.text(state.url);
42 | $route.text(state.route.description);
43 |
44 | if (state.distance) {
45 | $distance.text(state.distance.toFixed(2) + 'mi');
46 | } else {
47 | $distance.text('-');
48 | }
49 |
50 | if (state.plane) {
51 | $flightNumber.text(state.plane.flight.trim());
52 | $altitude.text(state.plane.altitude + 'ft' || '-');
53 | $latitude.text(state.plane.lat);
54 | $longitude.text(state.plane.lon);
55 | $speed.text(state.plane.speed + 'mph' || '-');
56 | } else {
57 | $flightNumber.text('-');
58 | $altitude.text('-');
59 | $latitude.text('-');
60 | $longitude.text('-');
61 | $speed.text('-');
62 | }
63 | }
64 |
65 | // detail
66 |
67 | $detail.on('click', function (e) {
68 | $body.toggleClass('show');
69 | });
70 |
71 | // reload
72 |
73 | $reload.on('click', function (e) {
74 | e.preventDefault();
75 | window.location.reload();
76 | })
77 |
78 | // debug
79 |
80 | socket.on('debug', function (data) {
81 | console.log.apply(console.log, data);
82 | })
--------------------------------------------------------------------------------
/client/css/plane-spotter.css:
--------------------------------------------------------------------------------
1 | /* elements */
2 |
3 | html {
4 | box-sizing: border-box;
5 | }
6 |
7 | html,
8 | body {
9 | height: 100%;
10 | }
11 |
12 | *,
13 | *:before,
14 | *:after {
15 | box-sizing: inherit;
16 | }
17 |
18 | body {
19 | cursor: none;
20 | font-family: Helvetica, Arial;
21 |
22 | -webkit-font-smoothing: antialiased;
23 | -moz-osx-font-smoothing: grayscale;
24 | }
25 |
26 | * {
27 | margin: 0;
28 | padding: 0;
29 | }
30 |
31 | /* bg */
32 |
33 | .bg {
34 | background-position: 50% 50%;
35 | background-size: cover;
36 | height: 100%;
37 | left: 0;
38 | position: fixed;
39 | top: 0;
40 | width: 100%;
41 | z-index: 1;
42 | }
43 |
44 | /* detail */
45 |
46 | .detail {
47 | align-items: center;
48 | display: flex;
49 | height: 100%;
50 | justify-content: center;
51 | opacity: 0;
52 | position: relative;
53 | transition: opacity 1s;
54 | z-index: 2;
55 |
56 | -webkit-user-select: none;
57 | -moz-user-select: none;
58 | -ms-user-select: none;
59 | -o-user-select: none;
60 | user-select: none;
61 | }
62 |
63 | body.show .detail {
64 | opacity: 1;
65 | }
66 |
67 | table {
68 | background: rgba(255, 255, 255, 0.7);
69 | border-radius: 20px;
70 | font-size: 14px;
71 | padding: 15px;
72 | }
73 |
74 | th, td {
75 | padding: 3px;
76 | text-transform: capitalize;
77 | }
78 |
79 | th {
80 | text-align: right;
81 | }
82 |
83 | /* reload */
84 |
85 | .reload {
86 | background: url(../images/reload.png) #000 no-repeat 50% 50%;
87 | background-size: 24px 24px;
88 | bottom: 10px;
89 | color: #fff;
90 | height: 24px;
91 | opacity: 0;
92 | padding: 15px 20px;
93 | position: fixed;
94 | right: 10px;
95 | text-decoration: none;
96 | width: 24px;
97 | z-index: 3;
98 | }
99 |
100 | body.show .reload {
101 | opacity: 1;
102 | }
103 |
104 | /* url */
105 |
106 | .url {
107 | background: #000;
108 | bottom:10px;
109 | color: #fff;
110 | font-size: 12px;
111 | left: 10px;
112 | opacity: 0;
113 | padding: 10px 15px;
114 | position: fixed;
115 | transition: opacity 1s;
116 | z-index: 3;
117 | }
118 |
119 | body.show .url {
120 | opacity: 0.75;
121 | }
122 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Plane Spotter
2 |
3 | ## Overview
4 |
5 | A Node app to identify nearby aeroplanes with a Raspberry Pi, Dump1090, Node, an LCD touchscreen and some magic
6 |
7 | ## Hardware
8 |
9 | - Raspberry Pi
10 | - Touchscreen (I used a [Waveshare Raspberry Pi 3.5](http://www.waveshare.com/3.5inch-rpi-lcd-a.htm))
11 | - DVB-T Receiver (I used an [RTLSDR RTL2832U DVB-T Tuner Dongle](http://www.rtl-sdr.com/buy-rtl-sdr-dvb-t-dongles/))
12 |
13 | ## Pi setup
14 | - [Install Raspbian](https://www.raspberrypi.org/downloads/raspbian/) (tested with Raspbian Jessie)
15 |
16 | - Add wifi/network settings
17 |
18 | - [Update Raspbian](https://www.raspberrypi.org/documentation/raspbian/updating.md)
19 |
20 | - [Add an SSH key for password-less login](https://www.raspberrypi.org/documentation/remote-access/ssh/passwordless.md) (optional)
21 |
22 | - [install LCD screen drivers](http://www.waveshare.com/wiki/3.5inch_RPi_LCD_(A)) (optional - these are the specific instructions for the screen I bought, a Waveshare Spotpear 3.5)
23 |
24 | - [Disable Pi screensaver](https://oshlab.com/raspberry-pi-kiosk-mode/)
25 |
26 | - [Install dump1090](https://github.com/mutability/dump1090)
27 |
28 | - [Update Node](http://thisdavej.com/upgrading-to-more-recent-versions-of-node-js-on-the-raspberry-pi/) (Raspbian Jessie comes with v0.12, this codebase requires v7+)
29 |
30 | - Install imagemagick:
31 |
32 | ```
33 | sudo apt-get install imagemagick graphicsmagick
34 | ```
35 | - [Find your current latitude and longitude](http://en.mygeoposition.com/)
36 |
37 | - [Create a custom Google search](https://github.com/vdemedes/google-images#set-up-google-custom-search-engine) (take note of the ID and API key)
38 |
39 | - Set up this project
40 | 1. Clone this repo to the Pi
41 | 2. Install node dependencies: ```npm install```
42 | 3. Set up config file (see below)
43 |
44 | - [Install PM2](http://arroyocode.com/raspberry-pi-nodejs-web-server-with-pm2/) and [fix permissions](https://github.com/Unitech/pm2/issues/1654)
45 |
46 | ## Node app setup
47 |
48 | The file `config.sample.js` should be copied and renamed to `config.js`
49 |
50 | There are a few options to set:
51 |
52 | - `planeData`: Change this to the demo json file to use test data
53 | - `debug`: Output debug statements to both node and chrome console
54 | - `port`: The port to use
55 | - `position`: Your current lat/lon position
56 | - `screen`: The width and height of the display screen, for image resizing
57 | - `maxAge`: How long to store cached images and data
58 | - `timeout`: How frequently to update the screen
59 | - `launch`: The application to launch (and flags to use) on startup
60 | - `googleCseId`: The Google custom search engine ID
61 | - `googleApiKey`: The Google custom search API key
62 |
63 | You should now be able to start the application directly with `node index.js` or via `pm2` to have it automatically restart when the Pi powers up
64 |
65 | ## Placeholder images
66 |
67 | The app uses a collection of placeholder images (selected at random) if no plane is found. These are located in the directory `client/images/placeholder/` and can be replaced with others.
68 |
69 | ## More information and inspiration
70 |
71 | - [ourairports.com](http://ourairports.com/) for the Airport data
72 | - [Virtual Radar Server](http://www.virtualradarserver.co.uk) for the Flight route data
73 | - [http://www.rtl-sdr.com/adsb-aircraft-radar-with-rtl-sdr/](http://www.rtl-sdr.com/adsb-aircraft-radar-with-rtl-sdr/)
74 | - [http://jeremybmerrill.com/uncategorized/2016/01/24/flyover.html](http://jeremybmerrill.com/uncategorized/2016/01/24/flyover.html)
75 |
--------------------------------------------------------------------------------
/example-plane-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "aircraft": [
3 | {
4 | "flight": "EZY76JN ",
5 | "altitude": 123,
6 | "messages": 123,
7 | "seen": 123,
8 | "speed": 123,
9 | "lat": 150.82,
10 | "lon": -0.14
11 | },
12 | {
13 | "hex": "344696",
14 | "squawk": "2067",
15 | "flight": "",
16 | "lat": 0.000000,
17 | "lon": 0.000000,
18 | "validposition": 0,
19 | "altitude": 8775,
20 | "vert_rate": 0,
21 | "track": 0,
22 | "validtrack": 0,
23 | "speed": 0,
24 | "messages": 2,
25 | "seen": 4
26 | },
27 | {
28 | "hex": "4ca5f0",
29 | "squawk": "7444",
30 | "flight": "RYR41MM ",
31 | "lat": 51.298285,
32 | "lon": -0.536194,
33 | "validposition": 1,
34 | "altitude": 19000,
35 | "vert_rate": 0,
36 | "track": 28,
37 | "validtrack": 1,
38 | "speed": 339,
39 | "messages": 89,
40 | "seen": 0
41 | },
42 | {
43 | "hex": "4ab42d",
44 | "squawk": "1211",
45 | "flight": "SWN022B ",
46 | "lat": 0.000000,
47 | "lon": 0.000000,
48 | "validposition": 0,
49 | "altitude": 19000,
50 | "vert_rate": 0,
51 | "track": 0,
52 | "validtrack": 0,
53 | "speed": 0,
54 | "messages": 237,
55 | "seen": 0
56 | },
57 | {
58 | "hex": "392ae9",
59 | "squawk": "6343",
60 | "flight": "AFR1565 ",
61 | "lat": 50.824011,
62 | "lon": 0.027161,
63 | "validposition": 1,
64 | "altitude": 28975,
65 | "vert_rate": 0,
66 | "track": 136,
67 | "validtrack": 1,
68 | "speed": 532,
69 | "messages": 636,
70 | "seen": 0
71 | },
72 | {
73 | "hex": "405a46",
74 | "squawk": "7707",
75 | "flight": "BAW364 ",
76 | "lat": 50.755280,
77 | "lon": -0.161751,
78 | "validposition": 1,
79 | "altitude": 18700,
80 | "vert_rate": 1920,
81 | "track": 153,
82 | "validtrack": 1,
83 | "speed": 424,
84 | "messages": 433,
85 | "seen": 0
86 | },
87 | {
88 | "hex": "4010ee",
89 | "squawk": "5516",
90 | "flight": "EZY87DR ",
91 | "lat": 50.976702,
92 | "lon": -0.062714,
93 | "nucp": 7,
94 | "seen_pos": 54.5,
95 | "altitude": 7200,
96 | "vert_rate": -1344,
97 | "track": 62,
98 | "speed": 277,
99 | "messages": 96,
100 | "seen": 38.8,
101 | "rssi": -34.3
102 | },
103 | {
104 | "hex": "400dad",
105 | "squawk": "6316",
106 | "flight": "EZY68ZR ",
107 | "altitude": 19450,
108 | "vert_rate": 2048,
109 | "track": 153,
110 | "speed": 422,
111 | "messages": 858,
112 | "seen": 94.8,
113 | "rssi": -34.5
114 | },
115 | {
116 | "hex": "440593",
117 | "squawk": "6352",
118 | "flight": "GAC391A ",
119 | "altitude": 27750,
120 | "messages": 832,
121 | "seen": 292.8,
122 | "rssi": -34.7
123 | },
124 | {
125 | "hex": "4ca9eb",
126 | "squawk": "7256",
127 | "flight": "RYR36YK ",
128 | "altitude": 35000,
129 | "vert_rate": -64,
130 | "track": 143,
131 | "speed": 480,
132 | "messages": 1298,
133 | "seen": 204.2,
134 | "rssi": -35.6
135 | }
136 | ]
137 | }
138 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const express = require('express')
3 | const app = express()
4 | const http = require('http').Server(app)
5 | const io = require('socket.io')(http)
6 | const exec = require('child_process').exec
7 |
8 | const config = require('./config')
9 | const debug = require('./lib/debug')
10 |
11 | debug.storeSocketReference(io)
12 |
13 | const updateCsvFiles = require('./lib/update-csv-files')
14 | const getIp = require('./lib/get-ip')
15 | const findClosestPlane = require('./lib/find-closest-plane')
16 | const findPlaneRoute = require('./lib/find-plane-route')
17 | const findImage = require('./lib/find-image')
18 |
19 | let state = {}
20 | let lastState = {}
21 |
22 | init()
23 |
24 | function init () {
25 | updateCsvFiles()
26 |
27 | app.use(express.static('client'))
28 |
29 | app.get('/', (req, res) => {
30 | res.sendFile('index.html', {root: `${__dirname}/client/`})
31 | })
32 |
33 | http.listen(config.port, () => {
34 | if (config.launch.application && config.launch.flags) {
35 | exec(`${config.launch.application} ${state.url} ${config.launch.flags}`, () => { debug.log(arguments) })
36 | }
37 | debug.log(`Server running: ${state.url}`)
38 | })
39 |
40 | io.on('connection', (socket) => {
41 | socket.emit('state', state)
42 | })
43 |
44 | updateState()
45 | }
46 |
47 | function updateState () {
48 | lastState = state
49 | state = {}
50 | state.url = `http://${getIp()}:${config.port}`
51 |
52 | findPlane(state)
53 | .then(findRoute)
54 | .then(retrieveImage)
55 | .catch(displayPlaceholderImage)
56 | .then(updateDisplay)
57 | .then(() => {
58 | setTimeout(updateState, config.timeout)
59 | })
60 | }
61 |
62 | function findPlane (state) {
63 | return new Promise((resolve, reject) => {
64 | state.planes = JSON.parse(fs.readFileSync(config.planeData))
65 | debug.log(`${state.planes.aircraft.length} planes found`)
66 |
67 | let {plane, distance} = findClosestPlane(state.planes.aircraft)
68 | state.plane = plane
69 | state.distance = distance
70 |
71 | if (!state.plane) {
72 | debug.log('No nearby plane found')
73 | reject(state)
74 | } else {
75 | debug.log('Nearby plane found:', state.plane.flight)
76 | resolve(state)
77 | }
78 | })
79 | }
80 |
81 | function findRoute (state) {
82 | return new Promise((resolve, reject) => {
83 | state.route = findPlaneRoute({plane: state.plane})
84 | if (!state.route) {
85 | debug.log('No route found')
86 | reject(state)
87 | } else {
88 | debug.log('Route found:', state.route.description)
89 | resolve(state)
90 | }
91 | })
92 | }
93 |
94 | function retrieveImage (state) {
95 | return new Promise((resolve, reject) => {
96 | const imagePromise = findImage(state.route.feature.municipality, state.route.feature.iso_country)
97 | imagePromise.then(imagePath => {
98 | state.imagePath = imagePath
99 | resolve(state)
100 | }).catch((err) => {
101 | reject(state)
102 | })
103 | })
104 | }
105 |
106 | function displayPlaceholderImage (state) {
107 | return new Promise((resolve, reject) => {
108 | delete state.plane
109 | delete state.distance
110 | state.route = {}
111 |
112 | const path = config.placeholderPath
113 | const files = fs.readdirSync(path)
114 | const image = files[Math.floor(Math.random() * files.length)]
115 |
116 | app.use(express.static(config.placeholderPath))
117 |
118 | state.imagePath = path.replace(config.placeholderPath, '') + image
119 | state.route.description = image.replace('.jpg', '').replace('-', ' ') + ' (placeholder)'
120 |
121 | debug.log('Displaying placeholder image', state.route.description)
122 | resolve(state)
123 | })
124 | }
125 |
126 | function updateDisplay (state) {
127 | io.emit('state', state)
128 | debug.log(`---`)
129 | }
130 |
--------------------------------------------------------------------------------
/lib/find-image.js:
--------------------------------------------------------------------------------
1 | const config = require('../config')
2 | const debug = require('./debug')
3 | const fs = require('fs')
4 | const googleImages = require('google-images')
5 | const request = require('request')
6 | const gm = require('gm').subClass({imageMagick: true})
7 | const smartcrop = require('smartcrop-gm')
8 |
9 | function findImage (municipality, country) {
10 | const location = `${municipality}, ${country}`
11 | const locationFile = location.replace(/[^a-z0-9]/gi, '-').toLowerCase()
12 |
13 | const imagePath = `./client/images/routes/${locationFile}.jpg`
14 | const imagePathPublic = `images/routes/${locationFile}.jpg`
15 |
16 | return new Promise((resolve, reject) => {
17 | fs.access(imagePath, fs.F_OK, err => {
18 | if (err) {
19 | debug.log(`Image doesn't exist:`, imagePath)
20 | return requestImage(location, imagePath, imagePathPublic)
21 | .then(image => {
22 | if (image) {
23 | return resolve(image)
24 | } else {
25 | return reject()
26 | }
27 | })
28 | .catch(reject)
29 | }
30 | fs.stat(imagePath, (err, stat) => {
31 | if (new Date(stat.ctime).getTime() < config.maxAge) {
32 | debug.log(`Image is too old:`, imagePath)
33 | return requestImage(location, imagePath, imagePathPublic)
34 | .then(image => {
35 | if (image) {
36 | return resolve(image)
37 | } else {
38 | return reject()
39 | }
40 | })
41 | .catch(reject)
42 | } else {
43 | debug.log(`Image already exists:`, imagePath)
44 | return resolve(imagePathPublic)
45 | }
46 | })
47 | })
48 | })
49 | }
50 |
51 | function requestImage (location, imagePath, imagePathPublic) {
52 | return searchForImage(location)
53 | .then(src => {
54 | debug.log('Downloading image:', src)
55 | return downloadCropImage({
56 | src,
57 | dest: imagePath
58 | })
59 | })
60 | .then(image => {
61 | debug.log('Image created:', image)
62 | return imagePathPublic
63 | })
64 | .catch(err => {
65 | debug.log('Image download error:', err)
66 | return
67 | })
68 | }
69 |
70 | function searchForImage (location) {
71 | return new Promise((resolve, reject) => {
72 | debug.log('Searching for:', location)
73 | const client = googleImages(config.googleCseId, config.googleApiKey)
74 | client
75 | .search(location, {size: 'large'})
76 | .then(images => {
77 | debug.log(`Found ${images.length} images`)
78 | let src = images[0].url
79 | resolve(src)
80 | })
81 | .catch(err => {
82 | reject(err)
83 | })
84 | })
85 | }
86 |
87 | function downloadCropImage ({src, dest}) {
88 | return new Promise((resolve, reject) => {
89 | request(src, {encoding: null}, (error, response, body) => {
90 | if (error) reject(error)
91 | smartcrop
92 | .crop(body, {width: config.screen.width, height: config.screen.height})
93 | .then(result => {
94 | var crop = result.topCrop
95 | gm(body)
96 | .crop(crop.width, crop.height, crop.x, crop.y)
97 | .resize(config.screen.width, config.screen.height)
98 | .write(dest, (error) => {
99 | if (error) reject(error)
100 | resolve(dest)
101 | })
102 | }).catch(err => {
103 | reject(err)
104 | })
105 | })
106 | })
107 | }
108 |
109 | module.exports = findImage
--------------------------------------------------------------------------------
/client/js/jquery-3.1.1.min.js:
--------------------------------------------------------------------------------
1 | /*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */
2 | !function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),
3 | a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/