Skip to content
This repository
Browse code

eyeo archived version, lots of fixes and improvements

  • Loading branch information...
commit 597be372f8ee1ad9833aa4750847ac890811c16f 1 parent 61e0e79
Moritz Stefaner authored
41,566 bin/data/eyeotweets.txt
41,566 additions, 0 deletions not shown
3,598 bin/data/eyeotweets_400.txt
3,598 additions, 0 deletions not shown
3,998 bin/data/eyeotweets_500.txt
3,998 additions, 0 deletions not shown
58 bin/index_eyeo.html
... ... @@ -0,0 +1,58 @@
  1 +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  2 +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
  3 +
  4 + <head>
  5 +
  6 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  7 + <title>revisit #eyeo . moritz.stefaner.eu</title>
  8 +
  9 + <link rel="stylesheet" type="text/css" href="css/style.css" media="screen" />
  10 + <script type="text/javascript" src="lib/jquery.js"></script>
  11 + <script type="text/javascript" src="lib/swfobject/swfobject.js"></script>
  12 + <script src="lib/swfaddress/swfaddress.js" type="text/javascript" charset="utf-8"></script>
  13 +
  14 + <script type="text/javascript">
  15 + var paramNames = ["searchterms", "appTitle", "maxItems", "showOnlyToday"];
  16 +
  17 + function embedFlash(){
  18 +
  19 + var flashvars = {
  20 +
  21 + };
  22 +
  23 + var URL="revisit_eyeo.swf";
  24 + var flashID = "flash";
  25 + var width = "100%";
  26 + var height = "100%";
  27 + var flashVersion = "10.0.0";
  28 + var expressInstallURL = "lib/swfobject/expressInstall.swf";
  29 +
  30 + var params = {
  31 + allowfullscreen: "true",
  32 + salign: "tl"
  33 + };
  34 +
  35 + var attributes = {
  36 + };
  37 +
  38 + swfobject.embedSWF(URL, flashID, width, height, flashVersion, expressInstallURL, flashvars, params, attributes);
  39 + }
  40 +
  41 + embedFlash();
  42 +
  43 +
  44 + </script>
  45 +
  46 + </head>
  47 +
  48 +<body style="width:100%; height:100%; margin:0; padding:0; background:#000000;">
  49 + <div id="flashcontainer" style="width:100%; height:100%; margin:0; padding:0; background:#000000;">
  50 + <div id="flash" style="width:100%; height:100%; margin:0; padding:0; background:#000000;">
  51 + <a href="http://www.adobe.com/go/getflashplayer">
  52 + <img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" />
  53 + </a>
  54 + </div>
  55 + </div>
  56 +</body>
  57 +
  58 +</html>
