//////////////////////////////////////////////////////////// // // Name: del.icio.us network explorer // Author: Michael Schieben // Url: http://www.twoantennas.com/projects/delicious-network-explorer/ // // Thanks to: processing and traer.physics // Inspired and parts of the code by: Sala (http://www.aharef.info) "Websites as Graphs" // and work by mkv25 (http://mkv25.net) // // This is my first java/processing code so don't blame me for some bad parts // Don't hestitate to contact me in case of bugs or other suggestions! // // Feel free to use / modify this code however you wish. // Maybe you'll spend credits to www.twoantennas.com then! // // //////////////////////////////////////////////////////////// // IMPORT ///////// ///////// ///////// ///////// ///////// ///////// ///////// // import traer.physics.*; import traer.animation.*; import java.util.Iterator; import java.util.ArrayList; import java.util.HashMap; import processing.net.*; // APPLICATION SETTINGS ///////// ///////// ///////// ///////// ///////// ///////// ///////// // final int maxNumberOfConnectedUsers = 1000; // LAYOUT GLOBALS ///////// ///////// ///////// ///////// ///////// ///////// ///////// // final float NODE_SIZE = 20; final float EDGE_LENGTH = 50; float EDGE_STRENGTH = 0.05; final float SPACER_STRENGTH = 2000; final color WHITE = color(255, 255, 255); final color BLACK = color(0, 0, 0); final color LIGHTGRAY = color(220,220,220); final color LIGHTERGRAY = color(238,238,238); final color GRAY = color(155,155,155); final color ORANGE = color(255,155,51); final color YELLOW = color(255,255,51); final color RED = color(255,51,51); final color BLUE = color(51,51,255); final color GREEN = color(0,155,0); final color VIOLETT = color(255,102,255); final String STANDARD_FONT = "TrebuchetMS-48.vlw"; final int LABEL_SIZE = 20; PFont font; // TEXT GLOBALS ///////// ///////// ///////// ///////// ///////// ///////// ///////// // final String addText = "add to your network"; final String visitText = "visit at del.icio.us"; String restartInfo = ""; // GLOBAL STUFF ///////// ///////// ///////// ///////// ///////// ///////// ///////// // ParticleSystem physics; Smoother3D centroid; HashMap userLookup = new HashMap(); HashMap particleLookup = new HashMap(); User startUser; // first user User currentUser; // user currently being explored User focusedUser; // user under the cursor boolean noConnectionError = false; // true if connection error boolean cursorHand = false; // if any object under mouse is clickable // (RE-)INITIALISATION ///////// ///////// ///////// ///////// ///////// ///////// ///////// // void setup() { size(750, 750); smooth(); framerate(24); strokeWeight(1); ellipseMode(CENTER); font = loadFont(STANDARD_FONT); physics = new ParticleSystem(0, 0.25); centroid = new Smoother3D(0.0, 0.0, 1.0, 0.8); physics.clear(); // if you want to run it locally from within processing: String name = param("username"); //comment this line //String name = "rockitbaby"; //uncomment this line and set name to your del.icio.us username //Particle newParticle; //User fanToAdd; startUser = new User(name.trim()); startUser.explore(); Particle p = startUser.build(null); userLookup.put(startUser.name, startUser); particleLookup.put(p, startUser); currentUser = startUser; } void restart(String name) { physics.clear(); userLookup.clear(); particleLookup.clear(); startUser = new User(name); startUser.explore(); Particle p = startUser.build(null); userLookup.put(startUser.name, startUser); particleLookup.put(p, startUser); currentUser = startUser; } // DRAWING ///////// ///////// ///////// ///////// ///////// ///////// ///////// // void draw() { //uncomment this if you want your network saved as pdfs //beginRecord(PDF, "frameimage-####.pdf"); cursorHand = false; //create related idols for currentUser if (!currentUser.addIdol()) { if (EDGE_STRENGTH < 0.05) EDGE_STRENGTH += 0.0001; } //tick if mosue is not pressed if (!mousePressed) { physics.tick( 1.0 ); } //physical magic if (physics.numberOfParticles() > 1) { updateCentroid(); } centroid.tick(); background(255); translate(width/2, height/2); scale(centroid.z()); translate(-centroid.x(), -centroid.y()); drawNetwork(); resetMatrix(); drawUserInfo(); drawOverlay(); if (cursorHand) { cursor(HAND); } else { cursor(ARROW); } //uncomment this if you want your network saved as pdfs //endRecord(); } void drawUserInfo() { resetMatrix(); fill(LIGHTERGRAY); rect(0, 0, width, 33); rect(0, height - 23, width, 23); textFont(font, 18); fill(YELLOW); if ((mouseX >= width - textWidth(addText) - 5) && (mouseY <= 33)) { cursorHand = true; rect(width - textWidth(addText) - 10, 0, textWidth(addText) + 10, 33); } else if ((mouseX <= width - textWidth(addText) - 10) && (mouseY <= 33)) { rect(0, 0, width - textWidth(addText) - 10, 33); cursorHand = true; } textFont(font, 12); if ((mouseX <= textWidth(restartInfo) + 10) && (mouseY >= height - 33)) { rect(0, height - 23, textWidth(restartInfo) + 10, 23); cursorHand = true; } if (currentUser == startUser) { fill(GREEN); } else { fill(ORANGE); } ellipse(15, 15, 20, 20); fill(BLACK); textFont(font, 18); String exploringInfo = "exploring: ".concat(currentUser.name); text(exploringInfo, 40, 5, width - 5, 20); text(addText, width - textWidth(addText) - 5, 5, textWidth(addText) + 5, 20); text(visitText, width - textWidth(visitText) - textWidth(addText) - 20, 5, textWidth(visitText) + 5, 20); textFont(font, 12); String particleInfo = "showing: ".concat(Integer.toString(physics.numberOfParticles())).concat(" people"); text(particleInfo, width - textWidth(particleInfo) - 5, height - 20, textWidth(particleInfo) + 5, 16); restartInfo = "restart exploration with ".concat(currentUser.name); text(restartInfo, 5, height - 20, textWidth(restartInfo) + 5, 16); if (noConnectionError) { fill(RED); text("Error: Can't establish conection to fetch data!", width / 2 - 300, height / 2 - 10, 600, 20); } smooth(); } void drawNetwork() { focusedUser = null; // DRAW EDGES stroke(LIGHTGRAY); beginShape( LINES ); for (int i = 0; i < physics.numberOfSprings(); ++i){ Spring e = physics.getSpring(i); Particle a = e.getOneEnd(); Particle b = e.getTheOtherEnd(); User oneUser = (User) particleLookup.get(a); User otherUser = (User) particleLookup.get(b); setEdgeColor(oneUser, otherUser); vertex(a.position().x(), a.position().y()); vertex(b.position().x(), b.position().y()); } endShape(); noStroke(); float xpos, ypos; for (int i = 0; i < physics.numberOfParticles(); ++i) { Particle v = physics.getParticle(i); User pu = (User) (particleLookup.get(v)); if (pu != null) { if (pu == startUser) { fill(GREEN); } else if (pu == currentUser) { fill(ORANGE); } else if (pu.explored) { fill(GRAY); } else { fill(LIGHTGRAY); } } else { fill(LIGHTGRAY); } xpos = v.position().x(); ypos = v.position().y(); // Draw basic image for user ellipse(xpos,ypos, getNodeSize(pu), getNodeSize(pu)); if(dist(mouseX, mouseY, relXPos((int)xpos), relYPos((int)ypos)) <= (getNodeSize(pu)/2*centroid.z()) ) { focusedUser = pu; cursorHand = true; } } } void setEdgeColor(User oneUser, User otherUser) { User relatedUser = (User) null; User exploreUser = (User) null; exploreUser = currentUser; strokeWeight(2); if (oneUser == exploreUser) { relatedUser = otherUser; } else if (otherUser == exploreUser) { relatedUser = oneUser; } if (relatedUser != null) { //CHANGE COLORS FOR TYPE OF RELATIONSHIP if(exploreUser.fans.contains(relatedUser.name) && exploreUser.idols.contains(relatedUser.name)) { stroke(RED); } else if (currentUser.idols.contains(relatedUser.name)) { stroke(BLUE); } else { stroke(YELLOW); } } else { stroke(LIGHTGRAY); strokeWeight(1); } } // INFORMATION ON ROLL OVER ///////// ///////// ///////// ///////// ///////// ///////// ///////// // void drawOverlay() { Particle v; // Check for rollover / focus if(focusedUser != null) { v = focusedUser.p; // Particle // Extract variables float xpos = relXPos(v.position().x()); float ypos = relYPos(v.position().y()); String user = focusedUser.name; // Draw higlight fill(YELLOW); float node_size = getNodeSize(focusedUser); int NNS = round((node_size - (node_size/4)) * centroid.z()); // New node size (overlay) ellipse(xpos, ypos, NNS, NNS); int numberOfFans = focusedUser.fans.size(); int numberOfIdols = focusedUser.idols.size(); String relationinfo; String faninfo = Integer.toString(numberOfFans); String idolinfo = Integer.toString(numberOfIdols); //CHANGE COLORS FOR TYPE OF RELATIONSHIP if(currentUser.fans.contains(focusedUser.name) && currentUser.idols.contains(focusedUser.name)) { relationinfo = focusedUser.name.concat(" and ").concat(currentUser.name).concat(" are friends"); } else if (currentUser.idols.contains(focusedUser.name)) { relationinfo = focusedUser.name.concat(" is in ").concat(currentUser.name).concat("'s network"); } else if (currentUser.fans.contains(focusedUser.name)) { relationinfo = focusedUser.name.concat(" is fan of ").concat(currentUser.name); } else { relationinfo = focusedUser.name.concat(" and ").concat(currentUser.name).concat(" have no direct relation"); } if(!focusedUser.explored) { faninfo = "click to explore"; } else { if (numberOfFans == 0) { faninfo = "no fans"; } else if (numberOfFans == 1) { faninfo = "".concat(faninfo).concat(" fan"); } else { faninfo = "".concat(faninfo).concat(" fans"); } if (numberOfIdols == 0) { idolinfo = "nobody in the network"; } else if (numberOfIdols == 1) { idolinfo = "".concat(idolinfo).concat(" person in the network"); } else { idolinfo = "".concat(idolinfo).concat(" people in the network"); } if (numberOfIdols > maxNumberOfConnectedUsers) { idolinfo = idolinfo.concat(" (displaying only the first ").concat(Integer.toString(maxNumberOfConnectedUsers)).concat(")"); } } // Display label pushMatrix(); translate(0, 0); textFont(font, 12); int textwidth = (int) textWidth(relationinfo) + 10; if (textwidth < 250) { textwidth = 250; } int textheight = 80; fill(red(YELLOW), green(YELLOW), blue(YELLOW), 80); if (xpos + textwidth > width) { xpos = xpos - textwidth; } if (ypos + textheight > height) { ypos = ypos - textheight; } rect(xpos, ypos, textwidth, textheight); fill(BLACK); textFont(font, 18); text(user, xpos + 5, ypos + 5, textwidth, textheight); textFont(font, 12); if (currentUser != focusedUser) { text(relationinfo, xpos + 5, ypos + 20 + 5, textwidth, textheight); } text(faninfo, xpos + 5, ypos + 36 + 5, textwidth, textheight); if (focusedUser.explored) { text(idolinfo, xpos + 5, ypos + 52 + 5, textwidth, textheight); } popMatrix(); smooth(); } } // PARTICLES, EDGES, ZOOM ///////// ///////// ///////// ///////// ///////// ///////// ///////// // void updateCentroid() { float xMax = Float.NEGATIVE_INFINITY, xMin = Float.POSITIVE_INFINITY, yMin = Float.POSITIVE_INFINITY, yMax = Float.NEGATIVE_INFINITY; for (int i = 0; i < physics.numberOfParticles(); ++i) { Particle p = physics.getParticle(i); xMax = max(xMax, p.position().x()); xMin = min(xMin, p.position().x()); yMin = min(yMin, p.position().y()); yMax = max(yMax, p.position().y()); } float deltaX = xMax-xMin; float deltaY = yMax-yMin; if ( deltaY > deltaX ) { centroid.setTarget(xMin + 0.5*deltaX, yMin + 0.5*deltaY, height/(deltaY+50)); } else { centroid.setTarget(xMin + 0.5*deltaX, yMin + 0.5*deltaY, width/(deltaX+50)); } } void addSpacersToNode( Particle p, Particle r ) { for ( int i = 0; i < physics.numberOfParticles(); ++i ) { Particle q = physics.getParticle(i); if (p != q && p != r) { physics.makeAttraction(p, q, -SPACER_STRENGTH, 20); } } } void makeEdgeBetween(Particle a, Particle b) { physics.makeSpring( a, b, EDGE_STRENGTH, EDGE_STRENGTH, EDGE_LENGTH ); } // EVENTS ///////// ///////// ///////// ///////// ///////// ///////// ///////// // void mouseReleased() { textFont(font, 12); if ((mouseX <= textWidth(restartInfo) + 10) && (mouseY >= height - 33)) { restart(currentUser.name); } else { textFont(font, 18); if ((mouseX >= width - textWidth(addText) - 5) && (mouseY <= 33)) { //add to network link("http://del.icio.us/network?add=".concat(currentUser.name)); } else if ((mouseX <= width - textWidth(addText) - 15) && (mouseY <= 33)) { // visit at delicius link("http://del.icio.us/".concat(currentUser.name)); } else if((mouseX < 400) && (mouseY >= height - 20)) { // restart restart(currentUser.name); } else if(focusedUser != null) { if(dist(mouseX, mouseY, relXPos((int)focusedUser.p.position().x()), relYPos((int)focusedUser.p.position().y())) <= (getNodeSize(focusedUser)/2*centroid.z()) ) { //explore user focusedUser.explore(); currentUser = focusedUser; focusedUser = null; } else { //unset focus focusedUser = null; } } } } // USER CLASS ///////// ///////// ///////// ///////// ///////// ///////// ///////// // class User { public String name; public Particle p; public ArrayList fans = new ArrayList(); public ArrayList idols = new ArrayList(); public boolean explored = false; private int idolsAdded = 0; User(String namei) { name = namei; } // name: addIdol // description: adds an Idol public boolean addIdol() { if (idolsAdded == idols.size()) { //all idols added already return false; } String idolname = (String) this.idols.get(this.idolsAdded); User idol = (User) userLookup.get(idolname); if (idol != null) { //user exists allready makeEdgeBetween(this.p, idol.p); } else { //add new user idol = new User(idolname); Particle p = idol.build(this.p); //idol.explore(); userLookup.put(idolname, idol); particleLookup.put(p, idol); } this.idolsAdded++; return true; } // name: build // description: builds the particle representation for this user // param: Particle q (parent particle) // return: Particle (the new particle) public Particle build(Particle q) { p = physics.makeParticle(); if (q == null) return p; addSpacersToNode(p, q); makeEdgeBetween(p, q); float randomX = (float)((Math.random() * 0.5) + 0.5); if (Math.random() < 0.5) randomX *= -1; float randomY = (float)((Math.random() * 0.5) + 0.5); if (Math.random() < 0.5) randomY *= -1; p.moveTo( q.position().x() +randomX, q.position().y() + randomY, 0 ); return p; } // name: explore // reads fans and idols from del.icio.us public void explore() { if (!explored) { //get Idols String[] idolsa = getIdols(name); int i = 0; idols.clear(); while(i < idolsa.length) { if(!idolsa[i].trim().equalsIgnoreCase("") && !idolsa[i].trim().equals(name)) { idols.add(idolsa[i].trim()); } i++; } //get Fans String[] fansa = getFans(name); fans.clear(); i = 0; while(i < fansa.length) { if(!fansa[i].trim().equalsIgnoreCase("") && !fansa[i].trim().equals(name)) { fans.add(fansa[i].trim()); } i++; } explored = true; } } } // CALCULATION OF SIZES AND POSITIONS ///////// ///////// ///////// ///////// ///////// ///////// ///////// // float getNodeSize(User u) { float fanFactor = (float) u.fans.size(); fanFactor = sqrt((fanFactor + 1) * 2); if (fanFactor > 30) { fanFactor = 30; } return NODE_SIZE + fanFactor; } float relXPos(float xin) { return width/2 + (xin - centroid.x()) * centroid.z(); } float relYPos(float yin) { return height/2 + (yin - centroid.y()) * centroid.z(); } float relXPos(int xin) { return relXPos((float)xin); } float relYPos(int yin) { return relYPos((float)yin); } // READ FROM DEL.ICIO.US JSON FEED ///////// ///////// ///////// ///////// ///////// ///////// ///////// // // Please don't use the two antennas url. Grab del.icio.us json feed directly. // Uncoment the second line on each get function. String[] getIdols(String name) { String url = "http://www.twoantennas.com/projects/delicious-network-explorer/json/?name=".concat(name).concat("&type=network"); //url = "http://del.icio.us/feeds/json/network/".concat("name").concat("?callback=networkNames="); return getJson(url, 14); } String[] getFans(String name) { String url = "http://www.twoantennas.com/projects/delicious-network-explorer/json/?name=".concat(name).concat("&type=fans"); //url = "http://del.icio.us/feeds/json/fans/".concat("name").concat("?callback=fanNames="); return getJson(url, 12); } String[] getJson(String url, int dirtysubsplit) { String input[]; try { input = loadStrings(url); //thats is so dirty that i will not say my name. String ret; ret = input[0]; ret = ret.replace('[', ' ').replace(']', ' ').replace('"', ' ').replace('=', ' ').replace(')', ' ').replace('(', ' '); ret = ret.substring(dirtysubsplit); input = ret.split(" , "); noConnectionError = false; return input; } catch (Exception e) { noConnectionError = true; input = "".split(","); return input; } }