Skip to content

Commit

Permalink
Selectable AvatarFetcher and smarter loading of avatars to eliminate lag
Browse files Browse the repository at this point in the history
Now supports the option "AvatarFetcher" which can be either GravatarFetcher or FreebaseAvatarFetcher.  Leave blank for no avatar images.  FreebaseAvatarFetcher is mostly useful for some visualizations I've been working on for visualizing freebase.com.

The avatar cache is primed as the xml is read in.  This, combined with a larger read ahead queue keeps lag pretty much nonexistent.
  • Loading branch information
rictic committed Oct 23, 2008
1 parent 70ec51f commit c93feaf
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 64 deletions.
3 changes: 3 additions & 0 deletions convert_logs/config.template
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,6 @@ IsInputSorted=true

# OpenGL is experimental. Use at your own risk.
UseOpenGL=true

# Leave blank to go without avatars
AvatarFetcher=GravatarFetcher
92 changes: 92 additions & 0 deletions src/AvatarFetcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


public class AvatarFetcher {
protected CodeSwarmConfig cfg;
public AvatarFetcher(CodeSwarmConfig cfg) {
this.cfg = cfg;
}

public String fetchUserImage(String username) {
//Override fetchUserImage in your Avatar Fetcher
return null;
}

protected static String getFilename(String key){
return "image_cache/" + key;
}

protected static boolean imageCached(String key) {
return new File(getFilename(key)).exists();
}

protected static String getImage(String key, URL url) {
String filename = getFilename(key);
if (!imageCached(key)){
boolean successful = fetchImage(filename, url);
if (!successful)
return null;
}

return filename;
}

protected static boolean fetchImage(String filename, URL url) {
try {
new File("image_cache").mkdirs();
URLConnection con = url.openConnection();
InputStream input = con.getInputStream();
FileOutputStream output = new FileOutputStream(filename);

int length = con.getContentLength();
if (length == -1){
//read until exhausted
while(true){
int val = input.read();
if (val == -1)
break;
output.write(input.read());
}
}
else{
//read length bytes
for(int i = 0; i < length; i++)
output.write(input.read());
}

output.close();
input.close();
return true;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
}

//these two methods taken from http://en.gravatar.com/site/implement/java
private static String hex(byte[] array) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < array.length; ++i)
sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1,3));
return sb.toString();
}
protected static String md5Hex (String message) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
return hex (md.digest(message.getBytes("CP1252")));
} catch (NoSuchAlgorithmException e) {
} catch (UnsupportedEncodingException e) {
}
return null;
}

}
75 changes: 75 additions & 0 deletions src/FreebaseAvatarFetcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class FreebaseAvatarFetcher extends AvatarFetcher{
public FreebaseAvatarFetcher(CodeSwarmConfig cfg) {
super(cfg);
}

static private Pattern imageIDPattern = Pattern.compile("\"image:id\"\\s*:\\s*\"(.*?)\"");

private static String readURLToString(URL url) {
try {
URLConnection con = url.openConnection();
BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()));
StringBuilder sb = new StringBuilder();
int length = con.getContentLength();
if (length == -1){
//read until exhausted
while(true){
String line = reader.readLine();
if (line == null) break;
sb.append(line);
}
}
else{
//read length bytes
for(int i = 0; i < length; i++)
sb.append((char)reader.read());
}
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

private static String getUserImageID(String username) {
try {
new File("image_cache").mkdirs();
String json = readURLToString(new URL("http://www.freebase.com/api/service/mqlread?query=" +
"{%22query%22:{%22!/common/image/appears_in_topic_gallery%22:"+
"[{%22image:id%22:null}],%22id%22:%22/user/" + username + "%22}}"));
if (json == null) return null;
Matcher m = imageIDPattern.matcher(json);
if (m.find())
return m.group(1);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}

public String fetchUserImage(String username) {
String key = md5Hex("metaweb:" + username);
if (imageCached(key))
return getFilename(key);
try {
String imageID = getUserImageID(username);
if (imageID == null) return null;
return getImage(key, new URL("http://www.freebase.com/api/trans/image_thumb/" +imageID+ "?maxheight=40&mode=fillcrop&maxwidth=40"));
} catch (MalformedURLException e) {
e.printStackTrace(); //should be impossible...
return null;
}
}
}
67 changes: 11 additions & 56 deletions src/GravatarFetcher.java
Original file line number Diff line number Diff line change
@@ -1,63 +1,18 @@
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.*;

public class GravatarFetcher {

//these two methods taken from http://en.gravatar.com/site/implement/java
private static String hex(byte[] array) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < array.length; ++i)
sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1,3));
return sb.toString();
public class GravatarFetcher extends AvatarFetcher {
public GravatarFetcher(CodeSwarmConfig cfg) {
super(cfg);
}
private static String md5Hex (String message) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
return hex (md.digest(message.getBytes("CP1252")));
} catch (NoSuchAlgorithmException e) {
} catch (UnsupportedEncodingException e) {
}
return null;
}

