Some changes - added multi-zoom level on client
authorRaphael Velt <raph.velt@gmail.com>
Mon, 17 Oct 2011 17:40:58 +0200
changeset 325 7d9c576bfaac
parent 315 77ccba188e08
child 326 c28048fb63b4
Some changes - added multi-zoom level on client
tweetcast/nodejs/client/css/style.css
tweetcast/nodejs/client/index.html
tweetcast/nodejs/client/js/script.js
tweetcast/nodejs/server/node-direct.js
--- a/tweetcast/nodejs/client/css/style.css	Fri Oct 14 17:36:34 2011 +0200
+++ b/tweetcast/nodejs/client/css/style.css	Mon Oct 17 17:40:58 2011 +0200
@@ -27,38 +27,78 @@
 
 /* Liste de Tweets */
 
-#tweetcontainer {
-	position: absolute; top: 25px; bottom: 5px; width: 270px; overflow: auto;
-}
-
 #tweetlist {
-	list-style: none;
+	position: absolute; left: 50px; top: 50px; width: 240px; height: 600px; list-style: none; overflow: hidden; border: 1px solid #999;
 }
 
 li.tweet {
-	border: 1px solid #999; margin: 5px; width: 240px; height: 100px; overflow: hidden;
+	border-width: 1px; border-style: none solid solid none; border-color: #ccc; overflow: hidden; float: left;
+}
+
+li.tweet.full {
+    width: 240px; height: 99px;
 }
 
