diff --git a/FBVis/Crawler.pde b/FBVis/Crawler.pde new file mode 100644 index 0000000..879b485 --- /dev/null +++ b/FBVis/Crawler.pde @@ -0,0 +1,82 @@ +class Crawler { + PVector pos; + PVector prev_pos; + + Node target; + + // Appearance + float travel_lerp; + float travel_y_lerp_mult; + + public Crawler(Node source, Node target) + { + this.pos = source.pos.copy(); + this.prev_pos = source.pos.copy(); + this.target = target; + + this.travel_lerp = random(CONFIG.payloadSegmentLerpMin, CONFIG.payloadSegmentLerpMax); + this.travel_y_lerp_mult = random(0.5, 0.8); + + // if the source is personnode + if (source instanceof PersonNode) { + ((PersonNode) source).refresh(); + } + } + + private void update() { + // Move towards target lerp + this.prev_pos.set(this.pos); + // this.pos = PVector.lerp(this.pos, this.target.pos, 0.1); + this.pos.x = lerp(this.pos.x, this.target.pos.x, this.travel_lerp); + this.pos.y = lerp(this.pos.y, this.target.pos.y, this.travel_lerp * this.travel_y_lerp_mult); + } + + public boolean hasArrived() { + if (this.getArrived()) { + + if (target instanceof PersonNode) { + ((PersonNode) target).refresh(); + } + return true; + } + + return false; + } + + protected boolean getArrived() { + // Return true if close enough to target + return (this.pos.dist(this.target.pos) < 10); + } +} + +class Crawlers { + public ArrayList inboundCrawlers; + public ArrayList outboundCrawlers; + + public Crawlers() { + this.inboundCrawlers = new ArrayList(); + this.outboundCrawlers = new ArrayList(); + } + + public void addCrawler(Node source, Node target, boolean inbound) { + if (inbound) { + this.inboundCrawlers.add(new Crawler(source, target)); + } else { + this.outboundCrawlers.add(new Crawler(source, target)); + } + } + + public void update() { + // Update crawlers + this.inboundCrawlers.forEach(Crawler::update); + this.outboundCrawlers.forEach(Crawler::update); + + // Remove crawlers that have arrived + this.inboundCrawlers.removeIf(Crawler::hasArrived); + this.outboundCrawlers.removeIf(Crawler::hasArrived); + } + + public int getNumCrawlers() { + return this.inboundCrawlers.size() + this.outboundCrawlers.size(); + } +} diff --git a/FBVis/CrawlerRender.pde b/FBVis/CrawlerRender.pde new file mode 100644 index 0000000..0f80488 --- /dev/null +++ b/FBVis/CrawlerRender.pde @@ -0,0 +1,37 @@ +class RenderCrawlerLayer extends RenderLayer { + + private Crawlers crawlers; + + public RenderCrawlerLayer(Crawlers crawlers) { + super(); + this.crawlers = crawlers; + } + + private void updateCrawlers() { + this.crawlers.update(); + } + + @Override + protected void renderGraphics() { + this.pg.clear(); + this.pg.pushMatrix(); + this.pg.translate(width/2, height/2); + this.pg.blendMode(SCREEN); + this.pg.strokeWeight(1); + + // Draw inbound crawlers + this.pg.stroke(160, 80, 80); + for (Crawler c : this.crawlers.inboundCrawlers) { + this.pg.line(c.pos.x, c.pos.y, c.prev_pos.x, c.prev_pos.y); + } + + // Draw outbound crawlers + this.pg.stroke(80, 160, 80); + for (Crawler c : this.crawlers.outboundCrawlers) { + this.pg.line(c.pos.x, c.pos.y, c.prev_pos.x, c.prev_pos.y); + } + this.pg.popMatrix(); + + this.updateCrawlers(); + } +} diff --git a/Exceptions.pde b/FBVis/Exceptions.pde similarity index 96% rename from Exceptions.pde rename to FBVis/Exceptions.pde index ee63160..ff40c68 100644 --- a/Exceptions.pde +++ b/FBVis/Exceptions.pde @@ -1,6 +1,5 @@ -public class NotDirectoryException extends Exception { - public NotDirectoryException(String errorMessage) { - super(errorMessage); - } -} - +public class NotDirectoryException extends Exception { + public NotDirectoryException(String errorMessage) { + super(errorMessage); + } +} diff --git a/FBVis/FBVis.pde b/FBVis/FBVis.pde new file mode 100644 index 0000000..c37f58d --- /dev/null +++ b/FBVis/FBVis.pde @@ -0,0 +1,169 @@ +import java.util.Map; + + + +// ============ application states enum ============ +enum AppState { + INIT, RUNNING, PAUSED +} +AppState state = AppState.INIT; + +// ============ data ============ + +FBVisConfig CONFIG; + +MessageManager msgManager; +MessageScheduler msgScheduler; + +MasterPersonNode root; +HashMap personNodes; + +// Crawers +Crawlers crawlers; + +// ============ render layers ============ + +RenderPeopleLayer peopleLayer; +RenderCrawlerLayer crawlerLayer; +String RENDERER = P2D; + +// ============ Sprites ============ + +Sprites sprites; + +// ============ visualization ============ + +void settings() { + size(1920, 1080, RENDERER); + // fullScreen(RENDERER); +} + +void setup() { + CONFIG = new FBVisConfig(); + thread("initializeData"); + thread("preRender"); + frameRate(144); +} + +void initializeData() { + // Start timer + int startTime = millis(); + msgManager = new MessageManager(CONFIG.dataRootPath); + int duration = millis() - startTime; + println("Done loading data"); + println(msgManager.organizedMessagesList.size()); //<>// + println("Took " + duration + "ms"); + + msgScheduler = new MessageScheduler(msgManager); + + // Initialize the person nodes map with root + personNodes = new HashMap(); //<>// //<>// + int root_id = msgManager.nameToIdMap.get(CONFIG.masterName); //<>// + root = new MasterPersonNode(root_id, CONFIG.masterName); + personNodes.put(root_id, root); + + // Initialize visualization + peopleLayer = new RenderPeopleLayer(root); + + crawlers = new Crawlers(); + crawlerLayer = new RenderCrawlerLayer(crawlers); + + state = AppState.RUNNING; +} + +void preRender() { + sprites = new Sprites(); +} + +void draw() { + if (state == AppState.INIT) { + background(255); + } else if (state == AppState.RUNNING) { + background(30); + fill(255); + + // Do something with msg data every turn + ArrayList msgs = msgScheduler.nextTimeStep(); + for (int i = 0; i < msgs.size(); i++) { + MessageData msg = msgs.get(i); + if (msg == null) { + state = AppState.PAUSED; + return; + } + updateIdNodeMap(msg); + updateCrawlers(msg); + } + + image(peopleLayer.getRender(), 0, 0); + blendMode(ADD); + image(crawlerLayer.getRender(), 0, 0); + text(frameRate, 10, 10); + text(msgScheduler.getCurrentTime(), 10, 30); + text("Crawlers: " + crawlers.getNumCrawlers(), 10, 50); + + if (msgScheduler.finished()) { + state = AppState.PAUSED; + } + + } else if (state == AppState.PAUSED) { + background(30); + fill(255); + + image(peopleLayer.getRender(), 0, 0); + blendMode(ADD); + image(crawlerLayer.getRender(), 0, 0); + text(frameRate, 10, 10); + text(msgScheduler.getCurrentTime(), 10, 30); + } + + if (frameCount % 60 == 0) { + println(frameRate); + } +} + +// ============ data processing ============ + +void updateIdNodeMap(MessageData msg) { + int sender_id = msg.sender_id; + int[] receiver_ids = msg.receiver_ids; + + if (personNodes.containsKey(sender_id) == false) { + PersonNode senderNode = new PersonNode(sender_id, msgManager.idToNameMap.get(sender_id)); + personNodes.put(sender_id, senderNode); + root.addNode(senderNode); + } + + for (int i = 0; i < receiver_ids.length; i++) { + int receiver_id = receiver_ids[i]; + if (personNodes.containsKey(receiver_id) == false) { + PersonNode receiverNode = new PersonNode(receiver_id, msgManager.idToNameMap.get(receiver_id)); + personNodes.put(receiver_id, receiverNode); + root.addNode(receiverNode); + } + } +} + +void updateCrawlers(MessageData msg) { + for (int i = 0; i < msg.receiver_ids.length; i++) { + crawlers.addCrawler( + personNodes.get(msg.sender_id), + personNodes.get(msg.receiver_ids[i]), + + // inbound? (if sender is not root, then it's inbound) + (msg.sender_id != root.id) + ); + } +} + + +// ============ input ============ + +void keyPressed() { + if (key == ' ') { + if (state == AppState.RUNNING) { + state = AppState.PAUSED; + } else if (state == AppState.PAUSED) { + state = AppState.RUNNING; + } + } +} diff --git a/FBVis.pde b/FBVis/FBVis.pde.old similarity index 90% rename from FBVis.pde rename to FBVis/FBVis.pde.old index 63983ee..20c2c3a 100644 --- a/FBVis.pde +++ b/FBVis/FBVis.pde.old @@ -1,431 +1,437 @@ -// Main entry point of the program - -// TODO: re arrange the persons based on groups -// TODO: filter out specific groups -// TODO: broadcast effect for personal wall postings -// TODO: stattrak send & receive metrics per person -// TODO: make rendering of the people and messages with shaders on a separate graphic layer - -import java.util.Map; -import ch.bildspur.postfx.builder.*; -import ch.bildspur.postfx.pass.*; -import ch.bildspur.postfx.*; - -// Configuration is the most important so it needs to be set up first -FBVisConfig CONFIG; -PostFX fx; - -// Render layers -RenderUILayer g_uiLayer; -RenderPeopleLayer g_pplLayer; - -StatCardHover statcardHover; - -// Hash map to hold to the person -IntDict nameToPersonIndexMap; -ArrayList persons; - -ArrayList payloads; -final int PAYLOADS_MAXSIZE = 2048; -PayloadFactory payloadFactory; -MessageManager man; - -PackedSpiral g_layoutGen; - -// For display loading bars on splashscreen -Progress progress; - -// timing -long currentTimestamp; -long nextTimestamp; -Timeline timeline; -SpeedControl speedControl; - -// Font -PFont font; -PFont monospaceFont; - -// Global togglable flags -Boolean g_toggle_UI = true; - -// Mouse dragging control -Boolean g_mouseLocked = false; -float mouseDown_x = 0; -float mouseDown_y = 0; - -float g_offsetX = 0.0; -float g_offsetY = 0.0; - -int g_state; -final int STATE_UNINIT = 0; -final int STATE_RUN = 1; -final int STATE_PAUSE = 2; - -void settings() { - // Size and fullscreen should go inside here - // But none of the Processing functions are available - size(1920, 1080, P2D); - smooth(2); -} - -void setup() { - // There are 3 main stages in the setup function - // [1] Load and read configuration file - // [2] Run regular Processing 3 setup stuff - // [3] Run the initialization routine - - // [1] Configuration - CONFIG = new FBVisConfig(); - - // [2] - g_state = STATE_UNINIT; - nameToPersonIndexMap = new IntDict(); - persons = new ArrayList(); - - payloads = new ArrayList(); - payloadFactory = new PayloadFactory(payloads); - - timeline = new Timeline(50, height - 50, width - 100, 30); - speedControl = new SpeedControl(); - statcardHover = new StatCardHover(); - - if (CONFIG.enableShaders) { - fx = new PostFX(this); - fx.preload(RGBSplitPass.class); - fx.preload(BloomPass.class); - } - frameRate(CONFIG.fps); - - // Geometry and layout - g_layoutGen = new PackedSpiral(70, width/2, height/2); - - // [3] - thread("initialize"); -} - -// Async initialization function -void initialize() { - // Load types - font = createFont("Helvetica", 32); - monospaceFont = createFont("Courier", 32); - - // Load and process - progress = new Progress(); - man = new MessageManager(CONFIG.dataRootPath); - - // Initialize layers - g_uiLayer = new RenderUILayer(); - g_uiLayer.timeline = timeline; - g_uiLayer.speedControl = speedControl; - g_uiLayer.statCardHover = statcardHover; - - g_pplLayer = new RenderPeopleLayer(); - g_pplLayer.persons = persons; - - // Set flag to true when done - g_state = STATE_RUN; -} - -int gi = 0; -boolean startFlag = true; - -// TODO: reset program -void reset() { - // not implemented -} - -void drawLoadingScreen() { - // Draws the loading screen (before finished initialization) - background(0); - fill(255); - noStroke(); - textAlign(LEFT, TOP); - text("FBVis version 0.6.1", 10, 10); - text("github.com/FSXAC/FBVis", 10, 25); - textAlign(CENTER, CENTER); - text("Loading Messenger data . . .", width/2, height/2); - - if (progress != null) { - stroke(50); - float start = 0.4 * width; - float end = 0.6 * width; - float y = height / 2 + 20; - line(start, y, end, y); - - stroke(255); - float totalProgress = ( - progress.getLoadingLargeProgress() + progress.getLoadingProgress() + - progress.getSortingProgress() - ) / 3; - float totalWidth = map(totalProgress, 0, 1, 0, 0.2 * width); - line(start, y, start + totalWidth, y); - } -} - -void draw() { - switch (g_state) { - case STATE_UNINIT: - drawLoadingScreen(); - break; - case STATE_RUN: - updateState(); - drawRun(); - break; - case STATE_PAUSE: - drawRun(); - break; - } -} - -void drawRun() { - // background(0); - fill(0, 100); - noStroke(); - rect(0, 0, width, height); - - // Draw a grid of people - pushMatrix(); - translate(g_offsetX, g_offsetY); - g_pplLayer.render(); - image(g_pplLayer.pg, 0, 0); - - // Draw and update payload - blendMode(SCREEN); - drawPayload(); - blendMode(BLEND); - - if (CONFIG.enableShaders) { - fx.render() - .bloom(0.8, 5, 30) - .rgbSplit(constrain(payloads.size(), 0, 20)) - .compose(); - } - popMatrix(); - - // Draw current date and timeline - if (g_toggle_UI) { - updateTimeline(); - g_uiLayer.timestamp = currentTimestamp; - g_uiLayer.render(); - image(g_uiLayer.pg, 0, 0); - } - - // HACK: we need another robust way to indicate global index - if (gi >= man.organizedMessagesList.size()) { - gi = 0; - g_state = STATE_PAUSE; - } -} - -void updateState() { - if (gi == 0) { - resetPersonStats(); - } - - if (CONFIG.enableUniformTime) { - if (startFlag) { - long firstTimeStamp = man.organizedMessagesList.get(gi).timestamp; - if (firstTimeStamp > CONFIG.startTimestamp) { - currentTimestamp = firstTimeStamp; - } else { - currentTimestamp = CONFIG.startTimestamp; - } - - if (CONFIG.enableVerbose) - println("currentTimestamp: " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date(currentTimestamp))); - - startFlag = false; - } - - // Get all the messages for the next time stamp - nextTimestamp = currentTimestamp + (CONFIG.deltaTimestamp * speedControl.getSpeed()); - - long messageTimestamp = man.organizedMessagesList.get(gi % man.organizedMessagesList.size()).timestamp; - - long dt = messageTimestamp - nextTimestamp; - if (dt > CONFIG.deltaAutoSkipTimestamp) { - // Then we know that we need to skip - nextTimestamp = messageTimestamp; - } else if (dt > 0) { - // Don't do anything - } else { - while (messageTimestamp < nextTimestamp) { - int di = gi % man.organizedMessagesList.size(); - MessageData current = man.organizedMessagesList.get(di); - - processCurrentmessageData(current); - gi++; - - messageTimestamp = current.timestamp; - } - } - - currentTimestamp = nextTimestamp; - - } else { - for (int i = 0; i < (CONFIG.numMsgPerFrame * speedControl.getSpeed()); i++) { - int di = gi % man.organizedMessagesList.size(); - MessageData current = man.organizedMessagesList.get(di); - processCurrentmessageData(current); - gi++; - currentTimestamp = current.timestamp; - } - } - - // Update persons - for (PersonNode person : persons) { - person.update(); - } -} - -void updateTimeline() { - timeline.setPercentage(((float) gi % man.organizedMessagesList.size()) / man.organizedMessagesList.size()); - timeline.handleMouseInput(); -} - -void processCurrentmessageData(MessageData current) { - // check if sender and receiver in the persons map - if (!nameToPersonIndexMap.hasKey(current.sender)) { - - // Add to array and get the index and put it in the map - addNewPerson(current.sender); - } - - for (String receiver : current.receivers) { - if (!nameToPersonIndexMap.hasKey(receiver)) { - addNewPerson(receiver); - } - - // For each receiving end, we make a payload - PersonNode senderPerson = persons.get(nameToPersonIndexMap.get(current.sender)); - PersonNode receivePerson = persons.get(nameToPersonIndexMap.get(receiver)); - - if (current.receivers.size() <= 1) { - payloadFactory.makeIndividualPayload(senderPerson, receivePerson, current.contentSizeSqrt); - } else { - payloadFactory.makeGroupPayload(senderPerson, receivePerson, current.contentSizeSqrt); - } - - // For each person, update their stats - senderPerson.incrementMsgSent(); - receivePerson.incrementMsgReceived(); - senderPerson.stats.lastInteractTimestamp = current.timestamp; - receivePerson.stats.lastInteractTimestamp = current.timestamp; - } -} - -void addNewPerson(String name) { - final int index = persons.size(); - - PersonNode new_person; - if (name.equals(CONFIG.masterName)) { - new_person = new PersonMasterNode(name); - } else { - new_person = new PersonNode(name); - } - - final PVector new_position = g_layoutGen.pos(index); - - new_person.setTargetPosition(new_position.x, new_position.y); - persons.add(new_person); - - nameToPersonIndexMap.set(name, index); -} - -// TODO: could be instanciated elsewhere and just cleared -ArrayList toBeRemoved = new ArrayList(); -void drawPayload() { - - toBeRemoved.clear(); - - // Draw and check - for (Payload payload : payloads) { - payload.draw(); - - if (payload.hasArrived()) { - toBeRemoved.add(payload); - } - } - - // Remove from active list - for (Payload payload : toBeRemoved) { - payloads.remove(payload); - } -} - - // Draw by listing all the messages one per frame -void drawListMode(MessageData current) { - float y = (frameCount % 40) * height / 40; - fill(50); - String date = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date(current.timestamp)); - textAlign(LEFT, TOP); - textSize(10); - text(date, 10, y); - text(current.sender, 80, y); - text(current.content, 200, y); -} - -void resetPersonStats() { - for (PersonNode p : persons) { - p.stats.reset(); - } -} - - -// Input handling -void keyPressed() { - if (key == 'l') { - currentTimestamp += CONFIG.deltaSkipTimestamp; - } else if (key == 'h') { - g_toggle_UI = !g_toggle_UI; - } - - // Speed control (test) - else if (key == '=') { - speedControl.incrementSpeed(); - } - else if (key == '-') { - speedControl.decrementSpeed(); - } - - // play/pause - else if (key == ' ') { - if (g_state == STATE_RUN) { - g_state = STATE_PAUSE; - } else if (g_state == STATE_PAUSE) { - g_state = STATE_RUN; - } - } -} - - -// Mouse input handling -void mousePressed() { - g_mouseLocked = true; - - mouseDown_x = mouseX - g_offsetX; - mouseDown_y = mouseY - g_offsetY; -} - -void mouseDragged() { - if (g_state < STATE_RUN) { - return; - } - - if (g_mouseLocked) { - g_offsetX = mouseX - mouseDown_x; - g_offsetY = mouseY - mouseDown_y; - } -} - -void mouseReleased() { - g_mouseLocked = false; -} - -float mouseXSpace() { - return mouseX - g_offsetX; -} - -float mouseYSpace() { - return mouseY - g_offsetY; -} +// Main entry point of the program + +// TODO: re arrange the persons based on groups +// TODO: filter out specific groups +// TODO: broadcast effect for personal wall postings +// TODO: stattrak send & receive metrics per person +// TODO: make rendering of the people and messages with shaders on a separate graphic layer + +import java.util.Map; +import ch.bildspur.postfx.builder.*; +import ch.bildspur.postfx.pass.*; +import ch.bildspur.postfx.*; + +// Configuration is the most important so it needs to be set up first +FBVisConfig CONFIG; +PostFX fx; + +// Render layers +RenderUILayer g_uiLayer; +RenderPeopleLayer g_pplLayer; + +StatCardHover statcardHover; + +// Hash map to hold to the person +IntDict nameToPersonIndexMap; + +// People +MasterPersonNode rootPerson; + +ArrayList payloads; +final int PAYLOADS_MAXSIZE = 2048; +PayloadFactory payloadFactory; +MessageManager man; + +// For display loading bars on splashscreen +Progress progress; + +// timing +long currentTimestamp; +long nextTimestamp; +Timeline timeline; +SpeedControl speedControl; + +// Font +PFont font; +PFont monospaceFont; + +// Global togglable flags +Boolean g_toggle_UI = true; + +// Mouse dragging control +Boolean g_mouseLocked = false; +float mouseDown_x = 0; +float mouseDown_y = 0; + +float g_offsetX = 0.0; +float g_offsetY = 0.0; + +int g_state; +final int STATE_UNINIT = 0; +final int STATE_RUN = 1; +final int STATE_PAUSE = 2; + +void settings() { + // Size and fullscreen should go inside here + // But none of the Processing functions are available + size(1920, 1080, P3D); + smooth(2); +} + +void setup() { + // There are 3 main stages in the setup function + // [1] Load and read configuration file + // [2] Run regular Processing 3 setup stuff + // [3] Run the initialization routine + + // [1] Configuration + CONFIG = new FBVisConfig(); + + // [2] + g_state = STATE_UNINIT; + nameToPersonIndexMap = new IntDict(); + rootPerson = new MasterPersonNode(); + + payloads = new ArrayList(); + payloadFactory = new PayloadFactory(payloads); + + timeline = new Timeline(50, height - 50, width - 100, 30); + speedControl = new SpeedControl(); + statcardHover = new StatCardHover(); + + if (CONFIG.enableShaders) { + fx = new PostFX(this); + fx.preload(RGBSplitPass.class); + fx.preload(BloomPass.class); + } + frameRate(CONFIG.fps); + + // [3] + thread("initialize"); +} + +// Async initialization function +void initialize() { + // Load types + font = createFont("Helvetica", 32); + monospaceFont = createFont("Courier", 32); + + // Load and process + progress = new Progress(); + man = new MessageManager(CONFIG.dataRootPath); + + // Initialize layers + g_uiLayer = new RenderUILayer(); + g_uiLayer.timeline = timeline; + g_uiLayer.speedControl = speedControl; + g_uiLayer.statCardHover = statcardHover; + + g_pplLayer = new RenderPeopleLayer(); + g_pplLayer.persons = persons; + + // Set flag to true when done + g_state = STATE_RUN; +} + +int gi = 0; +boolean startFlag = true; + +// TODO: reset program +void reset() { + // not implemented +} + +void drawLoadingScreen() { + // Draws the loading screen (before finished initialization) + background(0); + fill(255); + noStroke(); + textAlign(LEFT, TOP); + text("FBVis version 0.6.1", 10, 10); + text("github.com/FSXAC/FBVis", 10, 25); + textAlign(CENTER, CENTER); + text("Loading Messenger data . . .", width/2, height/2); + + if (progress != null) { + stroke(50); + float start = 0.4 * width; + float end = 0.6 * width; + float y = height / 2 + 20; + line(start, y, end, y); + + stroke(255); + float totalProgress = ( + progress.getLoadingLargeProgress() + progress.getLoadingProgress() + + progress.getSortingProgress() + ) / 3; + float totalWidth = map(totalProgress, 0, 1, 0, 0.2 * width); + line(start, y, start + totalWidth, y); + } +} + +void draw() { + switch (g_state) { + case STATE_UNINIT: + drawLoadingScreen(); + break; + case STATE_RUN: + updateState(); + drawRun(); + break; + case STATE_PAUSE: + drawRun(); + break; + } +} + +void drawRun() { + // background(0); + fill(0, 100); + noStroke(); + rect(0, 0, width, height); + + // Draw a grid of people + pushMatrix(); + translate(g_offsetX, g_offsetY); + g_pplLayer.render(); + image(g_pplLayer.pg, 0, 0); + + // Draw and update payload + blendMode(SCREEN); + drawPayload(); + blendMode(BLEND); + + if (CONFIG.enableShaders) { + fx.render() + .bloom(0.8, 5, 30) + .rgbSplit(constrain(payloads.size(), 0, 20)) + .compose(); + } + popMatrix(); + + // Draw current date and timeline + if (g_toggle_UI) { + updateTimeline(); + g_uiLayer.timestamp = currentTimestamp; + g_uiLayer.render(); + image(g_uiLayer.pg, 0, 0); + } + + // HACK: we need another robust way to indicate global index + if (gi >= man.organizedMessagesList.size()) { + gi = 0; + g_state = STATE_PAUSE; + } +} + +void updateState() { + if (gi == 0) { + resetPersonStats(); + } + + if (CONFIG.enableUniformTime) { + if (startFlag) { + long firstTimeStamp = man.organizedMessagesList.get(gi).timestamp; + if (firstTimeStamp > CONFIG.startTimestamp) { + currentTimestamp = firstTimeStamp; + } else { + currentTimestamp = CONFIG.startTimestamp; + } + + if (CONFIG.enableVerbose) + println("currentTimestamp: " + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date(currentTimestamp))); + + startFlag = false; + } + + // Get all the messages for the next time stamp + nextTimestamp = currentTimestamp + (CONFIG.deltaTimestamp * speedControl.getSpeed()); + + long messageTimestamp = man.organizedMessagesList.get(gi % man.organizedMessagesList.size()).timestamp; + + long dt = messageTimestamp - nextTimestamp; + if (dt > CONFIG.deltaAutoSkipTimestamp) { + // Then we know that we need to skip + nextTimestamp = messageTimestamp; + } else if (dt > 0) { + // Don't do anything + } else { + while (messageTimestamp < nextTimestamp) { + int di = gi % man.organizedMessagesList.size(); + MessageData current = man.organizedMessagesList.get(di); + + processCurrentmessageData(current); + gi++; + + messageTimestamp = current.timestamp; + } + } + + currentTimestamp = nextTimestamp; + + } else { + for (int i = 0; i < (CONFIG.numMsgPerFrame * speedControl.getSpeed()); i++) { + int di = gi % man.organizedMessagesList.size(); + MessageData current = man.organizedMessagesList.get(di); + processCurrentmessageData(current); + gi++; + currentTimestamp = current.timestamp; + } + } + + // Update persons + for (PersonNode person : persons) { + person.update(); + } +} + +void updateTimeline() { + timeline.setPercentage(((float) gi % man.organizedMessagesList.size()) / man.organizedMessagesList.size()); + timeline.handleMouseInput(); +} + +void processCurrentmessageData(MessageData current) { + // check if sender and receiver in the persons map + if (!nameToPersonIndexMap.hasKey(current.sender)) { + + // Add to array and get the index and put it in the map + addNewPerson(current.sender); + } + + for (String receiver : current.receivers) { + if (!nameToPersonIndexMap.hasKey(receiver)) { + addNewPerson(receiver); + } + + // For each receiving end, we make a payload + PersonNode senderPerson = persons.get(nameToPersonIndexMap.get(current.sender)); + PersonNode receivePerson = persons.get(nameToPersonIndexMap.get(receiver)); + + if (current.receivers.size() <= 1) { + payloadFactory.makeIndividualPayload(senderPerson, receivePerson, current.contentSizeSqrt); + } else { + payloadFactory.makeGroupPayload(senderPerson, receivePerson, current.contentSizeSqrt); + } + + // For each person, update their stats + senderPerson.incrementMsgSent(); + receivePerson.incrementMsgReceived(); + senderPerson.stats.lastInteractTimestamp = current.timestamp; + receivePerson.stats.lastInteractTimestamp = current.timestamp; + } +} + +void makeNewPerson(String name) { + // final int index = persons.size(); + + // PersonNode new_person; + // if (name.equals(CONFIG.masterName)) { + // new_person = new PersonMasterNode(name); + // } else { + // new_person = new PersonNode(name); + // } + + // final PVector new_position = g_layoutGen.pos(index); + + // new_person.setTargetPos(new_position.x, new_position.y); + // persons.add(new_person); + + // nameToPersonIndexMap.set(name, index); + + Node new_person; + if (name.equals(CONFIG.masterName)) { + new_person = new PersonMasterNode(name); + } else { + new_person = new PersonNode(name); + } + + return new_person; +} + +// TODO: could be instanciated elsewhere and just cleared +ArrayList toBeRemoved = new ArrayList(); +void drawPayload() { + + toBeRemoved.clear(); + + // Draw and check + for (Payload payload : payloads) { + payload.draw(); + + if (payload.hasArrived()) { + toBeRemoved.add(payload); + } + } + + // Remove from active list + for (Payload payload : toBeRemoved) { + payloads.remove(payload); + } +} + + // Draw by listing all the messages one per frame +void drawListMode(MessageData current) { + float y = (frameCount % 40) * height / 40; + fill(50); + String date = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date(current.timestamp)); + textAlign(LEFT, TOP); + textSize(10); + text(date, 10, y); + text(current.sender, 80, y); + text(current.content, 200, y); +} + +void resetPersonStats() { + for (PersonNode p : persons) { + p.stats.reset(); + } +} + + +// Input handling +void keyPressed() { + if (key == 'l') { + currentTimestamp += CONFIG.deltaSkipTimestamp; + } else if (key == 'h') { + g_toggle_UI = !g_toggle_UI; + } + + // Speed control (test) + else if (key == '=') { + speedControl.incrementSpeed(); + } + else if (key == '-') { + speedControl.decrementSpeed(); + } + + // play/pause + else if (key == ' ') { + if (g_state == STATE_RUN) { + g_state = STATE_PAUSE; + } else if (g_state == STATE_PAUSE) { + g_state = STATE_RUN; + } + } +} + + +// Mouse input handling +void mousePressed() { + g_mouseLocked = true; + + mouseDown_x = mouseX - g_offsetX; + mouseDown_y = mouseY - g_offsetY; +} + +void mouseDragged() { + if (g_state < STATE_RUN) { + return; + } + + if (g_mouseLocked) { + g_offsetX = mouseX - mouseDown_x; + g_offsetY = mouseY - mouseDown_y; + } +} + +void mouseReleased() { + g_mouseLocked = false; +} + +float mouseXSpace() { + return mouseX - g_offsetX; +} + +float mouseYSpace() { + return mouseY - g_offsetY; +} diff --git a/FBVisConfig.pde b/FBVis/FBVisConfig.pde similarity index 100% rename from FBVisConfig.pde rename to FBVis/FBVisConfig.pde diff --git a/FBVis/Geometry.pde b/FBVis/Geometry.pde new file mode 100644 index 0000000..1302e66 --- /dev/null +++ b/FBVis/Geometry.pde @@ -0,0 +1,71 @@ +// Helper functions for geometry +PVector cart2sph(PVector v) { + float r = sqrt(v.x * v.x + v.y * v.y + v.z * v.z); + float theta = acos(v.z / r); + float phi = atan2(v.y, v.x); + + return new PVector(r, theta, phi); +} + +PVector sph2cart(PVector v) { + float x = v.x * sin(v.y) * cos(v.z); + float y = v.x * sin(v.y) * sin(v.z); + float z = v.x * cos(v.y); + + return new PVector(x, y, z); +} + +// Generate a list of points on a sphere +PVector[] genPts3DSphere(int n, float sphereRadius) { + PVector[] points = new PVector[n]; + float phi = PI * (3 - sqrt(5)); + for (int i = 0; i < n; i++) { + float theta = phi * i; + float y = 1 - (i / (float) (n - 1)) * 2; + float radius = sqrt(1 - y * y); + float x = cos(theta) * radius; + float z = sin(theta) * radius; + + points[i] = new PVector(x * sphereRadius, y * sphereRadius, z * sphereRadius); + } + + return points; +} + +PVector[] genPts2DSpiral(int n, float radius) { + + if (n > 0) { + n += 1; + } + + PVector[] points = new PVector[n]; + for (int i = 0; i < n; i++) { + final float a = 2.4 * i; + final float r = radius * sqrt(i); + final float x = r * cos(a); + final float y = r * sin(a); + + points[i] = new PVector(x, y); + } + + return points; +} + +PVector[] genPts2DPackedSpiral(int n, float radius) { + PVector[] points = new PVector[n]; + + float power = 0.07; + float baseCoeff = 0.3; + float fanoutOffsetMult = 1.001; + float r; + + for (int i = 0; i < n; i++) { + r = -1 * pow(radius, pow(baseCoeff * i, power)); + float x = r * cos(i * fanoutOffsetMult); + float y = r * sin(i * fanoutOffsetMult); + + points[i] = new PVector(x, y); + } + + return points; +} \ No newline at end of file diff --git a/FBVis/Message.pde b/FBVis/Message.pde new file mode 100644 index 0000000..d2ff628 --- /dev/null +++ b/FBVis/Message.pde @@ -0,0 +1,290 @@ +import java.util.Collections; + +// This should be a class that manages all messages, and message utils +class MessageManager { + String rootPath; + ArrayList rootPaths; + + ArrayList organizedMessagesList; + private ArrayList messageUtils; + + HashMap nameToIdMap = new HashMap(); + HashMap idToNameMap = new HashMap(); + int id_counter = 0; + + public MessageManager(String root) { + this.organizedMessagesList = new ArrayList(); + this.messageUtils = new ArrayList(); + this.rootPaths = new ArrayList(); + + // TODO: this should go in the config + this.rootPaths.add(pathJoin(root, "messages", "inbox")); + this.rootPaths.add(pathJoin(root, "messages", "archived_threads")); + this.rootPaths.add(pathJoin(root, "messages", "filtered_threads")); + + StringList filenames; + for (String path : this.rootPaths) { + try { + filenames = listFileNames(path); + } catch (NotDirectoryException e) { + println("Error: " + path + " is not a directory"); + exit(); + return; + } + + // Null check + if (filenames == null) continue; + + // create a new messageutil for each entry + for (String filename : filenames) { + + // If in the ignore list, then skip + boolean ignore = false; + for (String ignoredItem : CONFIG.ignoreList) { + if (ignoredItem.equals(filename)) { + ignore = true; + break; + } + } + if (ignore) { + continue; + } + + String messageDataPath = pathJoin(path, filename); + MessageFileReader newMessageUtil = new MessageFileReader(messageDataPath); + this.messageUtils.add(newMessageUtil); + } + } + + // Now let's build the participants name to id map + this.processParticipants(); + + // Process each message util given the name to id map + this.processMessages(); + + // Sort the messages by timestamp + Collections.sort(this.organizedMessagesList, new MessageDataComparator()); + } + + private void processParticipants() { + for (MessageFileReader mfr : this.messageUtils) { + if (!mfr.valid) continue; + + for (String name : mfr.participants) { + if (!this.nameToIdMap.containsKey(name)) { + this.nameToIdMap.put(name, this.id_counter); + this.id_counter++; + } + } + } + + // Also make a reverse map + this.idToNameMap = new HashMap(); + for (String name : this.nameToIdMap.keySet()) { + this.idToNameMap.put(this.nameToIdMap.get(name), name); + } + } + + private void processMessages() { + for (MessageFileReader mfr : this.messageUtils) { + if (!mfr.valid) continue; + mfr.processMessages(this.nameToIdMap, this.organizedMessagesList); + } + } + + private int getInsersionIndex(long timestamp, int start, int end) { + // Recursive function that use binary search to get index to insert + + // Edge case + if (start == end) { + if (this.organizedMessagesList.get(start).timestamp > timestamp) { + return start; + } else { + return start + 1; + } + } + + // Exit condition + if (start > end) { + return start; + } + + // All other cases + int mid = (start + end) / 2; + long midTime = this.organizedMessagesList.get(mid).timestamp; + if (midTime < timestamp) { + return this.getInsersionIndex(timestamp, mid + 1, end); + } else if (midTime > timestamp) { + return this.getInsersionIndex(timestamp, start, mid - 1); + } else { + return mid; + } + } +} + + +// Primitive object for a single entry of message +class MessageData { + long timestamp; + + int sender_id; + int[] receiver_ids; + + String content; + + public MessageData(long timestamp, int sender_id, int[] receiver_ids, String content) { + this.timestamp = timestamp; + this.sender_id = sender_id; + this.receiver_ids = receiver_ids; + this.content = content; + } + + // Printable string + public String toString() { + return "MessageData: " + this.timestamp + " " + this.sender_id + " " + this.receiver_ids + " " + this.content; + } +} + +/* Comparator for sorting messages by timestamp */ +public class MessageDataComparator implements Comparator { + @Override + public int compare(MessageData md1, MessageData md2) { + return Long.compare(md1.timestamp, md2.timestamp); + } +} + +class MessageFileReader { + String filePath; + JSONObject[] jsonData; + + StringList participants; + + boolean valid = false; + + // Cache for sender to receiver map, int->int[] + HashMap senderToReceiversMap = new HashMap(); + + /** + * Constructor + * @param filePath Path to the folder containing all the json files for this thread + */ + public MessageFileReader(String filePath) { + this.filePath = filePath; + + // Populate json files + StringList jsonFiles; + try { + jsonFiles = listFileNames(this.filePath, "json"); + } catch (NotDirectoryException e) { + return; + } + + this.jsonData = new JSONObject[jsonFiles.size()]; + for (int i = 0; i < jsonFiles.size(); i++) { + String jsonFilePath = pathJoin(this.filePath, jsonFiles.get(i)); + this.jsonData[i] = loadJSONObject(jsonFilePath); + } + + // Check if this thread is useful + this.participants = new StringList(); + this.check(); + } + + /** + * Checks if this thread is useful, if so, the valid flag is set to true + * and the participants list is populated + */ + private void check() { + if (this.jsonData.length == 0) return; + + // Check number of participants + // TODO: add support for group chats + JSONArray participants = this.jsonData[0].getJSONArray("participants"); + if (this.jsonData[0].getJSONArray("participants").size() > 2) { + return; + } + + // If one of the participant doesn't have a name, ignore + for (int i = 0; i < participants.size(); i++) { + if (participants.getJSONObject(i).getString("name").equals(CONFIG.defaultName)) { + return; + } + this.participants.append(participants.getJSONObject(i).getString("name")); + } + + // If we made it here, this thread is useful + this.valid = true; + } + + /** + * Processes a single message and returns a MessageData object + * @param message JSONObject of the message + * @param nameToIdMap HashMap of name to id + * @return + */ + public MessageData processSingleMessage(JSONObject message, HashMap nameToIdMap) { + + // Ignore non-generic messages + if (!message.getString("type").equals("Generic")) return null; + + // Get timestamp + long timestamp = message.getLong("timestamp_ms"); + + // Get sender + String sender = message.getString("sender_name"); + + // if the sender is not in the name to id map, ignore + if (!nameToIdMap.containsKey(sender)) return null; + int sender_id = nameToIdMap.get(sender); + + // Get content + String content = message.getString("content"); + if (content == null) return null; + if (content.equals("")) return null; + + // Get receivers + if (this.senderToReceiversMap.containsKey(sender_id)) { + return new MessageData(timestamp, sender_id, this.senderToReceiversMap.get(sender_id), content); + } + + // Get receivers by taking the participants and removing the sender + int[] receivers_ids = new int[this.participants.size() - 1]; + for (int pi = 0, ri = 0; pi < this.participants.size(); pi++) { + if (this.participants.get(pi).equals(sender)) continue; + receivers_ids[ri] = nameToIdMap.get(this.participants.get(pi)); + ri++; + } + + // If the sender to receivers map doesn't contain the sender, add it + if (!this.senderToReceiversMap.containsKey(sender_id)) { + this.senderToReceiversMap.put(sender_id, receivers_ids); + } + + return new MessageData(timestamp, sender_id, receivers_ids, content); + } + + /** + * Process all messages in the thread + * @param nameToIdMap HashMap of name to id + * @param outputMessages ArrayList of MessageData objects from the thread + */ + public void processMessages(HashMap nameToIdMap, ArrayList outputMessages) { + if (!this.valid) return; + + // Process each json file + for (int i = 0; i < this.jsonData.length; i++) { + JSONObject json = this.jsonData[i]; + + // Get messages + JSONArray messages = json.getJSONArray("messages"); + + // Process each message + for (int j = 0; j < messages.size(); j++) { + MessageData md = this.processSingleMessage(messages.getJSONObject(j), nameToIdMap); + if (md == null) continue; + + outputMessages.add(md); + } + } + } +} diff --git a/FBVis/MessageScheduler.pde b/FBVis/MessageScheduler.pde new file mode 100644 index 0000000..fc09956 --- /dev/null +++ b/FBVis/MessageScheduler.pde @@ -0,0 +1,81 @@ + +// import date stuff +import java.util.Date; +import java.text.SimpleDateFormat; + + +class MessageScheduler { + + private ArrayList messages; + private int currentMessageIndex = 0; + + // Time-based configuration + private long currentTimestamp = 0; + + // Time step is ms between each second + private long timeStepPerSecond; + + // Keep track of how long since the last call to nextTimeStep + private long timeSinceLastTimeStep = 0; + + MessageScheduler(MessageManager messageManager) { + + // set timestep ratio to 1 day per second + this.timeStepPerSecond = 1000 * 60 * 60 * 24; + + this.messages = messageManager.organizedMessagesList; + this.currentTimestamp = this.messages.get(0).timestamp; + } + + /** + * Returns the next message in the list of messages. + * @return the next message in the list of messages. + */ + public MessageData next() { + if (currentMessageIndex < messages.size()) { + return messages.get(currentMessageIndex++); + } else { + return null; + } + } + + public boolean finished() { + return currentMessageIndex >= messages.size(); + } + + public ArrayList nextTimeStep() { + + // Calculate how long since the last call to nextTimeStep + float timeStepMultiplier = (millis() - this.timeSinceLastTimeStep) / 1000.0f; + long delta = (long) (timeStepMultiplier * this.timeStepPerSecond); + + // update the current time + this.currentTimestamp += delta; + + ArrayList nextMessages = new ArrayList(); + + // Get all messages from currentmMessageIndex to currentTimestamp + for (int i = currentMessageIndex; i < messages.size(); i++) { + MessageData message = messages.get(i); + if (message.timestamp > this.currentTimestamp) { + break; + } + nextMessages.add(message); + currentMessageIndex++; + } + + // If messages are empty, check condition for fast forward + if (nextMessages.size() == 0) { + if (messages.get(currentMessageIndex).timestamp - this.currentTimestamp > 2 * this.timeStepPerSecond) { + this.currentTimestamp = messages.get(currentMessageIndex).timestamp; + } + } + + this.timeSinceLastTimeStep = millis(); + return nextMessages; + } + + public String getCurrentTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(this.currentTimestamp)); + } +} diff --git a/PathUtil.pde b/FBVis/PathUtil.pde similarity index 60% rename from PathUtil.pde rename to FBVis/PathUtil.pde index 9b5ad34..bdd6d9c 100644 --- a/PathUtil.pde +++ b/FBVis/PathUtil.pde @@ -24,21 +24,6 @@ StringList listFileNames(String dir) throws NotDirectoryException { return filenames; } - -int extractNumber(String s, char startToken, char endToken) { - int i = 0; - try { - int start = s.indexOf(startToken) + 1; - int end = s.lastIndexOf(endToken); - String number = s.substring(start, end); - i = Integer.parseInt(number); - } catch (Exception e) { - i = 0; - } - - return i; -} - StringList listFileNames(String dir, String extention) throws NotDirectoryException { StringList allfiles = listFileNames(dir); StringList files = new StringList(); @@ -52,12 +37,27 @@ StringList listFileNames(String dir, String extention) throws NotDirectoryExcept return files; } -String[] sortFilenamesNumerically(StringList files) { - String[] sorted = new String[files.size()]; - for (String f : files) { - int index = extractNumber(f, '_', '.') - 1; - sorted[index] = f; - } - return sorted; -} +// int extractNumber(String s, char startToken, char endToken) { +// int i = 0; +// try { +// int start = s.indexOf(startToken) + 1; +// int end = s.lastIndexOf(endToken); +// String number = s.substring(start, end); +// i = Integer.parseInt(number); +// } catch (Exception e) { +// i = 0; +// } + +// return i; +// } + +// String[] sortFilenamesNumerically(StringList files) { +// String[] sorted = new String[files.size()]; +// for (String f : files) { +// int index = extractNumber(f, '_', '.') - 1; +// sorted[index] = f; +// } + +// return sorted; +// } diff --git a/FBVis/Payload.pde b/FBVis/Payload.pde new file mode 100644 index 0000000..e14324e --- /dev/null +++ b/FBVis/Payload.pde @@ -0,0 +1,219 @@ +// // This is a graphic version of the message + +// // Abstract payload +// class Payload { +// float x; +// float y; +// PersonNode targetPerson; +// public Payload(PersonNode source, PersonNode target) +// { +// //this.x = source.x; +// //this.y = source.y; +// this.targetPerson = target; + +// // Sender gets refreshed +// source.refresh(); +// } + +// public void draw() { +// //line(this.x, this.y, this.targetPerson.x, this.targetPerson.y); +// } + +// public void update() { +// return; +// } + +// public boolean hasArrived() { +// if (this.getArrived()) { +// this.targetPerson.refresh(); +// return true; +// } + +// return false; +// } + +// protected boolean getArrived() { +// return true; +// } +// } + + +// final float PAYLOAD_LERP = 0.1; +// final float ARRIVE_THRESHOLD_PX = 5.0; + +// final float RANDOM_START_D = 5.0; + + +// class PayloadDot extends Payload{ +// float x; +// float y; +// PersonNode targetPerson; + +// float r; +// float f; + +// public PayloadDot(PersonNode source, PersonNode target) { +// super(source, target); +// //this.x = source.x + random(-RANDOM_START_D, RANDOM_START_D); +// //this.y = source.y + random(-RANDOM_START_D, RANDOM_START_D); + +// this.targetPerson = target; + +// this.r = random(5, 12); +// this.f = random(80, 200); +// } + +// @Override +// public void draw() { +// pushMatrix(); +// translate(this.x, this.y); +// fill(this.f); +// ellipse(0, 0, this.r, this.r); +// popMatrix(); + +// this.update(); +// } + +// @Override +// public void update() { +// //this.x = lerp(this.x, this.targetPerson.x, PAYLOAD_LERP); +// //this.y = lerp(this.y, this.targetPerson.y, PAYLOAD_LERP); +// } + +// //@Override +// //protected boolean getArrived() { +// // //final float dx = abs(this.targetPerson.x - this.x); +// // //final float dy = abs(this.targetPerson.y - this.y); + +// // //return dx < ARRIVE_THRESHOLD_PX && dy < ARRIVE_THRESHOLD_PX; +// //} +// } + +// class PayloadLine extends Payload{ +// float x; +// float y; +// PersonNode targetPerson; + +// int life; + +// public PayloadLine(PersonNode source, PersonNode target) { +// super(source, target); +// //this.x = source.x + random(-RANDOM_START_D, RANDOM_START_D); +// //this.y = source.y + random(-RANDOM_START_D, RANDOM_START_D); + +// this.targetPerson = target; +// this.life = 15; +// } + +// public void draw() { +// stroke(255, 255, 0); +// strokeWeight(1); +// //line(this.x, this.y, this.targetPerson.x, this.targetPerson.y); +// } + +// protected boolean getArrived() { +// return life-- == 0; +// } +// } + +// class PayloadSegment extends Payload{ +// float x; +// float y; +// float prevX; +// float prevY; +// PersonNode targetPerson; + +// float radius = random(3, 8); +// float opacity = random(CONFIG.payloadOpacityMin, CONFIG.payloadOpacityMax); + +// float travel_lerp; +// float travel_y_lerp_mult; + +// boolean isMasterSending = false; + +// public PayloadSegment(PersonNode source, PersonNode target, float size) { +// super(source, target); +// //this.x = source.x + random(-RANDOM_START_D, RANDOM_START_D); +// //this.y = source.y + random(-RANDOM_START_D, RANDOM_START_D); +// this.prevX = this.x; +// this.prevY = this.y; + +// if (CONFIG.payloadSizeBasedOnMessageLength) { +// this.radius = size; +// } + +// this.travel_lerp = random(CONFIG.payloadSegmentLerpMin, CONFIG.payloadSegmentLerpMax); +// this.travel_y_lerp_mult = random(0.5, 0.8); + +// this.targetPerson = target; + +// // TODO: FIXME: +// if (source.equals(CONFIG.masterName)) { +// this.isMasterSending = true; +// } +// } + +// @Override +// public void draw() { +// pushMatrix(); +// stroke(this.isMasterSending ? CONFIG.payloadSendColor : CONFIG.payloadReceiveColor, this.opacity); +// strokeWeight(this.radius); +// line(this.x, this.y, this.prevX, this.prevY); +// popMatrix(); + +// this.update(); +// } + +// @Override +// public void update() { +// this.prevX = this.x; +// this.prevY = this.y; +// //this.x = lerp(this.x, this.target.x, this.travel_lerp); +// //this.y = lerp(this.y, this.target.y, this.travel_lerp * travel_y_lerp_mult); +// } + +// //@Override +// //protected boolean getArrived() { +// // //final float dx = abs(this.targetPerson.x - this.x); +// // //final float dy = abs(this.targetPerson.y - this.y); + +// // //return dx < ARRIVE_THRESHOLD_PX && dy < ARRIVE_THRESHOLD_PX; +// //} +// } + +// class PayloadSegment2 extends PayloadSegment { +// public PayloadSegment2(PersonNode source, PersonNode target, float size) { +// super(source, target, size); +// this.travel_lerp = random(CONFIG.payloadSegmentGroupLerpMin, CONFIG.payloadSegmentGroupLerpMax); +// } + +// @Override +// public void draw() { +// pushMatrix(); +// stroke(CONFIG.payloadGroupColor, this.opacity); +// strokeWeight(this.radius); +// line(this.x, this.y, this.prevX, this.prevY); +// popMatrix(); + +// this.update(); +// } +// } + +// class PayloadFactory { + +// ArrayList payloads; + +// public PayloadFactory(ArrayList payloads) { +// this.payloads = payloads; +// } + +// //public void makeIndividualPayload(PersonNode sender, PersonNode receiver, float size) { +// // if (PAYLOADS_MAXSIZE > this.payloads.size()) +// // this.payloads.add(new PayloadSegment(sender, receiver, size)); +// //} + +// //public void makeGroupPayload(PersonNode sender, PersonNode receiver, float size) { +// // if (PAYLOADS_MAXSIZE > this.payloads.size()) +// // this.payloads.add(new PayloadSegment2(sender, receiver, size)); +// //} +// } diff --git a/FBVis/PeopleRender.pde b/FBVis/PeopleRender.pde new file mode 100644 index 0000000..5147aa5 --- /dev/null +++ b/FBVis/PeopleRender.pde @@ -0,0 +1,52 @@ +class RenderPeopleLayer extends RenderLayer { + MasterPersonNode root; + + ArrayList cachedNodes; + + public RenderPeopleLayer(MasterPersonNode root) { + super(); + this.root = root; + this.cachedNodes = new ArrayList(); + } + + @Override + protected void renderGraphics() { + this.pg.clear(); + this.pg.pushMatrix(); + this.pg.translate(width/2, height/2); + + if (this.root.refreshNeeded) { + this.cachedNodes = this.root.getAllNodes(); + this.root.refreshNeeded = false; + } + + this.pg.textAlign(CENTER, CENTER); + this.pg.textSize(PERSON_NAME_TEXT_SIZE); + for (Node node : this.cachedNodes) { + + PVector pos = node.pos; + + // render the node differently depending on its type + if (node instanceof PersonNode) { + if (((PersonNode) node).refreshScore < REFRESH_THRES) { + continue; + } + this.pg.image( + sprites.personNodeSprites[floor(((PersonNode) node).refreshScore * 9)], pos.x-10, pos.y-10); + this.pg.fill(map(((PersonNode) node).refreshScore, 0, 1, 70, 255)); + this.pg.text(node.name, pos.x, pos.y+PERSON_NODE_SIZE); + + } else { + this.pg.image(sprites.personNodeSprites[8], pos.x-10, pos.y-10); + // gold fill + this.pg.fill(255, 215, 0); + this.pg.text(node.name, pos.x, pos.y+PERSON_NODE_SIZE); + } + + node.update(); + } + + + this.pg.popMatrix(); + } +} diff --git a/FBVis/Person.pde b/FBVis/Person.pde new file mode 100644 index 0000000..73436d5 --- /dev/null +++ b/FBVis/Person.pde @@ -0,0 +1,185 @@ +// Person class is a person node +// where messages and other information can travel to and from + +final float PERSON_LERP = 0.5; +final float PERSON_HITBOX_R = 15; +final float PERSON_NODE_SIZE = 15; +final float PERSON_MASTER_NODE_SIZE = 20; +final float PERSON_NAME_TEXT_SIZE = 12; + +final float REFRESH_DECAY = 0.99; +final float REFRESH_THRES = 0.001; + +class PersonStat { + int msgReceived; + int msgSent; + long lastInteractTimestamp; + + public PersonStat() { + this.reset(); + } + + public void reset() { + msgReceived = 0; + msgSent = 0; + lastInteractTimestamp = 0; + } +} + +class Node { + PVector pos; + PVector targetPos; + + String name; + int id; + + float NODE_RESPONSIVENESS = 0.2; + + public Node(int id, String name, PVector initPos, PVector targetPos) { + this.id = id; + this.pos = initPos; + this.targetPos = targetPos; + this.name = name; + } + + public void setPos(PVector pos) { + this.targetPos = pos; + } + + public void update() { + this.pos.lerp(this.targetPos, NODE_RESPONSIVENESS); + } +} + +class PersonNode extends Node { + + // Display properties + float refreshScore = 1.0; + + // Stats + PersonStat stats; + + public PersonNode(int id, String name) { + super(id, name, new PVector(0, 0, 0), new PVector(0, 0, 0)); + + // Set name + if (CONFIG.hideRealNames) { + this.name = CONFIG.hideNameReplacement; + } else { + this.name = name; + } + + this.stats = new PersonStat(); + } + + public void refresh() { + this.refreshScore = 1.0; + } + + public boolean equals(String name) { + return this.name.equals(name); + } + + public void incrementMsgReceived() { + this.stats.msgReceived++; + } + + public void incrementMsgSent() { + this.stats.msgSent++; + } + + @Override + public void update() { + // Super update + super.update(); + + // Update refresh score + this.refreshScore *= REFRESH_DECAY; + } +} + +class GroupNode extends Node { + + // Contains either PersonNode or GroupNode + ArrayList nodes = new ArrayList(); + + // Display properties + float groupRadius = 100; + + boolean refreshNeeded = false; + + public GroupNode(int id, String name) { + super(id, name, new PVector(0, 0, 0), new PVector(0, 0, 0)); + } + + public void addNode(Node node) { + this.nodes.add(node); + this.reposition(); + this.refreshNeeded = true; + } + + public void removeNode(Node node) { + this.nodes.remove(node); + this.reposition(); + this.refreshNeeded = true; + } + + public void reposition() { + final int N = this.nodes.size(); + + // Calculate new position for all nodes, +1 offset + PVector[] points; + if (RENDERER == P3D) { + points = genPts3DSphere(N + 1, this.groupRadius); + } else { + points = genPts2DPackedSpiral(N + 1, this.groupRadius); + } + + // Set new positions + for (int i = 0; i < N; i++) { + this.nodes.get(i).setPos(points[i + 1].add(this.pos)); + + if (this.nodes.get(i) instanceof GroupNode) { + + // Push group nodes away + PVector deltaPos = points[i + 1].copy().normalize().mult(2 * this.groupRadius); + this.nodes.get(i).setPos(points[i + 1].add(deltaPos)); + + // Recursively reposition group nodes + ((GroupNode) this.nodes.get(i)).reposition(); + } + } + } + + @Override + public void update() { + // Super update + super.update(); + + // Update all nodes + for (Node node : this.nodes) { + node.update(); + } + } + + // Recursively get all nodes in this group + public ArrayList getAllNodes() { + ArrayList allNodes = new ArrayList(); + for (Node node : this.nodes) { + allNodes.add(node); + if (node instanceof GroupNode) { + allNodes.addAll(((GroupNode) node).getAllNodes()); + } + } + // add self + allNodes.add(this); + return allNodes; + } +} + +// Singleton class for the master person node (root) +class MasterPersonNode extends GroupNode { + private MasterPersonNode(int id, String name) { + super(id, name); + } +} diff --git a/Progress.pde b/FBVis/Progress.pde similarity index 100% rename from Progress.pde rename to FBVis/Progress.pde diff --git a/FBVis/RenderLayer.pde b/FBVis/RenderLayer.pde new file mode 100644 index 0000000..1db318c --- /dev/null +++ b/FBVis/RenderLayer.pde @@ -0,0 +1,100 @@ +// The idea of the class "Renderlayer" is that +// it is a wrapper around PGraphics, for better organization +// of what is going to be rendered in the program. +// +// The derived classes of the RenderLayer abstract class +// could be used as singletons to draw layers with specific +// functions such as UI, payload, people, etc. + +abstract class RenderLayer { + PGraphics pg; + + public RenderLayer(int w, int h, String renderer) { + this.pg = createGraphics(w, h, renderer); + } + + public RenderLayer(int w, int h) { + this.pg = createGraphics(w, h, RENDERER); + } + + public RenderLayer() { + this.pg = createGraphics(width, height); + } + + protected void render() { + this.pg.beginDraw(); + renderGraphics(); + this.pg.endDraw(); + } + + protected void renderGraphics() { + this.pg.clear(); + } + + public PGraphics getRender() { + this.render(); + return this.pg; + } +} + +// class RenderUILayer extends RenderLayer { + +// // Layer states +// long timestamp; + +// // Reference to the timeline object +// Timeline timeline; +// SpeedControl speedControl; +// StatCardHover statCardHover; + +// public RenderUILayer() { +// super(); +// this.timestamp = 0; +// } + +// @Override +// protected void renderGraphics() { +// this.pg.clear(); +// this.renderTimestamp(); +// this.renderTimeline(); +// this.renderSpeedControl(); +// this.renderStatCardHover(); +// } + +// private void renderTimestamp() { +// this.pg.textFont(monospaceFont); +// this.pg.textAlign(CENTER, CENTER); +// String date = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date(this.timestamp)); +// this.pg.textSize(20); +// this.pg.fill(255); +// this.pg.text(date, width/2, 20); +// } + +// private void renderTimeline() { +// this.pg.noFill(); + +// // Set opacity of the timeline to 70 if not hovered or 255 if hovered +// this.pg.stroke(255, this.timeline.hovered ? 255 : 70); +// this.pg.strokeWeight(1); +// this.pg.rect(this.timeline.x, this.timeline.y, this.timeline.w, this.timeline.h); + +// // Draw a cursor of where in the timeline we're currently are at +// final float percentX = map(this.timeline.percentage, 0, 1, this.timeline.x, this.timeline.x + this.timeline.w); +// this.pg.strokeWeight(3); +// this.pg.line(percentX, this.timeline.y, percentX, this.timeline.y + this.timeline.h); +// } + +// private void renderSpeedControl() { +// if (this.speedControl == null) return; +// this.pg.image(this.speedControl.getSpeedIcon(), 10, 10); +// } + +// private void renderStatCardHover() { +// this.pg.textFont(font); +// if (this.statCardHover == null) return; +// this.statCardHover.draw(this.pg); +// } +// } + + + diff --git a/SpeedControl.pde b/FBVis/SpeedControl.pde similarity index 100% rename from SpeedControl.pde rename to FBVis/SpeedControl.pde diff --git a/FBVis/Sprites.pde b/FBVis/Sprites.pde new file mode 100644 index 0000000..268a354 --- /dev/null +++ b/FBVis/Sprites.pde @@ -0,0 +1,31 @@ +// For prerendering things and caching them +public class Sprites { + + PGraphics[] personNodeSprites; + + public Sprites() { + + // Render the person node sprites (10 levels) + personNodeSprites = new PGraphics[10]; + + for (int i = 0; i < personNodeSprites.length; i++) { + float refreshScore = exp(0.3 * (i - personNodeSprites.length)); + float fillScore = map(refreshScore, 0, 1, 0, 245); + float strokeScore = map(refreshScore, 0, 1, 5, 50); + + personNodeSprites[i] = createGraphics(20, 20); + personNodeSprites[i].beginDraw(); + personNodeSprites[i].clear(); + + if (i == personNodeSprites.length - 1) { + personNodeSprites[i].strokeWeight(2); + } else { + personNodeSprites[i].strokeWeight(4); + } + personNodeSprites[i].stroke(strokeScore); + personNodeSprites[i].fill(fillScore); + personNodeSprites[i].ellipse(10, 10, 15, 15); + personNodeSprites[i].endDraw(); + } + } +} diff --git a/StatCardHover.pde b/FBVis/StatCardHover.pde similarity index 100% rename from StatCardHover.pde rename to FBVis/StatCardHover.pde diff --git a/FBVis/Timeline.pde b/FBVis/Timeline.pde new file mode 100644 index 0000000..d0e543f --- /dev/null +++ b/FBVis/Timeline.pde @@ -0,0 +1,53 @@ +//class Timeline { + +// float x; +// float y; +// float w; +// float h; + +// float percentage; +// boolean hovered; + +// public Timeline(float x, float y, float w, float h) { +// this.x = x; +// this.y = y; +// this.w = w; +// this.h = h; + +// this.percentage = 0; + +// this.hovered = false; +// } + +// public void setPercentage(float percentage) { +// this.percentage = constrain(percentage, 0, 1); +// } + +// public void handleMouseInput() { +// // Check if mouse is inside the box +// final float x2 = this.x + this.w; + +// this.hovered = mouseX > this.x && mouseX < x2 && mouseY > this.y && mouseY < this.y + this.h; + +// if (this.hovered) { +// if (mousePressed) { +// final float new_percentage = map(mouseX, this.x, x2, 0, 1); +// this.setPercentage(new_percentage); + +// final int new_gi = (int) map(new_percentage, 0, 1, 0, man.organizedMessagesList.size()); + +// if (gi < new_gi) { +// while (gi < new_gi) { +// int di = gi % man.organizedMessagesList.size(); +// MessageData current = man.organizedMessagesList.get(di); +// processCurrentmessageData(current); +// gi++; +// } +// } else { +// gi = new_gi; +// currentTimestamp = man.organizedMessagesList.get(gi).timestamp; +// } +// } +// } +// } +//} diff --git a/data/config.ini b/FBVis/data/config.ini similarity index 87% rename from data/config.ini rename to FBVis/data/config.ini index a205755..afbde64 100644 --- a/data/config.ini +++ b/FBVis/data/config.ini @@ -1,7 +1,8 @@ [data config] -data_root_path=/Users/muchen/Downloads/messages_toy +data_root_path=D:/drivefiles/facebook/facebook master_name=Muchen He -facebook_default_name=Unnamed +; facebook_default_name=Unnamed +facebook_default_name=Facebook user [program config] run_verbose=no diff --git a/data/ignorelist.txt b/FBVis/data/ignorelist.txt similarity index 100% rename from data/ignorelist.txt rename to FBVis/data/ignorelist.txt diff --git a/data/img/speed1.png b/FBVis/data/img/speed1.png similarity index 100% rename from data/img/speed1.png rename to FBVis/data/img/speed1.png diff --git a/data/img/speed2.png b/FBVis/data/img/speed2.png similarity index 100% rename from data/img/speed2.png rename to FBVis/data/img/speed2.png diff --git a/data/img/speed3.png b/FBVis/data/img/speed3.png similarity index 100% rename from data/img/speed3.png rename to FBVis/data/img/speed3.png diff --git a/Layout.pde b/Layout.pde deleted file mode 100644 index ee23c35..0000000 --- a/Layout.pde +++ /dev/null @@ -1,64 +0,0 @@ -class LayoutGenerator { - float centerX; - float centerY; - - public LayoutGenerator(float centerX, float centerY) { - this.centerX = centerX; - this.centerY = centerY; - } - - public PVector pos(int n) { - if (n == 0) { - return new PVector(width/2, height/2); - } else { - final float x = (n % 20) * ((width - 50) / 20) + 25; - final float y = floor(n / 20) * ((height - 50) / 10) + 25; - return new PVector(x, y); - } - } -} - -class Spiral extends LayoutGenerator { - - float radius; - - public Spiral(float radius, float centerX, float centerY) { - super(centerX, centerY); - this.radius = radius; - } - - @Override - public PVector pos(int n) { - if (n > 0) { - n += 1; - } - - final float a = 2.4 * n; - final float r = this.radius * sqrt(n); - final float x = r * cos(a) + this.centerX; - final float y = r * sin(a) + this.centerY; - - return new PVector(x, y); - } -} - -class PackedSpiral extends Spiral { - - float power = 0.07; - float baseCoeff = 0.3; - float fanoutOffsetMult = 1.001; - float r; - - public PackedSpiral(float radius, float centerX, float centerY) { - super(radius, centerX, centerY); - } - - @Override - public PVector pos(int n) { - this.r = -1 * pow(this.radius, pow(this.baseCoeff * n, this.power)); - final float x = width/2 + this.r * cos(n * this.fanoutOffsetMult); - final float y = height/2 + this.r * sin(n * this.fanoutOffsetMult); - - return new PVector(x, y); - } -} \ No newline at end of file diff --git a/Message.pde b/Message.pde deleted file mode 100644 index cfd0657..0000000 --- a/Message.pde +++ /dev/null @@ -1,293 +0,0 @@ -// if number of participants in a thread is bigger than this number, ignore it -final int LARGE_GROUP_PARTICIPANT_THRES = 20; - -// This should be a class that manages all messages, and message utils -class MessageManager { - ArrayList organizedMessagesList; - ArrayList messageUtils; - String rootPath; - ArrayList rootPaths; - - public MessageManager(String root) { - this.organizedMessagesList = new ArrayList(); - this.messageUtils = new ArrayList(); - this.rootPaths = new ArrayList(); - - // TODO: this should go in the config - this.rootPaths.add(pathJoin(root, "messages", "inbox")); - this.rootPaths.add(pathJoin(root, "messages", "archived_threads")); - this.rootPaths.add(pathJoin(root, "messages", "filtered_threads")); - - int j = 0; - StringList filenames; - for (String path : this.rootPaths) { - try { - filenames = listFileNames(path); - } catch (NotDirectoryException e) { - println("Error"); - exit(); - return; - - // TODO: throw exception - } - - // Null check - if (filenames == null) continue; - - // create a new messageutil for each entry - int i = 0; - for (String filename : filenames) { - - // If in the ignore list, then skip - boolean ignore = false; - for (String ignoredItem : CONFIG.ignoreList) { - if (ignoredItem.equals(filename)) { - ignore = true; - break; - } - } - if (ignore) { - i++; - continue; - } - - String messageDataPath = pathJoin(path, filename); - - /* Find all the JSON files in the path */ - String[] sortedJsonFiles; - try { - sortedJsonFiles = sortFilenamesNumerically(listFileNames(messageDataPath, "json")); - } catch (NotDirectoryException e) { - continue; - } - - /* Get message util object and populate with all the json files */ - if (CONFIG.enableVerbose) println("Loading: " + messageDataPath); - MessageUtil newMessageUtil = new MessageUtil(messageDataPath); - - /* Populate in reverse order */ - for (int idx = sortedJsonFiles.length - 1; idx >= 0; idx--) { - String jsonFilePath = pathJoin(messageDataPath, sortedJsonFiles[idx]); - newMessageUtil.processMessageFile(jsonFilePath); - } - - newMessageUtil.initialized = true; - this.messageUtils.add(newMessageUtil); - - i++; - - // Status - progress.setLoadingProgress(i / filenames.size()); - } - - j++; - progress.setLoadingLargeProgress(j / this.rootPaths.size()); - } - - this.buildMessagesList(); - } - - // Builds an timely ordered list - public void buildMessagesList() { - if (CONFIG.enableVerbose) println("Building ordered messages list, sorting through all messages by time"); - for (int i = 0; i < this.messageUtils.size(); i++) { - - // Status - if (CONFIG.enableVerbose) println("Sorting " + str(i) + "/" + str(messageUtils.size()) + " entries"); - progress.setSortingProgress(i / messageUtils.size()); - - MessageUtil mu = this.messageUtils.get(i); - for (MessageData md : mu.getMessagesList()) { - // No sorting required for the first entry - if (this.organizedMessagesList.size() == 0) { - this.organizedMessagesList.add(0, md); - continue; - } - - int index = this.getInsersionIndex(md.timestamp, 0, this.organizedMessagesList.size() - 1); - this.organizedMessagesList.add(index, md); - } - } - - // Print first 200 results to verify - println("Sorted " + this.organizedMessagesList.size() + " total messages"); - } - - private int getInsersionIndex(long timestamp, int start, int end) { - // Recursive function that use binary search to get index to insert - - // Edge case - if (start == end) { - if (this.organizedMessagesList.get(start).timestamp > timestamp) { - return start; - } else { - return start + 1; - } - } - - // Exit condition - if (start > end) { - return start; - } - - // All other cases - int mid = (start + end) / 2; - long midTime = this.organizedMessagesList.get(mid).timestamp; - if (midTime < timestamp) { - return this.getInsersionIndex(timestamp, mid + 1, end); - } else if (midTime > timestamp) { - return this.getInsersionIndex(timestamp, start, mid - 1); - } else { - return mid; - } - } -} - - -// Primitive object for a single entry of message -class MessageData{ - long timestamp; - String sender; - ArrayList receivers; - String content; - float contentSizeSqrt; - - public MessageData(long timestamp, String sender, ArrayList receivers, String content) { - super(); - this.timestamp = timestamp; - this.sender = sender; - this.receivers = receivers; - this.content = content; - this.contentSizeSqrt = sqrt(this.content.length()); - } -} - - -// Takes a file or arrays of files and construct a single array of -// uniform time sorted list - -// For now, suppose we are only dealing with one chat file -int globalUnknownUserCount = 0; -class MessageUtil { - String filePath; - ArrayList messagesList; - - boolean initialized; - - /* Message util takes a file path and populates - * messagesList with the data read from the file - * @param path, the path to the inbox mail folder - */ - public MessageUtil(String path) { - this.filePath = filePath; - this.messagesList = new ArrayList(); - this.initialized = false; - } - - ////////////////// PUBLIC FUNCTIONS - public long getFirstMessageTimestamp() { - if (this.messagesList.size() != 0) { - return this.messagesList.get(this.messagesList.size() - 1).timestamp; - } - - return 0; - } - - public long getLastMessageTimestamp() { - if (this.messagesList.size() != 0) { - return this.messagesList.get(0).timestamp; - } - - return 0; - } - - public ArrayList getMessagesList() { - return messagesList; - } - - public void processMessageFile(String filePath) { - // We expect the file path to be JSON - - // TODO: wrap in try - JSONObject jsonData = loadJSONObject(filePath); - - // Get participants - JSONArray participantsData = jsonData.getJSONArray("participants"); - if (participantsData.size() > LARGE_GROUP_PARTICIPANT_THRES) { - return; - } - - ArrayList participants = new ArrayList(); - - for (int i = 0; i < participantsData.size(); i++) { - JSONObject nameObj = participantsData.getJSONObject(i); - - - String name = nameObj.getString("name"); - - if (name.equals(CONFIG.defaultName)) { - name += ' ' + str(globalUnknownUserCount); - globalUnknownUserCount++; - } - - participants.add(name); - } - - // Get messages - JSONArray messages = jsonData.getJSONArray("messages"); - - // A hashmap / table is used to cache the sender -> receiver mapping - // TODO: we have to recompute the table if someone adds/removes people from group chat - HashMap> receiverMapping = new HashMap>(); - - // We go backwards because the messages are sorted - // by most recent on top - for (int i = messages.size() - 1; i >= 0; i--) { - JSONObject message = messages.getJSONObject(i); - - if (message.getString("type").equals("Generic")) { - String content = message.getString("content"); - String sender = message.getString("sender_name"); - final long timestamp = message.getLong("timestamp_ms"); - - if (sender == null) { - sender = "{UNKNOWN USER}"; - } - - if (content == null) { - content = "{NO CONTENT}"; - } - - // Get a single or list of receivers - ArrayList receivers; - if (receiverMapping.containsKey(sender)) { - receivers = receiverMapping.get(sender); - - } else { - - receivers = new ArrayList(); - - // Find all receivers from the participants list - for (int j = 0; j < participants.size(); j++) { - String name = participants.get(j); - - // if participant name is not sender, it must be receiver - if (!sender.equals(name)) { - receivers.add(name); - } - } - - // Add the list to the table to save computing - receiverMapping.put(sender, receivers); - } - - messagesList.add(new MessageData(timestamp, sender, receivers, content)); - } - } - - if (CONFIG.enableVerbose) { - println("Finished processsing file"); - println("Total of " + str(this.messagesList.size()) + " messages"); - } - } -} diff --git a/Payload.pde b/Payload.pde deleted file mode 100644 index 76677ea..0000000 --- a/Payload.pde +++ /dev/null @@ -1,219 +0,0 @@ -// This is a graphic version of the message - -// Abstract payload -class Payload { - float x; - float y; - PersonNode targetPerson; - public Payload(PersonNode source, PersonNode target) - { - this.x = source.x; - this.y = source.y; - this.targetPerson = target; - - // Sender gets refreshed - source.refresh(); - } - - public void draw() { - line(this.x, this.y, this.targetPerson.x, this.targetPerson.y); - } - - public void update() { - return; - } - - public boolean hasArrived() { - if (this.getArrived()) { - this.targetPerson.refresh(); - return true; - } - - return false; - } - - protected boolean getArrived() { - return true; - } -} - - -final float PAYLOAD_LERP = 0.1; -final float ARRIVE_THRESHOLD_PX = 5.0; - -final float RANDOM_START_D = 5.0; - - -class PayloadDot extends Payload{ - float x; - float y; - PersonNode targetPerson; - - float r; - float f; - - public PayloadDot(PersonNode source, PersonNode target) { - super(source, target); - this.x = source.x + random(-RANDOM_START_D, RANDOM_START_D); - this.y = source.y + random(-RANDOM_START_D, RANDOM_START_D); - - this.targetPerson = target; - - this.r = random(5, 12); - this.f = random(80, 200); - } - - @Override - public void draw() { - pushMatrix(); - translate(this.x, this.y); - fill(this.f); - ellipse(0, 0, this.r, this.r); - popMatrix(); - - this.update(); - } - - @Override - public void update() { - this.x = lerp(this.x, this.targetPerson.x, PAYLOAD_LERP); - this.y = lerp(this.y, this.targetPerson.y, PAYLOAD_LERP); - } - - @Override - protected boolean getArrived() { - final float dx = abs(this.targetPerson.x - this.x); - final float dy = abs(this.targetPerson.y - this.y); - - return dx < ARRIVE_THRESHOLD_PX && dy < ARRIVE_THRESHOLD_PX; - } -} - -class PayloadLine extends Payload{ - float x; - float y; - PersonNode targetPerson; - - int life; - - public PayloadLine(PersonNode source, PersonNode target) { - super(source, target); - this.x = source.x + random(-RANDOM_START_D, RANDOM_START_D); - this.y = source.y + random(-RANDOM_START_D, RANDOM_START_D); - - this.targetPerson = target; - this.life = 15; - } - - public void draw() { - stroke(255, 255, 0); - strokeWeight(1); - line(this.x, this.y, this.targetPerson.x, this.targetPerson.y); - } - - protected boolean getArrived() { - return life-- == 0; - } -} - -class PayloadSegment extends Payload{ - float x; - float y; - float prevX; - float prevY; - PersonNode targetPerson; - - float radius = random(3, 8); - float opacity = random(CONFIG.payloadOpacityMin, CONFIG.payloadOpacityMax); - - float travel_lerp; - float travel_y_lerp_mult; - - boolean isMasterSending = false; - - public PayloadSegment(PersonNode source, PersonNode target, float size) { - super(source, target); - this.x = source.x + random(-RANDOM_START_D, RANDOM_START_D); - this.y = source.y + random(-RANDOM_START_D, RANDOM_START_D); - this.prevX = this.x; - this.prevY = this.y; - - if (CONFIG.payloadSizeBasedOnMessageLength) { - this.radius = size; - } - - this.travel_lerp = random(CONFIG.payloadSegmentLerpMin, CONFIG.payloadSegmentLerpMax); - this.travel_y_lerp_mult = random(0.5, 0.8); - - this.targetPerson = target; - - // TODO: FIXME: - if (source.equals(CONFIG.masterName)) { - this.isMasterSending = true; - } - } - - @Override - public void draw() { - pushMatrix(); - stroke(this.isMasterSending ? CONFIG.payloadSendColor : CONFIG.payloadReceiveColor, this.opacity); - strokeWeight(this.radius); - line(this.x, this.y, this.prevX, this.prevY); - popMatrix(); - - this.update(); - } - - @Override - public void update() { - this.prevX = this.x; - this.prevY = this.y; - this.x = lerp(this.x, this.targetPerson.x, this.travel_lerp); - this.y = lerp(this.y, this.targetPerson.y, this.travel_lerp * travel_y_lerp_mult); - } - - @Override - protected boolean getArrived() { - final float dx = abs(this.targetPerson.x - this.x); - final float dy = abs(this.targetPerson.y - this.y); - - return dx < ARRIVE_THRESHOLD_PX && dy < ARRIVE_THRESHOLD_PX; - } -} - -class PayloadSegment2 extends PayloadSegment { - public PayloadSegment2(PersonNode source, PersonNode target, float size) { - super(source, target, size); - this.travel_lerp = random(CONFIG.payloadSegmentGroupLerpMin, CONFIG.payloadSegmentGroupLerpMax); - } - - @Override - public void draw() { - pushMatrix(); - stroke(CONFIG.payloadGroupColor, this.opacity); - strokeWeight(this.radius); - line(this.x, this.y, this.prevX, this.prevY); - popMatrix(); - - this.update(); - } -} - -class PayloadFactory { - - ArrayList payloads; - - public PayloadFactory(ArrayList payloads) { - this.payloads = payloads; - } - - public void makeIndividualPayload(PersonNode sender, PersonNode receiver, float size) { - if (PAYLOADS_MAXSIZE > this.payloads.size()) - this.payloads.add(new PayloadSegment(sender, receiver, size)); - } - - public void makeGroupPayload(PersonNode sender, PersonNode receiver, float size) { - if (PAYLOADS_MAXSIZE > this.payloads.size()) - this.payloads.add(new PayloadSegment2(sender, receiver, size)); - } -} diff --git a/Person.pde b/Person.pde deleted file mode 100644 index 2cad31b..0000000 --- a/Person.pde +++ /dev/null @@ -1,190 +0,0 @@ -// Person class is a person node -// where messages and other information can travel to and from - -final float PERSON_LERP = 0.5; -final float PERSON_HITBOX_R = 15; -final float PERSON_NODE_SIZE = 15; -final float PERSON_MASTER_NODE_SIZE = 20; -final float PERSON_NAME_TEXT_SIZE = 12; - -final float REFRESH_DECAY = 0.99; -final float REFRESH_THRES = 0.01; - -class PersonStat { - int msgReceived; - int msgSent; - long lastInteractTimestamp; - - public PersonStat() { - this.reset(); - } - - public void reset() { - msgReceived = 0; - msgSent = 0; - lastInteractTimestamp = 0; - } -} - -class PersonNode { - float x, targetX; - float y, targetY; - - String name; - float refreshScore; - - // Stats - PersonStat stats; - - public PersonNode(String name) { - this.refreshScore = 1.0; - - this.x = width / 2; - this.y = height / 2; - - // Reset stats - this.stats = new PersonStat(); - - // Set name - if (CONFIG.hideRealNames) { - this.name = CONFIG.hideNameReplacement; - } else { - this.name = name; - } - } - - public void setPosition(float x, float y) { - this.x = x; - this.y = y; - } - - public void setTargetPosition(float x, float y) { - this.targetX = x; - this.targetY = y; - } - - public void refresh() { - this.refreshScore = 1.0; - } - - public boolean equals(String name) { - return this.name.equals(name); - } - - public void incrementMsgReceived() { - this.stats.msgReceived++; - } - - public void incrementMsgSent() { - this.stats.msgSent++; - } - - public void draw() { - // If no PGraphics object is selected, then we draw to default PGraphics instead - this.draw(g); - } - - public void draw(PGraphics pg) { - // If mouse position is over the person, change UI - if (abs(mouseXSpace() - this.x) < PERSON_HITBOX_R && abs(mouseYSpace() - this.y) < PERSON_HITBOX_R) { - this.drawNodeInFocus(pg); - } else if (this.refreshScore < REFRESH_THRES) { - return; - } else { - this.drawNode(pg); - } - } - - protected void drawNodeInFocus(PGraphics pg) { - pg.pushMatrix(); - pg.translate(this.x, this.y); - - // Draw circle outline - pg.strokeWeight(4); - pg.stroke(50); - - // Draw inner circle - pg.fill(50, 255, 50); - pg.ellipse(0, 0, PERSON_NODE_SIZE, PERSON_NODE_SIZE); - - // Draw name tag - pg.textAlign(CENTER, CENTER); - pg.fill(255); - pg.textSize(PERSON_NAME_TEXT_SIZE); - - pg.text(this.name, 0, PERSON_NODE_SIZE); - - // Done - pg.popMatrix(); - - // Set hover state for UI (todo: add mutex lock so only one hover is possible and mouse input is consumed) - statcardHover.person = this; - statcardHover.show = true; - } - - protected void drawNode(PGraphics pg) { - pg.pushMatrix(); - pg.translate(this.x, this.y); - - float fillScore = map(this.refreshScore, 0, 1, 0, 245); - float strokeFillScore = map(this.refreshScore, 0, 1, 5, 50); - - // Draw circle outline - pg.strokeWeight(4); - pg.stroke(strokeFillScore); - - // Draw inner circle - pg.fill(10 + fillScore); - pg.ellipse(0, 0, PERSON_NODE_SIZE, PERSON_NODE_SIZE); - - // Draw name tag - pg.textAlign(CENTER, CENTER); - pg.fill(255, fillScore); - pg.textSize(PERSON_NAME_TEXT_SIZE); - pg.text(this.name, 0, PERSON_NODE_SIZE); - - // Done - pg.popMatrix(); - } - - private void update() { - // Update position by lerping - this.x = lerp(this.x, this.targetX, PERSON_LERP); - this.y = lerp(this.y, this.targetY, PERSON_LERP); - - // Update refresh score - this.refreshScore *= REFRESH_DECAY; - } -} - -class PersonMasterNode extends PersonNode { - public PersonMasterNode(String name) { - super(name); - - // Actually don't hide the name - this.name = name; - } - - @Override - protected void drawNode(PGraphics pg) { - pg.pushMatrix(); - pg.translate(this.x, this.y); - - // Draw circle outline - pg.strokeWeight(4); - pg.stroke(50); - - // Draw inner circle - pg.fill(255, 230, 64); - pg.ellipse(0, 0, PERSON_MASTER_NODE_SIZE, PERSON_MASTER_NODE_SIZE); - - // Draw name tag - pg.textAlign(CENTER, CENTER); - pg.fill(255); - pg.textSize(PERSON_NAME_TEXT_SIZE); - pg.text(this.name, 0, PERSON_MASTER_NODE_SIZE); - - // Done - pg.popMatrix(); - } -} diff --git a/RenderLayer.pde b/RenderLayer.pde deleted file mode 100644 index 01983b9..0000000 --- a/RenderLayer.pde +++ /dev/null @@ -1,114 +0,0 @@ -// The idea of the class "Renderlayer" is that -// it is a wrapper around PGraphics, for better organization -// of what is going to be rendered in the program. -// -// The derived classes of the RenderLayer abstract class -// could be used as singletons to draw layers with specific -// functions such as UI, payload, people, etc. - -abstract class RenderLayer { - PGraphics pg; - - public RenderLayer(int w, int h, String renderer) { - this.pg = createGraphics(w, h, renderer); - } - - public RenderLayer(int w, int h) { - this.pg = createGraphics(w, h, P2D); - } - - public RenderLayer() { - this.pg = createGraphics(width, height, P2D); - } - - public void render() { - this.pg.beginDraw(); - renderGraphics(); - this.pg.endDraw(); - } - - protected void renderGraphics() { - this.pg.clear(); - } -} - -class RenderUILayer extends RenderLayer { - - // Layer states - long timestamp; - - // Reference to the timeline object - Timeline timeline; - SpeedControl speedControl; - StatCardHover statCardHover; - - public RenderUILayer() { - super(); - this.timestamp = 0; - } - - @Override - protected void renderGraphics() { - this.pg.clear(); - this.renderTimestamp(); - this.renderTimeline(); - this.renderSpeedControl(); - this.renderStatCardHover(); - } - - private void renderTimestamp() { - this.pg.textFont(monospaceFont); - this.pg.textAlign(CENTER, CENTER); - String date = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date(this.timestamp)); - this.pg.textSize(20); - this.pg.fill(255); - this.pg.text(date, width/2, 20); - } - - private void renderTimeline() { - this.pg.noFill(); - - // Set opacity of the timeline to 70 if not hovered or 255 if hovered - this.pg.stroke(255, this.timeline.hovered ? 255 : 70); - this.pg.strokeWeight(1); - this.pg.rect(this.timeline.x, this.timeline.y, this.timeline.w, this.timeline.h); - - // Draw a cursor of where in the timeline we're currently are at - final float percentX = map(this.timeline.percentage, 0, 1, this.timeline.x, this.timeline.x + this.timeline.w); - this.pg.strokeWeight(3); - this.pg.line(percentX, this.timeline.y, percentX, this.timeline.y + this.timeline.h); - } - - private void renderSpeedControl() { - if (this.speedControl == null) return; - this.pg.image(this.speedControl.getSpeedIcon(), 10, 10); - } - - private void renderStatCardHover() { - this.pg.textFont(font); - if (this.statCardHover == null) return; - this.statCardHover.draw(this.pg); - } -} - -class RenderPeopleLayer extends RenderLayer { - ArrayList persons; - - public RenderPeopleLayer() { - super(); - } - - @Override - protected void renderGraphics() { - this.pg.clear(); - - if (this.persons == null) { - println("Error: reference to persons arraylist is null"); - return; - } - - for (PersonNode person : persons) { - person.draw(this.pg); - } - } -} diff --git a/Timeline.pde b/Timeline.pde deleted file mode 100644 index 13d1ee8..0000000 --- a/Timeline.pde +++ /dev/null @@ -1,53 +0,0 @@ -class Timeline { - - float x; - float y; - float w; - float h; - - float percentage; - boolean hovered; - - public Timeline(float x, float y, float w, float h) { - this.x = x; - this.y = y; - this.w = w; - this.h = h; - - this.percentage = 0; - - this.hovered = false; - } - - public void setPercentage(float percentage) { - this.percentage = constrain(percentage, 0, 1); - } - - public void handleMouseInput() { - // Check if mouse is inside the box - final float x2 = this.x + this.w; - - this.hovered = mouseX > this.x && mouseX < x2 && mouseY > this.y && mouseY < this.y + this.h; - - if (this.hovered) { - if (mousePressed) { - final float new_percentage = map(mouseX, this.x, x2, 0, 1); - this.setPercentage(new_percentage); - - final int new_gi = (int) map(new_percentage, 0, 1, 0, man.organizedMessagesList.size()); - - if (gi < new_gi) { - while (gi < new_gi) { - int di = gi % man.organizedMessagesList.size(); - MessageData current = man.organizedMessagesList.get(di); - processCurrentmessageData(current); - gi++; - } - } else { - gi = new_gi; - currentTimestamp = man.organizedMessagesList.get(gi).timestamp; - } - } - } - } -}