public static String fetchUserImage(String username) {

public String fetchUserImage(String username) {
String hash = md5Hex(username);
String filename = "image_cache/" + hash;
if (!new File(filename).exists()){
try {
new File("image_cache").mkdirs();
URL url = new URL("http://www.gravatar.com/avatar/" + hash + "?d=identicon&s=40");
URLConnection con = url.openConnection();
InputStream input = con.getInputStream();
FileOutputStream output = new FileOutputStream(filename);

int length = con.getContentLength();
if (length == -1){
//read until exhausted
while(true){
int val = input.read();
if (val == -1)
break;
output.write(input.read());
}
}
else{
//read length bytes
for(int i = 0; i < length; i++)
output.write(input.read());
}

output.close();
input.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
try {
return getImage(hash, new URL("http://www.gravatar.com/avatar/" + hash + "?d=identicon&s=40"));
} catch (MalformedURLException e) {
e.printStackTrace(); //should be impossible...
return null;
}

return filename;
}
}
38 changes: 30 additions & 8 deletions src/code_swarm.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import java.util.Map;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
Expand Down Expand Up @@ -73,8 +75,8 @@ public class code_swarm extends PApplet {
// Liveness cache
static List<PersonNode> livingPeople = new ArrayList<PersonNode>();
static List<Edge> livingEdges = new ArrayList<Edge>();
static List<FileNode> livingNodes = new ArrayList<FileNode>();

static List<FileNode> livingNodes = new ArrayList<FileNode>();
LinkedList<ColorBins> history;
boolean finishedLoading = false;

Expand Down Expand Up @@ -166,7 +168,8 @@ public class code_swarm extends PApplet {
* drawLine: Pass coords and color
*/
public static Utils utils = null;

public AvatarFetcher avatarFetcher;

/**
* Initialization
*/
Expand Down Expand Up @@ -240,6 +243,8 @@ public void setup() {

isInputSorted = cfg.getBooleanProperty(CodeSwarmConfig.IS_INPUT_SORTED_KEY, false);

avatarFetcher = getAvatarFetcher(cfg.getStringProperty("AvatarFetcher","AvatarFetcher"));

/**
* This section loads config files and calls the setup method for all physics engines.
*/
Expand Down Expand Up @@ -292,6 +297,8 @@ public void setup() {
System.exit(1);
}



smooth();
frameRate(FRAME_RATE);

Expand All @@ -302,7 +309,7 @@ public void setup() {
history = new LinkedList<ColorBins>();
if (isInputSorted)
//If the input is sorted, we only need to store the next few events
eventsQueue = new ArrayBlockingQueue<FileEvent>(5000);
eventsQueue = new ArrayBlockingQueue<FileEvent>(50000);
else
//Otherwise we need to store them all at once in a data structure that will sort them
eventsQueue = new PriorityBlockingQueue<FileEvent>();
Expand Down Expand Up @@ -339,6 +346,16 @@ public void setup() {
sprite.mask(sprite);
}

@SuppressWarnings("unchecked")
private AvatarFetcher getAvatarFetcher(String avatarFetcherName) {
try {
Class<AvatarFetcher> c = (Class<AvatarFetcher>)Class.forName(avatarFetcherName);
return c.getConstructor(CodeSwarmConfig.class).newInstance(cfg);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

/**
* Load a colormap
*/
Expand Down Expand Up @@ -715,7 +732,6 @@ public void update() {
try {
currentEvent = eventsQueue.take();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println("Interrupted while fetching current event from eventsQueue");
e.printStackTrace();
continue;
Expand Down Expand Up @@ -1018,6 +1034,7 @@ private class XMLQueueLoader implements Runnable {
private final String fullFilename;
private BlockingQueue<FileEvent> queue;
boolean isXMLSorted;
private Set<String> peopleSeen = new TreeSet<String>();

private XMLQueueLoader(String fullFilename, BlockingQueue<FileEvent> queue, boolean isXMLSorted) {
this.fullFilename = fullFilename;
Expand Down Expand Up @@ -1058,10 +1075,16 @@ public void startElement(String uri, String localName, String name,
// int eventLinesRemoved = atts.getValue( "linesremoved" );

FileEvent evt = new FileEvent(eventDate, eventAuthor, "", eventFilename);

//We want to pre-fetch images to minimize lag as images are loaded
if (!peopleSeen.contains(eventAuthor)){
avatarFetcher.fetchUserImage(eventAuthor);
peopleSeen.add(eventAuthor);
}

try {
queue.put(evt);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println("Interrupted while trying to put into eventsQueue");
e.printStackTrace();
System.exit(1);
Expand All @@ -1075,7 +1098,6 @@ public void endDocument(){
try {
reader.parse(fullFilename);
} catch (Exception e) {
// TODO Auto-generated catch block
System.out.println("Error parsing xml:");
e.printStackTrace();
System.exit(1);
Expand Down Expand Up @@ -1473,7 +1495,7 @@ class PersonNode extends Node {
mLastPosition.set(new Vector2f(mPosition));
mLastPosition.add(mPhysicsEngine.startVelocity(this));
mFriction = 0.99f;
String iconFile = GravatarFetcher.fetchUserImage(name);
String iconFile = avatarFetcher.fetchUserImage(name);
if (iconFile != null) icon = loadImage(iconFile);
}

Expand Down

0 comments on commit c93feaf

Please sign in to comment.