-.tweet h4, .tweet p {
-	margin: 5px;
+li.tweet.half {
+    width: 240px; height: 49px;
+}
+
+li.tweet.quarter {
+    width: 119px; height: 49px;
+}
+
+li.tweet.icons {
+    width: 29px; height: 29px;
 }
 
 .tweet h4 {
+    font-weight: bold;
+}
+
+.full h4, .full p, .half p {
+	margin: 5px;
+}
+
+.full h4 {
 	font-size: 15px;
 }
 
-.tweet p {
+.full p, .half p {
 	font-size: 12px;
 }
 
+.quarter p {
+    font-size: 10px; margin: 2px 2px 2px 28px;
+}
+
 img.tweet_profile_image {
-	float: left; margin: 5px; width: 32px; height: 32px;
+	float: left; opacity: .7; filter:alpha(opacity=70);
+}
+
+.full .tweet_profile_image {
+    margin: 5px; width: 48px; height: 48px;
+}
+
+.half .tweet_profile_image {
+    margin: 5px; width: 39px; height: 39px;
+}
+
+.quarter .tweet_profile_image {
+    margin: 2px; width: 25px; height: 25px;
+}
+
+ .icons .tweet_profile_image {
+    margin: 3px; width: 23px; height: 23px;
 }
 
 p.tweet_created_at {
 	font-style: italic; color: #999;
 }
 
+.half h4, .half p.tweet_created_at, .quarter h4, .quarter p.tweet_created_at, .icons h4, .icons p {
+    display: none
+}
+
 .a_positive {
 	background: #c5e7cd;
 }
--- a/tweetcast/nodejs/client/index.html	Fri Oct 14 17:36:34 2011 +0200
+++ b/tweetcast/nodejs/client/index.html	Mon Oct 17 17:40:58 2011 +0200
@@ -13,10 +13,8 @@
   <link rel="stylesheet" href="css/style.css" type="text/css" />
  </head>
  <body>
- <h1>Liste de Tweets</h1>
- <div id="tweetcontainer">
+ <p>Niveau de zoom (liste de tweets) <a href="javascript:setZoom(0)">1</a> - <a href="javascript:setZoom(1)">2</a> - <a href="javascript:setZoom(2)">3</a> - <a href="javascript:setZoom(3)">4</a></p>
   <ul id="tweetlist">
   </ul>
- </div>
  </body>
 </html>
\ No newline at end of file
--- a/tweetcast/nodejs/client/js/script.js	Fri Oct 14 17:36:34 2011 +0200
+++ b/tweetcast/nodejs/client/js/script.js	Mon Oct 17 17:40:58 2011 +0200
@@ -1,11 +1,35 @@
 var socket,
-    tweets = [],
-    waitoldtweets = true,
-    tl,
-    tc;
+    tweetData = {
+        "tweetcount" : 0,
+        "position" : -1,
+        "zoomLevel" : 3,
+        "timeLevel" : 2
+    }
+    zoomLevels = [
+        {
+            "description" : "160 tweets per page",
+            "className" : "icons",
+            "displayCount" : 160
+        },
+        {
+            "description" : "24 tweets per page",
+            "className" : "quarter",
+            "displayCount" : 24
+        },
+        {
+            "description" : "12 tweets per page",
+            "className" : "half",
+            "displayCount" : 12
+        },
+        {
+            "description" : "6 tweets per page",
+            "className" : "full",
+            "displayCount" : 6
+        }
+    ];
 
 function tweetToHtml(tweet) {
-	html = '<li class="tweet';
+	html = '<li class="tweet ' + zoomLevels[tweetData.zoomLevel].className;
 	for (var i in tweet.annotations) {
 		html += ' a_' + tweet.annotations[i]
 	}
@@ -14,7 +38,7 @@
 	if (tweet.user.profile_image_url) {
 		html += a_user + '<img class="tweet_profile_image" src="' + tweet.user.profile_image_url + '" /></a>';
 	}
-	html += '<h4>' + a_user + '@' + tweet.user.screen_name + '</a></h4><p class="tweet_created_at">' + tweet.created_at + '</p><p>';
+	html += '<h4>' + a_user + '@' + tweet.user.screen_name + '</a></h4><p class="tweet_created_at">' + new Date(tweet.created_at).toLocaleTimeString() + '</p><p>';
 	lastend = 0;
 	txt = '';
 	entities = [];
@@ -49,105 +73,33 @@
 	return html;
 }
 
-function discardTweets() {
-    if (tweets.length) {
-        while (tl.height() - (tc.scrollTop() + tc.height()) > 1000) {
- 	        tl.children().last().detach();
-            tweets.pop();
-        }
-    }
+function getTweets() {
+    var to = tweetData.position + 1,
+        from = Math.max(0, to - zoomLevels[tweetData.zoomLevel].displayCount);
+    socket.emit('getTweets',{ "from": from, "to": to });
+}
+
+function setZoom(level) {
+    tweetData.zoomLevel = Math.max(0, Math.min( zoomLevels.length - 1 , level ) );
+    getTweets();
 }
 
-// function scheduleTweets() {
-// 	var tl = $("#tweetlist"), tc = $("#tweetcontainer");
-// 	if (tweets.length != tl.children().length) {
-// 		console.log("Tweet count error");
-// 	}
-// 	if (tweets.length) {
-// 		while (tl.height() - (tc.scrollTop() + tc.height()) > 1000) {
-// 			tl.children().last().detach();
-// 			tweets.pop();
-// 		}
-// 		if (tl.height() - (tc.scrollTop() + tc.height()) < 120) {
-// 			getTweets({
-// 				before_id : tweets[tweets.length - 1].id,
-// 				limit : 5
-// 			});
-// 		}
-// 		getTweets({
-// 			after_id : tweets[0].id
-// 		});
-// 	} else {
-// 		getTweets({
-// 			limit: 15
-// 		});
-// 	}
-// }
-// 
-// function getTweets(params) {
-// 	$.getJSON("http://" + document.location.hostname + ":8888/?callback=?", params, function(data, a, b) {
-// 		if (data.tweets && data.tweets.length) {
-// 			var tl = $("#tweetlist"), tc = $("#tweetcontainer");
-// 			html = '';
-// 			for (var i in data.tweets) {
-// 				html += tweetToHtml(data.tweets[i]);
-// 			}
-// 			if (params.before_id) {
-// 				tl.append(html);
-// 				tweets = tweets.concat(data.tweets);
-// 			} else {
-// 				var pos = tc.scrollTop(), fixScroll = (pos > 80);
-// 				pos -= tl.height();
-// 				tl.prepend(html);
-// 				tweets = data.tweets.concat(tweets);
-// 				if (fixScroll) {
-// 					tc.scrollTop(tl.height() + pos);
-// 				}
-// 			}
-// 		}
-// 	});
-// }
-
 $(document).ready(function() {
-    tl = $("#tweetlist");
-    tc = $("#tweetcontainer");
     socket = io.connect('http://' + S_IO_HOST + ':' + S_IO_PORT );
+    socket.on('tweetSummary', function (data) {
+        if (tweetData.position == tweetData.tweetcount - 1) {
+            tweetData.position = data.tweetcount - 1;
+            getTweets();
+        }
+        tweetData.tweetcount = data.tweetcount;
+    });
     socket.on('tweets', function (data) {
-        tweets = data;
+        tweetData.tweetsOnDisplay = data;
         data.reverse();
         html = '';
         for (var i in data) {
             html += tweetToHtml(data[i]);
         }
         $("#tweetlist").html(html);
-        discardTweets();
-    });
-    socket.on('oldtweets', function (data) {
-        tweets = tweets.concat(data);
-        html = '';
-        for (var i = data.length - 1; i >= 0; i--) {
-            html += tweetToHtml(data[i]);
-        }
-        $("#tweetlist").append(html);
-        discardTweets();
-        waitoldtweets = true;
-    });
-    socket.on('newtweet', function (data) {
-        tweets.splice(0,0,data);
-        html = tweetToHtml(data);
-        var scrollpos = tc.scrollTop(),
-            fixScroll = (scrollpos > 80);
-        scrollpos -= tl.height();
-        $("#tweetlist").prepend(html);
-        if (fixScroll) {
-    		tc.scrollTop(tl.height() + scrollpos);
-		}
-        discardTweets();
-    });
-    $("#tweetcontainer").scroll(function() {
-        if ( waitoldtweets && tl.height() - (tc.scrollTop() + tc.height()) < 120 ) {
-            socket.emit('tweetsbefore', tweets[tweets.length-1].id );
-            waitoldtweets = false;
-        }
     });
 });
\ No newline at end of file
--- a/tweetcast/nodejs/server/node-direct.js	Fri Oct 14 17:36:34 2011 +0200
+++ b/tweetcast/nodejs/server/node-direct.js	Mon Oct 17 17:40:58 2011 +0200
@@ -1,10 +1,21 @@
+READ_OLD_TWEETS = true;
+RECORD_NEW_TWEETS = true;
+TWEET_FILE_PATH = 'tweets.txt';
+TRACKING_KEYWORD = 'Bieber';
+
 var fs = require('fs'),
     https = require('https'),
     io = require('socket.io')
-        .listen(8000)
-    keyword = "Bieber",
+        .listen(8000),
     tweets = [],
     tweet_ids = [],
+    date_struct = [],
+    date_levels = [
+        3600 * 1000,
+        15 * 60 * 1000,
+        5 * 60 * 1000,
+        60 * 1000
+    ],
     annkw = {
         'positive' : '++',
         'negative' : '--',
@@ -12,6 +23,67 @@
         'question' : '??'
     }
 
+function populateDateStruct(level, start) {
+    var end = start + date_levels[level],
+        struct = {
+            "level" : level,
+            "start" : new Date(start),
+            "end" : new Date(end)
+        };
+    if (level < date_levels.length - 1) {
+        struct.slices = [];
+        var newstart = start;
+        while (newstart < end) {
+            struct.slices.push(populateDateStruct(level + 1, newstart));
+            newstart += date_levels[level + 1];
+        }
+    } else {
+        struct.tweets = [];
+        struct.annotations = [];
+    }
+    return struct;
+}
+
+function insertIntoDateStruct(slices, tweet) {
+    var creadate = new Date(tweet.created_at);
+    for (var i in slices) {
+        if (creadate < slices[i].end) {
+            if (slices[i].slices) {
+                insertIntoDateStruct(slices[i].slices, tweet);
+            } else {
+                slices[i].tweets.push(tweet.pos);
+                for (var j in tweet.annotations) {
+                    if (slices[i].annotations[j]) {
+                        slices[i].annotations[j]++;
+                    } else {
+                        slices[i].annotations[j] = 1;
+                    }
+                    break;
+                }
+            }
+            break;
+        }
+    }
+}
+
+function addToList(tweet) {
+    if (tweet_ids.indexOf(tweet.id) != -1) {
+        console.log("Error: Tweet already in list");
+        return;
+    }
+    tweet.pos = tweets.length;
+    tweets.push(tweet);
+    tweet_ids.push(tweet.id);
+    var creadate = new Date(tweet.created_at);
+    if (!date_struct.length) {
+        date_struct = [ populateDateStruct(0, date_levels[0] * parseInt(creadate / date_levels[0])) ]
+    }
+    while (creadate > date_struct[date_struct.length - 1].end) {
+        date_struct.push( populateDateStruct(0, date_struct[date_struct.length - 1].end) );
+    }
+    insertIntoDateStruct(date_struct, tweet);
+}
+
 function textids(object) {
     for (var key in object) {
         if (key.substr(-2) == 'id') {
@@ -21,54 +93,84 @@
     }
 }
 
-var fd = fs.createWriteStream('tweets.txt');
-
-req = https.request({
-    host: "stream.twitter.com",
-    path: "/1/statuses/filter.json",
-    method: "POST",
-    headers: {
-        'Authorization': 'Basic cmFwaHY6N3czMzdMZkMyM2dF',
-        'Content-Type': 'application/x-www-form-urlencoded'
-    }
-}, function(res) {
-    console.log('STATUS: ' + res.statusCode);
-    console.log('HEADERS: ' + JSON.stringify(res.headers));
-    res.setEncoding('utf8');
-    res.on('data', function(chunk) {
-        newdata = chunk.split('\r\n');
-        try {
-            for (var i in newdata) {
-                if (newdata[i].length > 0) {
-                    tweet = JSON.parse(newdata[i]);
-                    annotations = [];
-                    for (var a in annkw) {
-                        if (tweet.text.indexOf(annkw[a]) != -1) {
-                            annotations.push(a);
-                        }
-                    }
-                    tweet.annotations = annotations;
-                    tweets.push(tweet);
-                    textids(tweet);
-                    tweet_ids.push(tweet.id_str);
-                    io.sockets.emit('newtweet', tweet);
+if (READ_OLD_TWEETS) {
+    try {
+        var filebuff = "";
+            readstream = fs.createReadStream(TWEET_FILE_PATH, { flags: 'r', encoding: 'utf-8' });
+        readstream.on("data", function(data) {
+            console.log("data");
+            filebuff += data;
+        });
+        readstream.on("end", function() {
+            console.log("end");
+            oldtweets = filebuff.split('\r\n');
+            var tweetscopied = 0;
+            for (var i in oldtweets) {
+                if (oldtweets[i].length > 0) {
+                    addToList(JSON.parse(oldtweets[i]));
+                    tweetscopied++;
                 }
             }
-            fd.write(chunk); 
-        }
-        catch(err) {
-            console.log(err);
+            console.log(tweetscopied, "tweets copied from", TWEET_FILE_PATH);
+        });
+    } catch(err) {
+        console.log(err);
+    }
+}
+
+if (RECORD_NEW_TWEETS) {
+    var writestream = null;
+    var req = https.request({
+        host: "stream.twitter.com",
+        path: "/1/statuses/filter.json",
+        method: "POST",
+        headers: {
+            'Authorization': 'Basic cmFwaHY6N3czMzdMZkMyM2dF',
+            'Content-Type': 'application/x-www-form-urlencoded'
         }
+    }, function(res) {
+        console.log('STATUS: ' + res.statusCode);
+        console.log('HEADERS: ' + JSON.stringify(res.headers));
+        res.setEncoding('utf8');
+        res.on('data', function(chunk) {
+            var newdata = chunk.split('\r\n');
+            try {
+                for (var i in newdata) {
+                    if (newdata[i].length > 0) {
+                        var tweet = JSON.parse(newdata[i]),
+                            annotations = [];
+                        for (var a in annkw) {
+                            if (tweet.text.indexOf(annkw[a]) != -1) {
+                                annotations.push(a);
+                            }
+                        }
+                        tweet.annotations = annotations;
+                        textids(tweet);
+                        addToList(tweet);
+                    }
+                }
+                if (!writestream) {
+                    writestream = fs.createWriteStream(TWEET_FILE_PATH, { flags: 'a', encoding: 'utf-8' });
+                }
+                writestream.write(chunk);
+                io.sockets.emit('tweetSummary', { tweetcount : tweets.length });
+                console.log("New tweets received. We now have", tweets.length, "tweets in memory");
+            }
+            catch(err) {
+                console.log(err);
+            }
+        });
     });
-});
+    
+    req.write('track=' + encodeURIComponent(TRACKING_KEYWORD));
+    req.end();
+}
 
-req.write('track=' + encodeURIComponent(keyword));
-req.end();
 io.set('log level', 0);
 io.sockets.on('connection', function(socket) {
-    socket.emit('tweets', tweets.slice(-10));
-    socket.on('tweetsbefore', function(data) {
-        tweetpos = tweet_ids.indexOf(data);
-        socket.emit('oldtweets', tweets.slice(0, tweetpos).slice(-10));
+    console.log("New connection", socket);
+    socket.emit('tweetSummary', { tweetcount : tweets.length });
+    socket.on('getTweets', function(data) {
+        socket.emit('tweets', tweets.slice(Math.max(0, data.from), data.to));
     });
 });
\ No newline at end of file