├── web-server ├── static │ ├── images │ │ ├── lines.png │ │ ├── njs.png │ │ ├── chruch.png │ │ ├── favicon.png │ │ ├── project_papper.png │ │ ├── subtle_zebra_3d.png │ │ └── html5-badge-h-connectivity-css3-graphics-multimedia-performance.png │ ├── impress │ │ ├── apple-touch-icon.png │ │ ├── README.md │ │ ├── js │ │ │ └── impress.js │ │ ├── css │ │ │ └── impress-demo.css │ │ └── index.html │ ├── index.html │ ├── basicemitter.js │ ├── init.js │ ├── reset.css │ ├── basics.js │ ├── overlay.js │ ├── interface.js │ ├── emitter.js │ └── style.css ├── express.js └── views │ ├── index.ejs │ └── pres.ejs ├── api-client ├── apiConf.js └── restreamer.js ├── configs ├── nginx │ ├── conf.d │ │ ├── vhosts.conf │ │ ├── example_ssl.conf │ │ └── default.conf │ └── nginx.conf ├── init.d │ ├── nodeforeverexpress │ └── nodeforeverrestreamer └── haproxy │ └── haproxy.cfg ├── lib ├── parser.js ├── Client.js ├── RestreamServer.js └── Streamer.js └── README.md /web-server/static/images/lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defly/tweereal/HEAD/web-server/static/images/lines.png -------------------------------------------------------------------------------- /web-server/static/images/njs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defly/tweereal/HEAD/web-server/static/images/njs.png -------------------------------------------------------------------------------- /web-server/static/images/chruch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defly/tweereal/HEAD/web-server/static/images/chruch.png -------------------------------------------------------------------------------- /web-server/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defly/tweereal/HEAD/web-server/static/images/favicon.png -------------------------------------------------------------------------------- /web-server/static/images/project_papper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defly/tweereal/HEAD/web-server/static/images/project_papper.png -------------------------------------------------------------------------------- /web-server/static/images/subtle_zebra_3d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defly/tweereal/HEAD/web-server/static/images/subtle_zebra_3d.png -------------------------------------------------------------------------------- /web-server/static/impress/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defly/tweereal/HEAD/web-server/static/impress/apple-touch-icon.png -------------------------------------------------------------------------------- /api-client/apiConf.js: -------------------------------------------------------------------------------- 1 | var authConf = { 2 | "consumerKey":"", 3 | "consumerSecret":"", 4 | "tokenKey":"", 5 | "tokenSecret":"" 6 | } 7 | 8 | exports.authConf = authConf; -------------------------------------------------------------------------------- /configs/nginx/conf.d/vhosts.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen *:8000; 3 | server_name tweereal.com www.tweereal.com; 4 | access_log /var/apps/real/clean.log main; 5 | root /var/apps/real; 6 | } 7 | -------------------------------------------------------------------------------- /web-server/static/images/html5-badge-h-connectivity-css3-graphics-multimedia-performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defly/tweereal/HEAD/web-server/static/images/html5-badge-h-connectivity-css3-graphics-multimedia-performance.png -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var parser = function(chunk, buffer, handler) { 2 | var index, json; 3 | var delimiter = "\r\n"; 4 | buffer += chunk.toString("utf8"); 5 | while ((index = buffer.indexOf(delimiter)) !== -1) { 6 | json = buffer.slice(0, index); 7 | buffer = buffer.slice(index + delimiter.length); 8 | if (json.length > 0) handler(json); 9 | } 10 | } 11 | 12 | exports.parser = parser; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tweereal 2 | --------------- 3 | 4 | Real-time twitter map based on Twitter Streaming API. Builded on node.js, websockets and canvas. 5 | 6 | ### Dependencies ### 7 | 8 | + socket.io 9 | + oauth-client 10 | + express 11 | 12 | ### Backend ### 13 | 14 | + HAProxy as front-end server 15 | + nginx for serving static content 16 | + forever (nodejitsu) for making fault tolerant application 17 | + init.d scripts 18 | 19 | ### Executables ### 20 | + api-client/restreamer.js - client for twitter api 21 | + web-server/express.js - webserver -------------------------------------------------------------------------------- /configs/nginx/conf.d/example_ssl.conf: -------------------------------------------------------------------------------- 1 | # HTTPS server 2 | # 3 | #server { 4 | # listen 443; 5 | # server_name localhost; 6 | 7 | # ssl on; 8 | # ssl_certificate /etc/nginx/cert.pem; 9 | # ssl_certificate_key /etc/nginx/cert.key; 10 | 11 | # ssl_session_timeout 5m; 12 | 13 | # ssl_protocols SSLv2 SSLv3 TLSv1; 14 | # ssl_ciphers HIGH:!aNULL:!MD5; 15 | # ssl_prefer_server_ciphers on; 16 | 17 | # location / { 18 | # root /usr/share/nginx/html; 19 | # index index.html index.htm; 20 | # } 21 | #} 22 | -------------------------------------------------------------------------------- /web-server/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /configs/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user nginx; 3 | worker_processes 1; 4 | 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | access_log /var/log/nginx/access.log main; 23 | 24 | sendfile on; 25 | #tcp_nopush on; 26 | 27 | keepalive_timeout 65; 28 | 29 | #gzip on; 30 | 31 | include /etc/nginx/conf.d/*.conf; 32 | } 33 | -------------------------------------------------------------------------------- /configs/init.d/nodeforeverexpress: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIR=/var/apps/real 3 | NODE=/usr/bin/node 4 | FOREVER=/usr/bin/forever 5 | NAME=express 6 | USER=defly 7 | 8 | test -x $NODE || exit 0 9 | 10 | function start_app { 11 | sudo -u $USER $FOREVER stop $DIR/$NAME.js 12 | # NODE_ENV=production $FOREVER start -a -l $DIR/logs/$NAME.log -o $DIR/logs/$NAME.output.log -e $DIR/logs/$NAME.error.log $DIR/$NAME.js 13 | sudo -u $USER NODE_ENV=production $FOREVER start -a -l $DIR/logs/$NAME.log $DIR/$NAME.js 14 | } 15 | 16 | function stop_app { 17 | sudo -u $USER $FOREVER stop $DIR/$NAME.js 18 | } 19 | 20 | function restart_app { 21 | sudo -u $USER $FOREVER restart $DIR/$NAME.js 22 | } 23 | 24 | case $1 in 25 | start) 26 | start_app ;; 27 | stop) 28 | stop_app ;; 29 | restart) 30 | restart_app 31 | ;; 32 | *) 33 | echo "usage: nodeforever$NAME {start|stop|restart}" ;; 34 | esac 35 | exit 0 36 | -------------------------------------------------------------------------------- /configs/init.d/nodeforeverrestreamer: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIR=/var/apps/real 3 | NODE=/usr/bin/node 4 | FOREVER=/usr/bin/forever 5 | NAME=restreamer 6 | USER=defly 7 | 8 | test -x $NODE || exit 0 9 | 10 | function start_app { 11 | sudo -u $USER $FOREVER stop $DIR/$NAME.js 12 | # NODE_ENV=production $FOREVER start -a -l $DIR/logs/$NAME.log -o /dev/null -e $DIR/logs/$NAME.error.log --sourceDir $DIR $DIR/$NAME.js 13 | sudo -u $USER NODE_ENV=production $FOREVER start -a -l $DIR/logs/$NAME.log $DIR/$NAME.js 14 | } 15 | 16 | function stop_app { 17 | sudo -u $USER $FOREVER stop $DIR/$NAME.js 18 | } 19 | 20 | function restart_app { 21 | sudo -u $USER $FOREVER restart $DIR/$NAME.js 22 | } 23 | 24 | case $1 in 25 | start) 26 | start_app ;; 27 | stop) 28 | stop_app ;; 29 | restart) 30 | restart_app 31 | ;; 32 | *) 33 | echo "usage: nodeforever$NAME {start|stop|restart}" ;; 34 | esac 35 | exit 0 36 | -------------------------------------------------------------------------------- /web-server/static/basicemitter.js: -------------------------------------------------------------------------------- 1 | Activity.BasicEmitter = function(div, options) { 2 | this.projection = null; 3 | this.div = div; 4 | // this.allowed = false; 5 | // this.map = map; 6 | this.options = this.options || {size:10, time:600}; 7 | 8 | // this.updateBounds = function() { 9 | // var mapBounds = this.map.getBounds(); 10 | // var sw = mapBounds.getSouthWest(); 11 | // var ne = mapBounds.getNorthEast(); 12 | // this.bounds = { 13 | // top: ne.lat(), 14 | // right: ne.lng(), 15 | // bottom: sw.lat(), 16 | // left: sw.lng() 17 | // } 18 | // } 19 | this.setProjection = function(projection) { 20 | this.projection = projection; 21 | } 22 | this.setCurrentOptions = function(opt) { 23 | this.options = opt; 24 | } 25 | this.start = function() { 26 | this.allowed = true; 27 | } 28 | this.stop = function() { 29 | this.allowed = false; 30 | } 31 | this.clear = function() { 32 | 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /lib/Client.js: -------------------------------------------------------------------------------- 1 | var Socket = require('net').Socket; 2 | var EventEmitter = require('events').EventEmitter; 3 | var parser = require("./parser.js").parser; 4 | var util = require('util'); 5 | 6 | var Client = function(port, host) { 7 | var self = this; 8 | var buffer = ""; 9 | var socket = new Socket(); 10 | var strHandler = function(str) { 11 | self.emit("str", str); 12 | } 13 | 14 | self.setMaxListeners(0); 15 | 16 | socket.setEncoding('utf8'); 17 | socket.connect(port, host); 18 | 19 | socket.on("data", function(chunk) { 20 | parser(chunk, buffer, strHandler); 21 | }); 22 | 23 | socket.on("error", function(error) { 24 | console.log("Error", error); 25 | }); 26 | 27 | socket.on("close", function() { 28 | console.log("Reconnect:", (new Date()).toUTCString()); 29 | setTimeout(function() { 30 | socket.connect(port, host); 31 | }, 1000); 32 | }); 33 | } 34 | 35 | util.inherits(Client, EventEmitter); 36 | 37 | exports.Client = Client; 38 | -------------------------------------------------------------------------------- /web-server/static/init.js: -------------------------------------------------------------------------------- 1 | // var map; 2 | Activity.initialize = function() { 3 | var latlng = new google.maps.LatLng(30, 10); 4 | var myOptions = { 5 | zoom: 2, 6 | minZoom: 2, 7 | mapTypeControl: true, 8 | mapTypeControlOptions: { 9 | style: google.maps.MapTypeControlStyle.DROPDOWN_MENU 10 | }, 11 | zoomControl: true, 12 | zoomControlOptions: { 13 | style: google.maps.ZoomControlStyle.SMALL 14 | }, 15 | 16 | center: latlng, 17 | mapTypeId: google.maps.MapTypeId.ROADMAP 18 | }; 19 | var map_canvas = document.getElementById("map_canvas"); 20 | Activity.map = new google.maps.Map(map_canvas, myOptions); 21 | 22 | Activity.overlay = new Activity.Overlay(Activity.map); 23 | Activity.socket = io.connect('http://ctweereal/'); 24 | }; 25 | 26 | Activity.dispatcher = function(msg) { 27 | var message = msg.split(" "); 28 | if (message[0] === "0") { 29 | Activity.overlay.emitter.emit([message[1], message[2]], message[0]); 30 | } else if (message[0] === "1") { 31 | Activity.overlay.emitter.emit([message[1], message[2]], message[0], message[3]); 32 | } 33 | }; 34 | 35 | window.onload = function() { 36 | Activity.initialize(); 37 | new Activity.Interface(); 38 | Activity.socket.on("message", Activity.dispatcher); 39 | }; -------------------------------------------------------------------------------- /configs/nginx/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8000; 3 | server_name localhost; 4 | 5 | #charset koi8-r; 6 | #access_log /var/log/nginx/log/host.access.log main; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.htm; 11 | } 12 | 13 | #error_page 404 /404.html; 14 | 15 | # redirect server error pages to the static page /50x.html 16 | # 17 | error_page 500 502 503 504 /50x.html; 18 | location = /50x.html { 19 | root /usr/share/nginx/html; 20 | } 21 | 22 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 23 | # 24 | #location ~ \.php$ { 25 | # proxy_pass http://127.0.0.1; 26 | #} 27 | 28 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 29 | # 30 | #location ~ \.php$ { 31 | # root html; 32 | # fastcgi_pass 127.0.0.1:9000; 33 | # fastcgi_index index.php; 34 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 35 | # include fastcgi_params; 36 | #} 37 | 38 | # deny access to .htaccess files, if Apache's document root 39 | # concurs with nginx's one 40 | # 41 | #location ~ /\.ht { 42 | # deny all; 43 | #} 44 | } 45 | 46 | -------------------------------------------------------------------------------- /web-server/static/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /configs/haproxy/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | log 127.0.0.1 local0 3 | log 127.0.0.1 local1 notice 4 | maxconn 4096 5 | user haproxy 6 | group haproxy 7 | daemon 8 | 9 | defaults 10 | log global 11 | mode http 12 | option httplog 13 | option dontlognull 14 | retries 3 15 | option redispatch 16 | maxconn 2000 17 | contimeout 5000 18 | clitimeout 5000 19 | srvtimeout 5000 20 | 21 | frontend http-in 22 | bind *:80 23 | acl url_static_dir path_beg /static 24 | acl url_static_end path_end .jpg .jpeg .gif .png .ico .pdf .js .css .flv .swf 25 | acl is_www_clean hdr_sub(host) -i ctweereal 26 | # acl is_www_domain_com hdr_end(host) -i domain.com 27 | 28 | use_backend www_static if url_static_dir url_static_end 29 | use_backend www_clean if is_www_clean 30 | # use_backend www_domain_com if is_www_domain_com 31 | # default_backend www_example_com 32 | 33 | backend www_static 34 | option httpclose 35 | option forwardfor 36 | server StaticNginx 127.0.0.1:8000 37 | 38 | backend www_clean 39 | # balance roundrobin 40 | # cookie SERVERID insert nocache indirect 41 | # option httpchk HEAD /check.txt HTTP/1.0 42 | option httpclose 43 | option forwardfor 44 | server Server1 127.0.0.1:8001 45 | # server Server1 10.1.1.1:80 cookie Server1 46 | # server Server2 10.1.1.2:80 cookie Server2 47 | -------------------------------------------------------------------------------- /lib/RestreamServer.js: -------------------------------------------------------------------------------- 1 | var Server = require('net').Server; 2 | var EventEmitter = require('events').EventEmitter; 3 | var util = require('util'); 4 | var RestreamServer = function(port, host) { 5 | var self = this; 6 | var server = new Server(); 7 | 8 | this.write = function(str) { 9 | self.emit("data", str); 10 | console.log(str); 11 | } 12 | 13 | server.listen(port, host); 14 | 15 | server.on("error",function(error) { 16 | console.log("ERROR at restreaming server:", error); 17 | setTimeout(function() { 18 | server.close(); 19 | server.listen(port, host); 20 | }, 1000); 21 | }); 22 | 23 | server.on("listening",function() { 24 | console.log("Server started at " + host + ":" + port); 25 | }); 26 | 27 | server.on("connection", function(socket) { 28 | console.log("Socket init", (new Date()).toUTCString()); 29 | 30 | socket.setKeepAlive(true); 31 | socket.setNoDelay(true); 32 | socket.setTimeout(1000); 33 | 34 | var write = function(data) { 35 | socket.write(data); 36 | }; 37 | 38 | socket.on("timeout", function() { 39 | socket.end(); 40 | }); 41 | 42 | socket.on("end", function() { 43 | console.log("Socket ended", (new Date()).toUTCString()); 44 | }); 45 | 46 | socket.on("error", function(error) { 47 | console.log("Socket error:", error); 48 | }); 49 | 50 | socket.on("close", function() { 51 | self.removeListener("data", write); 52 | }); 53 | 54 | self.on("data", write); 55 | }); 56 | 57 | server.on("close", function() { 58 | console.log("Server closed"); 59 | }); 60 | } 61 | 62 | util.inherits(RestreamServer, EventEmitter); 63 | 64 | exports.RestreamServer = RestreamServer; -------------------------------------------------------------------------------- /web-server/express.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express.createServer(); 3 | var io = require('socket.io').listen(app); 4 | var redis = require("redis"); 5 | var redisClient = redis.createClient(); 6 | var EventEmitter = require('events').EventEmitter; 7 | 8 | var messageProxy = new EventEmitter(); 9 | messageProxy.setMaxListeners(0); 10 | 11 | redisClient.subscribe("T"); 12 | redisClient.on("message", function(channel, message) { 13 | if (channel === "T") { 14 | messageProxy.emit("message", message); 15 | } 16 | }); 17 | 18 | 19 | app.listen(8001); 20 | 21 | io.configure(function() { 22 | io.enable('browser client minification'); // send minified client 23 | // io.enable('browser client etag'); // apply etag caching logic based on version number 24 | io.enable('browser client gzip'); // gzip the file 25 | io.set('log level', 1); // reduce logging 26 | io.set('transports', ['websocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']); 27 | }); 28 | 29 | // var client = new(require('./lib/Client.js').Client)(8090, "127.0.0.1"); 30 | console.log("Started"); 31 | app.configure(function() { 32 | app.use(express.errorHandler({ 33 | showStack: true, 34 | dumpExceptions: true 35 | })); 36 | app.set('views', __dirname + '/views'); 37 | app.disable('view cache'); 38 | app.set('view options', { 39 | layout: false 40 | }); 41 | }); 42 | 43 | app.get('/', function(req, res) { 44 | res.render('index.ejs'); 45 | }); 46 | 47 | app.get('/pres', function(req, res) { 48 | res.render('pres.ejs'); 49 | }); 50 | 51 | io.sockets.on('connection', function(socket) { 52 | var handler = function(str) { 53 | socket.send(str); 54 | }; 55 | messageProxy.on('message', handler); 56 | 57 | socket.on('disconnect', function() { 58 | messageProxy.removeListener("message", handler); 59 | }); 60 | 61 | socket.on("reconnect_failed", function() { 62 | messageProxy.removeListener("message", handler); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /web-server/static/basics.js: -------------------------------------------------------------------------------- 1 | var Activity = { 2 | "map": null 3 | }; 4 | 5 | Activity.options = { 6 | "width": 960, 7 | "height": 540 8 | } 9 | 10 | Activity.utils = { 11 | extender: function(obj, properties) { 12 | for (var key in properties) obj[key] = properties[key]; 13 | }, 14 | removeAllChilds: function(div) { 15 | while (div.firstChild) { 16 | div.removeChild(div.firstChild); 17 | } 18 | }, 19 | randomBgHex: function() { 20 | return "#" + (Math.floor(Math.random() * 255)).toString(16) + (Math.floor(Math.random() * 255)).toString(16) + (Math.floor(Math.random() * 255)).toString(16); 21 | }, 22 | latLngToPx: function(latLng, projection) { 23 | var glt = new google.maps.LatLng(latLng[0], latLng[1]); 24 | var point = projection.fromLatLngToContainerPixel(glt); 25 | return [point.x, point.y]; 26 | }, 27 | filter: function(w, h, projection) { 28 | var lat = []; 29 | var lng = []; 30 | var points = [ 31 | new google.maps.Point(0, 0), new google.maps.Point(w, 0), new google.maps.Point(w, h), new google.maps.Point(0, h)]; 32 | var latLng = points.map(function(point) { 33 | var ll = projection.fromContainerPixelToLatLng(point); 34 | lat.push(ll.lat()); 35 | lng.push(ll.lng()); 36 | }); 37 | return [[Math.min.apply(null, lat), Math.max.apply(null, lat)], [Math.min.apply(null, lng), Math.max.apply(null, lng)]]; 38 | }, 39 | randomColorArray: function() { 40 | return [Math.round(Math.random() * 255), Math.round(Math.random() * 255), Math.round(Math.random() * 255)]; 41 | }, 42 | tweetPhase: function(start, live) { 43 | return (Date.now() - start) / live; 44 | }, 45 | makeRGBA: function(r, g, b, a) { 46 | // return "rgba(" + Array.prototype.join.call(arguments,",") + ")"; 47 | return "rgba(" + r + "," + g + "," + b + "," + a + ")"; 48 | }, 49 | cachedRGB: function(c) { 50 | return "rgba(" + c[0] + "," + c[1] + "," + c[2] + ","; 51 | }, 52 | addRGBAlpha: function(str, alpha) { 53 | return str + alpha + ")"; 54 | } 55 | } -------------------------------------------------------------------------------- /web-server/static/overlay.js: -------------------------------------------------------------------------------- 1 | Activity.Overlay = function(map) { 2 | this.map = map; 3 | this.div = null; 4 | this.setMap(map); 5 | this.divStyle = window.getComputedStyle(this.map.getDiv()); 6 | }; 7 | 8 | Activity.Overlay.prototype = new google.maps.OverlayView(); 9 | 10 | Activity.Overlay.prototype.onAdd = function() { 11 | var overlay = this; 12 | var panes = overlay.getPanes(); 13 | var overlayProjection = overlay.getProjection(); 14 | var div = document.createElement('div'); 15 | var overlayStyles = { 16 | width : Activity.width + "px", 17 | height : Activity.height + "px", 18 | position : "relative", 19 | overflow : "hidden" 20 | }; 21 | 22 | overlay.div = div; 23 | div.id = "bubles"; 24 | Activity.utils.extender(div.style, overlayStyles); 25 | panes.overlayLayer.appendChild(div); 26 | 27 | overlay.emitter = new Activity.CanvasEmitter(div); 28 | overlay.emitter.setProjection(overlayProjection); 29 | overlay.emitter.stop(); 30 | overlay.emitter.start(); 31 | 32 | google.maps.event.addListener(overlay.map, 'bounds_changed', function(event) { 33 | overlay.emitter.stop(); 34 | }); 35 | 36 | google.maps.event.addListener(overlay.map, 'idle', function(event) { 37 | overlay.emitter.updateSize(overlay.map.getZoom()); 38 | overlay.draw(); 39 | overlay.emitter.start(); 40 | }); 41 | 42 | }; 43 | 44 | Activity.Overlay.prototype.draw = function() { 45 | var overlay = this; 46 | var overlayProjection = overlay.getProjection(); 47 | var bounds = overlay.map.getBounds(); 48 | var ne = bounds.getNorthEast(); 49 | var sw = bounds.getSouthWest(); 50 | var top = overlayProjection.fromLatLngToDivPixel(ne).y; 51 | var left = overlayProjection.fromLatLngToDivPixel(sw).x; 52 | var overlayStyles = { 53 | top : Math.round(top) + "px", 54 | left : Math.round(left) + "px" 55 | }; 56 | 57 | Activity.utils.extender(overlay.div.style, overlayStyles); 58 | }; 59 | 60 | Activity.Overlay.prototype.onRemove = function() { 61 | var overlay = this; 62 | overlay.div.parentNode.removeChild(overlay.div); 63 | overlay.div = null; 64 | }; -------------------------------------------------------------------------------- /api-client/restreamer.js: -------------------------------------------------------------------------------- 1 | var authConf = require('./apiConf.js').authConf; 2 | var reqConf = { 3 | port: 443, 4 | host: 'stream.twitter.com', 5 | https: true, 6 | path: '/1/statuses/filter.json', 7 | oauth_signature: "", 8 | method: 'POST', 9 | body: { 10 | locations: '-180,-90,180,90' 11 | } 12 | }; 13 | var redis = require("redis"); 14 | var streamer = new(require("./lib/Streamer.js").Streamer)(authConf, reqConf); 15 | var filters = {}; 16 | 17 | var roundWithDepth = function(x, depthNum) { 18 | return Math.round(x * depthNum) / depthNum; 19 | }; 20 | 21 | var boundingToCoordinates = function(bounding, /*maximal long*/ max) { 22 | var deltaLat = bounding[2][1] - bounding[0][1]; 23 | var deltaLng = bounding[2][0] - bounding[0][0]; 24 | 25 | if ((deltaLat > max) || (deltaLng > max)) return false; 26 | 27 | var lat = bounding[0][1] + deltaLat * Math.random(); 28 | var lng = bounding[0][0] + deltaLng * Math.random(); 29 | var precision = Math.max(deltaLat, deltaLng); 30 | 31 | var depthNum = 100000; 32 | 33 | return [ 34 | roundWithDepth(lat, depthNum), roundWithDepth(lng, depthNum), roundWithDepth(precision, depthNum) //accuracy 35 | ]; 36 | }; 37 | 38 | var Quality = function() { 39 | var lastTrack = 0; 40 | var tweets = 0; 41 | this.getQuality = function(track) { 42 | var delta = track - lastTrack; 43 | if (delta < 0) { 44 | delta = track; 45 | } 46 | var quality = tweets / (tweets + delta); 47 | lastTrack = track; 48 | tweets = 0; 49 | return roundWithDepth(quality, 1000); 50 | }; 51 | this.upTweets = function() { 52 | tweets++; 53 | }; 54 | }; 55 | 56 | var q = new Quality(); 57 | 58 | var customFilter = function(tweet) { 59 | if (tweet.coordinates) { 60 | filters.exact(tweet); 61 | q.upTweets(); 62 | } else if (tweet.place) { 63 | filters.notExact(tweet); 64 | q.upTweets(); 65 | } else if (typeof tweet.limit !== "undefined") { 66 | filters.limit(tweet); 67 | } else { 68 | console.log("else", tweet); 69 | return false; 70 | } 71 | }; 72 | 73 | var redisClient = redis.createClient(); 74 | 75 | var write = function(message) { 76 | redisClient.publish("T", message); 77 | }; 78 | 79 | filters.exact = function(tweet) { 80 | var coordinates = tweet.coordinates.coordinates.reverse(); 81 | if (!(coordinates[0] === 0 && coordinates[1] === 0)) { 82 | write("0 " + coordinates[0] + " " + coordinates[1] + "\r\n"); 83 | } 84 | }; 85 | 86 | filters.notExact = function(tweet) { 87 | var boundCoordinates = boundingToCoordinates(tweet.place.bounding_box.coordinates[0], 2); 88 | if (boundCoordinates !== false) { 89 | write("1 " + boundCoordinates[0] + " " + boundCoordinates[1] + " " + boundCoordinates[2] + "\r\n"); 90 | } 91 | }; 92 | 93 | filters.limit = function(tweet) { 94 | var quality = q.getQuality(tweet.limit.track); 95 | write("2 " + quality + "\r\n"); 96 | }; 97 | 98 | streamer.stream(); 99 | streamer.on("tweet", function(tweet) { 100 | customFilter(tweet); 101 | }); 102 | -------------------------------------------------------------------------------- /lib/Streamer.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), oauth = require('oauth-client'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var parser = require("./parser.js").parser; 4 | 5 | //REMEMBER ABOUT CLOCK TIMESTAMP 6 | 7 | var Streamer = function(authConf, reqConf) { 8 | var self = this; 9 | var signer = oauth.createHmac(oauth.createConsumer(authConf.consumerKey, authConf.consumerSecret), oauth.createToken(authConf.tokenKey, authConf.tokenSecret)); 10 | reqConf["oauth_signature"] = signer; 11 | var buffer = ""; 12 | 13 | var socketTimeout = 1000; 14 | var networkTimeoutStart = 250; 15 | var networkTimeoutEnd = 16000; 16 | var httpTimeoutStart = 10000; 17 | var httpTimeoutEnd = 240000; 18 | 19 | var blockReconnection = false; 20 | var lastInterval = networkTimeoutStart; 21 | 22 | var reconnect = function(code) { 23 | if (blockReconnection) return false; 24 | blockReconnection = true; 25 | if (code) { 26 | if (lastInterval > httpTimeoutEnd) { 27 | lastInterval = httpTimeoutStart; 28 | } else { 29 | lastInterval *= 2; 30 | } 31 | } else { 32 | if (lastInterval > networkTimeoutEnd) { 33 | lastInterval = networkTimeoutStart; 34 | } else { 35 | lastInterval += networkTimeoutStart; 36 | } 37 | } 38 | 39 | setTimeout(function() { 40 | blockReconnection = false; 41 | self.stream() 42 | 43 | }, lastInterval); 44 | 45 | } 46 | 47 | var emitment = function(req, code, err) { 48 | return { 49 | "request":req, 50 | "wrongCode":code, 51 | "error":err 52 | } 53 | } 54 | 55 | var jsonHandler = function(json) { 56 | try { 57 | self.emit("tweet", JSON.parse(json)); 58 | } catch (error) { 59 | console.log("Error in Streamer parser:", error); 60 | } 61 | } 62 | 63 | self.on("reconnect", function(opt) { 64 | console.log("Reconnect:", (new Date()).toUTCString()); 65 | if (opt.error) { 66 | console.log("Error:", opt.error); 67 | } 68 | opt.request.abort(); 69 | reconnect(opt.wrongCode); 70 | }); 71 | 72 | self.stream = function() { 73 | var request = oauth.request(reqConf, function(response) { 74 | response.setEncoding('utf8'); 75 | console.log("STATUS:", response.statusCode); 76 | 77 | if (response.statusCode !== 200) { 78 | self.emit("reconnect", emitment(request, true)); 79 | } 80 | 81 | response.on("data", function(chunk) { 82 | // console.log(chunk); 83 | parser(chunk, buffer, jsonHandler); 84 | }); 85 | 86 | response.on("end", function() { 87 | self.emit("reconnect", emitment(request, false)); 88 | }); 89 | 90 | response.on("close", function() { 91 | self.emit("reconnect", emitment(request, false)); 92 | }); 93 | 94 | response.on("error", function(err) { 95 | self.emit("reconnect", emitment(request, false, err)); 96 | }); 97 | 98 | }); 99 | 100 | request.write(reqConf.body); 101 | request.end(); 102 | 103 | request.on("error", function(err) { 104 | self.emit("reconnect", emitment(request, false, err)); 105 | }); 106 | 107 | request.on("socket", function(socket) { 108 | var socket = socket.socket; 109 | socket.setTimeout(socketTimeout); 110 | socket.setKeepAlive(true); 111 | socket.on("timeout", function() { 112 | self.emit("reconnect", emitment(request, false)); 113 | }); 114 | }); 115 | } 116 | } 117 | 118 | util.inherits(Streamer, EventEmitter); 119 | 120 | exports.Streamer = Streamer; -------------------------------------------------------------------------------- /web-server/static/interface.js: -------------------------------------------------------------------------------- 1 | Activity.Interface = function() { 2 | var options = Activity.Bubble.prototype.options; 3 | 4 | var sliders = [{ 5 | "id": "all-sliders-size", 6 | "value": options.baseSize * 2, 7 | "handler": function(x) { 8 | options.baseSize = x; 9 | options.size = Math.round(Math.sqrt(options.zoom) * x); 10 | }, 11 | "transform": function(y) { 12 | // return Math.round(Math.sqrt(options.zoom) * y * 0.5); 13 | return y * 0.5; 14 | } 15 | }, { 16 | "id": "all-sliders-live", 17 | "value": options.live / 100, 18 | "handler": function(x) { 19 | options.live = x; 20 | }, 21 | "transform": function(y) { 22 | return Math.round(y * 100); 23 | } 24 | }, { 25 | "id": "exact-opacity", 26 | "value": 100, 27 | "handler": function(x) { 28 | options.neatFillOpacity = options.baseFillOpacity * x; 29 | options.neatStrokeOpacity = options.baseStrokeOpacity * x; 30 | }, 31 | "transform": function(y) { 32 | return 0.01 * Math.round(y); 33 | } 34 | }, { 35 | "id": "places-opacity", 36 | "value": 50, 37 | "handler": function(x) { 38 | options.blurFillOpacity = options.baseFillOpacity * x; 39 | options.blurStrokeOpacity = options.baseStrokeOpacity * x; 40 | }, 41 | "transform": function(y) { 42 | return 0.01 * Math.round(y); 43 | } 44 | }, { 45 | "id": "places-precision", 46 | "value": 50, 47 | "handler": function(x) { 48 | options.precision = x; 49 | }, 50 | "transform": function(y) { 51 | return 0.02*Math.round(y); 52 | } 53 | }]; 54 | 55 | for (var i = sliders.length - 1; i >= 0; i--) { 56 | sliders[i]["slider"] = new Activity.Interface.Slider(sliders[i]["id"], sliders[i]["value"], sliders[i]["handler"], sliders[i]["transform"]); 57 | }; 58 | 59 | var switcherExact = new Activity.Interface.Switcher("exact-switcher", function() { 60 | Activity.Bubble.prototype.renderNeat = Activity.Bubble.prototype.renderNeatOn; 61 | }, function() { 62 | Activity.Bubble.prototype.renderNeat = function() {}; 63 | }); 64 | 65 | var switcherPlaces = new Activity.Interface.Switcher("places-switcher", function() { 66 | Activity.Bubble.prototype.renderBlur = Activity.Bubble.prototype.renderBlurOn; 67 | }, function() { 68 | Activity.Bubble.prototype.renderBlur = function() {}; 69 | }); 70 | 71 | 72 | $("#switch-defaults").on("click", function() { 73 | for (var i = sliders.length - 1; i >= 0; i--) { 74 | sliders[i]["slider"].toDefault(); 75 | } 76 | switcherExact.on(); 77 | switcherPlaces.on(); 78 | }); 79 | 80 | }; 81 | 82 | Activity.Interface.Slider = function(id, value, handler, transform) { 83 | var self = this; 84 | this.defaults = value; 85 | this.handler = handler; 86 | this.transform = transform; 87 | this.slider = $("#" + id).slider({ 88 | step: 0.01, 89 | animate: true, 90 | value: value, 91 | slide: function(event, ui) { 92 | self.handler(self.transform(ui.value)); 93 | } 94 | }); 95 | }; 96 | 97 | Activity.Interface.Slider.prototype.toDefault = function() { 98 | this.slider.slider("value", this.defaults); 99 | this.handler(this.transform(this.defaults)); 100 | console.log(this.transform(this.defaults)); 101 | }; 102 | 103 | Activity.Interface.Switcher = function(id, switchOn, switchOff) { 104 | var self = this; 105 | self.$el = $("#" + id); 106 | self.state = true; 107 | self.switchOn = switchOn; 108 | self.switchOff = switchOff; 109 | 110 | self.$el.on("click", function(event) { 111 | event.preventDefault(); 112 | if (self.state) { 113 | self.off(); 114 | } else { 115 | self.on(); 116 | } 117 | return false; 118 | }); 119 | 120 | }; 121 | 122 | Activity.Interface.Switcher.prototype.on = function() { 123 | this.switchOn(); 124 | this.$el.removeClass("off").addClass("on").text("on"); 125 | this.state = true; 126 | } 127 | 128 | Activity.Interface.Switcher.prototype.off = function() { 129 | this.switchOff(); 130 | this.$el.removeClass("on").addClass("off").text("off"); 131 | this.state = false; 132 | } 133 | -------------------------------------------------------------------------------- /web-server/static/emitter.js: -------------------------------------------------------------------------------- 1 | window.requestAnimFrame = (function() { 2 | return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || 3 | function( /* function */ callback, /* DOMElement */ element) { 4 | window.setTimeout(callback, 1000 / 60); 5 | }; 6 | })(); 7 | 8 | 9 | Activity.Bubble = function(center, exact, precision) { 10 | var color = Activity.utils.randomColorArray(); 11 | var self = this; 12 | this.precision = precision; 13 | // this.exact = exact; 14 | this.time = Date.now(); 15 | this.cachedRGB = Activity.utils.cachedRGB(color); 16 | this.center = center; 17 | this.render = function(phase, ctx) { 18 | if (exact === "1") { 19 | if (precision <= self.options.precision) self.renderBlur(phase, ctx); 20 | } else { 21 | self.renderNeat(phase, ctx); 22 | } 23 | } 24 | }; 25 | 26 | 27 | Activity.Bubble.prototype.renderNeatOn = function(phase, ctx) { 28 | ctx.beginPath(); 29 | ctx.arc(this.center[0], this.center[1], this.options.size * phase, 0, 2 * Math.PI); 30 | ctx.fillStyle = Activity.utils.addRGBAlpha(this.cachedRGB, this.options.neatFillOpacity * (1 - phase)); 31 | ctx.fill(); 32 | ctx.strokeStyle = Activity.utils.addRGBAlpha(this.cachedRGB, this.options.neatStrokeOpacity * Math.sin((1 - phase) * Math.PI)); 33 | ctx.stroke(); 34 | ctx.closePath(); 35 | }; 36 | 37 | Activity.Bubble.prototype.renderNeat = Activity.Bubble.prototype.renderNeatOn; 38 | 39 | Activity.Bubble.prototype.renderBlurOn = function(phase, ctx) { 40 | ctx.beginPath(); 41 | ctx.arc(this.center[0], this.center[1], this.options.size * phase, 0, 2 * Math.PI); 42 | ctx.fillStyle = Activity.utils.addRGBAlpha(this.cachedRGB, this.options.blurFillOpacity * (1 - phase)); 43 | ctx.fill(); 44 | ctx.strokeStyle = Activity.utils.addRGBAlpha(this.cachedRGB, this.options.blurStrokeOpacity * Math.sin((1 - phase) * Math.PI)); 45 | ctx.stroke(); 46 | ctx.closePath(); 47 | }; 48 | 49 | Activity.Bubble.prototype.renderBlur = Activity.Bubble.prototype.renderBlurOn; 50 | 51 | Activity.Bubble.prototype.options = { 52 | "size": 10, 53 | "baseSize": 10, 54 | "baseFillOpacity": 0.8, 55 | "baseStrokeOpacity": 1, 56 | "live": 750, 57 | "neatFillOpacity": 0.8, 58 | "neatStrokeOpacity": 1, 59 | "blurFillOpacity": 0.4, 60 | "blurStrokeOpacity": 0.5, 61 | "zoom": 2, 62 | "precision": 1 63 | }; 64 | 65 | Activity.CanvasEmitter = function(div, options) { 66 | var self = this; 67 | var canvas = document.createElement("canvas"); 68 | var bubbles = []; 69 | var allowed = true; 70 | var bubbleOptions = Activity.Bubble.prototype.options; 71 | var ctx = canvas.getContext("2d"); 72 | 73 | var notInProjection = function(center) { 74 | return (center[0] < -bubbleOptions.size || center[0] > self.width + bubbleOptions.size || center[1] < -bubbleOptions.size || center[1] > self.height + bubbleOptions.size); 75 | }; 76 | 77 | var tweetPhase = function(start, live) { 78 | return (Date.now() - start) / live; 79 | }; 80 | 81 | var filterHandler = function(el) { 82 | var phase = tweetPhase(el.time, bubbleOptions.live); 83 | if (phase < 1) { 84 | el.render(phase, ctx); 85 | return true; 86 | } else { 87 | return false; 88 | } 89 | }; 90 | 91 | var animation = function() { 92 | ctx.clearRect(0, 0, self.width, self.height); 93 | 94 | if (!allowed) { 95 | return false; 96 | } 97 | 98 | bubbles = bubbles.filter(filterHandler); 99 | 100 | window.requestAnimFrame(animation); 101 | }; 102 | 103 | Activity.BasicEmitter.call(this, div, options); 104 | 105 | this.width = Activity.options.width; 106 | this.height = Activity.options.height; 107 | 108 | canvas.width = this.width; 109 | canvas.height = this.height; 110 | 111 | div.appendChild(canvas); 112 | 113 | this.emit = function(crd, exact, precision) { 114 | var center = Activity.utils.latLngToPx(crd, this.projection); 115 | 116 | if (!notInProjection(center)) { 117 | // console.log("o"); 118 | bubbles.push(new Activity.Bubble(center, exact, precision)); 119 | } 120 | 121 | }; 122 | 123 | this.start = function() { 124 | allowed = true; 125 | window.requestAnimFrame(animation); 126 | }; 127 | 128 | this.stop = function() { 129 | allowed = false; 130 | this.clear(); 131 | }; 132 | 133 | this.clear = function() { 134 | bubbles = []; 135 | ctx.clearRect(0, 0, self.width, self.height); 136 | }; 137 | 138 | this.updateSize = function(zoom) { 139 | bubbleOptions.size = Math.round(Math.sqrt(zoom) * bubbleOptions.baseSize); 140 | // console.log(zoom); 141 | bubbleOptions.zoom = zoom; 142 | }; 143 | 144 | }; 145 | -------------------------------------------------------------------------------- /web-server/static/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Arial, sans-serif; 3 | } 4 | 5 | body { 6 | background-image: url("images/project_papper.png"); 7 | background-color: #eee; 8 | color: #515151; 9 | } 10 | 11 | .wrapper { 12 | min-width: 1000px; 13 | max-width: 1040px; 14 | 15 | margin: 0 auto; 16 | overflow: hidden; 17 | } 18 | 19 | .content { 20 | background-color: white; 21 | border-radius: 3px; 22 | outline: 10px solid rgba(255,255,255,0.4); 23 | width:960px; 24 | padding: 20px; 25 | overflow: hidden; 26 | position: relative; 27 | margin: 10px auto; 28 | margin-bottom: 20px; 29 | } 30 | 31 | .footer { 32 | width: 960px; 33 | padding: 20px; 34 | overflow: hidden;; 35 | } 36 | 37 | .footer a { 38 | display: block; 39 | float: left; 40 | opacity: 0.5; 41 | -webkit-transition: opacity ease-in 0.2s; 42 | -moz-transition: opacity ease-in 0.2s; 43 | -ms-transition: opacity ease-in 0.2s; 44 | -o-transition: opacity ease-in 0.2s; 45 | } 46 | 47 | .footer a:hover { 48 | opacity: 1; 49 | } 50 | 51 | .footer a:first-child { 52 | margin-bottom: 10px; 53 | margin-left: 15%; 54 | } 55 | 56 | .footer a:last-child { 57 | margin-left: 33%; 58 | } 59 | 60 | .head { 61 | width: 960px; 62 | padding: 20px; 63 | margin: 0 auto; 64 | } 65 | 66 | .linefirst a { 67 | text-decoration: none; 68 | } 69 | 70 | h1,h2,h3,h4 { 71 | font-family: Ubuntu, sans-serif; 72 | font-style: italic; 73 | font-size: 36px; 74 | color: rgba(227,50,88,1); 75 | } 76 | 77 | h2 { 78 | font-size: 30px; 79 | margin-bottom: 10px; 80 | } 81 | 82 | .sliders { 83 | margin-bottom: 10px; 84 | 85 | -moz-user-select:none; 86 | -webkit-user-select:none; 87 | user-select:none; 88 | -ms-user-select:none; 89 | 90 | clear: right; 91 | } 92 | 93 | .sliders-label, .slider-def, .switch-defaults { 94 | font-family: sans-serif; 95 | color: rgba(227,50,88,1); 96 | /*color: #515151;*/ 97 | font-style: italic; 98 | 99 | } 100 | 101 | .sliders-label { 102 | font-size: 20px; 103 | margin-bottom: 7px; 104 | } 105 | 106 | .sliders-label > div { 107 | display: inline-block; 108 | } 109 | 110 | .slider-def { 111 | font-size: 14px; 112 | margin-bottom: 7px; 113 | 114 | } 115 | 116 | .linefirst h1 { 117 | float: left; 118 | text-shadow: 2px 2px 0 rgba(255,255,255,0.8); 119 | } 120 | 121 | .linefirst { 122 | overflow: hidden; 123 | width: 100%; 124 | 125 | } 126 | 127 | .social { 128 | float: right; 129 | margin-bottom: 6px; 130 | } 131 | 132 | #gplus { 133 | margin-bottom: 2px; 134 | } 135 | 136 | .like { 137 | float: left; 138 | } 139 | 140 | .like:last-child { 141 | margin: 0; 142 | } 143 | 144 | .contain { 145 | position: relative; 146 | overflow: hidden; 147 | width: 960px; 148 | height: 540px; 149 | padding-bottom: 10px; 150 | } 151 | 152 | #map_canvas { 153 | overflow: hidden; 154 | width: 100%; 155 | height: 100%; 156 | } 157 | 158 | #bubles { 159 | transform: translateZ(0); 160 | -webkit-transform: translateZ(0); 161 | -ms-transform: translateZ(0); 162 | -o-transform: translateZ(0); 163 | -moz-transform: translateZ(0); 164 | } 165 | 166 | .txt { 167 | width: 45%; 168 | line-height: 1.5; 169 | float: left; 170 | font-size: 14px; 171 | } 172 | 173 | .txt:last-child { 174 | margin-left:10%; 175 | } 176 | 177 | .link-twitter { 178 | color: rgba(0,180,255,1); 179 | text-decoration: none; 180 | } 181 | 182 | .switch-defaults { 183 | font-size: 12px; 184 | float: right; 185 | border-bottom: 1px dashed rgba(0, 180, 255, 1); 186 | padding-bottom: 1px; 187 | clear: left; 188 | cursor: pointer; 189 | color: rgba(0, 180, 255, 1); 190 | } 191 | 192 | .switch-on-off { 193 | color: white; 194 | background-color: rgba(0, 180, 255, 1); 195 | font-family: sans-serif; 196 | padding:1px 5px 1px 5px; 197 | line-height: 1.3; 198 | font-style: italic; 199 | font-size: 14px; 200 | border-radius: 3px; 201 | margin-left: 5px; 202 | /*font-weight: bold;*/ 203 | cursor: pointer; 204 | } 205 | 206 | .on { 207 | box-shadow: 0 2px 0px rgba(0,180,255,1); 208 | background-color: rgba(0, 180, 255, 1); 209 | background: -moz-linear-gradient(bottom, rgba(84,206,255,1) 0%, rgba(0,180,255,1) 100%); 210 | background: -webkit-linear-gradient(bottom, rgba(84,206,255,1) 0%,rgba(0,180,255,1) 100%); 211 | background: -o-linear-gradient(bottom, rgba(84,206,255,1) 0%,rgba(0,180,255,1) 100%); 212 | background: -ms-linear-gradient(bottom, rgba(84,206,255,1) 0%,rgba(0,180,255,1) 100%); 213 | background: linear-gradient(bottom, rgba(84,206,255,1) 0%,rgba(0,180,255,1) 100%); 214 | } 215 | 216 | .off { 217 | box-shadow: 0 2px 0px rgba(227,50,88,1); 218 | background-color: rgba(227,50,88,1); 219 | background: -moz-linear-gradient(bottom, rgba(224,112,134,1) 0%, rgba(227,50,88,1) 100%); 220 | background: -webkit-linear-gradient(bottom, rgba(224,112,134,1) 0%,rgba(227,50,88,1) 100%); 221 | background: -o-linear-gradient(bottom, rgba(224,112,134,1) 0%,rgba(227,50,88,1) 100%); 222 | background: -ms-linear-gradient(bottom, rgba(224,112,134,1) 0%,rgba(227,50,88,1) 100%); 223 | background: linear-gradient(bottom, rgba(224,112,134,1) 0%,rgba(227,50,88,1) 100%); 224 | 225 | } 226 | 227 | .slider { 228 | width:100%; 229 | margin-bottom: 20px; 230 | } 231 | 232 | .slider-section { 233 | width: 30%; 234 | line-height: 1.5; 235 | float: left; 236 | 237 | } 238 | 239 | .slider-section:first-child { 240 | margin-right: 5%; 241 | } 242 | 243 | .slider-section:last-child { 244 | float: right; 245 | } 246 | 247 | .ui-widget-content { 248 | border: none; 249 | 250 | background: none !important; 251 | background-color: rgba(227,50,88,0.03) !important; 252 | 253 | box-shadow: inset 0 1px 1px rgba(200,200,200,1), inset 0 -1px 1px rgba(200,200,200,0.1) !important; 254 | cursor: pointer; 255 | } 256 | 257 | .ui-slider-handle { 258 | cursor: pointer !important; 259 | height: 16px !important; 260 | width: 22px !important; 261 | bottom: -5px !important; 262 | background: none !important; 263 | background-color: rgba(250,247,247,1) !important; 264 | background-image: url("images/lines.png") !important; 265 | background-repeat: no-repeat !important; 266 | background-position: center !important; 267 | border-color: rgba(200,200,200,0.2) !important; 268 | box-shadow: 0 1px 1px rgba(200,200,200,0.8); 269 | } 270 | 271 | .ui-state-active { 272 | outline:none; 273 | } 274 | 275 | .ui-state-focus { 276 | outline:none; 277 | } 278 | 279 | .ui-state-hover { 280 | } 281 | 282 | .ui-slider-horizontal { 283 | height: 8px; 284 | } -------------------------------------------------------------------------------- /web-server/static/impress/README.md: -------------------------------------------------------------------------------- 1 | impress.js 2 | ============ 3 | 4 | It's a presentation framework based on the power of CSS3 transforms and 5 | transitions in modern browsers and inspired by the idea behind prezi.com. 6 | 7 | **WARNING** 8 | 9 | impress.js may not help you if you have nothing interesting to say ;) 10 | 11 | 12 | ABOUT THE NAME 13 | ---------------- 14 | 15 | impress.js name in [courtesy of @skuzniak](http://twitter.com/skuzniak/status/143627215165333504). 16 | 17 | It's an (un)fortunate coincidence that a Open/LibreOffice presentation tool is called Impress ;) 18 | 19 | 20 | VERSION HISTORY 21 | ----------------- 22 | 23 | ### master (in development) 24 | 25 | **CONTAINS UNRELEASED CHANGES, MAY BE UNSTABLE** 26 | 27 | * minor CSS 3D fixes 28 | * basic API to control the presentation flow from JavaScript 29 | * touch event support 30 | 31 | 32 | ### 0.2 ([browse](http://github.com/bartaz/impress.js/tree/0.2), [zip](http://github.com/bartaz/impress.js/zipball/0.2), [tar](http://github.com/bartaz/impress.js/tarball/0.2)) 33 | 34 | * tutorial/documentation added to `index.html` source file 35 | * being even more strict with strict mode 36 | * code clean-up 37 | * couple of small bug-fixes 38 | 39 | 40 | ### 0.1 ([browse](http://github.com/bartaz/impress.js/tree/0.1), [zip](http://github.com/bartaz/impress.js/zipball/0.1), [tar](http://github.com/bartaz/impress.js/tarball/0.1)) 41 | 42 | First release. 43 | 44 | Contains basic functionality for step placement and transitions between them 45 | with simple fallback for non-supporting browsers. 46 | 47 | 48 | 49 | HOW TO USE IT 50 | --------------- 51 | 52 | [Use the source](http://github.com/bartaz/impress.js/blob/master/index.html), Luke ;) 53 | 54 | If you have no idea what I mean by that, or you just clicked that link above and got 55 | very confused by all these strange characters that got displayed on your screen, 56 | it's a sign, that impress.js is not for you. 57 | 58 | Sorry. 59 | 60 | Fortunately there are some guys on GitHub that got quite excited with the idea of building 61 | editing tool for impress.js. Let's hope they will manage to do it. 62 | 63 | 64 | EXAMPLES AND DEMOS 65 | -------------------- 66 | 67 | ### Official demo 68 | 69 | [impress.js demo](http://bartaz.github.com/impress.js) by [@bartaz](http://twitter.com/bartaz) 70 | 71 | ### Presentations 72 | 73 | [CSS 3D transforms](http://bartaz.github.com/meetjs/css3d-summit) from [meet.js summit](http://summit.meetjs.pl) by [@bartaz](http://twitter.com/bartaz) 74 | 75 | [What the Heck is Responsive Web Design](http://johnpolacek.github.com/WhatTheHeckIsResponsiveWebDesign-impressjs/) by John Polacek [@johnpolacek](http://twitter.com/johnpolacek) 76 | 77 | [12412.org presentation to Digibury](http://extra.12412.org/digibury/) by Stephen Fulljames [@fulljames](http://twitter.com/fulljames) 78 | 79 | [Data center virtualization with Wakame-VDC](http://wakame.jp/wiki/materials/20120114_TLUG/) by Andreas Kieckens [@Metallion98](https://twitter.com/#!/Metallion98) 80 | 81 | [Asynchronous JavaScript](http://www.medikoo.com/asynchronous-javascript/3d/) by Mariusz Nowak [@medikoo](http://twitter.com/medikoo) 82 | 83 | [Introduction to Responsive Design](http://www.alecrust.com/factory/rd-presentation/) by Alec Rust [@alecrust] (http://twitter.com/alecrust) 84 | 85 | [Bonne année 2012](http://duael.fr/voeux/2012/) by Edouard Cunibil [@DuaelFr](http://twitter.com/DuaelFr) 86 | 87 | [Careers in Free and Open Source Software](http://exequiel09.github.com/symposium-presentation/) by Exequiel Ceasar Navarrete [@ichigo1411](http://twitter.com/ichigo1411) 88 | 89 | ### Websites and portfolios 90 | 91 | [lioshi.com](http://lioshi.com) by @lioshi 92 | 93 | [alingham.com](http://www.alingham.com) by Al Ingham [@alingham](http://twitter.com/alingham) 94 | 95 | [nice-shots.de](http://nice-shots.de) by [@NiceShots](http://twitter.com/NiceShots) 96 | 97 | [museum140](http://www.youtube.com/watch?v=ObLiikJEt94) Shorty Award promo video [entirely made with ImpressJS](http://thingsinjars.com/post/446/museum140-shorty/) by [@thingsinjars](http://twitter.com/thingsinjars) 98 | 99 | 100 | If you have used impress.js in your presentation (or website) and would like to have it listed here, 101 | please contact me via GitHub or send me a pull request to updated `README.md` file. 102 | 103 | 104 | 105 | BROWSER SUPPORT 106 | ----------------- 107 | 108 | ### TL;DR; 109 | 110 | Currently impress.js works fine in latest Chrome/Chromium browser, Safari 5.1 and Firefox 10 111 | (to be released in January 2012). IE is currently not supported (IE10 is close, but not there 112 | yet - see below for details). It also doesn't work in Opera. 113 | 114 | As it was not developed with mobile browsers in mind, it currently doesn't work on 115 | any mobile devices, including tablets. 116 | 117 | ### Still interested? Read more... 118 | 119 | Additionally for the animations to run smoothly it's required to have hardware 120 | acceleration support in your browser. This depends on the browser, your operating 121 | system and even kind of graphic hardware you have in your machine. 122 | 123 | For browsers not supporting CSS3 3D transforms impress.js adds `impress-not-supported` 124 | class on `#impress` element, so fallback styles can be applied to make all the content accessible. 125 | 126 | 127 | ### Even more explanation and technical stuff 128 | 129 | Let's put this straight -- wide browser support was (and is) not on top of my priority list for 130 | impress.js. It's built on top of fresh technologies that just start to appear in the browsers 131 | and I'd like to rather look forward and develop for the future than being slowed down by the past. 132 | 133 | But it's not "hard-coded" for any particular browser or engine. If any browser in future will 134 | support features required to run impress.js, it will just begin to work there without changes in 135 | the code. 136 | 137 | From technical point of view all the positioning of presentation elements in 3D requires CSS 3D 138 | transforms support. Transitions between presentation steps are based on CSS transitions. 139 | So these two features are required by impress.js to display presentation correctly. 140 | 141 | Unfortunately the support for CSS 3D transforms and transitions is not enough for animations to 142 | run smoothly. If the browser doesn't support hardware acceleration or the graphic card is not 143 | good enough the transitions will be laggy. 144 | 145 | Additionally the code of impress.js relies on APIs proposed in HTML5 specification, including 146 | `classList` and `dataset` APIs. If they are not available in the browser, impress.js will not work. 147 | 148 | Fortunately, as these are JavaScript APIs there are polyfill libraries that patch older browsers 149 | with these APIs. 150 | 151 | For example IE10 is said to support CSS 3D transforms and transitions, but it doesn't have `classList` 152 | not `dataset` APIs implemented at the moment. So including polyfill libraries *should* help IE10 153 | with running impress.js. 154 | 155 | 156 | ### And few more details about mobile support 157 | 158 | Mobile browsers are currently not supported. Even iOS and Android browsers that support 159 | CSS 3D transforms are forced into fallback view at this point. 160 | 161 | Anyway, I'm really curious to see how modern mobile devices such as iPhone or iPad can 162 | handle such animations, so future mobile support is considered. 163 | 164 | iOS supports `classList` and `dataset` APIs starting with version 5, so iOS 4.X and older is not 165 | likely to be supported (without polyfill code). 166 | 167 | 168 | LICENSE 169 | --------- 170 | 171 | Copyright 2011-2012 Bartek Szopka. Released under MIT License. 172 | 173 | -------------------------------------------------------------------------------- /web-server/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sTweereal - real-time twitter activity map 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 |

