tweetcast/nodejs-bis/client/js/script.js
changeset 360 d49991fe4892
child 374 0c4acfa2aea1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tweetcast/nodejs-bis/client/js/script.js	Thu Nov 10 17:54:37 2011 +0100
@@ -0,0 +1,589 @@
+/**
+ * @author raph
+ */
+
+var socket,
+    tlPaper,
+    twCx = {
+        zoomLevel : 1,
+        followLast : true,
+        position : 0,
+        date_levels : [
+            15 * 60 * 1000,
+            5 * 60 * 1000,
+            60 * 1000,
+            15 * 1000
+        ],
+        timeLevel : 2,
+        deltaX : 30,
+        tlWidth : 98,
+        tlHeight : 450,
+        globalWords : {}
+        },
+    tlBuffer = '',
+    relHover = null,
+    wheelDelta = 0,
+    rx_word = /[^ \.&;,'"!\?@#\d\(\)\+\[\]\\\…\-«»:\/]{3,}/g;
+
+function arc(source, target) {
+    var x3 = .3 * target.y - .3 * source.y + .8 * source.x + .2 * target.x;
+    var y3 = .8 * source.y + .2 * target.y - .3 * target.x + .3 * source.x;
+    var x4 = .3 * target.y - .3 * source.y + .2 * source.x + .8 * target.x;
+    var y4 = .2 * source.y + .8 * target.y - .3 * target.x + .3 * source.x;
+    return "M" + source.x + " " + source.y + "C" + [x3, y3, x4, y4, target.x, target.y].join(" ");
+}
+
+function countWords(text, wordobj) {
+    var tab = text.match(rx_word);
+    for (var i in tab) {
+        var word = tab[i].toLowerCase();
+        if (wordobj[word]) {
+            wordobj[word]++;
+        } else {
+            wordobj[word] = 1;
+        }
+    }
+}
+
+function addTweet(tweet) {
+    function backRef(source_id, target_id, type) {
+        var target = tweetById(target_id);
+        if (target) {
+            var brobj = {
+                "referenced_by_id" : source_id,
+                "type" : type
+            }
+            if (target.backRefs) {
+                target.backRefs.push(brobj);
+            } else {
+                target.backRefs = [ brobj ]
+            }
+        }
+    }
+    
+    var tab = tweet.text.split(/\&\#|\;/),
+        txt = '';
+    for (i = 0; i < tab.length; i++) {
+        txt += (i % 2 && parseInt(tab[i]) != NaN) ? String.fromCharCode(tab[i]) : tab[i];
+    }
+    tweet.text = txt;
+    
+    twCx.tweets.push(tweet);
+    twCx.idIndex.push(tweet.id);
+
+    if (tweet.in_reply_to_status_id) {
+        backRef( tweet.id, tweet.in_reply_to_status_id, "reply" );
+    }
+    if (tweet.retweeted_status) {
+        backRef( tweet.id,  tweet.retweeted_status.id, "retweet" );
+    }
+    
+    countWords(tweet.text, twCx.globalWords);
+    
+    var creadate = new Date(tweet.created_at).valueOf();
+    if (!twCx.timeline.length) {
+        twCx.timeline = [ populateDateStruct(0, twCx.date_levels[0] * parseInt(creadate / twCx.date_levels[0])) ]
+    }
+    while (creadate > twCx.timeline[twCx.timeline.length - 1].end) {
+        twCx.timeline.push( populateDateStruct(0, twCx.timeline[twCx.timeline.length - 1].end) );
+    }
+    insertIntoDateStruct(twCx.timeline, tweet);
+}
+
+function getSliceContent(slice) {
+    if (slice.slices) {
+        var result = [];
+        for (var i in slice.slices) {
+            result = result.concat(getSliceContent(slice.slices[i]));
+        }
+    } else {
+        var result = slice.tweets;
+    }
+    return result;
+}
+
+function flattenDateStruct(slices, target_level) {
+    var current_level = slices[0].level,
+        result = [];
+    if (current_level < target_level) {
+        if (slices[0].slices) {
+            for (var i in slices) {
+                result = result.concat(flattenDateStruct(slices[i].slices, target_level));
+            }
+        }
+    }
+    else {
+        for (var i in slices) {
+            result.push({
+                "start" : slices[i].start,
+                "end" : slices[i].end,
+                "tweets" : getSliceContent(slices[i])
+            });
+        }
+    }
+    return result;
+}
+
+function trimFDS(slices) {
+    while (slices[0].tweets.length == 0) {
+        slices.splice(0,1);
+    }
+    while (slices[slices.length - 1].tweets.length == 0) {
+        slices.pop();
+    }
+    return slices;
+}
+
+function populateDateStruct(level, start) {
+    var end = start + twCx.date_levels[level],
+        struct = {
+            "level" : level,
+            "start" : start,
+            "end" : end
+        };
+    if (level < twCx.date_levels.length - 1) {
+        struct.slices = [];
+        var newstart = start;
+        while (newstart < end) {
+            struct.slices.push(populateDateStruct(level + 1, newstart));
+            newstart += twCx.date_levels[level + 1];
+        }
+    } else {
+        struct.tweets = [];
+    }
+    return struct;
+}
+
+function insertIntoDateStruct(slices, tweet) {
+    var creadate = new Date(tweet.created_at).valueOf();
+    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.id);
+            }
+            break;
+        }
+    }
+}
+
+function placeHolder(className) {
+    return '<li class="placeholder ' + className + '"></li>';
+}
+
+function tweetById(tweetid) {
+    var pos = twCx.idIndex.indexOf(tweetid);
+    return (pos == -1) ? false : twCx.tweets[pos];
+}
+
+function selectTweet(tweetid) {
+    var pos = twCx.idIndex.indexOf(tweetid);
+    if (pos != -1) {
+        twCx.position = pos;
+        twCx.followLast = (twCx.position == twCx.tweets.length - 1);
+        updateDisplay()
+    }
+}
+
+function tweetToHtml(tweet, className, elName) {
+    if (!tweet) {
+        return placeHolder(className);
+    }
+    var el = (elName ? elName : 'li');
+    var html = '<' + el + ' class="tweet ' + className + '" id="tweet_' + tweet.id + '"';
+    if (className != 'full') {
+        html += ' onclick="selectTweet(\'' + tweet.id + '\'); return false;" onmouseover="rolloverTweet(\'' + tweet.id + '\');"';
+    } else {
+        html += ' onmouseover="$(\'#hovertweet\').hide();"';
+    }
+    if (twCx.followLast && className == 'full' && el == 'li') {
+        html += ' style="display: none"';
+    }
+    html += '>';
+    if (tweet.annotations.length) {
+        html += '<div class="annotations">';
+        for (var i in tweet.annotations) {
+            html += '<div class="annotation" style="width:' + (100/tweet.annotations.length) + '%; background:' + annotations[tweet.annotations[i]].colors[(className == 'icons' ? 'timeline' : 'tweet')] + '"></div>';
+        }
+        html += '</div>';
+    }
+    html += '<div class="twmain">';
+    a_user = '<a href="http://twitter.com/' + tweet.user.screen_name + '" var target="_blank" title="' + tweet.user.name + '">';
+    html += '<div class="around_img">' + a_user + '<img class="profile_image" src="' + tweet.user.profile_image_url + '" /></a>';
+    if (className == 'full') {
+        html += '<p class="created_at">' + new Date(tweet.created_at).toLocaleTimeString() + '</p>';
+    }
+    html += '</div>';
+    if (className != 'icons') {
+        lastend = 0;
+        var txt = '',
+            entities = [];
+        for (var i in tweet.entities.hashtags) {
+            entities.push({
+                "start" : tweet.entities.hashtags[i].indices[0],
+                "end" : tweet.entities.hashtags[i].indices[1],
+                "html" : '<a href="http://twitter.com/search?q=%23' + tweet.entities.hashtags[i].text + '" target="_blank">#' + tweet.entities.hashtags[i].text + '</a>'
+            });
+        }
+        for (var i in tweet.entities.urls) {
+            entities.push({
+                "start" : tweet.entities.urls[i].indices[0],
+                "end" : tweet.entities.urls[i].indices[1],
+                "html" : '<a href="' + tweet.entities.urls[i].expanded_url + '" target="_blank">' + tweet.entities.urls[i].expanded_url + '</a>'
+            });
+        }
+        for (var i in tweet.entities.user_mentions) {
+            entities.push({
+                "start" : tweet.entities.user_mentions[i].indices[0],
+                "end" : tweet.entities.user_mentions[i].indices[1],
+                "html" : '<a href="http://twitter.com/' + tweet.entities.user_mentions[i].screen_name + '" target="_blank" title="' + tweet.entities.user_mentions[i].name + '">@' + tweet.entities.user_mentions[i].screen_name + '</a>'
+            });
+        }
+        entities.sort(function(a, b) { return a.start - b.start });
+        for (var i in entities) {
+            txt += tweet.text.substring(lastend, entities[i].start) + entities[i].html;
+            lastend = entities[i].end;
+        }
+        txt += tweet.text.substring(lastend);
+        html += '<p class="tweet_text"><b>' + a_user + '@' + tweet.user.screen_name + '</b></a>: ' + txt + '</p>';
+    }
+    html += '</div></' + el + '>';
+    return html;
+}
+
+function tlIdFromPos(x, y) {
+    if (x < twCx.deltaX) {
+        return null;
+    }
+    var ligne = Math.floor(( twCx.tlHeight - y ) / twCx.scaleY),
+        colonne = Math.floor(( x - twCx.deltaX ) / twCx.scaleX ),
+        l = 0;
+    if (colonne >= twCx.tlOnDisplay[ligne].totalTweets) {
+        return null;
+    }
+    for (var i in twCx.tlOnDisplay[ligne].displayData) {
+        var nl = l + twCx.tlOnDisplay[ligne].displayData[i].length;
+        if (colonne < nl) {
+            return {
+                "id" : twCx.tlOnDisplay[ligne].displayData[i][colonne - l],
+                "annotation" : i
+            }
+        }
+        l = nl;
+    }
+}
+
+function tlPosTweet(tweet, annotation) {
+    var x,
+        y,
+        dt = new Date(tweet.created_at).valueOf(),
+        ann = ( annotation ? annotation : ( tweet.annotations.length ? tweet.annotations[0] : 'default' ) );
+    for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
+        if (twCx.tlOnDisplay[i].end > dt) {
+            y = twCx.tlHeight - (i + .5) * twCx.scaleY;
+            var l = 0;
+            for (var j in twCx.tlOnDisplay[i].displayData) {
+                if (j == ann) {
+                    var p = twCx.tlOnDisplay[i].displayData[j].indexOf(tweet.id);
+                    if (p != -1) {
+                        x = twCx.deltaX + twCx.scaleX * ( p + l + .5 );
+                    }
+                    break;
+                }
+                l += twCx.tlOnDisplay[i].displayData[j].length;
+            }
+            break;
+        }
+    }
+    return ( x && y ? { "x" : x, "y" : y } : null);
+}
+
+function rolloverTweet(tweetid, annotation) {
+    var t = tweetById(tweetid),
+        p = tlPosTweet(t, annotation);
+    if (t && p) {
+        var ptl = $("#timeline").offset();
+        $("#hovercontent").html(tweetToHtml(t, 'full', 'div'));
+        $("#hovertweet").css({
+            "left" : parseInt(ptl.left + p.x) + "px",
+            "top" : parseInt(ptl.top + p.y),
+            "display" : "block"});
+        if (relHover) {
+            relHover.remove();
+        }
+        relHover = drawTweetPos(p, '#ffffff')
+    }
+}
+
+function drawTweetPos(pos, color) {
+    var rel = tlPaper.rect(pos.x - .5 * twCx.scaleX, pos.y - .5 * twCx.scaleY, twCx.scaleX, twCx.scaleY);
+    rel.attr({ "stroke" : color });
+    return rel;
+}
+
+function updateDisplay() {
+    var p = twCx.position,
+        l = twCx.tweets.length,
+        lines = 0,
+        html = '',
+        tweetsOnDisplay = [],
+        localWords = {};
+        
+    function pushTweet(tp, className) {
+        if (tp < l && tp >= 0) {
+            html += tweetToHtml(twCx.tweets[tp], className)
+            tweetsOnDisplay.push(tp);
+            countWords(twCx.tweets[tp].text, localWords);
+        } else {
+            html += placeHolder(className);
+        }
+    }
+    
+    if (l > p + 18) {
+        lines++;
+        for (var i = p + 31; i >= p + 18; i--) {
+            pushTweet(i, 'icons');
+        }
+    }
+    if (l > p + 4) {
+        lines++;
+        for (var i = p + 17; i >= p + 4; i--) {
+            pushTweet(i, 'icons');
+        }
+    }
+    for (var k = 3; k >= 1; k--) {
+        if (l > p + k) {
+            lines++;
+            pushTweet(p + k, 'half');
+        }
+    }
+    pushTweet(p, 'full');
+    var n = p - 1;
+    for (var i = 0; i < Math.min(6, Math.max(3, 6 - lines)); i++) {
+        if (n < 0) {
+            break;
+        }
+        pushTweet(n, 'half');
+        n--;
+    }
+    for (var i = 0; i < 14 * Math.min(4, Math.max(2, 7 - lines)); i++) {
+        if (n < 0) {
+            break;
+        }
+        pushTweet(n, 'icons');
+        n--;
+    }
+    if (html != tlBuffer) {
+        $("#tweetlist").html(html);
+        $(".tweet.full").fadeIn();
+        tlBuffer = html;
+    }
+    
+    for (var j in localWords) {
+        if (localWords[j] < 2) delete localWords[j];
+    }
+    var tab = [];
+    for (var j in localWords) {
+        tab.push({
+            "word": j,
+            "freq" : localWords[j] / Math.log(twCx.globalWords[j])
+        });
+    }
+    tab.sort( function(a,b){ return b.freq - a.freq }).splice(10);
+    $("#motscles").html(tab.map(function(t) { return t.word }).join(", "))
+    
+    twCx.tlOnDisplay = trimFDS(flattenDateStruct(twCx.timeline, twCx.timeLevel));
+    twCx.scaleY = twCx.tlHeight / twCx.tlOnDisplay.length;
+    var maxTweets = 0,
+        startTl = 0,
+        endTl = 0,
+        startTw = new Date(twCx.tweets[tweetsOnDisplay[tweetsOnDisplay.length - 1]].created_at).valueOf(),
+        endTw = new Date(twCx.tweets[tweetsOnDisplay[0]].created_at).valueOf();
+    for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
+        if (startTw >= twCx.tlOnDisplay[i].start && startTw < twCx.tlOnDisplay[i].end) {
+            startTl = i;
+        }
+        if (endTw >= twCx.tlOnDisplay[i].start && endTw < twCx.tlOnDisplay[i].end) {
+            endTl = i;
+        }
+        var displayData = {};
+        for (var j in annotations) {
+            displayData[j] = [];
+        }
+        for (var j in twCx.tlOnDisplay[i].tweets) {
+            var tweetid = twCx.tlOnDisplay[i].tweets[j],
+                tweet = tweetById(tweetid);
+            if (tweet) {
+                if (tweet.annotations.length) {
+                    for (var k in tweet.annotations) {
+                        displayData[tweet.annotations[k]].push(tweetid);
+                    }
+                } else {
+                    displayData['default'].push(tweetid);
+                }
+            }
+        }
+        var nbT = 0;
+        for (var j in displayData) {
+            nbT += displayData[j].length;
+        }
+        maxTweets = Math.max(maxTweets, nbT);
+        twCx.tlOnDisplay[i].displayData = displayData;
+        twCx.tlOnDisplay[i].totalTweets = nbT;
+    }
+    twCx.scaleX = ( twCx.tlWidth - twCx.deltaX ) / maxTweets;
+    tlPaper.clear();
+    relHover = null;
+    
+    // dessin de la date de début
+    
+    tlPaper.text(2, twCx.tlHeight - 7, new Date(twCx.tlOnDisplay[0].start).toTimeString().substr(0,5))
+        .attr({ "text-anchor" : "start", "font-size": "9px" });
+    
+    // dessin de la date de fin
+    
+    tlPaper.text(2, 7, new Date(twCx.tlOnDisplay[twCx.tlOnDisplay.length - 1].end).toTimeString().substr(0,5))
+        .attr({ "text-anchor" : "start", "font-size": "9px" });
+    
+    for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
+        var n = 0,
+            posY = twCx.tlHeight - ( i + 1 ) * twCx.scaleY;
+        for (var j in twCx.tlOnDisplay[i].displayData) {
+            var l = twCx.tlOnDisplay[i].displayData[j].length;
+            if (l > 0) {
+                tlPaper.rect( twCx.deltaX + n * twCx.scaleX, posY, l * twCx.scaleX, twCx.scaleY )
+                    .attr({"stroke" : "none", "fill" : annotations[j].colors.timeline });
+                n += l;
+            }
+        }
+        
+        // Si on est à une demi-heure, on trace un axe secondaire + heure
+        
+        if (i < twCx.tlOnDisplay.length - 1 && !(new Date(twCx.tlOnDisplay[i].end).valueOf() % 1800000)) {
+            tlPaper.path("M0 "+posY+"L" + twCx.tlWidth +" "+posY).attr({"stroke":"#ccc"});
+            tlPaper.text(2, posY, new Date(twCx.tlOnDisplay[i].end).toTimeString().substr(0,5)).attr({ "text-anchor" : "start", "font-size": "9px" });
+        }
+    }
+    
+    // Dessin de la correspondance liste-timeline
+    
+    var startY = twCx.tlHeight - startTl * twCx.scaleY,
+        endY = twCx.tlHeight - ( endTl + 1 ) * twCx.scaleY;
+    tlPaper.path("M0 " + twCx.tlHeight + "L" + twCx.deltaX + " " + startY + "L" + twCx.tlWidth + " " + startY + "L" + twCx.tlWidth + " " + endY + "L" + twCx.deltaX + " " + endY + "L0 0" )
+        .attr({ "stroke" : "none", "fill" : "#000080", "opacity" : .1 });
+    
+    // dessin du tweet courant
+        
+    var posp = tlPosTweet(twCx.tweets[p]);
+    if (posp) {
+        
+        drawTweetPos(posp, "#ffff00");
+        
+        // dessin des liens entre tweets
+        
+        function tweetAndArc(a, b, aorb) {
+            if (a && b) {
+                drawTweetPos(aorb ? a : b, "#00c000");
+                tlPaper.path(arc(a,b))
+                    .attr({ "stroke" : "#e000e0", "stroke-width" : 1.5 });
+            }
+        }
+        
+        if (twCx.tweets[p].retweeted_status) {
+            var t = tweetById(twCx.tweets[p].retweeted_status.id);
+            if (t) {
+                tweetAndArc(posp, tlPosTweet(t));
+            }
+        }
+        
+        if (twCx.tweets[p].in_reply_to_status_id) {
+            var t = tweetById(twCx.tweets[p].in_reply_to_status_id);
+            if (t) {
+                tweetAndArc(posp, tlPosTweet(t));
+            }
+        }
+        
+        if (twCx.tweets[p].backRefs) {
+            for (var i in twCx.tweets[p].backRefs) {
+                var t = tweetById(twCx.tweets[p].backRefs[i].referenced_by_id);
+                if (t) {
+                tweetAndArc(tlPosTweet(t), posp, true);
+                }
+            }
+        }
+        
+    }
+    
+}
+
+$(document).ready(function() {
+    tlPaper = Raphael("timeline", twCx.tlWidth, twCx.tlHeight);
+    socket = io.connect('http://' + document.location.hostname );
+    socket.on("initial_data", function(data) {
+        twCx.timeline = [];
+        twCx.idIndex = [];
+        twCx.tweets = [];
+        for (var i in data.tweets) {
+            addTweet(data.tweets[i]);
+        }
+        if (twCx.followLast) {
+            twCx.position = twCx.tweets.length - 1;
+        }
+        updateDisplay();
+    });
+    socket.on("update", function(data) {
+        for (var i in data.new_tweets) {
+            addTweet(data.new_tweets[i]);
+        }
+        if (twCx.followLast) {
+            twCx.position = twCx.tweets.length - 1;
+        }
+        updateDisplay();
+    });
+    
+    
+    $("#tweetlist").mousewheel(function(e, d) {
+        wheelDelta += d;
+        if (Math.abs(wheelDelta) >= 1) {
+            twCx.position = Math.min( twCx.tweets.length - 1, Math.max(0, parseInt(wheelDelta) + twCx.position ) );
+            twCx.followLast = (twCx.position == twCx.tweets.length - 1);
+            updateDisplay();
+            wheelDelta = 0;
+        }
+        return false;
+    });
+    $("#timeline").mousewheel(function(e, d) {
+        wheelDelta += d;
+        if (Math.abs(wheelDelta) >= 1) {
+            if (wheelDelta > 0) {
+                tl = Math.min(twCx.date_levels.length - 1, twCx.timeLevel + 1);
+            } else {
+                tl = Math.max(0, twCx.timeLevel - 1);
+            }
+            if (tl != twCx.timeLevel) {
+                twCx.timeLevel = tl;
+                updateDisplay();
+            }
+            wheelDelta = 0;
+        }
+        return false;
+    });
+    $("#timeline, #tweetlist").mouseout(function() {
+        $("#hovertweet").hide();
+    });
+    $("#timeline").mousemove(function(evt) {
+        var twid = tlIdFromPos(evt.offsetX, evt.offsetY);
+        if (twid) {
+            rolloverTweet(twid.id, twid.annotation);
+        } else {
+            $("#hovertweet").hide();
+        }
+    });
+    $("#timeline").click(function(evt) {
+        var twid = tlIdFromPos(evt.offsetX, evt.offsetY);
+        if (twid) {
+            selectTweet(twid.id);
+        }
+    });
+});
\ No newline at end of file