BIN  bin/revisit.swf
Binary file not shown
BIN  bin/revisit_eyeo.swf
Binary file not shown
BIN  fla/revisit_eyeo.fla
Binary file not shown
6 lib/com/swfjunkie/tweetr/utils/TweetUtil.as
@@ -302,9 +302,9 @@ package com.swfjunkie.tweetr.utils {
302 302 var seconds : Number;
303 303 var timezone : Number;
304 304
305   - if (created_at.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/g).length == 1) {
  305 + if (created_at.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/g).length == 1 || created_at.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/g).length == 1) {
306 306 // match 2008-12-07T16:24:24Z
307   - tp = created_at.split(/[-T:Z]/g);
  307 + tp = created_at.split(/[-T:Z]|\ /g);
308 308 year = tp[0];
309 309 month = tp[1];
310 310 date = tp[2];
@@ -350,7 +350,7 @@ package com.swfjunkie.tweetr.utils {
350 350 }
351 351
352 352 time.setUTCFullYear(year, month, date);
353   - time.setUTCHours(hour, minutes, seconds);
  353 + time.setUTCHours(hour-5, minutes, seconds);
354 354 return time;
355 355 }
356 356
2  src/eu/stefaner/revisit/App.as
... ... @@ -1 +1 @@
1   -/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import com.swfjunkie.tweetr.Tweetr; import com.swfjunkie.tweetr.data.objects.SearchResultData; import com.swfjunkie.tweetr.events.TweetEvent; import flare.util.Strings; import flash.display.SimpleButton; import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageDisplayState; import flash.events.Event; import flash.events.FullScreenEvent; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.geom.Rectangle; import flash.text.TextField; import flash.ui.Keyboard; import flash.utils.Timer; import gs.TweenFilterLite; import nl.demonsters.debugger.MonsterDebugger; public class App extends Sprite { public static var monsterDebugger : MonsterDebugger; private static const LIVEUPDATES : String = "LIVEUPDATES"; private static const ARCHIVE : String = "ARCHIVE"; private static const NEW_TWEETS : String = "NEW_TWEETS"; private static const OVERVIEW : String = "OVERVIEW"; private static const RANDOM : String = "RANDOM"; private var searchTweetr : Tweetr; private var loadTimer : Timer; private var searchRunning : Boolean; private var advanceTimer : Timer; private var visualization : TweetVisualization; private var latestId : Number = 0; public var search_tf : TextField; public var settings : Class = Settings; public var status_tf : TextField; private var loadCounter : Number = 1; public var newTweets : Array = []; private var advanceTimerTime : Number = new Date().time; private var searchMode : String = "ARCHIVE"; public var numTweets_tf : TextField; public var numRetweets_tf : TextField; public var numReplies_tf : TextField; public var numNew_tf : TextField; public var appTitle_tf : TextField; public var footer : Sprite; public var newIcon : Sprite; public var icons : Sprite; private var displayMode : String; public var fullscreen_btn : SimpleButton; public function App() { Settings.overwriteDefaults(loaderInfo.parameters); App.monsterDebugger = new MonsterDebugger(this); initStage(); initKeyControl(); initVis(); initTweetrs(); loadData(); initTimers(); initIconsAndTitle(); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function initIconsAndTitle() : void { displayStatus("starting up"); appTitle_tf.text = Settings.appTitle; status_tf.alpha = 0; footer["description_tf"].text = Strings.format(footer["description_tf"].text, Settings.maxItems, Settings.searchterms.join(" OR ")); icons.x = Math.min(fullscreen_btn.x - icons.width - 10, appTitle_tf.x + appTitle_tf.textWidth + 50); appTitle_tf.width = icons.x - appTitle_tf.x; numTweets_tf = icons["numTweets_tf"]; numNew_tf = icons["numNew_tf"]; numReplies_tf = icons["numReplies_tf"]; numRetweets_tf = icons["numRetweets_tf"]; newIcon = icons["newIcon"]; icons.alpha = newIcon.alpha = 0; fullscreen_btn.addEventListener(MouseEvent.CLICK, onFullScreenClick); stage.addEventListener(FullScreenEvent.FULL_SCREEN, onToggleFullScreen); } private function onToggleFullScreen(event : Event) : void { fullscreen_btn.visible = !(stage.displayState == StageDisplayState.FULL_SCREEN); if (fullscreen_btn.visible) { icons.x = Math.min(fullscreen_btn.x - icons.width - 10, appTitle_tf.x + appTitle_tf.textWidth + 50); appTitle_tf.width = icons.x - appTitle_tf.x; } else { icons.x = Math.min(stage.stageWidth - icons.width - 10, appTitle_tf.x + appTitle_tf.textWidth + 50); appTitle_tf.width = icons.x - appTitle_tf.x; } } private function onFullScreenClick(event : MouseEvent = null) : void { try { stage.displayState = StageDisplayState.FULL_SCREEN; fullscreen_btn.visible = false; } catch (e : Error) { fullscreen_btn.visible = true; } } public static function log(target : Object, object : *, color : uint = 0x111111, functions : Boolean = false, depth : int = 4) { MonsterDebugger.trace(target, object, color, functions, depth); } private function displayStatus(string : String) : void { if (Settings.showStatus) { status_tf.text = string; status_tf.alpha = 1; TweenFilterLite.to(status_tf, 3, {alpha:0}); } App.log(this, string); } private function initStage() : void { stage.align = StageAlign.TOP_LEFT; } private function initKeyControl() : void { stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressedDown); } private function onEnterFrame(event : Event) : void { renderAdvanceTimer(); } private function renderAdvanceTimer() : void { graphics.clear(); graphics.beginFill(0x444444); graphics.drawRect(20, 1, 92 * (new Date().time - advanceTimerTime) / advanceTimer.delay, 3); } private function initVis() : void { visualization = new TweetVisualization(this, new Rectangle(0, 0, stage.stageWidth - 80, stage.stageHeight - 240)); visualization.x = 15; visualization.y = 100; addChildAt(visualization, 0); } private function keyPressedDown(event : KeyboardEvent) : void { switch (event.keyCode) { case Keyboard.LEFT : visualization.displayPrevious(); resetAdvanceTimer(); break; case Keyboard.RIGHT : visualization.displayNext(); resetAdvanceTimer(); break; case Keyboard.UP : visualization.advance(5); resetAdvanceTimer(); break; case Keyboard.DOWN : visualization.advance(-5); resetAdvanceTimer(); break; } } public function onNodeClick(ts : TweetSprite) : void { visualization.displayTweet(ts); resetAdvanceTimer(); } /* * TIMERS */ private function initTimers() : void { loadTimer = new Timer(Settings.loadTime); loadTimer.start(); loadTimer.addEventListener(TimerEvent.TIMER, onLoadTick); advanceTimer = new Timer(Settings.advanceTime); advanceTimer.start(); advanceTimer.addEventListener(TimerEvent.TIMER, onAdvanceTick); } private function onLoadTick(event : TimerEvent) : void { loadData(); } private function onAdvanceTick(event : TimerEvent) : void { displayNext(); } private function resetAdvanceTimer() : void { advanceTimer.reset(); advanceTimer.delay = Math.max(1000, Settings.advanceTime * (1 - Math.sqrt(newTweets.length) / 10)); advanceTimer.start(); advanceTimerTime = new Date().time; } private function resetLoadTimer() : void { loadTimer.reset(); loadTimer.start(); } private function displayNext() : void { if (newTweets.length) { displayMode = NEW_TWEETS; var ts : TweetSprite = TweetSprite(newTweets.shift()); visualization.displayTweet(ts); } else if (searchMode == ARCHIVE || displayMode == NEW_TWEETS) { displayMode = OVERVIEW; visualization.displayTweet(); } else { displayMode = RANDOM; visualization.displayRandom(); } resetAdvanceTimer(); updateStats(); } /* * DATA LOADING */ private function initTweetrs() : void { searchTweetr = new Tweetr(); searchTweetr.addEventListener(TweetEvent.COMPLETE, onSearchDataLoaded); searchTweetr.addEventListener(TweetEvent.FAILED, handleTweetsFail); } private function handleTweetsFail(event : TweetEvent) : void { displayStatus(event.info); log(this, event, 0x991111); searchRunning = false; } private function loadData() : void { if (searchMode == ARCHIVE && !searchRunning) { searchTweetr.search(Settings.searchterms.join("\" OR \""), null, Settings.resultsPerPage, (Settings.numPreloadPages - loadCounter ) + 1); displayStatus("loading archived results (page " + ((Settings.numPreloadPages - loadCounter ) + 1) + ")"); } else if (searchMode == LIVEUPDATES && !searchRunning) { displayStatus("loading new tweets"); searchTweetr.search(Settings.searchterms.join("\" OR \""), null, 50, 1, latestId); } searchRunning = true; } private function onSearchDataLoaded(event : TweetEvent) : void { searchRunning = false; var results : Array = []; var td : TweetData; if (searchMode == LIVEUPDATES) { updateLatestId(event.data as String); } for each (var tweet:SearchResultData in event.responseArray) { if (!visualization.tweetByID[tweet.id]) { td = TweetData.parseSearchResult(tweet); if (Settings.minDate && td.dateTime < Settings.minDate.time) continue; if (Settings.maxDate && td.dateTime > Settings.maxDate.time) continue; results.push(td); } else { // Logger.info("already added", tweet.id); } } log(this, event.responseArray.length + " loaded / " + results.length + " new / latestId " + latestId); if (results.length) { results.sortOn("dateTime"); for each (td in results) { var ts : TweetSprite = visualization.addTweetSprite(td); if (searchMode == LIVEUPDATES) { newTweets.push(ts); ts.isNew = true; } } // HACK: fix currentIndex in visualization for (var i : int = 0;i < visualization.data.nodes.length;i++) { if (visualization.data.nodes[i] == visualization.highlightedTweet) { visualization.currentIndex = i; break; } } displayStatus(results.length + " tweets loaded"); // updateStats(); } else { displayStatus("no new tweets"); } if (searchMode == ARCHIVE) { loadCounter++; if (loadCounter > Settings.numPreloadPages) { searchMode = LIVEUPDATES; } else { loadData(); } } } private function updateStats() : void { icons.alpha = 1; var reducedAlpha : Number = .66; var template : String = "<font size='24' color='#FFFFFF'>{0}</font> <font size='14' color='#999999'>{1}</font>"; var oldText : String; oldText = numTweets_tf.text; numTweets_tf.htmlText = Strings.format(template, visualization.data.nodes.length, "tweets"); if (numTweets_tf.text != oldText) { numTweets_tf.alpha = 1; TweenFilterLite.to(numTweets_tf, 3, {alpha:reducedAlpha}); } var numRetweets : Number = 0; var numReplies : Number = 0; for each (var e:TweetConnection in visualization.data.edges) { if (e.type == "retweet") numRetweets++; if (e.type == "reference") numReplies++; } oldText = numRetweets_tf.text; numRetweets_tf.htmlText = Strings.format(template, numRetweets, "retweets"); if (numRetweets_tf.text != oldText) { numRetweets_tf.alpha = 1; TweenFilterLite.to(numRetweets_tf, 3, {alpha:reducedAlpha}); } oldText = numReplies_tf.text; numReplies_tf.htmlText = Strings.format(template, numReplies, "replies"); if (numReplies_tf.text != oldText) { numReplies_tf.alpha = 1; TweenFilterLite.to(numReplies_tf, 3, {alpha:reducedAlpha}); } oldText = numNew_tf.text; numNew_tf.htmlText = Strings.format(template, newTweets.length, "new"); if (newTweets.length) { if (numNew_tf.text != oldText) { numNew_tf.alpha = 1; TweenFilterLite.to(numNew_tf, 3, {alpha:reducedAlpha}); newIcon.alpha = 1; TweenFilterLite.to(newIcon, 3, {alpha:reducedAlpha}); } } else { TweenFilterLite.to(numNew_tf, 3, {alpha:reducedAlpha}); TweenFilterLite.to(newIcon, 3, {alpha:reducedAlpha}); } } private function updateLatestId(s : String) : void { var re : RegExp = /since_id=(.+?)\" rel=\"refresh\"\/>/g; var matches : Array = re.exec(s); if (matches.length > 1) latestId = matches[1]; } } }
  1 +/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import flare.util.Strings; import gs.TweenFilterLite; import nl.demonsters.debugger.MonsterDebugger; import com.swfjunkie.tweetr.Tweetr; import com.swfjunkie.tweetr.data.objects.SearchResultData; import com.swfjunkie.tweetr.events.TweetEvent; import flash.display.SimpleButton; import flash.display.Sprite; import flash.display.StageAlign; import flash.display.StageDisplayState; import flash.events.Event; import flash.events.FullScreenEvent; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.geom.Rectangle; import flash.text.TextField; import flash.ui.Keyboard; import flash.utils.Timer; public class App extends Sprite { public static var monsterDebugger : MonsterDebugger; protected static const LIVEUPDATES : String = "LIVEUPDATES"; protected static const ARCHIVE : String = "ARCHIVE"; protected static const NEW_TWEETS : String = "NEW_TWEETS"; protected static const OVERVIEW : String = "OVERVIEW"; protected static const RANDOM : String = "RANDOM"; public static const REPLAY : String = "REPLAY"; private var searchTweetr : Tweetr; private var loadTimer : Timer; private var searchRunning : Boolean; private var advanceTimer : Timer; protected var visualization : TweetVisualization; private var latestId : Number = 0; public var search_tf : TextField; public var settings : Class = Settings; public var status_tf : TextField; private var loadCounter : Number = 1; public var newTweets : Array = []; private var advanceTimerTime : Number = new Date().time; protected var searchMode : String = "ARCHIVE"; public var numTweets_tf : TextField; public var numRetweets_tf : TextField; public var numReplies_tf : TextField; public var numNew_tf : TextField; public var appTitle_tf : TextField; public var footer : Sprite; public var newIcon : Sprite; public var icons : Sprite; private var displayMode : String; public var fullscreen_btn : SimpleButton; public static var appMode : String = "LIVEUPDATES"; public function App() { Settings.overwriteDefaults(loaderInfo.parameters); App.monsterDebugger = new MonsterDebugger(this); initStage(); initKeyControl(); initVis(); initTweetrs(); loadData(); initTimers(); initIconsAndTitle(); addEventListener(Event.ENTER_FRAME, onEnterFrame); } private function initIconsAndTitle() : void { displayStatus("starting up"); appTitle_tf.text = Settings.appTitle; status_tf.alpha = 0; footer["description_tf"].text = Strings.format(footer["description_tf"].text, Settings.maxItems, Settings.searchterms.join(" OR ")); icons.x = Math.min(fullscreen_btn.x - icons.width - 10, appTitle_tf.x + appTitle_tf.textWidth + 50); appTitle_tf.width = icons.x - appTitle_tf.x; numTweets_tf = icons["numTweets_tf"]; numNew_tf = icons["numNew_tf"]; numReplies_tf = icons["numReplies_tf"]; numRetweets_tf = icons["numRetweets_tf"]; newIcon = icons["newIcon"]; icons.alpha = newIcon.alpha = 0; fullscreen_btn.addEventListener(MouseEvent.CLICK, onFullScreenClick); stage.addEventListener(FullScreenEvent.FULL_SCREEN, onToggleFullScreen); } private function onToggleFullScreen(event : Event) : void { fullscreen_btn.visible = !(stage.displayState == StageDisplayState.FULL_SCREEN); if (fullscreen_btn.visible) { icons.x = Math.min(fullscreen_btn.x - icons.width - 10, appTitle_tf.x + appTitle_tf.textWidth + 50); appTitle_tf.width = icons.x - appTitle_tf.x; } else { icons.x = Math.min(stage.stageWidth - icons.width - 10, appTitle_tf.x + appTitle_tf.textWidth + 50); appTitle_tf.width = icons.x - appTitle_tf.x; } } private function onFullScreenClick(event : MouseEvent = null) : void { try { stage.displayState = StageDisplayState.FULL_SCREEN; fullscreen_btn.visible = false; } catch (e : Error) { fullscreen_btn.visible = true; } } public static function log(target : Object, object : *, color : uint = 0x111111, functions : Boolean = false, depth : int = 4) { MonsterDebugger.trace(target, object, color, functions, depth); } protected function displayStatus(string : String) : void { if (Settings.showStatus) { status_tf.text = string; status_tf.alpha = 1; TweenFilterLite.to(status_tf, 3, {alpha:0}); } App.log(this, string); } private function initStage() : void { stage.align = StageAlign.TOP_LEFT; } private function initKeyControl() : void { stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressedDown); } private function onEnterFrame(event : Event) : void { renderAdvanceTimer(); } private function renderAdvanceTimer() : void { graphics.clear(); graphics.beginFill(0x444444); graphics.drawRect(20, 1, 92 * (new Date().time - advanceTimerTime) / advanceTimer.delay, 3); } private function initVis() : void { visualization = new TweetVisualization(this, new Rectangle(0, 0, stage.stageWidth - 80, stage.stageHeight - 240)); visualization.x = 15; visualization.y = 100; addChildAt(visualization, 0); } private function keyPressedDown(event : KeyboardEvent) : void { switch (event.keyCode) { case Keyboard.LEFT : visualization.displayPrevious(); resetAdvanceTimer(); break; case Keyboard.RIGHT : visualization.displayNext(); resetAdvanceTimer(); break; case Keyboard.UP : visualization.advance(5); resetAdvanceTimer(); break; case Keyboard.DOWN : visualization.advance(-5); resetAdvanceTimer(); break; } } public function onNodeClick(ts : TweetSprite) : void { visualization.displayTweet(ts); resetAdvanceTimer(); } /* * TIMERS */ private function initTimers() : void { loadTimer = new Timer(Settings.loadTime); loadTimer.start(); loadTimer.addEventListener(TimerEvent.TIMER, onLoadTick); advanceTimer = new Timer(Settings.advanceTime); advanceTimer.start(); advanceTimer.addEventListener(TimerEvent.TIMER, onAdvanceTick); } private function onLoadTick(event : TimerEvent) : void { loadData(); } private function onAdvanceTick(event : TimerEvent) : void { displayNext(); } protected function resetAdvanceTimer() : void { advanceTimer.reset(); advanceTimer.delay = Math.max(1000, Settings.advanceTime * (1 - Math.sqrt(newTweets.length) / 10)); advanceTimer.start(); advanceTimerTime = new Date().time; } private function resetLoadTimer() : void { loadTimer.reset(); loadTimer.start(); } private function displayNext() : void { if (newTweets.length) { displayMode = NEW_TWEETS; var ts : TweetSprite = TweetSprite(newTweets.shift()); visualization.displayTweet(ts); } else if (searchMode == ARCHIVE || displayMode == NEW_TWEETS) { displayMode = OVERVIEW; visualization.displayTweet(); } else { displayMode = RANDOM; visualization.displayRandom(); } resetAdvanceTimer(); updateStats(); } /* * DATA LOADING */ private function initTweetrs() : void { searchTweetr = new Tweetr(); searchTweetr.addEventListener(TweetEvent.COMPLETE, onSearchDataLoaded); searchTweetr.addEventListener(TweetEvent.FAILED, handleTweetsFail); } private function handleTweetsFail(event : TweetEvent) : void { displayStatus(event.info); log(this, event, 0x991111); searchRunning = false; } protected function loadData() : void { if (searchMode == ARCHIVE && !searchRunning) { searchTweetr.search(Settings.searchterms.join("\" OR \""), null, Settings.resultsPerPage, (Settings.numPreloadPages - loadCounter ) + 1); displayStatus("loading archived results (page " + ((Settings.numPreloadPages - loadCounter ) + 1) + ")"); } else if (searchMode == LIVEUPDATES && !searchRunning) { displayStatus("loading new tweets"); searchTweetr.search(Settings.searchterms.join("\" OR \""), null, 50, 1, latestId); } searchRunning = true; } private function onSearchDataLoaded(event : TweetEvent) : void { searchRunning = false; var results : Array = []; var td : TweetData; if (searchMode == LIVEUPDATES) { updateLatestId(event.data as String); } for each (var tweet:SearchResultData in event.responseArray) { if (!visualization.tweetByID[tweet.id]) { td = TweetData.parseSearchResult(tweet); if (Settings.minDate && td.dateTime < Settings.minDate.time) continue; if (Settings.maxDate && td.dateTime > Settings.maxDate.time) continue; results.push(td); } else { // Logger.info("already added", tweet.id); } } log(this, event.responseArray.length + " loaded / " + results.length + " new / latestId " + latestId); if (results.length) { results.sortOn("dateTime"); for each (td in results) { var ts : TweetSprite = visualization.addTweetSprite(td); if (searchMode == LIVEUPDATES) { newTweets.push(ts); ts.isNew = true; } } // HACK: fix currentIndex in visualization for (var i : int = 0;i < visualization.data.nodes.length;i++) { if (visualization.data.nodes[i] == visualization.highlightedTweet) { visualization.currentIndex = i; break; } } displayStatus(results.length + " tweets loaded"); // updateStats(); } else { displayStatus("no new tweets"); } if (searchMode == ARCHIVE) { loadCounter++; if (loadCounter > Settings.numPreloadPages) { searchMode = LIVEUPDATES; } else { loadData(); } } } private function updateStats() : void { icons.alpha = 1; var reducedAlpha : Number = .66; var template : String = "<font size='24' color='#FFFFFF'>{0}</font> <font size='14' color='#999999'>{1}</font>"; var oldText : String; oldText = numTweets_tf.text; numTweets_tf.htmlText = Strings.format(template, visualization.data.nodes.length, "tweets"); if (numTweets_tf.text != oldText) { numTweets_tf.alpha = 1; TweenFilterLite.to(numTweets_tf, 3, {alpha:reducedAlpha}); } var numRetweets : Number = 0; var numReplies : Number = 0; for each (var e:TweetConnection in visualization.data.edges) { if (e.type == "retweet") numRetweets++; if (e.type == "reference") numReplies++; } oldText = numRetweets_tf.text; numRetweets_tf.htmlText = Strings.format(template, numRetweets, "retweets"); if (numRetweets_tf.text != oldText) { numRetweets_tf.alpha = 1; TweenFilterLite.to(numRetweets_tf, 3, {alpha:reducedAlpha}); } oldText = numReplies_tf.text; numReplies_tf.htmlText = Strings.format(template, numReplies, "replies"); if (numReplies_tf.text != oldText) { numReplies_tf.alpha = 1; TweenFilterLite.to(numReplies_tf, 3, {alpha:reducedAlpha}); } oldText = numNew_tf.text; numNew_tf.htmlText = Strings.format(template, newTweets.length, "new"); if (newTweets.length) { if (numNew_tf.text != oldText) { numNew_tf.alpha = 1; TweenFilterLite.to(numNew_tf, 3, {alpha:reducedAlpha}); newIcon.alpha = 1; TweenFilterLite.to(newIcon, 3, {alpha:reducedAlpha}); } } else { TweenFilterLite.to(numNew_tf, 3, {alpha:reducedAlpha}); TweenFilterLite.to(newIcon, 3, {alpha:reducedAlpha}); } } private function updateLatestId(s : String) : void { var re : RegExp = /since_id=(.+?)\" rel=\"refresh\"\/>/g; var matches : Array = re.exec(s); if (matches.length > 1) latestId = matches[1]; } } }
1  src/eu/stefaner/revisit/EyeoApp.as
... ... @@ -0,0 +1 @@
  1 +package eu.stefaner.revisit { import flare.util.Strings; import flash.events.Event; import flash.net.FileReference; import flash.net.URLLoader; import flash.net.URLRequest; /** * @author mo */ public class EyeoApp extends App { private static const SAVE_SELECTED : Boolean = false; private var loader : URLLoader; public function EyeoApp() { App.appMode = REPLAY; Settings.maxItems = 400; Settings.searchterms = ["eyeo", "@eyeofestival"]; Settings.appTitle = "#eyeo"; super(); TweetSprite.baseScale = .66; Settings.baseScaleGrow = Settings.baseScaleShrink = 0; } override protected function loadData() : void { if (!loader) { loader = new URLLoader(); // loader.load(new URLRequest("data/eyeotweets.txt")); loader.load(new URLRequest("data/eyeotweets_400.txt")); loader.addEventListener(Event.COMPLETE, onDataLoaded); } else { searchMode = App.LIVEUPDATES; resetAdvanceTimer(); } } private function onDataLoaded(event : Event) : void { var s : String = event.target.data; var a : Array = s.split("\-\n"); trace(a.length + " tweets loaded", ""); /* author=creativeapps id=tag:search.twitter.com,2005:83929594540982272 published=2011-06-23 16:09:09 source=twitter title=@moritz_stefaner I'd love to but plane tickets are just too expensive... / have a great time! - Dave is a fantastic host /cc @eyeofestival url=http://twitter.com/creativeapps/statuses/83929594540982272 */ var results : Array = []; var td : TweetData; displayStatus(results.length + " tweets loaded"); var tweets : Array = []; for each (var l:String in a) { try { var o : * = {}; var aa : Array = l.split("\n"); for each (var l2:String in aa) { var aaa : Array = l2.split("="); o[aaa.shift()] = aaa.join("="); } var td : TweetData = new TweetData(); td.id = o.id; td.link = o.url || ""; td.text = o.title || ""; td.createdAt = o.published; td.user = o.author; td.userLink = o.author; td.userName = o.author; if (td.date.date < 26 || td.date.date > 30 ) continue; tweets.push(td); td.props.originalDataString = l; td.userProfileImage = Strings.format("http://api.twitter.com/1/users/profile_image/{0}?size=bigger", o.author); } catch(error : Error) { } // trace(o.title); } tweets.sortOn("dateTime", Array.NUMERIC); for each (var td:TweetData in tweets) { visualization.addTweetSprite(td, true); } var output : String = ""; if (SAVE_SELECTED) { visualization.checkForTooManyTweets(ts); for each (var ts:TweetSprite in visualization.data.nodes) { output += ts.data.props.originalDataString + "\n-\n"; } new FileReference().save(output, "eyeotweets_" + Settings.maxItems + ".txt"); } // searchMode = App.LIVEUPDATES; resetAdvanceTimer(); } } }
2  src/eu/stefaner/revisit/Settings.as
... ... @@ -1 +1 @@
1   -/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import flare.util.Dates; import com.swfjunkie.tweetr.Tweetr; /** * @author mo */ public class Settings { // externally configurable public static var maxItems : int = 300; public static var showOnlyToday : Boolean = false; public static var twitterProxyURL : String; public static var searchterms : Array = ["@twitter"]; public static var appTitle : String; // internal // public static var minDate : Date = Dates.roundTime(new Date(), Dates.DAYS); public static var minDate : Date = null; public static var maxDate : Date = null; public static var resultsPerPage : int; public static var numPreloadPages : int; public static var advanceSteps : int = 1; public static var windowSize : int = 0; public static var minSpacing : Number = 5; public static var transitionLength : Number = 1; public static var advanceTime : Number = 6000; public static var loadTime : Number = 10000; public static var retweetColor : uint = 0xFF32CCFF; public static var atReplyColor : uint = 0xFF99FF99; public static var showStatus : Boolean = true; public static var tweetConnectionAlpha : Number = .33; public static var tweetConnectionDimmedAlpha : Number = .16; public static var tweetConnectionCollapsedAlpha : Number = .66; public static var focusWindowWidth : Number = 600; public static var minAxisWidth : Number = 150; // see 5 special public static var STANDALONE : Boolean = false; public static function overwriteDefaults(params : Object) : void { if (params) { // overwrite defaults maxItems = params.maxItems || maxItems ; resultsPerPage = Math.min(100, maxItems); numPreloadPages = Math.ceil(maxItems / resultsPerPage); showOnlyToday = (params.showOnlyToday == "true") || showOnlyToday; twitterProxyURL = params.twitterProxyURL || twitterProxyURL ; if (params.searchterms) { searchterms = params.searchterms.split(","); } appTitle = params.appTitle || appTitle; } if (showOnlyToday) { minDate = Dates.addHours(Dates.roundTime(new Date(), Dates.DAYS), 7); } if (twitterProxyURL) { Tweetr.URL_TWITTER_SEARCH_OVERRIDE = twitterProxyURL; } if (!appTitle) { appTitle = searchterms.join(","); } } } }
  1 +/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import flare.util.Dates; import com.swfjunkie.tweetr.Tweetr; /** * @author mo */ public class Settings { // externally configurable public static var maxItems : int = 300; public static var showOnlyToday : Boolean = false; public static var twitterProxyURL : String; public static var searchterms : Array = ["@twitter"]; public static var appTitle : String; // internal // public static var minDate : Date = Dates.roundTime(new Date(), Dates.DAYS); public static var minDate : Date = null; public static var maxDate : Date = null; public static var resultsPerPage : int; public static var numPreloadPages : int; public static var advanceSteps : int = 1; public static var windowSize : int = 0; public static var minSpacing : Number = 5; public static var transitionLength : Number = 1; public static var advanceTime : Number = 6000; public static var loadTime : Number = 10000; public static var retweetColor : uint = 0xFF32CCFF; public static var atReplyColor : uint = 0xFF99FF99; public static var showStatus : Boolean = true; public static var tweetConnectionAlpha : Number = .33; public static var tweetConnectionDimmedAlpha : Number = .16; public static var tweetConnectionCollapsedAlpha : Number = .66; public static var focusWindowWidth : Number = 600; public static var minAxisWidth : Number = 150; // see 5 special public static var STANDALONE : Boolean = false; public static var baseScaleShrink : Number = .05; public static var baseScaleGrow : Number = .05; public static var baseScaleMin : Number = .5; public static var baseScaleMax : Number = 2; public static function overwriteDefaults(params : Object) : void { if (params) { // overwrite defaults maxItems = params.maxItems || maxItems ; resultsPerPage = Math.min(100, maxItems); numPreloadPages = Math.ceil(maxItems / resultsPerPage); showOnlyToday = (params.showOnlyToday == "true") || showOnlyToday; twitterProxyURL = params.twitterProxyURL || twitterProxyURL ; if (params.searchterms) { searchterms = params.searchterms.split(","); } appTitle = params.appTitle || appTitle; } if (showOnlyToday) { minDate = Dates.addHours(Dates.roundTime(new Date(), Dates.DAYS), 7); } if (twitterProxyURL) { Tweetr.URL_TWITTER_SEARCH_OVERRIDE = twitterProxyURL; } if (!appTitle) { appTitle = searchterms.join(","); } } } }
104 src/eu/stefaner/revisit/TweetData.as
... ... @@ -1,103 +1 @@
1   -/*
2   -
3   - Copyright 2010, Moritz Stefaner
4   -
5   - Licensed under the Apache License, Version 2.0 (the "License");
6   - you may not use this file except in compliance with the License.
7   - You may obtain a copy of the License at
8   -
9   - http://www.apache.org/licenses/LICENSE-2.0
10   -
11   - Unless required by applicable law or agreed to in writing, software
12   - distributed under the License is distributed on an "AS IS" BASIS,
13   - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   - See the License for the specific language governing permissions and
15   - limitations under the License.
16   -
17   - */
18   -
19   -package eu.stefaner.revisit {
20   -
21   - import com.swfjunkie.tweetr.data.objects.SearchResultData;
22   - import com.swfjunkie.tweetr.utils.TweetUtil;
23   -
24   - /**
25   - * @author mo
26   - */
27   - public class TweetData {
28   -
29   - // from SearchResultData
30   - public var id : Number;
31   - public var link : String;
32   - private var _text : String;
33   - private var _createdAt : String;
34   - public var userProfileImage : String;
35   - public var user : String;
36   - public var userLink : String;
37   - // custom
38   - public var date : Date;
39   - public var references : Array;
40   - public var retweets : Array;
41   - public var simpleText : String;
42   - public var dateTime : Number;
43   - public var random : Number = Math.random();
44   - public var userName : String;
45   -
46   - public function TweetData() {
47   - }
48   -
49   - public static function parseSearchResult(tweet : SearchResultData) : TweetData {
50   - var td : TweetData = new TweetData();
51   - td.id = tweet.id;
52   - td.link = tweet.link;
53   - td.text = tweet.text;
54   - td.createdAt = tweet.createdAt;
55   - td.userProfileImage = tweet.userProfileImage;
56   - td.user = tweet.user;
57   - td.userLink = tweet.userLink;
58   - td.userName = tweet.user.split(" ")[0].toLowerCase();
59   - return td;
60   - }
61   -
62   - public function get createdAt() : String {
63   - return _createdAt;
64   - }
65   -
66   - public function set createdAt(c : String) : void {
67   - _createdAt = c;
68   - date = TweetUtil.returnTweetDate(c);
69   - dateTime = date.time;
70   - }
71   -
72   - public function get text() : String {
73   - return _text;
74   - }
75   -
76   - public function set text(text : String) : void {
77   - _text = text;
78   - processText();
79   - }
80   -
81   - private function processText() : void {
82   - var refRE : RegExp = /[@]+[A-Za-z0-9-_]+/g;
83   - var rtRE : RegExp = /(RT |via )[@]+[A-Za-z0-9-_]+/g;
84   - var tempText : String = text;
85   -
86   - retweets = tempText.match(rtRE);
87   - tempText = tempText.replace(rtRE, "");
88   -
89   - references = tempText.match(refRE);
90   -
91   - simpleText = text;
92   - simpleText = simpleText.replace(rtRE, "");
93   - simpleText = simpleText.replace(refRE, "");
94   -
95   - simpleText = simpleText.replace(/\W/g, "");
96   - simpleText = simpleText.toLowerCase();
97   - }
98   -
99   - public function isRetweetOf(t : TweetData) : Boolean {
100   - return simpleText.indexOf(t.simpleText.substring(0, 10)) > -1;
101   - }
102   - }
103   -}
  1 +/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import flare.util.Strings; import com.swfjunkie.tweetr.data.objects.SearchResultData; import com.swfjunkie.tweetr.utils.TweetUtil; /** * @author mo */ public class TweetData { // from SearchResultData public var id : Number; public var link : String; private var _text : String; private var _createdAt : String; public var userProfileImage : String; public var user : String; public var userLink : String; // custom public var date : Date; public var references : Array; public var retweets : Array; public var simpleText : String; public var dateTime : Number; public var random : Number = Math.random(); public var userName : String; public var props : * = {}; public var htmlText : String; public function TweetData() { } public static function parseSearchResult(tweet : SearchResultData) : TweetData { var td : TweetData = new TweetData(); td.id = tweet.id; td.link = tweet.link; td.text = tweet.text; td.createdAt = tweet.createdAt; td.userProfileImage = tweet.userProfileImage; td.user = tweet.user; td.userLink = tweet.userLink; td.userName = tweet.user.split(" ")[0].toLowerCase(); // test // td.userProfileImage = Strings.format("http://api.twitter.com/1/users/profile_image/{0}?size=bigger", td.userName); return td; } public function get createdAt() : String { return _createdAt; } public function set createdAt(c : String) : void { _createdAt = c; date = TweetUtil.returnTweetDate(c); dateTime = date.time; } public function get text() : String { return _text; } public function set text(text : String) : void { _text = text; processText(); } private function processText() : void { var refRE : RegExp = /[@]+[A-Za-z0-9-_]+/g; var rtRE : RegExp = /(RT |via )[@]+[A-Za-z0-9-_]+/g; var tempText : String = text || ""; retweets = tempText.match(rtRE); tempText = tempText.replace(rtRE, ""); references = tempText.match(refRE); simpleText = text; simpleText = simpleText.replace(rtRE, ""); simpleText = simpleText.replace(refRE, ""); simpleText = simpleText.replace(/\W/g, ""); simpleText = simpleText.toLowerCase(); htmlText = text.replace(/(http(s)?:\/\/((\d+\.\d+\.\d+\.\d+)|(([\w-]+\.)+([a-z,A-Z][\w-]*)))(:[1-9][0-9]*)?(\/([\w-.\/:%+@&=]+[\w- .\/?:%+@&=]*)?)?(#(.*))?)/gi, "<a href='$1' target='blank'>$1</a>"); } public function isRetweetOf(t : TweetData) : Boolean { return simpleText.indexOf(t.simpleText.substring(0, 10)) > -1; } } }
2  src/eu/stefaner/revisit/TweetSprite.as
... ... @@ -1 +1 @@
1   -/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import flare.animate.Transitioner; import flare.util.Maths; import flare.vis.axis.Axis; import flare.vis.data.NodeSprite; import com.swfjunkie.tweetr.utils.TweetUtil; import flash.display.Loader; import flash.display.Sprite; import flash.events.Event; import flash.events.IOErrorEvent; import flash.geom.Rectangle; import flash.net.URLRequest; import flash.text.TextField; import flash.utils.getDefinitionByName; /** * @author mo */ public class TweetSprite extends NodeSprite { public static const EXPANDED : String = "EXPANDED"; public static const COLLAPSED : String = "COLLAPSED"; private static const EXPANDED_WIDTH : Number = 330; private static const EXPANDED_HEIGHT : Number = 100; private static const COLLAPSED_WIDTH : Number = 50; private static const COLLAPSED_HEIGHT : Number = 50; public static var baseScale : Number = 1; public var text_tf : TextField; public var author_tf : TextField; private var newIcon : Sprite; public var lightBG : Sprite; public var darkBG : Sprite; public var ago_tf : TextField; private var imageLoader : Loader; public var axis : Axis; private var endBounds : Rectangle; private var targetWidth : Number; private var targetHeight : Number; public var retweetSourceHighlighted : Boolean; public var retweetTargetHighlighted : Boolean; public var referenceSourceHighlighted : Boolean; public var referenceTargetHighlighted : Boolean; public var activationLevel : Number; public var isNew : Boolean; private var imageHolder : Sprite; public function TweetSprite(o : Object = null) { imageLoader = new Loader(); addChild(imageLoader); super(); mouseChildren = false; data = o; renderer = null; appearance = COLLAPSED; text_tf.mouseEnabled = author_tf.mouseEnabled = ago_tf.mouseEnabled = false; cacheAsBitmap = true; updateActivationLevel(); renderAppearance(); } override public function set data(o : Object) : void { if (o is TweetData) { super.data = o; text_tf.text = (o as TweetData).text; author_tf.text = (o as TweetData).user; updateAgoField(); // loadRetweets(); loadImage(); } else { throw new Error("SearchResultData or StatusData expected"); } } private function updateAgoField() : void { ago_tf.htmlText = TweetUtil.returnShortTweetAge((data as TweetData).createdAt); } private function loadImage() : void { var imageURL : String = (data as TweetData).userProfileImage; imageLoader.load(new URLRequest(imageURL)); imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onPicLoaded, false, 0, true); imageLoader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onPicLoadError, false, 0, true); } private function onPicLoadError(event : IOErrorEvent) : void { trace("error loading pic ", (data as TweetData).userProfileImage); } private function onPicLoaded(event : Event) : void { imageHolder = new Sprite(); addChild(imageHolder); imageHolder.addChild(imageLoader); imageHolder.width = 50; imageHolder.height = 50; imageHolder.scaleX = imageHolder.scaleY = Math.min(imageHolder.scaleX, imageHolder.scaleY); } private var _appearance : String; public function get appearance() : String { return _appearance; } public function set appearance(appearance : String) : void { if (appearance != _appearance) { _appearance = appearance; if (appearance == EXPANDED) { updateAgoField(); } // layout(); } } public var highlighted : Boolean = false; public function getEndBounds(t : Transitioner) : Rectangle { if (!endBounds) { var x : Number = Number(t.$(this).x != null ? t.$(this).x : x); var y : Number = Number(t.$(this).y != null ? t.$(this).y : y); var w : Number = t.$(this).scaleX != null ? Number(t.$(this).scaleX) * targetWidth : scaleX * targetWidth; var h : Number = t.$(this).scaleY != null ? Number(t.$(this).scaleY) * targetHeight : scaleY * targetHeight; endBounds = new Rectangle(x, y, w, h); } return endBounds; } public function invalidateEndBounds() : void { endBounds = null; } public function renderAppearance(t : Transitioner = null) : void { t = Transitioner.instance(t); switch (appearance) { case EXPANDED : t.$(text_tf).visible = true; t.$(author_tf).visible = true; t.$(ago_tf).visible = true; t.$(author_tf).alpha = 1; t.$(text_tf).alpha = 1; t.$(ago_tf).alpha = 1; targetWidth = EXPANDED_WIDTH; targetHeight = EXPANDED_HEIGHT; lightBG.visible = true; t.$(lightBG).width = EXPANDED_WIDTH; t.$(lightBG).height = EXPANDED_HEIGHT; t.$(lightBG).alpha = 1; darkBG.visible = false; darkBG.width = darkBG.height = darkBG.alpha = 0; break; case COLLAPSED : t.$(text_tf).visible = false; t.$(author_tf).visible = false; t.$(ago_tf).visible = false; t.$(author_tf).alpha = 0; t.$(text_tf).alpha = 0; t.$(ago_tf).alpha = 0; targetWidth = COLLAPSED_WIDTH; targetHeight = COLLAPSED_HEIGHT; darkBG.visible = true; t.$(darkBG).width = COLLAPSED_WIDTH; t.$(darkBG).height = COLLAPSED_HEIGHT; t.$(darkBG).alpha = 1; lightBG.visible = false; lightBG.width = lightBG.height = lightBG.alpha = 0; break; } } public function updateAppearance(t : Transitioner, inFocusArea : Boolean = false, middleY : Number = 0) : void { appearance = ((inFocusArea || retweetTargetHighlighted || referenceTargetHighlighted || referenceSourceHighlighted) ? EXPANDED : COLLAPSED); renderAppearance(t); activationLevel = updateActivationLevel(); renderNewIcon(); if (highlighted) { t.$(this).scaleX = t.$(this).scaleY = 16.0 / 13.0; t.$(imageLoader).alpha = 1; isNew = false; } else if (appearance == EXPANDED) { t.$(this).scaleX = t.$(this).scaleY = 10.0 / 13.0; t.$(imageLoader).alpha = 1; } else { t.$(this).scaleX = t.$(this).scaleY = Maths.linearInterp(activationLevel, .2, 1) * baseScale; t.$(imageLoader).alpha = Math.min(1, activationLevel * 2 + .2); } invalidateEndBounds(); t.$(this).y = middleY - getEndBounds(t).height; var xx : Number = 0; if (highlighted) { xx = axis.x1 + .5 * (axis.x2 - axis.x1) - getEndBounds(t).width * .5; } else { xx = axis.X(data.date); xx -= axis.axisScale.interpolate(data.date) * getEndBounds(t).width; } // xx -= getEndBounds(t).width * .5; if (xx < 0) xx = 0; if (xx > 1200 - getEndBounds(t).width) xx = 1200 - getEndBounds(t).width; xx = Math.floor(xx); t.$(this).x = xx; invalidateEndBounds(); } private function renderNewIcon() : void { if (isNew && !newIcon) { newIcon = new (getDefinitionByName("NewIcon") as Class)() as Sprite; // newIcon.scaleX = newIcon.scaleY = 1.5; newIcon.x = newIcon.y = -10; addChild(newIcon); } else if (!isNew && newIcon) { removeChild(newIcon); newIcon = null; } } public function updateEdges(t : Transitioner) : void { var totalHeight : Number = 0; visitEdges(function(tc : TweetConnection) : void { tc.updateSize(t); totalHeight += tc.targetHeight + 1; }, NodeSprite.IN_LINKS); sortEdgesBy(NodeSprite.IN_LINKS, "targetAngle"); invalidateEndBounds(); var b : Rectangle = getEndBounds(t); var currentX : Number = b.right + 1; var currentY : Number = (appearance == EXPANDED) ? b.top + b.height * .5 - totalHeight * .5 : b.top; // var maxSize : Number = 0; visitEdges(function(tc : TweetConnection) : void { t.$(tc).x = currentX; t.$(tc).y = currentY; tc.updatePositions(t); currentY += tc.targetHeight + 1; /* // make grid, does not work that well because connections overlap much more maxSize = Math.max(maxSize, tc.targetHeight); if(currentY > b.bottom) { currentY = b.top; currentX += maxSize + 1; maxSize = 0; } * */ }, NodeSprite.IN_LINKS); } private function updateActivationLevel() : Number { var s : Number = 0; if (highlighted) return 1.1; if (appearance == EXPANDED) return 1.05; s = 0; s += .25 * Math.sqrt(inDegree); s += .1 * Math.sqrt(outDegree); s += .33 * Number(isNew); s += .2 * Number(retweetSourceHighlighted); s += .4 * Number(retweetTargetHighlighted); s += .1 * Number(referenceSourceHighlighted); s += .2 * Number(referenceTargetHighlighted); s = Math.min(1, s); return s; } } }
  1 +/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import flare.animate.Transitioner; import flare.util.Maths; import flare.vis.axis.Axis; import flare.vis.data.NodeSprite; import com.swfjunkie.tweetr.utils.TweetUtil; import flash.display.Loader; import flash.display.Sprite; import flash.events.Event; import flash.events.IOErrorEvent; import flash.geom.Rectangle; import flash.net.URLRequest; import flash.text.TextField; import flash.utils.getDefinitionByName; /** * @author mo */ public class TweetSprite extends NodeSprite { public static const EXPANDED : String = "EXPANDED"; public static const COLLAPSED : String = "COLLAPSED"; private static const EXPANDED_WIDTH : Number = 330; private static const EXPANDED_HEIGHT : Number = 100; private static const COLLAPSED_WIDTH : Number = 50; private static const COLLAPSED_HEIGHT : Number = 50; public static var baseScale : Number = 1; public var text_tf : TextField; public var author_tf : TextField; private var newIcon : Sprite; public var lightBG : Sprite; public var darkBG : Sprite; public var ago_tf : TextField; private var imageLoader : Loader; public var axis : Axis; private var endBounds : Rectangle; private var targetWidth : Number; private var targetHeight : Number; public var retweetSourceHighlighted : Boolean; public var retweetTargetHighlighted : Boolean; public var referenceSourceHighlighted : Boolean; public var referenceTargetHighlighted : Boolean; public var activationLevel : Number; public var isNew : Boolean; private var imageHolder : Sprite; public function TweetSprite(o : Object = null) { imageLoader = new Loader(); addChild(imageLoader); super(); // mouseChildren = false; data = o; renderer = null; appearance = COLLAPSED; author_tf.mouseEnabled = ago_tf.mouseEnabled = false; cacheAsBitmap = true; updateActivationLevel(); renderAppearance(); addEventListener(Event.ADDED_TO_STAGE, onStageInit); } private function onStageInit(event : Event) : void { // x = -100; // y = stage.stageHeight * .5; } override public function set data(o : Object) : void { if (o is TweetData) { super.data = o; text_tf.htmlText = (o as TweetData).htmlText; author_tf.text = (o as TweetData).user; updateAgoField(); // loadRetweets(); loadImage(); activationLevel = updateActivationLevel(); } else { throw new Error("SearchResultData or StatusData expected"); } } private function updateAgoField() : void { ago_tf.htmlText = TweetUtil.returnShortTweetAge((data as TweetData).createdAt); } private function loadImage() : void { var imageURL : String = (data as TweetData).userProfileImage; imageLoader.load(new URLRequest(imageURL)); imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onPicLoaded, false, 0, true); imageLoader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, onPicLoadError, false, 0, true); } private function onPicLoadError(event : IOErrorEvent) : void { trace("error loading pic ", (data as TweetData).userProfileImage); } private function onPicLoaded(event : Event) : void { imageHolder = new Sprite(); addChild(imageHolder); imageHolder.addChild(imageLoader); imageHolder.width = 50; imageHolder.height = 50; imageHolder.scaleX = imageHolder.scaleY = Math.min(imageHolder.scaleX, imageHolder.scaleY); } private var _appearance : String; public function get appearance() : String { return _appearance; } public function set appearance(appearance : String) : void { if (appearance != _appearance) { _appearance = appearance; if (appearance == EXPANDED) { updateAgoField(); } // layout(); } } public var highlighted : Boolean = false; public function getEndBounds(t : Transitioner) : Rectangle { if (!endBounds) { var x : Number = Number(t.$(this).x != null ? t.$(this).x : x); var y : Number = Number(t.$(this).y != null ? t.$(this).y : y); var w : Number = t.$(this).scaleX != null ? Number(t.$(this).scaleX) * targetWidth : scaleX * targetWidth; var h : Number = t.$(this).scaleY != null ? Number(t.$(this).scaleY) * targetHeight : scaleY * targetHeight; endBounds = new Rectangle(x, y, w, h); } return endBounds; } public function invalidateEndBounds() : void { endBounds = null; } public function renderAppearance(t : Transitioner = null) : void { t = Transitioner.instance(t); switch (appearance) { case EXPANDED : t.$(text_tf).visible = true; t.$(author_tf).visible = true; t.$(ago_tf).visible = true; t.$(author_tf).alpha = 1; t.$(text_tf).alpha = 1; t.$(ago_tf).alpha = 1; targetWidth = EXPANDED_WIDTH; targetHeight = EXPANDED_HEIGHT; lightBG.visible = true; t.$(lightBG).width = EXPANDED_WIDTH; t.$(lightBG).height = EXPANDED_HEIGHT; t.$(lightBG).alpha = 1; darkBG.visible = false; darkBG.width = darkBG.height = darkBG.alpha = 0; break; case COLLAPSED : t.$(text_tf).visible = false; t.$(author_tf).visible = false; t.$(ago_tf).visible = false; t.$(author_tf).alpha = 0; t.$(text_tf).alpha = 0; t.$(ago_tf).alpha = 0; targetWidth = COLLAPSED_WIDTH; targetHeight = COLLAPSED_HEIGHT; darkBG.visible = true; t.$(darkBG).width = COLLAPSED_WIDTH; t.$(darkBG).height = COLLAPSED_HEIGHT; t.$(darkBG).alpha = 1; lightBG.visible = false; lightBG.width = lightBG.height = lightBG.alpha = 0; break; } } public function updateAppearance(t : Transitioner, inFocusArea : Boolean = false, middleY : Number = 0) : void { appearance = ((inFocusArea || retweetTargetHighlighted || referenceTargetHighlighted || referenceSourceHighlighted) ? EXPANDED : COLLAPSED); renderAppearance(t); updateActivationLevel(); renderNewIcon(); if (highlighted) { t.$(this).scaleX = t.$(this).scaleY = 16.0 / 13.0; t.$(imageLoader).alpha = 1; isNew = false; } else if (appearance == EXPANDED) { t.$(this).scaleX = t.$(this).scaleY = 10.0 / 13.0; t.$(imageLoader).alpha = 1; } else { t.$(this).scaleX = t.$(this).scaleY = Maths.linearInterp(activationLevel, .2, 1) * baseScale; t.$(imageLoader).alpha = Math.min(1, activationLevel * 2 + .2); } invalidateEndBounds(); t.$(this).y = middleY - getEndBounds(t).height; var xx : Number = 0; if (highlighted) { xx = axis.x1 + .5 * (axis.x2 - axis.x1) - getEndBounds(t).width * .5; } else { xx = axis.X(data.date); xx -= axis.axisScale.interpolate(data.date) * getEndBounds(t).width; } // xx -= getEndBounds(t).width * .5; if (xx < 0) xx = 0; if (xx > 1200 - getEndBounds(t).width) xx = 1200 - getEndBounds(t).width; xx = Math.floor(xx); t.$(this).x = xx; invalidateEndBounds(); } private function renderNewIcon() : void { if (isNew && !newIcon) { newIcon = new (getDefinitionByName("NewIcon") as Class)() as Sprite; // newIcon.scaleX = newIcon.scaleY = 1.5; newIcon.x = newIcon.y = -10; addChild(newIcon); } else if (!isNew && newIcon) { removeChild(newIcon); newIcon = null; } } public function updateEdges(t : Transitioner) : void { var totalHeight : Number = 0; visitEdges(function(tc : TweetConnection) : void { tc.updateSize(t); totalHeight += tc.targetHeight + 1; }, NodeSprite.IN_LINKS); sortEdgesBy(NodeSprite.IN_LINKS, "targetAngle"); invalidateEndBounds(); var b : Rectangle = getEndBounds(t); var currentX : Number = b.right + 1; var currentY : Number = (appearance == EXPANDED) ? b.top + b.height * .5 - totalHeight * .5 : b.top; // var maxSize : Number = 0; visitEdges(function(tc : TweetConnection) : void { t.$(tc).x = currentX; t.$(tc).y = currentY; tc.updatePositions(t); currentY += tc.targetHeight + 1; /* // make grid, does not work that well because connections overlap much more maxSize = Math.max(maxSize, tc.targetHeight); if(currentY > b.bottom) { currentY = b.top; currentX += maxSize + 1; maxSize = 0; } * */ }, NodeSprite.IN_LINKS); } public function updateActivationLevel() : Number { var s : Number = 0; if (highlighted) return activationLevel = 1.1; if (appearance == EXPANDED) return activationLevel = 1.05; s = 0; s += .25 * Math.sqrt(inDegree); s += .1 * Math.sqrt(outDegree); s += .33 * Number(isNew); s += .2 * Number(retweetSourceHighlighted); s += .4 * Number(retweetTargetHighlighted); s += .1 * Number(referenceSourceHighlighted); s += .2 * Number(referenceTargetHighlighted); s = Math.min(1, s); return activationLevel = s; } } }
2  src/eu/stefaner/revisit/TweetVisualization.as
... ... @@ -1 +1 @@
1   -/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import flare.animate.TransitionEvent; import flare.animate.Transitioner; import flare.display.TextSprite; import flare.scale.ScaleType; import flare.scale.TimeScale; import flare.util.Arrays; import flare.util.Maths; import flare.vis.Visualization; import flare.vis.axis.Axis; import flare.vis.axis.AxisGridLine; import flare.vis.data.Data; import flare.vis.data.DataList; import flare.vis.data.NodeSprite; import flare.vis.data.ScaleBinding; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Rectangle; import flash.text.TextFormat; import flash.utils.Dictionary; /** * @author mo */ public class TweetVisualization extends Visualization { private var app : App; private static const LEFT : String = "LEFT"; private static const MIDDLE : String = "MIDDLE"; private static const RIGHT : String = "RIGHT"; private var leftAxis : Axis; private var middleAxis : Axis; private var rightAxis : Axis; private var mainAxis : Axis; // left end of focus time axis (date.time) private var leftBorder : Number = -1; // left end of focus time axis (date.time) private var rightBorder : Number = -1; // index of leftmost focussed item private var leftIndex : int = -1; // index of rightmost focussed item private var rightIndex : int = -1; // index of currently selected item public var currentIndex : int = -1; public var highlightedTweet : TweetSprite; public var tweetByID : Dictionary = new Dictionary(); private var tweetsByUser : Dictionary = new Dictionary(); public var tweetsByImportance : DataList = new DataList("tweetsByImportance"); private var transitioner : Transitioner; private var oldGridlinePositions : Dictionary = new Dictionary(); public var transitionProgress : Number = 0; private var minDate : Date; public var maxDate : Date; private var leftNodes : DataList; private var middleNodes : DataList; private var rightNodes : DataList; public function TweetVisualization(app : App, b : Rectangle) { super(); bounds = b; this.app = app; data = new Data(); leftNodes = data.addGroup(LEFT); middleNodes = data.addGroup(MIDDLE); rightNodes = data.addGroup(RIGHT); addEventListener(MouseEvent.CLICK, onClick); } protected function initAxes() : void { leftAxis = getAxis(getScaleBinding(LEFT)); middleAxis = getAxis(getScaleBinding(MIDDLE, true)); rightAxis = getAxis(getScaleBinding(RIGHT)); mainAxis = createMainAxis(); leftAxis.y1 = 0; leftAxis.y2 = 0; middleAxis.y1 = 20; middleAxis.y2 = 20; rightAxis.y1 = 0; rightAxis.y2 = 0; middleAxis.labelTextFormat = new TextFormat("FlamaMedium", 14, 0xAAAAAA, true); middleAxis.labelTextMode = TextSprite.EMBED; } private function createMainAxis() : Axis { var a : Axis = new Axis(); a.axisScale = new TimeScale(); a.axisScale.flush = false; a.showLabels = true; if (Settings.showOnlyToday) a.labelFormat = "HH:mm"; a.showLines = true; a.lineLengthY = -10; a.labelOffsetY = 0; a.lineColor = 0x999999; a.lineWidth = 1; a.x1 = 0; a.x2 = bounds.width; a.y1 = bounds.height; a.y2 = bounds.height; a.fixLabelOverlap = true; a.labelTextFormat = new TextFormat("FlamaMedium", 12, 0xCCCCCC, true); a.labelTextMode = TextSprite.EMBED; a.labelOffsetX = -3; a.lineColor = 0x333333; addChild(a); return a; } private function getScaleBinding(group : String, inCenter : Boolean = false) : ScaleBinding { var s : ScaleBinding = new ScaleBinding(); s.scaleType = ScaleType.TIME; s.group = group; s.property = "data.date"; s.data = data; s.flush = true; return s; } private function getAxis(timeScaleBinding : ScaleBinding, showLabels : Boolean = false) : Axis { var a : Axis; a = new Axis(timeScaleBinding); a.showLabels = showLabels; a.showLines = showLabels; a.lineLengthY = 10; a.labelOffsetY = 15; a.lineColor = 0x999999; a.lineWidth = 1; a.y1 = bounds.height; a.y2 = bounds.height; a.fixLabelOverlap = true; a.labelTextFormat = new TextFormat("FlamaMedium", 10, 0x777777, true); a.labelOffsetX = -3; a.lineColor = 0x333333; addChild(a); return a; } // EVENT HANDLERS private function onClick(e : MouseEvent) : void { if (e.target is TweetSprite) app.onNodeClick(TweetSprite(e.target)); } protected function onTransitionStep(event : Event) : void { drawConnectionsToTimeLine(); } // CONTROLS public function displayTweet(ts : TweetSprite = null) : void { for (var i : int = 0;i < data.nodes.length;i++) { if (data.nodes[i] == ts) { currentIndex = i; ts.isNew = false; doUpdate(); return; } } currentIndex = -1; doUpdate(); } public function displayNext() : void { advance(1); } public function displayPrevious() : void { advance(-1); } public function advance(advanceSteps : Number = 1) : void { currentIndex += advanceSteps; doUpdate(); } public function displayRandom() : void { if (!data.nodes.length) return; var ts : TweetSprite ; if (Math.random() > .75 && highlightedTweet) { // display nothing ts = null; } else if (Math.random() > .5) { // pick important ts = tweetsByImportance[Math.floor(tweetsByImportance.length * (1 - Math.pow(Math.random(), .66)))]; } else { // pick new ts = data.nodes[Math.floor(data.nodes.length * (Math.pow(Math.random(), .66)))]; } displayTweet(ts); } override public function update(t : * = null, ...operators) : Transitioner { // t = super.update(t); t = Transitioner.instance(t); if (!data.nodes.length) { return t; } data.nodes.sortBy("data.dateTime"); minDate = data.nodes[0].data.date; maxDate = data.nodes[data.nodes.length - 1].data.date; updateWindow(); updateGroups(); updateAxisPositions(t); updateTweetSprites(t); fixOverlaps(t); updateEdges(t); if (highlightedTweet) addChild(highlightedTweet); return t; } private function updateEdges(t : Transitioner) : void { var n : TweetSprite; for each (n in data.nodes) { n.updateEdges(t); } /* for each (var e:TweetConnection in data.edges) { e.update(t); } */ } private function updateGroups() : void { leftNodes.clear(); middleNodes.clear(); rightNodes.clear(); for (var i : int = 0;i <= leftIndex;i++) { leftNodes.add(data.nodes[i]); data.nodes[i].axis = leftAxis; } for (;i < rightIndex;i++) { middleNodes.add(data.nodes[i]); data.nodes[i].axis = middleAxis; } for (;i < data.nodes.length;i++) { rightNodes.add(data.nodes[i]); data.nodes[i].axis = rightAxis; } } private function updateAxisPositions(t : Transitioner) : void { var pastSpan : Number = leftBorder - minDate.time; var futureSpan : Number = maxDate.time - rightBorder; var totalSpan : Number = maxDate.time - minDate.time; var focusWindowWidth : Number = middleNodes.length ? Settings.focusWindowWidth : 0; var minSize : Number = Settings.minAxisWidth; var w1 : Number = (bounds.width - focusWindowWidth) * (pastSpan / totalSpan); var w2 : Number = focusWindowWidth; var w3 : Number = (bounds.width - focusWindowWidth) * (futureSpan / totalSpan); if (leftNodes.length && (w1 < minSize)) { w3 -= minSize - w1; w1 = minSize; } if (rightNodes.length && (w3 < minSize)) { w1 -= minSize - w3; w3 = minSize; } leftAxis.x1 = 0; leftAxis.x2 = w1; middleAxis.x1 = leftAxis.x2; middleAxis.x2 = leftAxis.x2 + w2; rightAxis.x1 = middleAxis.x2; rightAxis.x2 = middleAxis.x2 + w3; (leftAxis.axisScale as ScaleBinding).updateBinding(); (middleAxis.axisScale as ScaleBinding).updateBinding(); (rightAxis.axisScale as ScaleBinding).updateBinding(); (mainAxis.axisScale as TimeScale).min = minDate; (mainAxis.axisScale as TimeScale).max = maxDate; leftAxis.update(t); middleAxis.update(t); rightAxis.update(t); mainAxis.update(t); } private function updateTweetSprites(t : Transitioner) : void { var n : TweetSprite; data.edges.setProperties({"expanded":false}); data.nodes.setProperties({"highlighted":false, "retweetSourceHighlighted":false, "retweetTargetHighlighted":false, "referenceSourceHighlighted":false, "referenceTargetHighlighted":false}); if (!highlightedTweet) { for each (n in data.nodes) { n.updateAppearance(t, n.axis == middleAxis, bounds.height * .5); } return; } // highlight focused tweet highlightedTweet.highlighted = true; // highlight connected tweets highlightedTweet.visitEdges(function(e : TweetConnection) { e.expanded = true; var target : TweetSprite = TweetSprite(e.other(highlightedTweet)); if (e.type == "retweet") { if (target == e.target) { // target is retweet of highlighted target.retweetTargetHighlighted = true; // show the other retweets, too target.visitEdges(function(e2 : TweetConnection) { e2.expanded = true; }); } else { // highlighted is retweet of target target.retweetSourceHighlighted = true; } } if (e.type == "reference") { if (target == e.source) { target.referenceSourceHighlighted = true; } else { target.referenceTargetHighlighted = true; } } }, NodeSprite.ALL_LINKS); for each (n in data.nodes) { n.updateAppearance(t, n.axis == middleAxis, bounds.height * .5); if (n.appearance == TweetSprite.EXPANDED) { addChild(n); } else { addChildAt(n, 0); } } addChild(highlightedTweet); } private function fixOverlaps(t : Transitioner) : void { updateTweetsByImportanceSorting(); var minY : Number = 100000; var maxY : Number = -100000; var margin : Number; for each (var n:TweetSprite in tweetsByImportance) { var yy : Number = t.$(n).y; if (n.appearance == TweetSprite.EXPANDED) { margin = 4 * Settings.minSpacing; } else { margin = TweetSprite.baseScale * Settings.minSpacing; } var intersects : Boolean = true; while (intersects) { yy += n.props.moveDirection; t.$(n).y = yy; n.invalidateEndBounds(); intersects = false; var b1 : Rectangle = n.getEndBounds(t).clone(); b1.width += margin; for each (var n2:TweetSprite in tweetsByImportance) { if (n == n2) { break; } var b2 : Rectangle = n2.getEndBounds(t).clone(); b2.width += margin; /* var b1 : Rectangle = n.getEndBounds(t); var b2 : Rectangle = n2.getEndBounds(t); */ if (b1.intersects(b2)) { intersects = true; break; } } } yy += n.props.moveDirection * margin; minY = Math.min(yy, minY); maxY = Math.max(yy + n.getEndBounds(t).height, maxY); t.$(n).y = Math.floor(yy); n.invalidateEndBounds(); } // adjust basescale if (highlightedTweet) { var heightDiff : Number = (maxY - minY) - bounds.height; if (heightDiff > 0) { TweetSprite.baseScale -= .1; } else if (heightDiff < -200) { TweetSprite.baseScale += .05; } TweetSprite.baseScale = Math.min(Math.max(TweetSprite.baseScale, .5), 2); } // Logger.info("new baseScale", TweetSprite.baseScale); } private function updateTweetsByImportanceSorting() : void { tweetsByImportance.sortBy(["-activationLevel", "data.random"]); } public function doUpdate() : void { if (!leftAxis) { initAxes(); } storeOldGridlinePositions(); if (transitioner) { if (transitioner.running) { transitioner.stop(); } } transitioner = new Transitioner(Settings.transitionLength, null, true); transitioner.addEventListener(TransitionEvent.STEP, onTransitionStep, false, 0, true); transitionProgress = 0; transitioner.$(this).transitionProgress = 1; update(transitioner); transitioner.play(); } private var moveDirection : Number = -1; public function addTweetSprite(tweet : TweetData) : TweetSprite { var ts : TweetSprite = new TweetSprite(tweet); ts.x = bounds.width; ts.y = bounds.height * .5; ts.scaleX = 0; ts.scaleY = 0; ts.props.moveDirection = moveDirection; moveDirection *= -1; tweetByID[tweet.id] = ts; data.nodes.add(ts); tweetsByImportance.add(ts); if (!tweetsByUser[tweet.userName]) { tweetsByUser[tweet.userName] = []; } tweetsByUser[tweet.userName].push(ts); var userName : String; for each (var retweet:String in tweet.retweets) { userName = retweet.split("@")[1].toLowerCase(); // App.log(this, "retweet " + retweet + ":" + userName); for each (var ts3:TweetSprite in tweetsByUser[userName]) { if (tweet.isRetweetOf(TweetData(ts3.data))) { createEdge(ts, ts3, "retweet"); } } } for each (var reference:String in tweet.references) { userName = reference.split("@")[1].toLowerCase(); var previous : TweetSprite = null; for each (var ts3:TweetSprite in tweetsByUser[userName]) { if (ts3.data.dateTime >= ts.data.dateTime) { break; } previous = ts3; } if (previous) { createEdge(ts, previous, "reference"); } } if (data.nodes.length > Settings.maxItems) { updateTweetsByImportanceSorting(); while (data.nodes.length > Settings.maxItems) { if (tweetsByImportance[tweetsByImportance.length - 1] != ts) { removeTweetSprite(tweetsByImportance[tweetsByImportance.length - 1]); } else { removeTweetSprite(tweetsByImportance[tweetsByImportance.length - 2]); } } } return ts; } private function removeTweetSprite(ts : TweetSprite) : void { Arrays.remove(tweetsByUser[ts.data.userName], ts); tweetsByImportance.remove(ts); tweetByID[ts.data.id] = null; data.removeNode(ts); Arrays.remove(app.newTweets, ts); } private function createEdge(ts : NodeSprite, ts2 : NodeSprite, type : String = "") : void { var e : TweetConnection = new TweetConnection(ts, ts2, type); ts.addOutEdge(e); ts2.addInEdge(e); data.addEdge(e); } private function updateWindow() : void { // currentIndex = Math.max(0, Math.min(data.nodes.length - 1, currentIndex)); if (currentIndex > data.nodes.length - 1 || currentIndex < 0) { currentIndex = -1; highlightedTweet = null; leftIndex = data.nodes.length - 1; rightIndex = data.nodes.length - 1; } else { highlightedTweet = data.nodes[currentIndex]; leftIndex = Math.max(0, currentIndex - Math.max(0, (Settings.windowSize - 1) / 2)) - 1; rightIndex = Math.min(data.nodes.length - 1, currentIndex + Math.max(0, (Settings.windowSize - 1) / 2)) + 1; } leftBorder = leftIndex > 0 ? data.nodes[leftIndex].data.date.time : data.nodes[leftIndex + 1].data.date.time - 1; rightBorder = rightIndex < data.nodes.length ? data.nodes[rightIndex].data.date.time : data.nodes[rightIndex - 1].data.date.time + 1; } private function storeOldGridlinePositions() : void { var _glines : Sprite = mainAxis.gridLines; for (var i : uint = 0;i < _glines.numChildren;++i) { try { var agl : AxisGridLine = _glines.getChildAt(i) as AxisGridLine; var ts : Axis = (agl.value.time <= leftBorder) ? leftAxis : rightAxis; oldGridlinePositions[agl] = ts.X(agl.value); } catch(error : Error) { } } } protected function drawConnectionsToTimeLine() : void { graphics.clear(); var _glines : Sprite = mainAxis.gridLines; for (var i : uint = 0;i < _glines.numChildren;++i) { var agl : AxisGridLine = _glines.getChildAt(i) as AxisGridLine; if (agl.visible && agl.alpha > 0) drawConnectionToGridLine(agl); } } private function drawConnectionToGridLine(agl : AxisGridLine) : void { var ts : Axis = (agl.value.time <= leftBorder) ? leftAxis : rightAxis; graphics.lineStyle(1, 0x43494F, .8 * agl.alpha); var x1 : Number = agl.x1; var y1 : Number = agl.y1; var x2 : Number = Maths.linearInterp(transitionProgress, oldGridlinePositions[agl] || 0, ts.X(agl.value)); var y2 : Number = bounds.height * .61; if (x2 < bounds.width && x2 > 0) { drawCurve(x1, y1, x2, y2); graphics.lineTo(x2, 0); } } private function drawCurve(x1 : Number, y1 : Number, x2 : Number, y2 : Number) : void { graphics.moveTo(x1, y1); graphics.curveTo(x1, y1 + (y2 - y1) * .33, x1 + (x2 - x1) * .5, y1 + (y2 - y1) * .5); graphics.curveTo(x2, y2 - (y2 - y1) * .33, x2, y2); } } }
  1 +/* Copyright 2010, Moritz Stefaner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package eu.stefaner.revisit { import flare.animate.TransitionEvent; import flare.animate.Transitioner; import flare.display.TextSprite; import flare.scale.ScaleType; import flare.scale.TimeScale; import flare.util.Arrays; import flare.util.Maths; import flare.vis.Visualization; import flare.vis.axis.Axis; import flare.vis.axis.AxisGridLine; import flare.vis.data.Data; import flare.vis.data.DataList; import flare.vis.data.NodeSprite; import flare.vis.data.ScaleBinding; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.geom.Rectangle; import flash.text.TextFormat; import flash.utils.Dictionary; /** * @author mo */ public class TweetVisualization extends Visualization { private var app : App; private static const LEFT : String = "LEFT"; private static const MIDDLE : String = "MIDDLE"; private static const RIGHT : String = "RIGHT"; private var leftAxis : Axis; private var middleAxis : Axis; private var rightAxis : Axis; private var mainAxis : Axis; // left end of focus time axis (date.time) private var leftBorder : Number = -1; // left end of focus time axis (date.time) private var rightBorder : Number = -1; // index of leftmost focussed item private var leftIndex : int = -1; // index of rightmost focussed item private var rightIndex : int = -1; // index of currently selected item public var currentIndex : int = -1; public var highlightedTweet : TweetSprite; public var tweetByID : Dictionary = new Dictionary(); private var tweetsByUser : Dictionary = new Dictionary(); public var tweetsByImportance : DataList = new DataList("tweetsByImportance"); private var transitioner : Transitioner; private var oldGridlinePositions : Dictionary = new Dictionary(); public var transitionProgress : Number = 0; private var minDate : Date; public var maxDate : Date; private var leftNodes : DataList; private var middleNodes : DataList; private var rightNodes : DataList; public function TweetVisualization(app : App, b : Rectangle) { super(); bounds = b; this.app = app; data = new Data(); leftNodes = data.addGroup(LEFT); middleNodes = data.addGroup(MIDDLE); rightNodes = data.addGroup(RIGHT); addEventListener(MouseEvent.CLICK, onClick); } protected function initAxes() : void { leftAxis = getAxis(getScaleBinding(LEFT)); middleAxis = getAxis(getScaleBinding(MIDDLE, true)); rightAxis = getAxis(getScaleBinding(RIGHT)); mainAxis = createMainAxis(); leftAxis.y1 = 0; leftAxis.y2 = 0; middleAxis.y1 = 20; middleAxis.y2 = 20; rightAxis.y1 = 0; rightAxis.y2 = 0; middleAxis.labelTextFormat = new TextFormat("FlamaMedium", 14, 0xAAAAAA, true); middleAxis.labelTextMode = TextSprite.EMBED; } private function createMainAxis() : Axis { var a : Axis = new Axis(); a.axisScale = new TimeScale(); a.axisScale.flush = false; a.showLabels = true; if (Settings.showOnlyToday) a.labelFormat = "HH:mm"; a.showLines = true; a.lineLengthY = -10; a.labelOffsetY = 0; a.lineColor = 0x999999; a.lineWidth = 1; a.x1 = 0; a.x2 = bounds.width; a.y1 = bounds.height; a.y2 = bounds.height; a.fixLabelOverlap = true; a.labelTextFormat = new TextFormat("FlamaMedium", 12, 0xCCCCCC, true); a.labelTextMode = TextSprite.EMBED; a.labelOffsetX = -3; a.lineColor = 0x333333; addChild(a); return a; } private function getScaleBinding(group : String, inCenter : Boolean = false) : ScaleBinding { var s : ScaleBinding = new ScaleBinding(); s.scaleType = ScaleType.TIME; s.group = group; s.property = "data.date"; s.data = data; s.flush = true; return s; } private function getAxis(timeScaleBinding : ScaleBinding, showLabels : Boolean = false) : Axis { var a : Axis; a = new Axis(timeScaleBinding); a.showLabels = showLabels; a.showLines = showLabels; a.lineLengthY = 10; a.labelOffsetY = 15; a.lineColor = 0x999999; a.lineWidth = 1; a.y1 = bounds.height; a.y2 = bounds.height; a.fixLabelOverlap = true; a.labelTextFormat = new TextFormat("FlamaMedium", 10, 0x777777, true); a.labelOffsetX = -3; a.lineColor = 0x333333; addChild(a); return a; } // EVENT HANDLERS private function onClick(e : MouseEvent) : void { if (e.target is TweetSprite) { app.onNodeClick(TweetSprite(e.target)); return; } var p:* = e.target; while (p = p.parent){ if (p is TweetSprite) app.onNodeClick(TweetSprite(p)); } } protected function onTransitionStep(event : Event) : void { drawConnectionsToTimeLine(); } // CONTROLS public function displayTweet(ts : TweetSprite = null) : void { for (var i : int = 0;i < data.nodes.length;i++) { if (data.nodes[i] == ts) { currentIndex = i; ts.isNew = false; doUpdate(); return; } } currentIndex = -1; doUpdate(); } public function displayNext() : void { advance(1); } public function displayPrevious() : void { advance(-1); } public function advance(advanceSteps : Number = 1) : void { currentIndex += advanceSteps; doUpdate(); } public function displayRandom() : void { if (!data.nodes.length) return; var ts : TweetSprite ; if (Math.random() > .75 && highlightedTweet) { // display nothing ts = null; } else if (Math.random() > .5 || App.appMode == App.REPLAY) { // pick important ts = tweetsByImportance[Math.floor(tweetsByImportance.length * (1 - Math.pow(Math.random(), .66)))]; } else { // pick new ts = data.nodes[Math.floor(data.nodes.length * (Math.pow(Math.random(), .66)))]; } displayTweet(ts); } override public function update(t : * = null, ...operators) : Transitioner { // t = super.update(t); t = Transitioner.instance(t); if (!data.nodes.length) { return t; } data.nodes.sortBy("data.dateTime"); minDate = data.nodes[0].data.date; maxDate = data.nodes[data.nodes.length - 1].data.date; updateWindow(); updateGroups(); updateAxisPositions(t); updateTweetSprites(t); fixOverlaps(t); updateEdges(t); if (highlightedTweet) addChild(highlightedTweet); return t; } private function updateEdges(t : Transitioner) : void { var n : TweetSprite; for each (n in data.nodes) { n.updateEdges(t); } /* for each (var e:TweetConnection in data.edges) { e.update(t); } */ } private function updateGroups() : void { leftNodes.clear(); middleNodes.clear(); rightNodes.clear(); for (var i : int = 0;i <= leftIndex;i++) { leftNodes.add(data.nodes[i]); data.nodes[i].axis = leftAxis; } for (;i < rightIndex;i++) { middleNodes.add(data.nodes[i]); data.nodes[i].axis = middleAxis; } for (;i < data.nodes.length;i++) { rightNodes.add(data.nodes[i]); data.nodes[i].axis = rightAxis; } } private function updateAxisPositions(t : Transitioner) : void { var pastSpan : Number = leftBorder - minDate.time; var futureSpan : Number = maxDate.time - rightBorder; var totalSpan : Number = maxDate.time - minDate.time; var focusWindowWidth : Number = middleNodes.length ? Settings.focusWindowWidth : 0; var minSize : Number = Settings.minAxisWidth; var w1 : Number = (bounds.width - focusWindowWidth) * (pastSpan / totalSpan); var w2 : Number = focusWindowWidth; var w3 : Number = (bounds.width - focusWindowWidth) * (futureSpan / totalSpan); if (leftNodes.length && (w1 < minSize)) { w3 -= minSize - w1; w1 = minSize; } if (rightNodes.length && (w3 < minSize)) { w1 -= minSize - w3; w3 = minSize; } leftAxis.x1 = 0; leftAxis.x2 = w1; middleAxis.x1 = leftAxis.x2; middleAxis.x2 = leftAxis.x2 + w2; rightAxis.x1 = middleAxis.x2; rightAxis.x2 = middleAxis.x2 + w3; (leftAxis.axisScale as ScaleBinding).updateBinding(); (middleAxis.axisScale as ScaleBinding).updateBinding(); (rightAxis.axisScale as ScaleBinding).updateBinding(); (mainAxis.axisScale as TimeScale).min = minDate; (mainAxis.axisScale as TimeScale).max = maxDate; leftAxis.update(t); middleAxis.update(t); rightAxis.update(t); mainAxis.update(t); } private function updateTweetSprites(t : Transitioner) : void { var n : TweetSprite; data.edges.setProperties({"expanded":false}); data.nodes.setProperties({"highlighted":false, "retweetSourceHighlighted":false, "retweetTargetHighlighted":false, "referenceSourceHighlighted":false, "referenceTargetHighlighted":false}); if (!highlightedTweet) { for each (n in data.nodes) { n.updateAppearance(t, n.axis == middleAxis, bounds.height * .5); } return; } // highlight focused tweet highlightedTweet.highlighted = true; // highlight connected tweets highlightedTweet.visitEdges(function(e : TweetConnection) { e.expanded = true; var target : TweetSprite = TweetSprite(e.other(highlightedTweet)); if (e.type == "retweet") { if (target == e.target) { // target is retweet of highlighted target.retweetTargetHighlighted = true; // show the other retweets, too target.visitEdges(function(e2 : TweetConnection) { e2.expanded = true; }); } else { // highlighted is retweet of target target.retweetSourceHighlighted = true; } } if (e.type == "reference") { if (target == e.source) { target.referenceSourceHighlighted = true; } else { target.referenceTargetHighlighted = true; } } }, NodeSprite.ALL_LINKS); for each (n in data.nodes) { n.updateAppearance(t, n.axis == middleAxis, bounds.height * .5); if (n.appearance == TweetSprite.EXPANDED) { addChild(n); } else { addChildAt(n, 0); } } addChild(highlightedTweet); } private function fixOverlaps(t : Transitioner) : void { updateTweetsByImportanceSorting(); var minY : Number = 100000; var maxY : Number = -100000; var margin : Number; for each (var n:TweetSprite in tweetsByImportance) { var yy : Number = t.$(n).y; if (n.appearance == TweetSprite.EXPANDED) { margin = TweetSprite.baseScale *4 * Settings.minSpacing; } else { margin = TweetSprite.baseScale * Settings.minSpacing; } var intersects : Boolean = true; while (intersects) { yy += n.props.moveDirection; t.$(n).y = yy; n.invalidateEndBounds(); intersects = false; var b1 : Rectangle = n.getEndBounds(t); /* b1.clone() b1.width += margin; */ for each (var n2:TweetSprite in tweetsByImportance) { if (n == n2) { break; } var b2 : Rectangle = n2.getEndBounds(t); /* b2.clone(); b2.width += margin; */ /* var b1 : Rectangle = n.getEndBounds(t); var b2 : Rectangle = n2.getEndBounds(t); */ if (!(b1.right+margin<b2.left || b1.bottom<b2.top || b1.left-margin>b2.right || b1.top>b2.bottom)) { intersects = true; break; } } } yy += n.props.moveDirection * margin; minY = Math.min(yy, minY); maxY = Math.max(yy + n.getEndBounds(t).height, maxY); t.$(n).y = Math.floor(yy); n.invalidateEndBounds(); } // adjust basescale if (!highlightedTweet) { var heightDiff : Number = (maxY - minY) - bounds.height; if (heightDiff > 0) { TweetSprite.baseScale -= Settings.baseScaleShrink; } else if (heightDiff < -200) { TweetSprite.baseScale += Settings.baseScaleGrow; } TweetSprite.baseScale = Math.min(Math.max(TweetSprite.baseScale, Settings.baseScaleMin), Settings.baseScaleMax); } // Logger.info("new baseScale", TweetSprite.baseScale); } private function updateTweetsByImportanceSorting() : void { var a : Array = []; for each (var ts:TweetSprite in tweetsByImportance) { ts.updateActivationLevel(); // a.push(ts.activationLevel); } // trace(a.join("\t")); tweetsByImportance.sortBy(["-activationLevel", "data.random"]); } public function doUpdate() : void { if (!leftAxis) { initAxes(); } storeOldGridlinePositions(); if (transitioner) { if (transitioner.running) { transitioner.stop(); } } transitioner = new Transitioner(Settings.transitionLength, null, true); transitioner.addEventListener(TransitionEvent.STEP, onTransitionStep, false, 0, true); transitionProgress = 0; transitioner.$(this).transitionProgress = 1; update(transitioner); transitioner.play(); } private var moveDirection : Number = -1; public function addTweetSprite(tweet : TweetData, omitCheck : Boolean = false) : TweetSprite { var ts : TweetSprite = new TweetSprite(tweet); ts.x = bounds.width; ts.y = bounds.height * .5; ts.scaleX = 0; ts.scaleY = 0; ts.props.moveDirection = moveDirection; moveDirection *= -1; tweetByID[tweet.id] = ts; data.nodes.add(ts); tweetsByImportance.add(ts); if (!tweetsByUser[tweet.userName]) { tweetsByUser[tweet.userName] = []; } tweetsByUser[tweet.userName].push(ts); var userName : String; for each (var retweet:String in tweet.retweets) { userName = retweet.split("@")[1].toLowerCase(); // App.log(this, "retweet " + retweet + ":" + userName); for each (var ts3:TweetSprite in tweetsByUser[userName]) { if (tweet.isRetweetOf(TweetData(ts3.data))) { createEdge(ts, ts3, "retweet"); } } } for each (var reference:String in tweet.references) { userName = reference.split("@")[1].toLowerCase(); var previous : TweetSprite = null; for each (var ts3:TweetSprite in tweetsByUser[userName]) { if (ts3.data.dateTime >= ts.data.dateTime) { break; } previous = ts3; } if (previous) { createEdge(ts, previous, "reference"); } } if (!omitCheck) checkForTooManyTweets(ts); return ts; } public function checkForTooManyTweets(lastAdded : TweetSprite = null) : void { if (data.nodes.length > Settings.maxItems) { updateTweetsByImportanceSorting(); while (data.nodes.length > Settings.maxItems) { if (tweetsByImportance[tweetsByImportance.length - 1] != lastAdded) { removeTweetSprite(tweetsByImportance[tweetsByImportance.length - 1]); } else { removeTweetSprite(tweetsByImportance[tweetsByImportance.length - 2]); } } } } private function removeTweetSprite(ts : TweetSprite) : void { Arrays.remove(tweetsByUser[ts.data.userName], ts); tweetsByImportance.remove(ts); tweetByID[ts.data.id] = null; data.removeNode(ts); try { ts.parent.removeChild(ts); } catch(error : Error) { } Arrays.remove(app.newTweets, ts); } private function createEdge(ts : NodeSprite, ts2 : NodeSprite, type : String = "") : void { var e : TweetConnection = new TweetConnection(ts, ts2, type); ts.addOutEdge(e); ts2.addInEdge(e); data.addEdge(e); } private function updateWindow() : void { // currentIndex = Math.max(0, Math.min(data.nodes.length - 1, currentIndex)); if (currentIndex > data.nodes.length - 1 || currentIndex < 0) { currentIndex = -1; highlightedTweet = null; leftIndex = data.nodes.length - 1; rightIndex = data.nodes.length - 1; } else { highlightedTweet = data.nodes[currentIndex]; leftIndex = Math.max(0, currentIndex - Math.max(0, (Settings.windowSize - 1) / 2)) - 1; rightIndex = Math.min(data.nodes.length - 1, currentIndex + Math.max(0, (Settings.windowSize - 1) / 2)) + 1; } leftBorder = leftIndex > 0 ? data.nodes[leftIndex].data.date.time : data.nodes[leftIndex + 1].data.date.time - 1; rightBorder = rightIndex < data.nodes.length ? data.nodes[rightIndex].data.date.time : data.nodes[rightIndex - 1].data.date.time + 1; } private function storeOldGridlinePositions() : void { var _glines : Sprite = mainAxis.gridLines; for (var i : uint = 0;i < _glines.numChildren;++i) { try { var agl : AxisGridLine = _glines.getChildAt(i) as AxisGridLine; var ts : Axis = (agl.value.time <= leftBorder) ? leftAxis : rightAxis; oldGridlinePositions[agl] = ts.X(agl.value); } catch(error : Error) { } } } protected function drawConnectionsToTimeLine() : void { graphics.clear(); var _glines : Sprite = mainAxis.gridLines; for (var i : uint = 0;i < _glines.numChildren;++i) { var agl : AxisGridLine = _glines.getChildAt(i) as AxisGridLine; if (agl.visible && agl.alpha > 0) drawConnectionToGridLine(agl); } } private function drawConnectionToGridLine(agl : AxisGridLine) : void { var ts : Axis = (agl.value.time <= leftBorder) ? leftAxis : rightAxis; graphics.lineStyle(1, 0x43494F, .8 * agl.alpha); var x1 : Number = agl.x1; var y1 : Number = agl.y1; var x2 : Number = Maths.linearInterp(transitionProgress, oldGridlinePositions[agl] || 0, ts.X(agl.value)); var y2 : Number = bounds.height * .61; if (x2 < bounds.width && x2 > 0) { drawCurve(x1, y1, x2, y2); graphics.lineTo(x2, 0); } } private function drawCurve(x1 : Number, y1 : Number, x2 : Number, y2 : Number) : void { graphics.moveTo(x1, y1); graphics.curveTo(x1, y1 + (y2 - y1) * .33, x1 + (x2 - x1) * .5, y1 + (y2 - y1) * .5); graphics.curveTo(x2, y2 - (y2 - y1) * .33, x2, y2); } } }

0 comments on commit 597be37

Please sign in to comment.
Something went wrong with that request. Please try again.