Tweereal is real-time twitter map (beta)

31 | 39 |
40 |
41 |
42 |
43 |
44 |
45 |
Default settings
46 |
47 |
48 |
All tweets
49 |
Size
50 |
51 |
Live time
52 |
53 |
54 |
55 |
56 |
Exact tweets
57 |
on
58 |
59 |
Opacity
60 |
61 |
62 |
63 |
64 |
Places tweets
65 |
on
66 |
67 |
Opacity
68 |
69 |
Precision
70 |
71 |
72 |
73 |
74 | 75 |
76 |
77 |

About

78 |

79 | @Tweereal is the map of twitter users activity in real-time. Animation on the map include only tweets containing geo-tags. There are two types of tweets on a map: with the exact coordinates and the coordinates determined with an accuracy of 1 degree (more transparent). Based on Twitter Streaming API and Google Maps Javascript API. Developed by Artem Bey aka @defly_self 80 |

81 |
82 |
83 |

Please feedback

84 | 85 |
86 | 87 |
88 | 89 |
90 |
91 |
92 |
93 | 95 | 96 |
97 |
98 | 106 |
107 | 138 | 139 | -------------------------------------------------------------------------------- /web-server/static/impress/js/impress.js: -------------------------------------------------------------------------------- 1 | /** 2 | * impress.js 3 | * 4 | * impress.js is a presentation tool based on the power of CSS3 transforms and transitions 5 | * in modern browsers and inspired by the idea behind prezi.com. 6 | * 7 | * MIT Licensed. 8 | * 9 | * Copyright 2011 Bartek Szopka (@bartaz) 10 | */ 11 | 12 | (function ( document, window ) { 13 | 'use strict'; 14 | 15 | // HELPER FUNCTIONS 16 | 17 | var pfx = (function () { 18 | 19 | var style = document.createElement('dummy').style, 20 | prefixes = 'Webkit Moz O ms Khtml'.split(' '), 21 | memory = {}; 22 | 23 | return function ( prop ) { 24 | if ( typeof memory[ prop ] === "undefined" ) { 25 | 26 | var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1), 27 | props = (prop + ' ' + prefixes.join(ucProp + ' ') + ucProp).split(' '); 28 | 29 | memory[ prop ] = null; 30 | for ( var i in props ) { 31 | if ( style[ props[i] ] !== undefined ) { 32 | memory[ prop ] = props[i]; 33 | break; 34 | } 35 | } 36 | 37 | } 38 | 39 | return memory[ prop ]; 40 | } 41 | 42 | })(); 43 | 44 | var arrayify = function ( a ) { 45 | return [].slice.call( a ); 46 | }; 47 | 48 | var css = function ( el, props ) { 49 | var key, pkey; 50 | for ( key in props ) { 51 | if ( props.hasOwnProperty(key) ) { 52 | pkey = pfx(key); 53 | if ( pkey != null ) { 54 | el.style[pkey] = props[key]; 55 | } 56 | } 57 | } 58 | return el; 59 | } 60 | 61 | var byId = function ( id ) { 62 | return document.getElementById(id); 63 | } 64 | 65 | var $ = function ( selector, context ) { 66 | context = context || document; 67 | return context.querySelector(selector); 68 | }; 69 | 70 | var $$ = function ( selector, context ) { 71 | context = context || document; 72 | return arrayify( context.querySelectorAll(selector) ); 73 | }; 74 | 75 | var translate = function ( t ) { 76 | return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; 77 | }; 78 | 79 | var rotate = function ( r, revert ) { 80 | var rX = " rotateX(" + r.x + "deg) ", 81 | rY = " rotateY(" + r.y + "deg) ", 82 | rZ = " rotateZ(" + r.z + "deg) "; 83 | 84 | return revert ? rZ+rY+rX : rX+rY+rZ; 85 | }; 86 | 87 | var scale = function ( s ) { 88 | return " scale(" + s + ") "; 89 | }; 90 | 91 | var getElementFromUrl = function () { 92 | // get id from url # by removing `#` or `#/` from the beginning, 93 | // so both "fallback" `#slide-id` and "enhanced" `#/slide-id` will work 94 | return byId( window.location.hash.replace(/^#\/?/,"") ); 95 | }; 96 | 97 | // CHECK SUPPORT 98 | 99 | var ua = navigator.userAgent.toLowerCase(); 100 | var impressSupported = ( pfx("perspective") != null ) && 101 | ( document.body.classList ) && 102 | ( document.body.dataset ) && 103 | ( ua.search(/(iphone)|(ipod)|(android)/) == -1 ); 104 | 105 | var roots = {}; 106 | 107 | var impress = window.impress = function ( rootId ) { 108 | 109 | rootId = rootId || "impress"; 110 | 111 | // if already initialized just return the API 112 | if (roots["impress-root-" + rootId]) { 113 | return roots["impress-root-" + rootId]; 114 | } 115 | 116 | // DOM ELEMENTS 117 | 118 | var root = byId( rootId ); 119 | 120 | if (!impressSupported) { 121 | root.className = "impress-not-supported"; 122 | return; 123 | } else { 124 | root.className = ""; 125 | } 126 | 127 | // viewport updates for iPad 128 | var meta = $("meta[name='viewport']") || document.createElement("meta"); 129 | // hardcoding these values looks pretty bad, as they kind of depend on the content 130 | // so they should be at least configurable 131 | meta.content = "width=1024, minimum-scale=0.75, maximum-scale=0.75, user-scalable=no"; 132 | if (meta.parentNode != document.head) { 133 | meta.name = 'viewport'; 134 | document.head.appendChild(meta); 135 | } 136 | 137 | var canvas = document.createElement("div"); 138 | canvas.className = "canvas"; 139 | 140 | arrayify( root.childNodes ).forEach(function ( el ) { 141 | canvas.appendChild( el ); 142 | }); 143 | root.appendChild(canvas); 144 | 145 | var steps = $$(".step", root); 146 | 147 | // SETUP 148 | // set initial values and defaults 149 | 150 | document.documentElement.style.height = "100%"; 151 | 152 | css(document.body, { 153 | height: "100%", 154 | overflow: "hidden" 155 | }); 156 | 157 | var props = { 158 | position: "absolute", 159 | transformOrigin: "top left", 160 | transition: "all 0s ease-in-out", 161 | transformStyle: "preserve-3d" 162 | } 163 | 164 | css(root, props); 165 | css(root, { 166 | top: "50%", 167 | left: "50%", 168 | perspective: "1000px" 169 | }); 170 | css(canvas, props); 171 | 172 | var current = { 173 | translate: { x: 0, y: 0, z: 0 }, 174 | rotate: { x: 0, y: 0, z: 0 }, 175 | scale: 1 176 | }; 177 | 178 | var stepData = {}; 179 | 180 | var isStep = function ( el ) { 181 | return !!(el && el.id && stepData["impress-" + el.id]); 182 | } 183 | 184 | steps.forEach(function ( el, idx ) { 185 | var data = el.dataset, 186 | step = { 187 | translate: { 188 | x: data.x || 0, 189 | y: data.y || 0, 190 | z: data.z || 0 191 | }, 192 | rotate: { 193 | x: data.rotateX || 0, 194 | y: data.rotateY || 0, 195 | z: data.rotateZ || data.rotate || 0 196 | }, 197 | scale: data.scale || 1, 198 | el: el 199 | }; 200 | 201 | if ( !el.id ) { 202 | el.id = "step-" + (idx + 1); 203 | } 204 | 205 | stepData["impress-" + el.id] = step; 206 | 207 | css(el, { 208 | position: "absolute", 209 | transform: "translate(-50%,-50%)" + 210 | translate(step.translate) + 211 | rotate(step.rotate) + 212 | scale(step.scale), 213 | transformStyle: "preserve-3d" 214 | }); 215 | 216 | }); 217 | 218 | // making given step active 219 | 220 | var active = null; 221 | var hashTimeout = null; 222 | 223 | var goto = function ( el ) { 224 | if ( !isStep(el) || el == active) { 225 | // selected element is not defined as step or is already active 226 | return false; 227 | } 228 | 229 | // Sometimes it's possible to trigger focus on first link with some keyboard action. 230 | // Browser in such a case tries to scroll the page to make this element visible 231 | // (even that body overflow is set to hidden) and it breaks our careful positioning. 232 | // 233 | // So, as a lousy (and lazy) workaround we will make the page scroll back to the top 234 | // whenever slide is selected 235 | // 236 | // If you are reading this and know any better way to handle it, I'll be glad to hear about it! 237 | window.scrollTo(0, 0); 238 | 239 | var step = stepData["impress-" + el.id]; 240 | 241 | if ( active ) { 242 | active.classList.remove("active"); 243 | } 244 | el.classList.add("active"); 245 | 246 | root.className = "step-" + el.id; 247 | 248 | // `#/step-id` is used instead of `#step-id` to prevent default browser 249 | // scrolling to element in hash 250 | // 251 | // and it has to be set after animation finishes, because in chrome it 252 | // causes transtion being laggy 253 | window.clearTimeout( hashTimeout ); 254 | hashTimeout = window.setTimeout(function () { 255 | window.location.hash = "#/" + el.id; 256 | }, 1000); 257 | 258 | var target = { 259 | rotate: { 260 | x: -parseInt(step.rotate.x, 10), 261 | y: -parseInt(step.rotate.y, 10), 262 | z: -parseInt(step.rotate.z, 10) 263 | }, 264 | translate: { 265 | x: -step.translate.x, 266 | y: -step.translate.y, 267 | z: -step.translate.z 268 | }, 269 | scale: 1 / parseFloat(step.scale) 270 | }; 271 | 272 | // check if the transition is zooming in or not 273 | var zoomin = target.scale >= current.scale; 274 | 275 | // if presentation starts (nothing is active yet) 276 | // don't animate (set duration to 0) 277 | var duration = (active) ? "1s" : "0"; 278 | 279 | css(root, { 280 | // to keep the perspective look similar for different scales 281 | // we need to 'scale' the perspective, too 282 | perspective: step.scale * 1000 + "px", 283 | transform: scale(target.scale), 284 | transitionDuration: duration, 285 | transitionDelay: (zoomin ? "500ms" : "0ms") 286 | }); 287 | 288 | css(canvas, { 289 | transform: rotate(target.rotate, true) + translate(target.translate), 290 | transitionDuration: duration, 291 | transitionDelay: (zoomin ? "0ms" : "500ms") 292 | }); 293 | 294 | current = target; 295 | active = el; 296 | 297 | return el; 298 | }; 299 | 300 | var prev = function () { 301 | var prev = steps.indexOf( active ) - 1; 302 | prev = prev >= 0 ? steps[ prev ] : steps[ steps.length-1 ]; 303 | 304 | return goto(prev); 305 | }; 306 | 307 | var next = function () { 308 | var next = steps.indexOf( active ) + 1; 309 | next = next < steps.length ? steps[ next ] : steps[ 0 ]; 310 | 311 | return goto(next); 312 | }; 313 | 314 | window.addEventListener("hashchange", function () { 315 | goto( getElementFromUrl() ); 316 | }, false); 317 | 318 | window.addEventListener("orientationchange", function () { 319 | window.scrollTo(0, 0); 320 | }, false); 321 | 322 | // START 323 | // by selecting step defined in url or first step of the presentation 324 | goto(getElementFromUrl() || steps[0]); 325 | 326 | return (roots[ "impress-root-" + rootId ] = { 327 | goto: goto, 328 | next: next, 329 | prev: prev 330 | }); 331 | 332 | } 333 | })(document, window); 334 | 335 | // EVENTS 336 | 337 | (function ( document, window ) { 338 | 'use strict'; 339 | 340 | // keyboard navigation handler 341 | document.addEventListener("keydown", function ( event ) { 342 | if ( event.keyCode == 9 || ( event.keyCode >= 32 && event.keyCode <= 34 ) || (event.keyCode >= 37 && event.keyCode <= 40) ) { 343 | switch( event.keyCode ) { 344 | case 33: ; // pg up 345 | case 37: ; // left 346 | case 38: // up 347 | impress().prev(); 348 | break; 349 | case 9: ; // tab 350 | case 32: ; // space 351 | case 34: ; // pg down 352 | case 39: ; // right 353 | case 40: // down 354 | impress().next(); 355 | break; 356 | } 357 | 358 | event.preventDefault(); 359 | } 360 | }, false); 361 | 362 | // delegated handler for clicking on the links to presentation steps 363 | document.addEventListener("click", function ( event ) { 364 | // event delegation with "bubbling" 365 | // check if event target (or any of its parents is a link) 366 | var target = event.target; 367 | while ( (target.tagName != "A") && 368 | (target != document.body) ) { 369 | target = target.parentNode; 370 | } 371 | 372 | if ( target.tagName == "A" ) { 373 | var href = target.getAttribute("href"); 374 | 375 | // if it's a link to presentation step, target this step 376 | if ( href && href[0] == '#' ) { 377 | target = document.getElementById( href.slice(1) ); 378 | } 379 | } 380 | 381 | if ( impress().goto(target) ) { 382 | event.stopImmediatePropagation(); 383 | event.preventDefault(); 384 | } 385 | }, false); 386 | 387 | // delegated handler for clicking on step elements 388 | document.addEventListener("click", function ( event ) { 389 | var target = event.target; 390 | // find closest step element 391 | while ( !target.classList.contains("step") && 392 | (target != document.body) ) { 393 | target = target.parentNode; 394 | } 395 | 396 | if ( impress().goto(target) ) { 397 | event.preventDefault(); 398 | } 399 | }, false); 400 | 401 | // touch handler to detect taps on the left and right side of the screen 402 | document.addEventListener("touchstart", function ( event ) { 403 | if (event.touches.length === 1) { 404 | var x = event.touches[0].clientX, 405 | width = window.innerWidth * 0.3, 406 | result = null; 407 | 408 | if ( x < width ) { 409 | result = impress().prev(); 410 | } else if ( x > window.innerWidth - width ) { 411 | result = impress().next(); 412 | } 413 | 414 | if (result) { 415 | event.preventDefault(); 416 | } 417 | } 418 | }, false); 419 | })(document, window); 420 | 421 | -------------------------------------------------------------------------------- /web-server/static/impress/css/impress-demo.css: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a stylesheet for a demo presentation for impress.js 3 | * 4 | * It is not meant to be a part of impress.js and is not required by impress.js. 5 | * I expect that anyone creating a presentation for impress.js would create their own 6 | * set of styles. 7 | */ 8 | 9 | 10 | /* http://meyerweb.com/eric/tools/css/reset/ 11 | v2.0 | 20110126 12 | License: none (public domain) 13 | */ 14 | 15 | html, body, div, span, applet, object, iframe, 16 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 17 | a, abbr, acronym, address, big, cite, code, 18 | del, dfn, em, img, ins, kbd, q, s, samp, 19 | small, strike, strong, sub, sup, tt, var, 20 | b, u, i, center, 21 | dl, dt, dd, ol, ul, li, 22 | fieldset, form, label, legend, 23 | table, caption, tbody, tfoot, thead, tr, th, td, 24 | article, aside, canvas, details, embed, 25 | figure, figcaption, footer, header, hgroup, 26 | menu, nav, output, ruby, section, summary, 27 | time, mark, audio, video { 28 | margin: 0; 29 | padding: 0; 30 | border: 0; 31 | font-size: 100%; 32 | font: inherit; 33 | vertical-align: baseline; 34 | } 35 | 36 | /* HTML5 display-role reset for older browsers */ 37 | article, aside, details, figcaption, figure, 38 | footer, header, hgroup, menu, nav, section { 39 | display: block; 40 | } 41 | body { 42 | line-height: 1; 43 | } 44 | ol, ul { 45 | list-style: none; 46 | } 47 | blockquote, q { 48 | quotes: none; 49 | } 50 | blockquote:before, blockquote:after, 51 | q:before, q:after { 52 | content: ''; 53 | content: none; 54 | } 55 | 56 | table { 57 | border-collapse: collapse; 58 | border-spacing: 0; 59 | } 60 | 61 | 62 | body { 63 | font-family: 'PT Sans', sans-serif; 64 | 65 | min-height: 740px; 66 | 67 | /*background: rgb(215, 215, 215);*/ 68 | background: white; 69 | /*background: -webkit-gradient(radial, 50% 50%, 0, 50% 50%, 500, from(rgb(240, 240, 240)), to(rgb(190, 190, 190)));*/ 70 | /*background: -webkit-radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190));*/ 71 | /*background: -moz-radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190));*/ 72 | /*background: -o-radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190));*/ 73 | /*background: radial-gradient(rgb(240, 240, 240), rgb(190, 190, 190));*/ 74 | background-image: url(http://tweereal.com/static/images/project_papper.png); 75 | 76 | -webkit-font-smoothing: antialiased; 77 | } 78 | 79 | b, strong { font-weight: bold } 80 | i, em { font-style: italic} 81 | 82 | a { 83 | color: inherit; 84 | text-decoration: none; 85 | padding: 0 0.1em; 86 | background: rgba(255,255,255,0.5); 87 | text-shadow: -1px -1px 2px rgba(100,100,100,0.9); 88 | border-radius: 0.2em; 89 | 90 | -webkit-transition: 0.5s; 91 | -moz-transition: 0.5s; 92 | -ms-transition: 0.5s; 93 | -o-transition: 0.5s; 94 | transition: 0.5s; 95 | } 96 | 97 | a:hover { 98 | background: rgba(255,255,255,1); 99 | text-shadow: -1px -1px 2px rgba(100,100,100,0.5); 100 | } 101 | 102 | /* enable clicking on elements 'hiding' behind body in 3D */ 103 | body { pointer-events: none; } 104 | #impress { pointer-events: auto; } 105 | 106 | /* COMMON STEP STYLES */ 107 | 108 | .step { 109 | width: 900px; 110 | padding: 40px; 111 | 112 | -webkit-box-sizing: border-box; 113 | -moz-box-sizing: border-box; 114 | -ms-box-sizing: border-box; 115 | -o-box-sizing: border-box; 116 | box-sizing: border-box; 117 | 118 | font-family: 'PT Serif', georgia, serif; 119 | 120 | font-size: 48px; 121 | line-height: 1.5; 122 | } 123 | 124 | /* fade out inactive slides */ 125 | 126 | .step { 127 | -webkit-transition: opacity 1s; 128 | -moz-transition: opacity 1s; 129 | -ms-transition: opacity 1s; 130 | -o-transition: opacity 1s; 131 | transition: opacity 1s; 132 | } 133 | 134 | .step:not(.active) { 135 | opacity: 0.3; 136 | } 137 | 138 | /* STEP SPECIFIC STYLES */ 139 | 140 | /* hint on the first slide */ 141 | 142 | .hint { 143 | position: fixed; 144 | left: 0; 145 | right: 0; 146 | bottom: 200px; 147 | 148 | background: rgba(0,0,0,0.5); 149 | color: #EEE; 150 | text-align: center; 151 | 152 | font-size: 50px; 153 | padding: 20px; 154 | 155 | z-index: 100; 156 | 157 | opacity: 0; 158 | 159 | -webkit-transform: translateY(400px); 160 | -moz-transform: translateY(400px); 161 | -ms-transform: translateY(400px); 162 | -o-transform: translateY(400px); 163 | transform: translateY(400px); 164 | 165 | -webkit-transition: opacity 1s, -webkit-transform 0.5s 1s; 166 | -moz-transition: opacity 1s, -moz-transform 0.5s 1s; 167 | -ms-transition: opacity 1s, -ms-transform 0.5s 1s; 168 | -o-transition: opacity 1s, -o-transform 0.5s 1s; 169 | transition: opacity 1s, transform 0.5s 1s; 170 | } 171 | 172 | .step-bored + .hint { 173 | opacity: 1; 174 | 175 | -webkit-transition: opacity 1s 5s, -webkit-transform 0.5s; 176 | -moz-transition: opacity 1s 5s, -moz-transform 0.5s; 177 | -ms-transition: opacity 1s 5s, -ms-transform 0.5s; 178 | -o-transition: opacity 1s 5s, -o-transform 0.5s; 179 | transition: opacity 1s 5s, transform 0.5s; 180 | 181 | -webkit-transform: translateY(0px); 182 | -moz-transform: translateY(0px); 183 | -ms-transform: translateY(0px); 184 | -o-transform: translateY(0px); 185 | transform: translateY(0px); 186 | } 187 | 188 | /* impress.js title */ 189 | 190 | #title { 191 | padding: 0; 192 | } 193 | 194 | #title .try { 195 | font-size: 64px; 196 | position: absolute; 197 | top: -0.5em; 198 | left: 1.5em; 199 | 200 | -webkit-transform: translateZ(20px); 201 | -moz-transform: translateZ(20px); 202 | -ms-transform: translateZ(20px); 203 | -o-transform: translateZ(20px); 204 | transform: translateZ(20px); 205 | } 206 | 207 | #title h1 { 208 | font-size: 190px; 209 | 210 | -webkit-transform: translateZ(50px); 211 | -moz-transform: translateZ(50px); 212 | -ms-transform: translateZ(50px); 213 | -o-transform: translateZ(50px); 214 | transform: translateZ(50px); 215 | } 216 | 217 | #title .footnote { 218 | font-size: 32px; 219 | } 220 | 221 | /* big thoughts */ 222 | 223 | #big { 224 | width: 600px; 225 | text-align: center; 226 | font-size: 60px; 227 | line-height: 1; 228 | } 229 | 230 | #big b { 231 | display: block; 232 | font-size: 250px; 233 | line-height: 250px; 234 | } 235 | 236 | #big .thoughts { 237 | font-size: 90px; 238 | line-height: 150px; 239 | } 240 | 241 | /* tiny ideas */ 242 | 243 | #tiny { 244 | width: 500px; 245 | text-align: center; 246 | } 247 | 248 | #ing { 249 | width: 500px; 250 | } 251 | 252 | #ing b { 253 | display: inline-block; 254 | -webkit-transition: 0.5s; 255 | -moz-transition: 0.5s; 256 | -ms-transition: 0.5s; 257 | -o-transition: 0.5s; 258 | transition: 0.5s; 259 | } 260 | 261 | #ing.active .positioning { 262 | -webkit-transform: translateY(-10px); 263 | -moz-transform: translateY(-10px); 264 | -ms-transform: translateY(-10px); 265 | -o-transform: translateY(-10px); 266 | transform: translateY(-10px); 267 | 268 | -webkit-transition-delay: 1.5s; 269 | -moz-transition-delay: 1.5s; 270 | -ms-transition-delay: 1.5s; 271 | -o-transition-delay: 1.5s; 272 | transition-delay: 1.5s; 273 | } 274 | 275 | #ing.active .rotating { 276 | -webkit-transform: rotate(-10deg); 277 | -moz-transform: rotate(-10deg); 278 | -ms-transform: rotate(-10deg); 279 | -o-transform: rotate(-10deg); 280 | transform: rotate(-10deg); 281 | 282 | -webkit-transition-delay: 1.75s; 283 | -moz-transition-delay: 1.75s; 284 | -ms-transition-delay: 1.75s; 285 | -o-transition-delay: 1.75s; 286 | transition-delay: 1.75s; 287 | } 288 | 289 | #ing.active .scaling { 290 | -webkit-transform: scale(0.7); 291 | -moz-transform: scale(0.7); 292 | -ms-transform: scale(0.7); 293 | -o-transform: scale(0.7); 294 | transform: scale(0.7); 295 | 296 | -webkit-transition-delay: 2s; 297 | -moz-transition-delay: 2s; 298 | -ms-transition-delay: 2s; 299 | -o-transition-delay: 2s; 300 | transition-delay: 2s; 301 | 302 | } 303 | 304 | /* imagination */ 305 | 306 | #imagination { 307 | width: 600px; 308 | } 309 | 310 | #imagination .imagination { 311 | font-size: 78px; 312 | } 313 | 314 | /* use the source, Luke */ 315 | 316 | #source { 317 | width: 700px; 318 | padding-bottom: 300px; 319 | 320 | /* Yoda Icon :: Pixel Art from Star Wars http://www.pixeljoint.com/pixelart/1423.htm */ 321 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAEYCAMAAACwUBm+AAAAAXNSR0IArs4c6QAAAKtQTFRFsAAAvbWSLUUrLEQqY1s8UYJMqJ1vNTEgOiIdIzYhjIFVLhsXZ6lgSEIsP2U8JhcCVzMsSXZEgXdOO145XJdWOl03LzAYMk4vSXNExr+hwcuxRTs1Qmk+RW9Am49eFRANQz4pUoNMQWc+OSMDTz0wLBsCNVMxa2NBOyUDUoNNSnlEWo9VRGxAVzYFl6tXCggHbLNmMUIcHhwTXkk5f3VNRT8wUT8xAAAACQocRBWFFwAAAAF0Uk5TAEDm2GYAAAPCSURBVHja7d3JctNAFIZRMwRCCGEmzPM8z/D+T8bu/ptbXXJFdij5fMt2Wuo+2UgqxVmtttq5WVotLzBgwIABAwYMGDCn0qVqbo69psPqVpWx+1XG5iaavF8wYMCAAQMGDBgwi4DJ6Y6qkxB1HNlcN3a92gbR5P2CAQMGDBgwYMCAWSxMlrU+UY5yu2l9okfV4bAxUVbf7TJnAwMGDBgwYMCAAbMLMHeqbGR82Zy+VR1Ht81nVca6R+UdTLaU24Ruzd3qM/e4yjnAgAEDBgwYMGDA7AJMd1l/3NRdVGcj3eX/2WEhCmDGxnM7yqygu8XIPjJj8iN/MGDAgAEDBgwYMAuDGb8q0RGlLCHLv1t9qDKWn3vdNHVuEI6HPaxO9Jo3GDBgwIABAwYMmIXBdC9ShGgMk+XnkXUeuGcsP/e1+lhNnZsL/G5Vs3OAAQMGDBgwYMCAWSxMR3SzOmraG5atdy9wZKzb+vg16qyqe2FltbnAgAEDBgwYMGDALAxmTJSuN3WA76rnVca6GTnemGN1WoEBAwYMGDBgwIBZGMxUomy4+xO899V4LAg5Xnc2MGDAgAEDBgwYMGA218Wq+2K1LDqvY9xZu8zN8fICdM6btYABAwYMGDBgwIABMzfH0+pGU5afze2tXebmeAfVz+p8BQYMGDBgwIABAwbMPBzZ+oWmfJrln1273FhkbHzee9WWbw7AgAEDBgwYMGDALAKm43hcdctKgblcPamOhuXnXlY5Xs6bsW4FGyQCAwYMGDBgwIABswiYMceZKgvMo+h8mrHLTdn676rj+FEFoTtHd8MwOxEYMGDAgAEDBgyYRcBM5UhXqiymW3R3c9ARhWO/OmjqfjVZy+xEYMCAAQMGDBgwYBYG073OnCV0RFNhMhaOa9WfKmOB6XjHMN1tQmaAAQMGDBgwYMCA2VWY7vXjz1U4croAzgPztwIDBgwYMGDAgAEDZhswh035NBw59Dww3RgYMGDAgAEDBgwYMJuD6f4tXT7NUqfCdBvZLkxXdgQGDBgwYMCAAQNmt2DGj8WzwAfV/w7T/aq7mxwwYMCAAQMGDBgwuwqTOo7uTwTngflSzQ3TdaJvAwEDBgwYMGDAgAED5gSvgbyo5oHZ4Pc+gwEDBgwYMGDAgAEzhOm+5G0qTGaAAQMGDBgwYMCAAXNaMOcnls3tNwWm+zRzp54NDBgwYMCAAQMGDJh5YNL36k1TLuGvVq+qnKMbS5n7tulT9asCAwYMGDBgwIABA2ZumKuztLnjgQEDBgwYMGDAgNl5mH/4/ltKA6vBNAAAAABJRU5ErkJggg==); 322 | background-position: bottom right; 323 | background-repeat: no-repeat; 324 | } 325 | 326 | #source q { 327 | font-size: 60px; 328 | } 329 | 330 | /* it's in 3D */ 331 | 332 | #its-in-3d p { 333 | -webkit-transform-style: preserve-3d; 334 | -moz-transform-style: preserve-3d; /* Y U need this Firefox?! */ 335 | -ms-transform-style: preserve-3d; 336 | -o-transform-style: preserve-3d; 337 | transform-style: preserve-3d; 338 | } 339 | 340 | #its-in-3d span, 341 | #its-in-3d b { 342 | display: inline-block; 343 | -webkit-transform: translateZ(40px); 344 | -moz-transform: translateZ(40px); 345 | -ms-transform: translateZ(40px); 346 | -o-transform: translateZ(40px); 347 | transform: translateZ(40px); 348 | 349 | -webkit-transition: 0.5s; 350 | -moz-transition: 0.5s; 351 | -ms-transition: 0.5s; 352 | -o-transition: 0.5s; 353 | transition: 0.5s; 354 | } 355 | 356 | #its-in-3d .have { 357 | -webkit-transform: translateZ(-40px); 358 | -moz-transform: translateZ(-40px); 359 | -ms-transform: translateZ(-40px); 360 | -o-transform: translateZ(-40px); 361 | transform: translateZ(-40px); 362 | } 363 | 364 | #its-in-3d .you { 365 | -webkit-transform: translateZ(20px); 366 | -moz-transform: translateZ(20px); 367 | -ms-transform: translateZ(20px); 368 | -o-transform: translateZ(20px); 369 | transform: translateZ(20px); 370 | } 371 | 372 | #its-in-3d .noticed { 373 | -webkit-transform: translateZ(-40px); 374 | -moz-transform: translateZ(-40px); 375 | -ms-transform: translateZ(-40px); 376 | -o-transform: translateZ(-40px); 377 | transform: translateZ(-40px); 378 | } 379 | 380 | #its-in-3d .its { 381 | -webkit-transform: translateZ(60px); 382 | -moz-transform: translateZ(60px); 383 | -ms-transform: translateZ(60px); 384 | -o-transform: translateZ(60px); 385 | transform: translateZ(60px); 386 | } 387 | 388 | #its-in-3d .in { 389 | -webkit-transform: translateZ(-10px); 390 | -moz-transform: translateZ(-10px); 391 | -ms-transform: translateZ(-10px); 392 | -o-transform: translateZ(-10px); 393 | transform: translateZ(-10px); 394 | } 395 | 396 | #its-in-3d .footnote { 397 | font-size: 32px; 398 | 399 | -webkit-transform: translateZ(-10px); 400 | -moz-transform: translateZ(-10px); 401 | -ms-transform: translateZ(-10px); 402 | -o-transform: translateZ(-10px); 403 | transform: translateZ(-10px); 404 | } 405 | 406 | #its-in-3d.active span, 407 | #its-in-3d.active b { 408 | -webkit-transform: translateZ(0px); 409 | -moz-transform: translateZ(0px); 410 | -ms-transform: translateZ(0px); 411 | -o-transform: translateZ(0px); 412 | transform: translateZ(0px); 413 | 414 | -webkit-transition-delay: 1s; 415 | -moz-transition-delay: 1s; 416 | -ms-transition-delay: 1s; 417 | -o-transition-delay: 1s; 418 | transition-delay: 1s; 419 | } 420 | 421 | /* overview step */ 422 | 423 | #overview { 424 | z-index: -1; 425 | padding: 0; 426 | } 427 | 428 | /* on overview step everything is visible */ 429 | 430 | #impress.step-overview .step { 431 | opacity: 1; 432 | cursor: pointer; 433 | } 434 | 435 | /* 436 | * SLIDE STEP STYLES 437 | * 438 | * inspired by: http://html5slides.googlecode.com/svn/trunk/styles.css 439 | * 440 | * ;) 441 | */ 442 | 443 | .lstul { 444 | /*margin: 0 auto;*/ 445 | } 446 | 447 | .slide { 448 | display: block; 449 | 450 | width: 900px; 451 | height: 700px; 452 | 453 | padding: 40px 60px; 454 | 455 | border-radius: 10px; 456 | 457 | background-color: white; 458 | 459 | box-shadow: 0 2px 6px rgba(0, 0, 0, .1); 460 | border: 1px solid rgba(0, 0, 0, .3); 461 | 462 | font-family: 'Open Sans', Arial, sans-serif; 463 | 464 | /*color: rgb(102, 102, 102);*/ 465 | color: rgb(227, 50, 88); 466 | text-shadow: 0 2px 2px rgba(0, 0, 0, .1); 467 | 468 | font-size: 30px; 469 | line-height: 36px; 470 | 471 | letter-spacing: -1px; 472 | } 473 | 474 | .slide q { 475 | display: block; 476 | font-size: 50px; 477 | line-height: 72px; 478 | 479 | margin-top: 100px; 480 | } 481 | 482 | .slide q strong { 483 | white-space: nowrap; 484 | } 485 | 486 | 487 | /* IMPRESS NOT SUPPORTED STYLES */ 488 | 489 | .fallback-message { 490 | font-family: sans-serif; 491 | line-height: 1.3; 492 | 493 | display: none; 494 | width: 780px; 495 | padding: 10px 10px 0; 496 | margin: 20px auto; 497 | 498 | border-radius: 10px; 499 | border: 1px solid #E4C652; 500 | background: #EEDC94; 501 | } 502 | 503 | .fallback-message p { 504 | margin-bottom: 10px; 505 | } 506 | 507 | .impress-not-supported .step { 508 | position: relative; 509 | opacity: 1; 510 | margin: 20px auto; 511 | } 512 | 513 | .impress-not-supported .fallback-message { 514 | display: block; 515 | } 516 | 517 | -------------------------------------------------------------------------------- /web-server/static/impress/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46 | 47 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | impress.js | presentation tool based on the power of CSS3 transforms and transitions in modern browsers | by Bartek Szopka @bartaz 72 | 73 | 74 | 75 | 76 | 77 | 78 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 114 |
115 | 116 |
117 |

Your browser doesn't support the features required by impress.js, so you are presented with a simplified version of this presentation.

118 |

For the best experience please use the latest Chrome or Safari browser. Firefox 10 (to be released soon) will also handle it.

119 |
120 | 121 | 138 |
139 | Aren't you just bored with all those slides-based presentations? 140 |
141 | 142 | 157 |
158 | Don't you think that presentations given in modern browsers shouldn't copy the limits of 'classic' slide decks? 159 |
160 | 161 |
162 | Would you like to impress your audience with stunning visualization of your talk? 163 |
164 | 165 | 175 |
176 | then you should try 177 |

impress.js*

178 | * no rhyme intended 179 |
180 | 181 | 189 |
190 |

It's a presentation tool
191 | inspired by the idea behind prezi.com
192 | and based on the power of CSS3 transforms and transitions in modern browsers.

193 |
194 | 195 |
196 |

visualize your big thoughts

197 |
198 | 199 | 208 |
209 |

and tiny ideas

210 |
211 | 212 |
213 |

by positioning, rotating and scaling them on an infinite canvas

214 |
215 | 216 |
217 |

the only limit is your imagination

218 |
219 | 220 |
221 |

want to know more?

222 | use the source, Luke! 223 |
224 | 225 |
226 |

one more thing...

227 |
228 | 229 | 241 |
242 |

have you noticed it's in 3D*?

243 | * beat that, prezi ;) 244 |
245 | 246 | 258 |
259 |
260 | 261 |
262 | 263 | 277 |
278 |

Use a spacebar or arrow keys to navigate

279 |
280 | 285 | 286 | 301 | 302 | 303 | 304 | 325 | 326 | 327 | 328 | 329 | 357 | 358 | 373 | 374 | -------------------------------------------------------------------------------- /web-server/views/pres.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 46 | 47 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Tweereal 72 | 73 | 74 | 75 | 76 | 77 | 78 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 114 |
115 | 116 |
117 |

Your browser doesn't support the features required by impress.js, so you are presented with a simplified version of this presentation.

118 |

For the best experience please use the latest Chrome or Safari browser. Firefox 10 (to be released soon) will also handle it.

119 |
120 | 121 | 138 |
139 |

Node.js Real-time Application

140 | Tweereal.com – example of real-time node.js application 141 |
142 | 143 | 158 |
159 | 163 |

Framework

164 | 165 |
    166 |
  • Node.js
  • 167 |
  • Socket.io
  • 168 |
  • Express
  • 169 |
170 |
171 |
172 | 173 |
174 |

Architecture

175 | Streaming API → Filtering → Socket.io → HAProxy 176 | Client → Express → HAProxy 177 | Client → nginx 178 |
179 | 180 |
181 |

Stability

182 | init.d 183 | forever 184 | 185 |
186 | 187 |
188 |

Senq for your attention

189 | Artem Bey: defly.self@gmail.com 190 | http://tweereal.com 191 | @tweereal 192 | @defly_self 193 | 194 |
195 | 196 | 206 | 211 | 212 | 220 | 229 | 230 | 239 | 260 | 272 | 277 | 289 |
290 |
291 | 292 |
293 | 294 | 308 |
309 |

Use a spacebar or arrow keys to navigate

310 |
311 | 316 | 317 | 332 | 333 | 334 | 335 | 356 | 357 | 358 | 359 | 360 | 388 | 389 | 404 | 405 | --------------------------------------------------------------------------------