tweetcast/nodejs-bis/client/js/script.js
changeset 404 89968844eb7d
parent 403 dd1686ae5506
child 405 6626b728b142
equal deleted inserted replaced
403:dd1686ae5506 404:89968844eb7d
     1 /**
       
     2  * @author raph
       
     3  */
       
     4 
       
     5 var socket,
       
     6     tlPaper,
       
     7     twCx = {
       
     8         followLast : true,
       
     9         position : "0",
       
    10         date_levels : [
       
    11             3600 * 1000,
       
    12             15 * 60 * 1000,
       
    13             5 * 60 * 1000,
       
    14             60 * 1000
       
    15         ],
       
    16         timeLevel : 1,
       
    17         deltaX : 40,
       
    18         tlWidth : 150,
       
    19         tlHeight : 480,
       
    20         globalWords : {},
       
    21         refMouse : { x : 0, y : 0},
       
    22         refPosTl : { x : 0, y : 0},
       
    23         tlMouseMoved : false,
       
    24         tlMouseClicked : false,
       
    25         filtre : null
       
    26         },
       
    27     tlBuffer = '',
       
    28     relHover = [],
       
    29     wheelDelta = 0,
       
    30     scrollEnabled = false,
       
    31     scrollExtent = 8000 - 480,
       
    32     lastScrollPos = 0,
       
    33     rx_url = /https?:\/\/[0-9a-zA-Z\.%\/-_]+/g,
       
    34     rx_word = /[^ \.&;,'"!\?\d\(\)\+\[\]\\\…\-«»:\/]{3,}/g,
       
    35     stop_list = [ 'and', 'avec', 'aux', 'car', 'comme', 'dans', 'donc', 'des', 'elle', 'est', 'être', 'eux', 'ils', 'les', 'leur', 'leurs', 'mes', 'mon', 'tes', 'ton', 'notre', 'nos', 'nous', 'ont', 'pas', 'que', 'qui', 'sont', 'the', 'une', 'votre', 'vos', 'vous' ];
       
    36 
       
    37 function getColor(annotation, lum) {
       
    38     return Raphael.hsl2rgb(annotations[annotation].colors.h, annotations[annotation].colors.s, lum);
       
    39 }
       
    40 
       
    41 function tweetPopup(url) {
       
    42     var popW = 550,
       
    43         popH = 350,
       
    44         scrW = screen.width,
       
    45         scrH = screen.height,
       
    46         posX = Math.round((scrW/2)-(popW/2)),
       
    47         posY = (scrH > popH ? Math.round((scrH/2)-(popH/2)) : 0);
       
    48     window.open(url,
       
    49         '',
       
    50         'left=' + posX + ',top=' + posY + ',width=' + popW + ',height=' + popH + ',personalbar=0,toolbar=0,scrollbars=1,resizable=1');
       
    51 }
       
    52 
       
    53 function arc(source, target) {
       
    54     var x3 = .3 * target.y - .3 * source.y + .8 * source.x + .2 * target.x;
       
    55     var y3 = .8 * source.y + .2 * target.y - .3 * target.x + .3 * source.x;
       
    56     var x4 = .3 * target.y - .3 * source.y + .2 * source.x + .8 * target.x;
       
    57     var y4 = .2 * source.y + .8 * target.y - .3 * target.x + .3 * source.x;
       
    58     return "M" + source.x + " " + source.y + "C" + [x3, y3, x4, y4, target.x, target.y].join(" ");
       
    59 }
       
    60 
       
    61 function addTweet(tweet) {
       
    62     function backRef(source_id, target_id, type) {
       
    63         var target = tweetById(target_id);
       
    64         if (target) {
       
    65             var brobj = {
       
    66                 "referenced_by_id" : source_id,
       
    67                 "type" : type
       
    68             }
       
    69             if (target.backRefs) {
       
    70                 target.backRefs.push(brobj);
       
    71             } else {
       
    72                 target.backRefs = [ brobj ]
       
    73             }
       
    74         }
       
    75     }
       
    76     
       
    77     if (twCx.idIndex.indexOf(tweet.id) != -1) {
       
    78         return;
       
    79     }
       
    80     
       
    81     var txt_date = tweet.created_at;
       
    82     if (navigator.userAgent.search(/MSIE/) != -1) {
       
    83         txt_date = txt_date.replace(/( \+)/, ' UTC$1');
       
    84     }
       
    85     tweet.date_value = Date.parse(txt_date);
       
    86     
       
    87     var tab = tweet.text.match(/\&\#[0-9]+\;/g);
       
    88     for (var i in tab) {
       
    89         var n = parseInt(tab[i].substr(2));
       
    90         if (n != NaN) {
       
    91             tweet.text = tweet.text.replace(tab[i], String.fromCharCode(n));
       
    92         }
       
    93     }
       
    94     
       
    95     var ann = [];
       
    96     for (var j in annotations) {
       
    97         if (j != "default") {
       
    98             for (var k in annotations[j].keywords) {
       
    99                 if (tweet.text.search(annotations[j].keywords[k]) != -1) {
       
   100                     ann.push(j);
       
   101                     break;
       
   102                 }
       
   103             }
       
   104         }
       
   105     }
       
   106     tweet.annotations = ann;
       
   107 
       
   108     if (tweet.in_reply_to_status_id) {
       
   109         backRef( tweet.id, tweet.in_reply_to_status_id, "reply" );
       
   110     }
       
   111     if (tweet.retweeted_status) {
       
   112         backRef( tweet.id,  tweet.retweeted_status.id, "retweet" );
       
   113     }
       
   114     
       
   115     var localWords = []
       
   116     
       
   117     var tab = tweet.text.replace(rx_url,'').match(rx_word);
       
   118     for (var i in tab) {
       
   119         var word = tab[i].toLowerCase();
       
   120         if (stop_list.indexOf(word) == -1 && tracking_keywords.indexOf(word) == -1) {
       
   121             if (twCx.globalWords[word]) {
       
   122                 twCx.globalWords[word]++;
       
   123             } else {
       
   124                 twCx.globalWords[word] = 1;
       
   125             }
       
   126             localWords.push(word);
       
   127         }
       
   128     }
       
   129     
       
   130     tweet.words = localWords;
       
   131     
       
   132     var p = twCx.idIndex.length;
       
   133     while (p && tweet.id < twCx.idIndex[p-1]) {
       
   134         p--;
       
   135     }
       
   136     twCx.tweets.splice(p, 0, tweet);
       
   137     twCx.idIndex.splice(p, 0, tweet.id);
       
   138     
       
   139     if (!twCx.timeline.length) {
       
   140         twCx.timeline = [ populateDateStruct(0, twCx.date_levels[0] * parseInt(tweet.date_value / twCx.date_levels[0])) ]
       
   141     }
       
   142     while (tweet.date_value > twCx.timeline[twCx.timeline.length - 1].end) {
       
   143         twCx.timeline.push( populateDateStruct(0, twCx.timeline[twCx.timeline.length - 1].end) );
       
   144     }
       
   145     
       
   146     insertIntoDateStruct(twCx.timeline, tweet);
       
   147 }
       
   148 
       
   149 function getSliceContent(slice) {
       
   150     if (slice.slices) {
       
   151         var result = [];
       
   152         for (var i in slice.slices) {
       
   153             result = result.concat(getSliceContent(slice.slices[i]));
       
   154         }
       
   155     } else {
       
   156         var result = slice.tweets;
       
   157     }
       
   158     return result;
       
   159 }
       
   160 
       
   161 function flattenDateStruct(slices, target_level) {
       
   162     var current_level = slices[0].level,
       
   163         result = [];
       
   164     if (current_level < target_level) {
       
   165         if (slices[0].slices) {
       
   166             for (var i in slices) {
       
   167                 result = result.concat(flattenDateStruct(slices[i].slices, target_level));
       
   168             }
       
   169         }
       
   170     }
       
   171     else {
       
   172         for (var i in slices) {
       
   173             result.push({
       
   174                 "start" : slices[i].start,
       
   175                 "end" : slices[i].end,
       
   176                 "tweets" : getSliceContent(slices[i])
       
   177             });
       
   178         }
       
   179     }
       
   180     return result;
       
   181 }
       
   182 
       
   183 function trimFDS() {
       
   184     var slices = flattenDateStruct(twCx.timeline, twCx.timeLevel);
       
   185     while (slices[0].tweets.length == 0) {
       
   186         slices.splice(0,1);
       
   187     }
       
   188     while (slices[slices.length - 1].tweets.length == 0) {
       
   189         slices.pop();
       
   190     }    
       
   191     var centralTweet = ( twCx.centralTweet ? twCx.centralTweet : twCx.tweets[twCx.tweets.length - 1] ),
       
   192         delta = 30 * twCx.date_levels[twCx.timeLevel],
       
   193         centre = Math.min(slices[slices.length - 1].end - delta , Math.max(slices[0].start + delta, centralTweet.date_value)),
       
   194         min = centre - delta,
       
   195         max = centre + delta;
       
   196     while (slices[0].start < min) {
       
   197         slices.splice(0,1);
       
   198     }
       
   199     while (slices[slices.length - 1].end > max) {
       
   200         slices.pop();
       
   201     }
       
   202     return slices;
       
   203 }
       
   204 
       
   205 function populateDateStruct(level, start) {
       
   206     var end = start + twCx.date_levels[level],
       
   207         struct = {
       
   208             "level" : level,
       
   209             "start" : start,
       
   210             "end" : end
       
   211         };
       
   212     if (level < twCx.date_levels.length - 1) {
       
   213         struct.slices = [];
       
   214         var newstart = start;
       
   215         while (newstart < end) {
       
   216             struct.slices.push(populateDateStruct(level + 1, newstart));
       
   217             newstart += twCx.date_levels[level + 1];
       
   218         }
       
   219     } else {
       
   220         struct.tweets = [];
       
   221     }
       
   222     return struct;
       
   223 }
       
   224 
       
   225 function insertIntoDateStruct(slices, tweet) {
       
   226     var creadate = tweet.date_value;
       
   227     for (var i in slices) {
       
   228         if (creadate < slices[i].end) {
       
   229             if (slices[i].slices) {
       
   230                 insertIntoDateStruct(slices[i].slices, tweet);
       
   231             } else {
       
   232                 slices[i].tweets.push(tweet.id);
       
   233             }
       
   234             break;
       
   235         }
       
   236     }
       
   237 }
       
   238 
       
   239 function placeHolder(className) {
       
   240     return '<li class="placeholder ' + className + '"></li>';
       
   241 }
       
   242 
       
   243 function tweetById(tweetid) {
       
   244     var pos = twCx.idIndex.indexOf(tweetid);
       
   245     return (pos == -1) ? false : twCx.tweets[pos];
       
   246 }
       
   247 
       
   248 function selectTweet(tweetid) {
       
   249     twCx.position = tweetid;
       
   250     twCx.followLast = (twCx.position == twCx.idIndex[twCx.tweets.length - 1]);
       
   251     updateDisplay();
       
   252 }
       
   253 
       
   254 function goToPos(nPos) {
       
   255     twCx.position = twCx.currentIdIndex[Math.min( twCx.currentIdIndex.length - 1, Math.max(0, nPos ) )];
       
   256     twCx.followLast = (!twCx.filtre && nPos == twCx.tweets.length - 1);
       
   257     updateDisplay();
       
   258 }
       
   259 
       
   260 function movePos(delta) {
       
   261     goToPos( delta + twCx.currentIdIndex.indexOf(twCx.position) );
       
   262 }
       
   263 
       
   264 function tweetToHtml(tweet, className, elName) {
       
   265     
       
   266     function highlight(texte) {
       
   267         return ( twCx.filtre ? texte.replace(twCx.filtre, '<span class="highlight">$1</span>' ) : texte );
       
   268     }
       
   269     
       
   270     if (!tweet) {
       
   271         return placeHolder(className);
       
   272     }
       
   273     var el = (elName ? elName : 'li');
       
   274     var html = '<' + el + ' class="tweet ' + className + '" id="tweet_' + tweet.id + '"';
       
   275     if (className != 'full') {
       
   276         html += ' onclick="selectTweet(\'' + tweet.id + '\'); return false;"';
       
   277     }
       
   278     html += ' onmouseover="rolloverTweet(\'' + tweet.id + "', " + ( className == 'icons' ) + ');"';
       
   279     if (twCx.followLast && className == 'full' && el == 'li') {
       
   280         html += ' style="display: none"';
       
   281     }
       
   282     html += '>';
       
   283     if (tweet.annotations.length) {
       
   284         html += '<div class="annotations">';
       
   285         for (var i in tweet.annotations) {
       
   286             html += '<div class="annotation" style="width:' + (100/tweet.annotations.length) + '%; background:' + getColor(tweet.annotations[i], (className == 'icons' ? .4 : .85)).hex + '"></div>';
       
   287         }
       
   288         html += '</div>';
       
   289     }
       
   290     html += '<div class="twmain">';
       
   291     var a_user = '<a href="http://twitter.com/' + tweet.user.screen_name + '"  onclick="filtrerTexte(\'@' + tweet.user.screen_name + '\'); return false;" target="_blank">';
       
   292     html += '<div class="around_img">' + a_user + '<img class="profile_image" src="' + tweet.user.profile_image_url + '" /></a>';
       
   293     if (className == 'full') {
       
   294         html += '<p class="created_at">' + new Date(tweet.date_value).toTimeString().substr(0,8) + '</a></p>';
       
   295     }
       
   296     html += '</div>';
       
   297     if (className != 'icons') {
       
   298         lastend = 0;
       
   299         var txt = '',
       
   300             entities = [];
       
   301         for (var i in tweet.entities.hashtags) {
       
   302             entities.push({
       
   303                 "start" : tweet.entities.hashtags[i].indices[0],
       
   304                 "end" : tweet.entities.hashtags[i].indices[1],
       
   305                 "link" : '<a href="http://twitter.com/search?q=%23' + tweet.entities.hashtags[i].text + '" onclick="filtrerTexte(\'#' + tweet.entities.hashtags[i].text + '\'); return false;" target="_blank">',
       
   306                 "text" : '#' + tweet.entities.hashtags[i].text
       
   307             });
       
   308         }
       
   309         for (var i in tweet.entities.urls) {
       
   310             var linkurl = ( tweet.entities.urls[i].expanded_url ? tweet.entities.urls[i].expanded_url : tweet.entities.urls[i].url ),
       
   311                 dispurl = linkurl.replace(/https?:\/\//,'');
       
   312             if (linkurl.search(/https?:\/\//) == -1) {
       
   313                 linkurl = 'http://' + linkurl;
       
   314             }
       
   315             entities.push({
       
   316                 "start" : tweet.entities.urls[i].indices[0],
       
   317                 "end" : tweet.entities.urls[i].indices[1],
       
   318                 "link" : '<a href="' + linkurl  + '" target="_blank">',
       
   319                 "text" : dispurl
       
   320             });
       
   321         }
       
   322         for (var i in tweet.entities.user_mentions) {
       
   323             entities.push({
       
   324                 "start" : tweet.entities.user_mentions[i].indices[0],
       
   325                 "end" : tweet.entities.user_mentions[i].indices[1],
       
   326                 "link" : '<a href="http://twitter.com/' + tweet.entities.user_mentions[i].screen_name + '" onclick="filtrerTexte(\'@' + tweet.entities.user_mentions[i].screen_name + '\'); return false;" target="_blank">',
       
   327                 "text" : '@' + tweet.entities.user_mentions[i].screen_name
       
   328             });
       
   329         }
       
   330         entities.sort(function(a, b) { return a.start - b.start });
       
   331         for (var i in entities) {
       
   332             txt += highlight( tweet.text.substring(lastend, entities[i].start) ) + entities[i].link + highlight( entities[i].text ) + '</a>';
       
   333             lastend = entities[i].end;
       
   334         }
       
   335         txt += highlight( tweet.text.substring(lastend) );
       
   336         html += '<p class="tweet_text"><b>' + a_user + highlight('@' + tweet.user.screen_name) + '</a>' + ( className == 'full' ? ' (' + tweet.user.name + ')</b><br />' : '</b> : ') + txt + '</p>';
       
   337         if (className == 'full' && el == 'li') {
       
   338             html += '<div class="tweet_actions"><a href="http://twitter.com/' + tweet.user.screen_name + '/status/' + tweet.id + '" onclick="tweetPopup(this.href); return false;" target="_blank">afficher tweet</a> - ';
       
   339             html += '<a href="http://twitter.com/intent/tweet?in_reply_to=' + tweet.id + '" onclick="tweetPopup(this.href); return false;" target="_blank">répondre</a> · ';
       
   340             html += '<a href="http://twitter.com/intent/retweet?tweet_id=' + tweet.id + '" onclick="tweetPopup(this.href); return false;" target="_blank">retweeter</a> · ';
       
   341             html += '<a href="http://twitter.com/intent/favorite?tweet_id=' + tweet.id + '" onclick="tweetPopup(this.href); return false;" target="_blank">favori</a></div>';
       
   342         }
       
   343     }
       
   344     html += '</div></' + el + '>';
       
   345     return html;
       
   346 }
       
   347 
       
   348 function tlIdFromPos(x, y, outside) {
       
   349     if (!twCx.tlOnDisplay) {
       
   350         return;
       
   351     }
       
   352     var ligne = Math.min( twCx.tlOnDisplay.length - 1, Math.max( 0, Math.floor(( twCx.tlHeight - y ) / twCx.scaleY) ) ),
       
   353         colonne = Math.floor(( x - twCx.deltaX ) / twCx.scaleX ),
       
   354         l = 0;
       
   355     if (colonne >= twCx.tlOnDisplay[ligne].totalTweets || colonne < 0 ) {
       
   356         if (outside) {
       
   357             colonne = Math.min( twCx.tlOnDisplay[ligne].totalTweets - 1, Math.max( 0, colonne ));
       
   358         } else {
       
   359             return null;
       
   360         }
       
   361     }
       
   362     for (var i in twCx.tlOnDisplay[ligne].displayData) {
       
   363         var nl = l + twCx.tlOnDisplay[ligne].displayData[i].length;
       
   364         if (colonne < nl) {
       
   365             return {
       
   366                 "id" : twCx.tlOnDisplay[ligne].displayData[i][colonne - l],
       
   367                 "annotation" : i
       
   368             }
       
   369         }
       
   370         l = nl;
       
   371     }
       
   372 }
       
   373 
       
   374 function tlPosTweet(tweet, annotation) {
       
   375     if (!twCx.tweets) {
       
   376         return;
       
   377     }
       
   378     var x,
       
   379         y,
       
   380         dt = tweet.date_value,
       
   381         ann = ( annotation ? annotation : ( tweet.annotations.length ? tweet.annotations[0] : 'default' ) );
       
   382     for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
       
   383         if (twCx.tlOnDisplay[i].end > dt) {
       
   384             y = twCx.tlHeight - (i + .5) * twCx.scaleY;
       
   385             var l = 0;
       
   386             for (var j in twCx.tlOnDisplay[i].displayData) {
       
   387                 if (j == ann) {
       
   388                     var p = twCx.tlOnDisplay[i].displayData[j].indexOf(tweet.id);
       
   389                     if (p != -1) {
       
   390                         x = twCx.deltaX + twCx.scaleX * ( p + l + .5 );
       
   391                     }
       
   392                     break;
       
   393                 }
       
   394                 l += twCx.tlOnDisplay[i].displayData[j].length;
       
   395             }
       
   396             break;
       
   397         }
       
   398     }
       
   399     return ( x && y ? { "x" : x, "y" : y } : null);
       
   400 }
       
   401 
       
   402 function rolloverTweet(tweetid, showPopup, annotation) {
       
   403     var t = tweetById(tweetid);
       
   404     if (!t) {
       
   405         return;
       
   406     }
       
   407     var p = tlPosTweet(t, annotation);
       
   408     if (!p) {
       
   409         return;
       
   410     }
       
   411     var ptl = $("#timeline").offset();
       
   412     if (showPopup) {
       
   413         $("#hovercontent").html(tweetToHtml(t, 'full', 'div'));
       
   414         $("#hovertweet").css({
       
   415             "left" : parseInt(ptl.left + p.x) + "px",
       
   416             "top" : parseInt(ptl.top + p.y),
       
   417             "display" : "block"});
       
   418     } else {
       
   419         $("#hovertweet").hide();
       
   420     }
       
   421     for (var i in relHover) {
       
   422         relHover[i].remove();
       
   423     }
       
   424     relHover = drawTweetArcs(t, p, '#303030');
       
   425     relHover.push(drawTweetPos(p, '#ffffff'));
       
   426 }
       
   427 
       
   428 function drawTweetPos(pos, color) {
       
   429     var rel = tlPaper.rect(pos.x - .5 * twCx.scaleX, pos.y - .5 * twCx.scaleY, twCx.scaleX, twCx.scaleY);
       
   430     rel.attr({ "stroke" : color, "fill" : color, "fill-opacity" : .25 });
       
   431     return rel;
       
   432 }
       
   433 
       
   434 function drawTweetArcs(tweet, pos, color) {
       
   435     
       
   436     var res = [];
       
   437 
       
   438     function tweetAndArc(a, b, aorb) {
       
   439         if (a && b) {
       
   440             res.push(drawTweetPos(aorb ? a : b, color));
       
   441             var aa = tlPaper.path(arc(a,b))
       
   442                 .attr({ "stroke" : color, "stroke-width" : 1.5, "stroke-opacity" : .8 });
       
   443             res.push(aa);
       
   444         }
       
   445     }
       
   446     
       
   447     if (tweet.retweeted_status) {
       
   448         var t = tweetById(tweet.retweeted_status.id);
       
   449         if (t) {
       
   450             tweetAndArc(pos, tlPosTweet(t));
       
   451         }
       
   452     }
       
   453     
       
   454     if (tweet.in_reply_to_status_id) {
       
   455         var t = tweetById(tweet.in_reply_to_status_id);
       
   456         if (t) {
       
   457             tweetAndArc(pos, tlPosTweet(t));
       
   458         }
       
   459     }
       
   460     
       
   461     if (tweet.backRefs) {
       
   462         for (var i in tweet.backRefs) {
       
   463             var t = tweetById(tweet.backRefs[i].referenced_by_id);
       
   464             if (t) {
       
   465             tweetAndArc(tlPosTweet(t), pos, true);
       
   466             }
       
   467         }
       
   468     }
       
   469     
       
   470     return res;
       
   471 }
       
   472 
       
   473 function updateDisplay() {
       
   474     if (!twCx.tweets) {
       
   475         return;
       
   476     }
       
   477     if (twCx.filtre) {
       
   478         var tweets = twCx.tweets.filter(function(tweet) {
       
   479             var mention = '@' + tweet.user.screen_name;
       
   480             return ( tweet.text.search(twCx.filtre) != -1 ) || ( mention.search(twCx.filtre) != -1 );
       
   481         });
       
   482         $("#inp_q").val(twCx.filtreTexte + ' (' + tweets.length + ' tweets)');
       
   483         if (tweets.length) {
       
   484             var idIndex = tweets.map(function(tweet) {
       
   485                 return tweet.id;
       
   486             });
       
   487             var p = idIndex.indexOf(twCx.position);
       
   488             if (p == -1) {
       
   489                 for (p = idIndex.length - 1; p > 0 && idIndex[p] > twCx.position; p--) {
       
   490                 }
       
   491             }
       
   492             twCx.position = idIndex[p];
       
   493             twCx.currentIdIndex = idIndex;
       
   494         }
       
   495         
       
   496     } else {
       
   497         twCx.currentIdIndex = twCx.idIndex;
       
   498         var tweets = twCx.tweets;
       
   499         var p = twCx.idIndex.indexOf(twCx.position);
       
   500         if (p == -1) {
       
   501             p = (twCx.followLast ? twCx.idIndex.length - 1 : 0);
       
   502         }
       
   503     }
       
   504 
       
   505     var l = tweets.length,
       
   506         lines = 0,
       
   507         ppy = 0,
       
   508         html = '',
       
   509         tweetsOnDisplay = [],
       
   510         localWords = {};
       
   511     
       
   512     function pushTweet(tp, className) {
       
   513         if (tp < l && tp >= 0) {
       
   514             html += tweetToHtml(tweets[tp], className)
       
   515             tweetsOnDisplay.push(tp);
       
   516             for (var i in tweets[tp].words) {
       
   517                 var w = tweets[tp].words[i];
       
   518                 if (localWords[w]) {
       
   519                     localWords[w].freq++
       
   520                 } else {
       
   521                     localWords[w] = {
       
   522                         "freq" : 1,
       
   523                         "annotations" : {}
       
   524                     }
       
   525                     for (var j in annotations) {
       
   526                         if (j != 'default') {
       
   527                             localWords[w].annotations[j] = 0;
       
   528                         }
       
   529                     }
       
   530                 }
       
   531                 for (var j in tweets[tp].annotations) {
       
   532                     localWords[w].annotations[tweets[tp].annotations[j]]++;
       
   533                 }
       
   534             }
       
   535         } else {
       
   536             html += placeHolder(className);
       
   537         }
       
   538     }
       
   539     
       
   540     if (l) {
       
   541     
       
   542         lastScrollPos = Math.floor( scrollExtent * ( 1 - ( p / l ) ) );
       
   543         $("#scrollcont").scrollTop(lastScrollPos);
       
   544         
       
   545         if (l > p + 18) {
       
   546             lines++;
       
   547             ppy += 20;
       
   548             for (var i = p + 31; i >= p + 18; i--) {
       
   549                 pushTweet(i, 'icons');
       
   550             }
       
   551         }
       
   552         if (l > p + 4) {
       
   553             lines++;
       
   554             ppy += 20;
       
   555             for (var i = p + 17; i >= p + 4; i--) {
       
   556                 pushTweet(i, 'icons');
       
   557             }
       
   558         }
       
   559         for (var k = 3; k >= 1; k--) {
       
   560             if (l > p + k) {
       
   561                 ppy += 47;
       
   562                 lines++;
       
   563                 pushTweet(p + k, 'half');
       
   564             }
       
   565         }
       
   566         pushTweet(p, 'full');
       
   567         var n = p - 1;
       
   568         for (var i = 0; i < Math.min(6, Math.max(3, 6 - lines)); i++) {
       
   569             if (n < 0) {
       
   570                 break;
       
   571             }
       
   572             pushTweet(n, 'half');
       
   573             n--;
       
   574         }
       
   575         for (var i = 0; i < 14 * Math.min(4, Math.max(2, 7 - lines)); i++) {
       
   576             if (n < 0) {
       
   577                 break;
       
   578             }
       
   579             pushTweet(n, 'icons');
       
   580             n--;
       
   581         }
       
   582         if (html != tlBuffer) {
       
   583             $("#tweetlist").html(html);
       
   584             $(".tweet.full").fadeIn();
       
   585             tlBuffer = html;
       
   586         }
       
   587         
       
   588         /* Recherche des mots pertinents correspondant à la sélection */
       
   589         
       
   590         var tab = _(localWords).map(function(v, k) {
       
   591             return {
       
   592                 "word": k,
       
   593                 "freq" : v.freq,
       
   594                 "annotations" : v.annotations,
       
   595                 "score" : v.freq / Math.log( 2 + twCx.globalWords[j] )
       
   596             };
       
   597         }).filter(function(v) {
       
   598             return v.freq > 1;
       
   599         });
       
   600         
       
   601         if (tab.length) {
       
   602             
       
   603             tab = _(tab).sortBy( function(a) { return ( - a.score ) }).slice(0,20);
       
   604             var minfreq = _(tab).min( function(a) { return a.freq} ).freq,
       
   605                 maxfreq = Math.max(minfreq + .1, _(tab).max( function(a) { return a.freq} ).freq),
       
   606                 echfreq = 8 / Math.sqrt( maxfreq - minfreq ),
       
   607                 html = '';
       
   608             for (var j in tab) {
       
   609                 var maxann = 0,
       
   610                     ann = "default";
       
   611                 for (var k in tab[j].annotations) {
       
   612                     if (tab[j].annotations[k] == maxann) {
       
   613                         ann = "default";
       
   614                     }
       
   615                     if (tab[j].annotations[k] > maxann) {
       
   616                         ann = k;
       
   617                         maxann = tab[j].annotations[k];
       
   618                     }
       
   619                 }
       
   620                 if (ann == "default") {
       
   621                     var coul = '';
       
   622                 } else {
       
   623                     var c = getColor(ann, .6),
       
   624                         coul = "background: rgba(" + [ Math.floor(c.r), Math.floor(c.g), Math.floor(c.b), ( tab[j].annotations[ann] / tab[j].freq )].join(',') + ")";
       
   625                 }
       
   626                 var fontsize = Math.floor( ( 12 + Math.sqrt( tab[j].freq - minfreq ) * echfreq ) );
       
   627                 html += '<span style="line-height: ' + (8 + fontsize) + 'px; font-size: ' + fontsize + 'px;' + coul + '" onclick="filtrerTexte(\'' + tab[j].word.replace(/('|")/g, '\\$1') + '\')">' + tab[j].word + '</span> ';
       
   628             }
       
   629             $("#motscles").html(html);
       
   630         } else {
       
   631             $("#motscles").html('');
       
   632         }
       
   633         twCx.centralTweet = tweets[p];
       
   634     } else {
       
   635         $("#tweetlist").html('');
       
   636         tlBuffer = '';
       
   637         $("#motscles").html('');
       
   638     }
       
   639     
       
   640     twCx.tlOnDisplay = trimFDS();
       
   641     twCx.scaleY = twCx.tlHeight / twCx.tlOnDisplay.length;
       
   642     var maxTweets = 0,
       
   643         startTl = 0,
       
   644         endTl = twCx.tlOnDisplay.length - 1;
       
   645     if (l) {
       
   646         var startTw = tweets[tweetsOnDisplay[tweetsOnDisplay.length - 1]].date_value,
       
   647             endTw = tweets[tweetsOnDisplay[0]].date_value;
       
   648     }
       
   649     for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
       
   650         if (l) {
       
   651             if (startTw >= twCx.tlOnDisplay[i].start && startTw < twCx.tlOnDisplay[i].end) {
       
   652                 startTl = i;
       
   653             }
       
   654             if (endTw >= twCx.tlOnDisplay[i].start && endTw < twCx.tlOnDisplay[i].end) {
       
   655                 endTl = i;
       
   656             }
       
   657         }
       
   658         var displayData = {};
       
   659         for (var j in annotations) {
       
   660             displayData[j] = [];
       
   661         }
       
   662         for (var j in twCx.tlOnDisplay[i].tweets) {
       
   663             var tweetid = twCx.tlOnDisplay[i].tweets[j],
       
   664                 tweet = tweetById(tweetid);
       
   665             if (tweet) {
       
   666                 if (tweet.annotations.length) {
       
   667                     for (var k in tweet.annotations) {
       
   668                         displayData[tweet.annotations[k]].push(tweetid);
       
   669                     }
       
   670                 } else {
       
   671                     displayData['default'].push(tweetid);
       
   672                 }
       
   673             }
       
   674         }
       
   675         var nbT = 0;
       
   676         for (var j in displayData) {
       
   677             nbT += displayData[j].length;
       
   678         }
       
   679         maxTweets = Math.max(maxTweets, nbT);
       
   680         twCx.tlOnDisplay[i].displayData = displayData;
       
   681         twCx.tlOnDisplay[i].totalTweets = nbT;
       
   682     }
       
   683     twCx.scaleX = ( twCx.tlWidth - twCx.deltaX ) / maxTweets;
       
   684     tlPaper.clear();
       
   685     relHover = null;
       
   686     
       
   687     // Dessin de la correspondance liste-timeline
       
   688     if (l) {
       
   689         var startY = twCx.tlHeight - startTl * twCx.scaleY,
       
   690             endY = twCx.tlHeight - ( endTl + 1 ) * twCx.scaleY,
       
   691             path = "M0 " + twCx.tlHeight + "C" + .7*twCx.deltaX + " " + twCx.tlHeight + " " + .3*twCx.deltaX + " " + startY + " " + twCx.deltaX + " " + startY + "L" + twCx.tlWidth + " " + startY + "L" + twCx.tlWidth + " " + endY + "L" + twCx.deltaX + " " + endY + "C" + .3*twCx.deltaX + " " + endY + " " + .7*twCx.deltaX + " 0 0 0";
       
   692         tlPaper.path( path ).attr({ "stroke" : "none", "fill" : "#000080", "opacity" : .2 });
       
   693     }   
       
   694     // dessin de la date de début
       
   695     
       
   696     tlPaper.text(twCx.deltaX / 2, twCx.tlHeight - 7, new Date(twCx.tlOnDisplay[0].start).toTimeString().substr(0,5))
       
   697         .attr({ "text-anchor" : "middle", "font-size": "9px" });
       
   698     
       
   699     // dessin de la date de fin
       
   700     
       
   701     tlPaper.text(twCx.deltaX / 2, 7, new Date(twCx.tlOnDisplay[twCx.tlOnDisplay.length - 1].end).toTimeString().substr(0,5))
       
   702         .attr({ "text-anchor" : "middle", "font-size": "9px" });
       
   703     
       
   704     for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
       
   705         var n = 0,
       
   706             posY = twCx.tlHeight - ( i + 1 ) * twCx.scaleY;
       
   707         for (var j in twCx.tlOnDisplay[i].displayData) {
       
   708             var ll = twCx.tlOnDisplay[i].displayData[j].length;
       
   709             if (ll > 0) {
       
   710                 tlPaper.rect( twCx.deltaX + n * twCx.scaleX, posY, ll * twCx.scaleX, twCx.scaleY )
       
   711                     .attr({"stroke" : "none", "fill" : getColor(j, .4).hex });
       
   712                 n += ll;
       
   713             }
       
   714         }
       
   715         
       
   716         // Si on est à une demi-heure, on trace un axe secondaire + heure
       
   717         
       
   718         if (i < twCx.tlOnDisplay.length - 1 && !(twCx.tlOnDisplay[i].end % 1800000)) {
       
   719             tlPaper.path("M0 "+posY+"L" + twCx.tlWidth +" "+posY).attr({"stroke":"#ccc"});
       
   720             tlPaper.text(twCx.deltaX / 2, posY, new Date(twCx.tlOnDisplay[i].end).toTimeString().substr(0,5)).attr({ "text-anchor" : "middle", "font-size": "9px" });
       
   721         }
       
   722     }
       
   723     
       
   724     // dessin du tweet courant
       
   725     
       
   726     if (l) {
       
   727         
       
   728         if (twCx.filtre) {
       
   729             for (var i = 0; i < tweets.length; i++) {
       
   730                 if (i != p) {
       
   731                     var pos = tlPosTweet(tweets[i]);
       
   732                     if (pos) {
       
   733                         drawTweetPos(pos, "#ffccff");
       
   734                     }
       
   735                 }
       
   736             }
       
   737             
       
   738         }
       
   739         
       
   740         var posp = tlPosTweet(tweets[p]);
       
   741         if (posp) {
       
   742             
       
   743             drawTweetPos(posp, "#ffff00");
       
   744             var yy = posp.y - .5 * twCx.scaleY,
       
   745                 path = "M0 " + ppy + "C" + ( .7 * twCx.deltaX ) + " " + ppy + " " + ( .2 * twCx.deltaX ) + " " + yy + " " + ( twCx.deltaX ) + " " + yy + "L" + ( posp.x - .5 * twCx.scaleX ) + " " + yy;
       
   746             yy = posp.y + .5 * twCx.scaleY;
       
   747             ppy += 117;
       
   748             path += "L" + ( posp.x - .5 * twCx.scaleX ) + " " + yy + "L" + twCx.deltaX + " " + yy + "C"  + ( .2 * twCx.deltaX ) + " " + yy + " " + ( .7 * twCx.deltaX ) + " " + ppy + " 0 " + ppy;
       
   749             tlPaper.path( path ).attr({"stroke":"#ffff00", "fill" : "#ffff00", "fill-opacity" : .15});
       
   750             
       
   751             drawTweetArcs(tweets[p], posp, '#800080');
       
   752         }
       
   753     }
       
   754 }
       
   755 
       
   756 function filtrerAnnotation(annotation) {
       
   757     if (annotations[annotation]) {
       
   758         effectuerFiltrage(annotations[annotation].display_name,
       
   759             new RegExp( "(" + annotations[annotation].keywords.map(function(a) { return a.source }).join("|") + ")", "gi" ) );
       
   760     } else {
       
   761         effectuerFiltrage('', null)
       
   762     }
       
   763 }
       
   764 
       
   765 function filtrerTexte(valeur) {
       
   766     effectuerFiltrage( valeur, valeur ? new RegExp("(" + valeur.replace(/(\W)/g, '\\$1') + ")" ,'gi') : null );
       
   767 }
       
   768 
       
   769 function effectuerFiltrage(filtreTexte, tabRegexp) {
       
   770     $("#recherche_annot").slideUp();
       
   771     $("#inp_q").val(filtreTexte).attr("class","rechercheCourante");
       
   772     twCx.filtreTexte = filtreTexte;
       
   773     twCx.filtre = tabRegexp;
       
   774     twCx.followLast = !tabRegexp && (twCx.position == twCx.idIndex[twCx.idIndex.length - 1]);
       
   775     updateDisplay();
       
   776 }
       
   777 
       
   778 function clicTl(evt) {
       
   779     var o = $("#timeline").offset();
       
   780     if (twCx.tlMouseClicked && twCx.tlMouseMoved) {
       
   781         var twid = tlIdFromPos(evt.pageX - o.left + twCx.refPosTl.x - twCx.refMouse.x, evt.pageY - o.top + twCx.refPosTl.y - twCx.refMouse.y, true);
       
   782         if (twid) {
       
   783             selectTweet(twid.id);
       
   784         }
       
   785     } else {
       
   786         var twid = tlIdFromPos(evt.pageX - o.left, evt.pageY - o.top, twCx.tlMouseClicked);
       
   787         if (twCx.tlMouseMoved && !twCx.tlMouseClicked) { 
       
   788             if (twid) {
       
   789                 rolloverTweet(twid.id, true, twid.annotation);
       
   790             } else {
       
   791                 $("#hovertweet").hide();
       
   792             }
       
   793         }
       
   794         if (twCx.tlMouseClicked && !twCx.tlMouseMoved) {
       
   795             if (twid) {
       
   796                 selectTweet(twid.id);
       
   797             }
       
   798         }
       
   799     }
       
   800 }
       
   801 
       
   802 function loadTweets(tweets) {
       
   803     twCx.timeline = [];
       
   804     twCx.idIndex = [];
       
   805     twCx.tweets = [];
       
   806     for (var i in tweets) {
       
   807         addTweet(tweets[i]);
       
   808     }
       
   809     if (twCx.followLast) {
       
   810         twCx.position = twCx.idIndex[twCx.tweets.length - 1];
       
   811     }
       
   812     updateDisplay();
       
   813 }
       
   814 
       
   815 function focusOutRecherche() {
       
   816     $("#recherche_annot").slideUp();
       
   817     var inpq = $("#inp_q"),
       
   818         val = inpq.val();
       
   819     if (val == '' || val == twCx.filtreTexte) {
       
   820         if (twCx.filtre) {
       
   821             inpq.attr("class", "rechercheCourante").val(twCx.filtreTexte);
       
   822         } else {
       
   823             inpq.attr("class", "greyed").val("Rechercher");
       
   824         }
       
   825     }
       
   826 }
       
   827 
       
   828 function chaineTimeZoom() {
       
   829     var chaine = "",
       
   830         t = twCx.date_levels[twCx.timeLevel],
       
   831         h = 3600*1000,
       
   832         m = 60*1000,
       
   833         s = 1000,
       
   834         heures = Math.floor(t/h);
       
   835     if (heures) { chaine += heures + ' h. ' };
       
   836     t -= (heures * h);
       
   837     var minutes = Math.floor(t/m);
       
   838     if (minutes) { chaine += minutes + ' min. ' };
       
   839     t -= (minutes * m);
       
   840     if (t) { chaine += Math.floor(t/s) + ' sec.' }
       
   841     $("#time_scale").html(chaine);
       
   842     $("#time_zoomout").attr("class",(twCx.timeLevel == 0 ? "inactive" : ""));
       
   843     $("#time_zoomin").attr("class",(twCx.timeLevel == twCx.date_levels.length - 1 ? "inactive" : ""));
       
   844 }
       
   845 
       
   846 $(document).ready(function() {
       
   847     tlPaper = Raphael("timeline", twCx.tlWidth, twCx.tlHeight);
       
   848     
       
   849     if (typeof STANDALONE_APP == "undefined" || !STANDALONE_APP) {
       
   850         console.log("Loading from socket.io");
       
   851         socket = io.connect('http://' + document.location.hostname );
       
   852         socket.on("initial_data", function(data) {
       
   853             loadTweets(data.tweets)
       
   854         });
       
   855         socket.on("update", function(data) {
       
   856             if (!twCx.tweets) {
       
   857                 return;
       
   858             }
       
   859             for (var i in data.new_tweets) {
       
   860                 addTweet(data.new_tweets[i]);
       
   861             }
       
   862             if (twCx.followLast) {
       
   863                 twCx.position = twCx.idIndex[twCx.tweets.length - 1];
       
   864             }
       
   865             updateDisplay();
       
   866         });
       
   867     } else {
       
   868         $.getScript("tweetdata.js");
       
   869     }
       
   870     
       
   871     var html = '';
       
   872     for (var j in annotations) {
       
   873         if (j != "default") {
       
   874             html += '<a href="#" style="background: ' + getColor(j, .7).hex + ';" onclick=filtrerAnnotation(\'' + j + '\'); return false;">' + annotations[j].display_name + '</a> '
       
   875         }
       
   876     }
       
   877     $("#rech_list_annot").html(html);
       
   878     
       
   879     chaineTimeZoom();
       
   880     
       
   881     $("#tweetlist").mousewheel(function(e, d) {
       
   882         wheelDelta += d;
       
   883         if (Math.abs(wheelDelta) >= 1) {
       
   884             movePos( parseInt(wheelDelta) );
       
   885             wheelDelta = 0;
       
   886         }
       
   887         return false;
       
   888     });
       
   889     $("#timeline").mousewheel(function(e, d) {
       
   890         wheelDelta += d;
       
   891         if (Math.abs(wheelDelta) >= 1) {
       
   892             if (wheelDelta > 0) {
       
   893                 tl = Math.min(twCx.date_levels.length - 1, twCx.timeLevel + 1);
       
   894             } else {
       
   895                 tl = Math.max(0, twCx.timeLevel - 1);
       
   896             }
       
   897             if (tl != twCx.timeLevel) {
       
   898                 twCx.timeLevel = tl;
       
   899                 chaineTimeZoom();
       
   900                 updateDisplay();
       
   901             }
       
   902             wheelDelta = 0;
       
   903         }
       
   904         return false;
       
   905     });
       
   906     $("#time_zoomin").click(function() {
       
   907         if (twCx.timeLevel < twCx.date_levels.length - 1) {
       
   908             twCx.timeLevel++;
       
   909             chaineTimeZoom();
       
   910             updateDisplay();
       
   911         }
       
   912     });
       
   913     $("#time_zoomout").click(function() {
       
   914         if (twCx.timeLevel > 0) {
       
   915             twCx.timeLevel--;
       
   916             chaineTimeZoom();
       
   917             updateDisplay();
       
   918         }
       
   919     });
       
   920     $("#timeline, #tweetlist").mouseout(function() {
       
   921         twCx.tlMouseClicked = false;
       
   922         twCx.tlMouseMoved = false;
       
   923         $("#hovertweet").hide();
       
   924     });
       
   925     $("#timeline").mousemove(function(evt) {
       
   926         twCx.tlMouseMoved = true;
       
   927         clicTl(evt);
       
   928     });
       
   929     $("#timeline").mousedown(function(evt) {
       
   930         twCx.tlMouseClicked = true;
       
   931         twCx.tlMouseMoved = false;
       
   932         var o = $(this).offset();
       
   933         twCx.refMouse = { x : evt.pageX - o.left, y : evt.pageY - o.top };
       
   934         twCx.refPosTl = tlPosTweet(tweetById(twCx.position)) || twCx.refMouse;
       
   935     });
       
   936     $("#timeline").mouseup(function(evt) {
       
   937         clicTl(evt);
       
   938         twCx.tlMouseClicked = false;
       
   939         twCx.tlMouseMoved = false;
       
   940     });
       
   941     $("#inp_q").focus(function() {
       
   942         $("#recherche_annot").slideDown();
       
   943         $(this).val($(this).val().replace(/ \(.+\)$/, ''))
       
   944         if ($(this).hasClass("greyed")) {
       
   945             $(this).val("");
       
   946         }
       
   947         $(this).attr("class","");
       
   948     });
       
   949     $("#inp_q").focusout(function() {
       
   950         focusOutRecherche();
       
   951     });
       
   952     $("#inp_reset").click(function() {
       
   953         $("#inp_q").val('');
       
   954         if (twCx.filtre) {
       
   955             twCx.filtre = null;
       
   956             updateDisplay();
       
   957         }
       
   958         twCx.filtreTexte = '';
       
   959         focusOutRecherche();
       
   960         return false;
       
   961     })
       
   962     $("#recherche").submit(function(evt) {
       
   963         evt.preventDefault();
       
   964         if (!$("#inp_q").hasClass("greyed")) {
       
   965             var valeur = $("#inp_q").val();
       
   966             filtrerTexte(valeur);
       
   967         }
       
   968         return false;
       
   969     });
       
   970     
       
   971     setInterval(function() {
       
   972         var sc = $("#scrollcont");
       
   973         if (sc.scrollTop() != lastScrollPos && twCx.tweets) {
       
   974             var p = Math.floor( twCx.currentIdIndex.length * ( 1 - sc.scrollTop() / scrollExtent ) );
       
   975             goToPos(p);
       
   976         }
       
   977         
       
   978     }, 100)
       
   979 });
       
   980