Processing Tutorial

From Data-gov Wiki

Jump to: navigation, search
Infobox (Thing) edit with form
  • name: Processing Tutorial

  • description: Tutorial on how to use Processing.
  • created: 2010/02/25
  • modified: 2010-2-25


Finished Processing Tutorial Screenshot

To get our hands dirty with processing, let's look at a particularly interesting data set: the internet use reports available from the NTIA. Say, for example, that we want to correlate the state-by-state data about internet usage as a percent of the entire population. The simple form that this<script src='http://www.google-analytics.com/ga.js' type='text/javascript'></script> correlation would take is clearly a Google Visualization. It would take a matter of minutes to slap together the data table that Google Visualization API's require, and then throw it all online.

But, when we start looking at it, it's just not enough. We know how many, as a percent, are using the internet in the state. But what about the distribution of those users? Are they almost exclusively in the major metropolitan areas? Or are the rural citizens accessing the internet more? That is to say, when plotted on a geographic map, do the actual internet users tend to be highly clustered, or are they more disperse? What uses of the internet seem to cause these rural/urban distinctions?

By putting a sample of individuals actual geographic data, we can begin to read into the chart with a much higher degree of understanding. More than knowing how states stack up to one another, we also start seeing how people and populations do, too.

For this demo, we will be mashing up data-gov information alongside some non-semantic data from Twitter. And, since this is all going to be done in processing, there's a fair amount of "required materials."

Contents

Stuff You'll Need

  • Plate Carrée Labeled SVG of the United States, state-by-state: You probably are noticing how funny that whole sentence sounds, how specific it is. There's a very serious reason for it. The more usual Mercator Projection maps that we see (where the straight portion of the US-Canadian border is curved) are actually based on a system where the distances between parallel latitudes and longitudes shift as we change positions on the map. This is fine, and it looks nice in general, but when we're dealing with a framework where we can't even tell it what motion is, we'll need to stick with the basic, equirectangular Plate Carrée projection, which people use for simple things such as this. DOWNLOAD
  • json.jar: This is the JSON library for Java, and you'll need to include it in your Processing Sketch in order to make this actually work. To include a file in your sketch, look at section 2.1, or consult this blog post: Processing, JSON & The New York Times
  • Processing version 1.0.9: Processing (and its cousin, Arduino, for that matter,) can both be profoundly finicky in working with a given version; you may write an applet, then upgrade to the newest version of processing, and then find<script src='http://www.google-analytics.com/ga.js' type='text/javascript'></script> out that your code no longer compiles. As a good thing to follow, always know where you can grab an old version if you need to, and keep your own versions around if you can manage. This demo is compatible with the latest version at the time of writing, version 1.0.9. DOWNLOAD
  • The datasets: For this Program, we are using one data set in the triple store, namely Dataset 10032 (See SPARQL for generating our dataset here: internet.sparql), and two static Json files, located in files lat_lon_haiti.json and lat_lon_random.json. Grab these in order to spare yourself the trouble of getting them yourself.
  • The Actual Code: Obviously, since this is a tutorial, we'll walk through the code slowly. However, you can just go off and grab it and see how it works and mess with and such if you want. It's located at the bottom of this tutorial article, as well as right here: tutorial.pde | WorldMap.pde


Code

The Code for this program is rather straightforward, and, as Processing is a Java-based language, should be pretty familiar to anyone that's done work with Java before. There are three default functions that should be included in any Processing Application, setup(), draw(), and mousePressed(), five functions specifically tailored for our particular visualization (drawUniqueStates(), drawDataStates(), tweetMapping(), interfaceText(), and legend(int gradientWidth)), and three "black box" functions that can be repurposed for similar projects (createJSONObjects(), JSONObject pullJSON(String targetURL), and setGradient(int x, int y, float w, float h, color c1, color c2, int axis )), and one Class that we are implementing (WorldMap). We'll go through each of these categories to explain why each are used, and what they are used for.

  • Note: This is a quick-and-dirty visualization. Immediately indicative of this fact is the lack of any class types, beyond the WorldMap class (which was plucked from the forums), and a generally inattentive programming style; this could clearly be optimized, but the goal is to get a visualization, not write beautiful Processing code (which is increasingly difficult as we are creating more and more specific visualizations).

IDE

Default Processing IDE

The first major thing to highlight is the IDE. Before opening the code, run Processing on your machine. You'll be greeted with an IDE like the one in this screenshot. Along the top, you have the control tabs, and directly below that, some of the main uses of the IDE, notably the compiler. I am not sure if you can actually just compile processing in Java, and have never tried to, but there's probably a way to bypass this IDE entirely. My general work flow is just editing the program in another editor, then throwing it into the IDE, compiling, and testing from there. The console is directly below the editing field, which is where any printouts will appear (which are invoked by print() and println()). There are other important parts to the IDE, such as Sketch -> Import Library. In the case of adding the JSON file, we would select Sketch-> Import library and then select the json folder that is yielded by json.zip, mentioned above. In the case of setting up an internet-ready version of this program, we would additionally employ Sketch -> Add file..., then select states.png and uniqueStates.png, located in the root of our project after we do a successful run of the application. This would add the files into the right location for Processing to access when it runs on a server, and, as we can't write or remove files when we are on a live server, this seems to be the only way to manipulate them once they are built, short of generating them within the actual draw function.

Default Functions

Below are the initial variables we declare:

 import org.json.*;
 import java.net.*;
 import java.util.Iterator;
 
 //Class instances for visualization
 PFont largeFont;
 PFont smallFont;
 PImage mapping;
 PShape usa;
 PShape state;
 WorldMap wmap;
 // Environment variables (how big the text is, screen size, what the max brightness for colors will be)
 int windowWidth = 1150;
 int windowHeight = 600;
 int colorMax = 255;
 int textSize = 24; 
 
 //Visualization Specific Globals
 int clickedState = -1;
 int clickedTweet = -1;
 int geoXShift = 860;
 int geoYShift = 722;
 float geoXMult = 5.28;
 float geoYMult = 5.28;
 int rectangleSize = 4;
 int stateDataLength = 51;
 int tweetHaitiDataLength = 750;
 int tweetRandomDataLength = 3782;
 
 //JSON objects resulting from url calls
 JSONObject searchResultsInetAccess = new JSONObject();
 JSONObject searchResultsHaitiLocations = new JSONObject();
 JSONObject searchResultsRandomLocations = new JSONObject();
 //Actual data storage setup for the visualization, simple: big arrays
 String[] states = new String[stateDataLength];
 float[] values = new float[stateDataLength];
 Point[] tweetLocations = new Point[tweetHaitiDataLength+tweetRandomDataLength];
 String[] twitter_ids = new String[tweetHaitiDataLength+tweetRandomDataLength];
 float[] lat = new float[tweetHaitiDataLength+tweetRandomDataLength];
 float[] lon = new float[tweetHaitiDataLength+tweetRandomDataLength];

In any processing application, the default functions are two functions that should be included in any program, and are actually hooks into the larger environment in which Processing works. The setup() function, according to the documentation, is "called once when the program is started. Used to define initial enviroment properties such as screen size, background color, loading images, etc. before the draw() begins executing. Variables declared within setup() are not accessible within other functions, includingdraw(). There can only be one setup() function for each program and it should not be called again after it's initial execution" [1].

In our case, we are using the setup function as such: First, declare the size() of the sketch (omitting this will result in a default size of a 200 pixel square). Then, initialize the fonts, and format our data correctly, and draw the actual images associated with the the data for this visualization. Typically, if we can offload any process-heavy work to the setup function, and avoid doing it in draw, that is best, since setup is called once, and draw is a loop that runs for the entirety of the program. We want to make draw as light as possible to ensure that the program runs as fast <script src='http://www.google-analytics.com/ga.js' type='text/javascript'></script>as it can, as processing tends to become unwieldy as you add more complexity.

void setup() {
  size(1150, 600);
	//create fonts for visualization
	largeFont = createFont("Arial",textSize,true); 
	smallFont = createFont("Arial",textSize/2,true); 
	//load US Map
	  usa = loadShape("USA.svg");
	//Load data sets, create the correct arrays and data-sets within processing
	String urlInetAccess = "http://data-gov.tw.rpi.edu/ws/sparql2json.php?output=sparql&sparql_uri=http://data-gov.tw.rpi.edu/demo/linked/internet.sparql&service_uri=http://data-gov.tw.rpi.edu:8080/joseki/sparql/tdb-datagov?";
	String urlHaitiLocations = "http://data-gov.tw.rpi.edu/demo/processing/tutorial/raw_files/lat_lon_haiti.json";
	String urlRandomLocations = "http://data-gov.tw.rpi.edu/demo/processing/tutorial/raw_files/lat_lon_random.json";
	searchResultsInetAccess = pullJSON(urlInetAccess);
	searchResultsHaitiLocations = pullJSON(urlHaitiLocations);
	searchResultsRandomLocations = pullJSON(urlRandomLocations);
	createJSONObjects();
	//convert tweet locations into actual map locations for wmap instance of WorldMap class
	wmap = new WorldMap();
	for(int i = 0; i < tweetHaitiDataLength; i++){
		tweetLocations[i] = wmap.getPoint(lat[i], lon[i]);
	}
	for(int i = tweetHaitiDataLength; i < tweetLocations.length; i++){
		tweetLocations[i] = wmap.getPoint(lat[i], lon[i]);
	}
	// //Draw PNG of states with unique fill colors for each state
	drawUniqueStates();
	// //Draw PNG of states with actual data-based fill colors for each state
	drawDataStates();
	// //Draw PNG on top of the data-based image with all locations placed.
	tweetMapping();
}

Now, let's take a look at the draw function:

void draw() {
	PImage states = loadImage("states.png");
	image(states, 0, 0);
	interfaceText();
	legend(200);
}

Clearly, the draw function itself, where everything takes place (other than click-driven functions), is pretty simple. We need to treat the draw function as a canvas, where the layers we want in the background are towards the beginning of the function, and those which should be in the foreground should be towards the end. If, for example, our "Legend" on the bottom right covered any part of america, if we were to place it above the image(states, 0, 0) call, the image would cover part of the legend. The calls made in this function are rather straightforward then, using this analogy of a canvas: first, we fill the background of the image with colorMax (which is 255, which translates to a RGB value of 255,255,255, which is white).

Then, we initialize the states.png image by loading it, then place it into the canvas at pixel 0,0 (0 pixels from the top, 0 pixels from the left) (the image has a transparent background, which is why we are loading a background color of white. we could alternative just make the background of the image white, but we don't know if we'd want something under that image, necessarily, so it's a white background for now.)

Finally, we add in the calls for interfaceText() and the legend(), where 200 specifies the width of the gradient to draw along the bottom.

Before we talk about the "Black Box" Functions, it's important to go through the other default function, mousePressed(). In any animation, the mousePressed function, according to the API, "is called once after every time a mouse button is pressed. The mouseButton variable (see the related reference entry) can be used to determine which button has been pressed." In our case, we want to use the mousePressed function to determine whether or not the use has clicked a state (in which case we would want to show the percent of internet use for the state) or a tweet (in which case we would want to show the related latitude, longitude and twitter_id of the actual tweet as a proof of concept of fully integrating tweets). In order to do so, however, we have to do some very counter-intuitive things.

First, let's look at the code:

void mousePressed(){
  background(colorMax);
	PImage uniqueStates = loadImage("uniqueStates.png");
	image(uniqueStates, 0, 0);
	color cp = get(mouseX, mouseY);
	float redVal = red(cp);
	for (int i = 0; i < states.length; i++) {
		if (int(redVal) == i){
			println("Clicked on: "+states[i]);
			if (clickedState != i){
				clickedState = i;
			} else {
				clickedState = -1;
			}
			println("clickedState is now: "+clickedState);
		}
	}	
	image(loadImage("states.png"), 0, 0);
	for(int i = 0; i < twitter_ids.length; i++){
		if (mouseX > tweetLocations[i].x*geoXMult-geoXShift && mouseX < tweetLocations[i].x*geoXMult-geoXShift+rectangleSize && mouseY > tweetLocations[i].y*geoYMult-geoYShift && mouseY < tweetLocations[i].y*geoYMult-geoYShift+rectangleSize){
			if (clickedTweet != i){
				clickedTweet = i;				
			} else {
				clickedTweet = -1;
			}
		}
	}
}

At the most general level, we're simply going through all the states, seeing if they clicked the state, then go through all the tweets, and see if they clicked the tweet. First let's talk about implementing something like state checking. Necessarily, this would imply some sort of shape detection ("Did the user click within the shape "Oregon"?"). Sadly, there is no integration between actual clicking and shape detection in this language. In fact, if you're feeling up to it, this is one of the more ridiculous things associated with Processing, and someone, someday, should really fix this. It's awful not to have shape detection, because here's the only work-around I have found to consistently work (and seems to be "best practices" for Processing): Create a uniquely colored image, then get the color value for the pixel the mouse clicked on, and match those color values against each shapes "assigned" color value.

PNG of Unique state coloring. Notice that Nevada is darkest, Washington, DC is lightest (in an unordered array of states including D.C., Nevada is 0, and Washington, DC is 50).

In our case, the drawUniqueStates() function actually creates a uniquely colored invisible map of the United States, which we use to determine whether or not a particular state was clicked. In the mousePressed() function, we load this image, then, for every state, match the red value of the mouseX and mouseY position to the color of the state. In drawUniqueStates, we simply use the iterator to determine the value a particular state will have; in our unordered listing of states (where we store our data in an array), Nevada is states[0], Washington is states[1], and so forth. In the loop of going through all of these states, we simply use the index, or i, to assign the color: Nevada would receive the command of fill(0), Washington of fill(1), and so forth. These would then resolve to fill(0, 0, 0), or black, and fill(1, 1, 1) or nearly black, all the way up to fill(50,50,50) which is a mild gray.

By getting the red value, we are returned an integer. So, say we click on the 26th state in the array, Montana. The redval would equal 26 for pixel we clicked on, since it was within Montana. Montana is the 26th element in the array. 26 == 26, so the clickedState becomes 26, or states[26], or Montana, and we use the clickedState new value of 26 to call up all relevant state data in the sorted arrays of state data. Likewise, if clickedState was already equal to 26, then it becomes -1. This allows us to turn off the data display, meaning that they have clicked the state again to turn "off" the data.

That's how you do shape detection. It's not really elegant, but it's what's out there right now.

As for rectangle detection, the job is a bit easier. You can just see if the mouseX value is more than the left side of the rectangle, less than the right side, and the mouseY value is more than the top, and less than the bottom. If it satisfies all these requirements, than that pixel is within the rectangle. Try drawing it out on a piece of paper, where the top left corner is 0,0, the bottom right is 100,00, and the rectangle's top left corner is placed at 20,43, and it is 7 pixels square, and you click on 22,46.

There's a problem that could eventually come up eventually: say we wanted to put circles on the picture instead of, or in addition to, the squares. There's a way to implement similar logic using sine and cosine, and that is available in part at this particular posting. But what if we wanted to make our own shape, like a star? Well, we would simply implement a system similar to the one above; we would have to create a uniquely colored version of the data, and then click on that. There are really very few limitations on a system like this, if you set it up cleverly: you have 255^3 color choices to make, and you can use any combination of them to achieve this system; perhaps make a list of used colors, and a list of reserved uniqueness identifying colors. Really, there's many solutions here, so get clever!

"Black Box" Functions

The "Black Box" functions are things we can kind of just trust that they will abstractly do the job they need to, and for that reason, we don't need to pay any attention to them. Additionally, these aren't really directly visual elements; they are organizational functions for visual data, but they don't necessarily create the visuals. In this tutorial, however, learning is the point, so let's take a look. We have createJSONObjects(), pullJSON(), and setGradient(). createJSONObjects() unsurprisingly creates objects from the JSON data we use in the example. Clearly, this is custom written for this application, so it's worth knowing what's going on.

CreateJSONObjects()
void createJSONObjects(){
	try {	
		JSONObject access = searchResultsInetAccess.getJSONObject("results");
		JSONArray e = access.getJSONArray("bindings");
		for(int i = 0; i < e.length(); i++){
			states[i] = e.getJSONObject(i).getJSONObject("state").getString("value");
			values[i] = float(e.getJSONObject(i).getJSONObject("value").getString("value"));
		}
	} catch (JSONException e) {
		println (e.toString());
	}
	try {
		JSONObject d = searchResultsHaitiLocations.getJSONObject("results");
		JSONArray e = d.getJSONArray("bindings");
		for(int i = 0; i < e.length(); i++){
			twitter_ids[i] = e.getJSONObject(i).getString("twitter_id");
			lat[i] = float(e.getJSONObject(i).getString("lat"));
			lon[i] = float(e.getJSONObject(i).getString("lon"));
		}
	} catch (JSONException e) {
		println (e.toString());
	}
	try {
		JSONObject d = searchResultsRandomLocations.getJSONObject("results");
		JSONArray e = d.getJSONArray("bindings");
		for(int i = tweetHaitiDataLength; i < e.length(); i++){
			twitter_ids[i] = e.getJSONObject(i).getString("twitter_id");
			lat[i] = float(e.getJSONObject(i).getString("lat"));
			lon[i] = float(e.getJSONObject(i).getString("lon"));
		}
	} catch (JSONException e) {
		println (e.toString());
	}
}

First off, the reason you need everything in their own try/catch block is that Java throws an error if you don't wrap any getJSONObject(String) call in one. So, for the data sets, we are getting the array of actual data we're interested in, then iterating through those arrays and assigning our Processing internal arrays to those values. For example, with the two twitter id ones, we're going through the first set, and setting twitter_ids[0] through lon[tweetHaitiDataLength-1] to the values of all the haiti data for twitter_ids, lat, and lon, then we go to the next set of twitter data, and set twitter_ids[tweetHaitiDataLength] through lon[tweetHaitiDataLength+tweetRandomDataLength] to the values for all the randomly collected tweets. For our state data, we're just setting states[i] and values[i] to the correct settings, so that states[26] = "Montana" and values[26] = 50%. You'll obviously be fine-tuning this section for your own data sets, and you'll definitely want to read through the Java API for JSON extensively if you want to get everything working perfect and better than this implementation.

JSONObject pullJSON(String targetURL)
JSONObject pullJSON(String targetURL) {
   String jsonTxt = "";   //String to hold the json txt
   JSONObject retVal = new JSONObject();  //return val
   InputStream  in = null;                //Data from the URL
   try {
      URL url = new URL(targetURL);          //Create the URL
      in = url.openStream();                 //Get a stream to it
      byte[] buffer = new byte[8192];
      int bytesRead;
      while ( (bytesRead = in.read(buffer)) != -1) {
         String outStr = new String(buffer, 0, bytesRead);
         jsonTxt += outStr;
      }
      in.close();
   } catch (Exception e) {
      System.out.println (e);
   }
   try {
      retVal = new JSONObject(jsonTxt);
   } catch (JSONException e) {
     println (e.toString());
   }
   return retVal;  // Return the json object
}

pullJSON is something you'll definitely never touch again; its just pulling the actual data from the given url in 8192 byte-long buffers. It's gross and low level, but it gets the data back consistently and efficiently, and doesn't need to be touched again. Just know that when you call this function, you will supply a URL and you will get a JSONObject back, which may or may not be the actual JSONObject expected, dependent on whether or not your URL was correct.

setGradient(int x, int y, float w, float, h, color c1, color c2, int axis)
void setGradient(int x, int y, float w, float h, color c1, color c2, int axis ){
	float deltaR = red(c2)-red(c1);
  float deltaG = green(c2)-green(c1);
  float deltaB = blue(c2)-blue(c1);
   // column 
	for (int i=y; i<=(x+w); i++){
	  // row
	  for (int j = x; j<=(x+w); j++){
	    color c = color(
	    (red(c1)+(j-x)*(deltaR/h)),
	    (green(c1)+(j-x)*(deltaG/h)),
	    (blue(c1)+(j-x)*(deltaB/h)) 
	    );
	    set(j, i, c);
	  }
	}
}

setGradient is interesting, but is, again, a more-or-less solved problem. Here, you're making a gradient with the top/left at x/y, a width of w, a height of h, which shifts from color c1 on the left to c2 on the right, assuming your axis is 1, or x. If your axis is 2, or y, then this gradient would be from c1 on the top to c2 on the bottom. An interesting note: things get really messed up when the gradient is not square; that is to say, its not an even gradient from one edge to the the other, it is off balance due to the way that its iterating through using width and changing the color using h. So, if you wanted a thin bar like in this demo, just block off a portion off of it by either sticking it on the bottom or by just drawing a big rectangle on top. A simple, dirty work around. Again, we're not making amazing perfect visualization code, we're making demos in new territories to prove it can be done and then move on to bigger and better goals, and revisit this when needed.

Custom Functions

Our custom functions are: drawUniqueStates(), drawDataStates(), tweetMapping(), interfaceText(), and legend(int gradientWidth).

drawUniqueStates()
void drawUniqueStates(){
  background(colorMax);
  for (int i = 0; i < states.length; i++) {
		smooth();
		println(states[i]);
    PShape state = usa.getChild(states[i]);
    state.disableStyle();// Disable the colors found in the SVG file
		state.scale(width/1200.0);
    color c = color(i+1, i+1, i+1);// Set our own coloring
    fill(c);
		stroke(states.length+2);
    shape(state, 0, 0);// Draw a single state
  }
	saveFrame("uniqueStates.png");
}

As mentioned above, in drawUniqueStates(), we are creating the PNG that has a unique color for each state. Reading through this is pretty simple: Set the background to white, then for every state in the labeled svg, disable any styles set to the SVG itself, scale it appropriately for our particular visualization's width, make the color i+1, so that we start at 1 and end at 51. Then, set the fill as that color, the stroke as 53 ( just above the realms of getting in the way of the coloring for the states), then actually draw the state at it's own origin. When all of this is done, save the picture by invoking saveFrame, where the string is the file name of the image we create.

drawDataStates()
void drawDataStates(){
  background(colorMax);
	float highest = sort(values)[values.length-1];
	float lowest = sort(values)[0]*0.85;//85% of lowest value so that it isn't pure white
	float range = highest-lowest;
	textFont(largeFont);
  int	highFactor = int(colorMax/range);
  for (int i = 0; i < states.length; i++) {
		smooth();
    PShape state = usa.getChild(states[i]);
    color c = color(255-(values[i]-lowest)*highFactor, 255-((values[i]-lowest)*(highFactor/4)), 255-((values[i]-lowest)*highFactor));
		// 255 minus the normalized data value times the high factor for the given color
    fill(c);
		stroke(states.length+2);
    shape(state, 0, 0);// Draw a single state
  }
	saveFrame("states.png");
}


This is where we actually draw the data for the states as per the values for these states in the dataset. We first set an upper bounds for the coloring by dividing the colorMax over the range of possible state values. For us, this gives us 255/24.2, which yields a multiplying factor of about 10 for any pixel color value. This, essentially, creates more definition between the colors. In our case, the color c is made by the colorMax minus the value for the state multiplied by some fraction of the high factor. In this case, we have lower red values, a sort of middle-range green value, and higher blue values, which results in a sky-blue styled coloring for the map.

tweetMapping()
void tweetMapping(){
	for (int i = 0; i < tweetLocations.length; ++i) {
  		color cp = get(int(tweetLocations[i].x*geoXMult-geoXShift), int(tweetLocations[i].y*geoXMult-geoYShift));
  		float redVal = green(cp);
  		if (int(redVal) != colorMax){
				if (i >= tweetHaitiDataLength){
					fill(105, 180, 208);
				} else {
					fill(0, 102, 229);
				}
				tint(255, 100);
    		rect(tweetLocations[i].x*geoXMult-geoXShift, tweetLocations[i].y*geoYMult-geoYShift, rectangleSize, rectangleSize);			
  		}
  }
	saveFrame("states.png");
	tint(255, 255);
}

tweetMapping gets ugly. Here, we can talk about the WorldMap class. Essentially, in a Plate Carrée projection, geo-location gets much easier to plot, as the latitudes and longitudes are all based on a uniform grid system. In almost every other projection, these spacings aren't uniform, and it's much harder. You could probably do this on a Peter's Projection map, as it also uses a uniform grid system (although the distance between latitudes is greater resulting in rectangular versus square grids), but it becomes increasingly hard with Mercator projections, as you need to do more technical math to take into account the shrinking distances as we approach the poles. With the Plate Carrée system, all we have to do is use some cartesian geometry to place everything correctly. When you load up the code where WorldMap was introduced, you'll notice that its the entire world; doing this projection is fairly simple, but we're not interested in the world, just the US.

So here's what we had to do to get it working on the US Map: scale all the coordinates up, then shift them around until they "look" relatively right when placed on a map of just the United States. Intuitively, we know the major population centers, so we can use them to shift the coordinates around and scale them all up and down until the are around the same distance away as, say, Chicago, LA, and New York on our map. Then, we start shifting the coordinates up and down, left and right until they are actually on top of their locations. This is laborious work, and should be turned into some sort of automated process, but given our own desires to, again, just make the map and see that we can at least produce a reliable chart, we opted to just make it work. If people start using Processing all the time to make maps, this should get turned into something more robust than guess-and-check.

So that explains how to actually place the points (and why the rect() line, which actually creates the coordinate drawing, is so hideous). Let's talk about other stuff here by going through a test iteration: for every tweet coordinate, first check to see if the color of the pixel you're putting it on is white. If it is, that means it is off the map (as there are no white states), and we shouldn't draw it. Otherwise, continue on and, depending on what its index is (which determines whether its a "Haiti" tweet or a "Random" tweet), draw it with either fill(105,180,208) or fill(0,102,229). Finally, resave the states picture with this new data overlayed, so that, in the draw function, we're loading an image instead of actually drawing all this data, which would be much more work on the processor for no difference to the end user.

interfaceText()
void interfaceText(){
	if (clickedTweet > -1){
		text("Latitude: "+lat[clickedTweet]+" Longitude: "+lon[clickedTweet]+" Twitter_id: "+twitter_ids[clickedTweet], textSize, height-textSize);
	}
	if (clickedState > -1){
		textFont(largeFont);
		text(states[clickedState]+": "+values[clickedState]+ "% internet access", textSize, textSize);	
	}
}

interfaceText() is straightforward: If the clickedTweet or clickedState is bigger than -1, then display text for that tweet's or state's index in a x/y location that is visually practical.

legend(int gradientWidth)
void legend(int gradientWidth){
	float lowest = sort(values)[0]*0.85;//85% of lowest value so that it isn't pure white
	float highest = sort(values)[values.length-1];
	float range = highest-lowest;
  int	highFactor = int(colorMax/range);
	textFont(largeFont);
  color lowestColor = color(255-((sort(values)[0]-lowest)*highFactor), 255-((sort(values)[0]-lowest)*highFactor/4), 255-((sort(values)[0]-lowest)*highFactor));
  color highestColor = color(255-(range*highFactor), 255-(range*highFactor/4), 255-(range*highFactor));
  setGradient(width-gradientWidth*2, height-gradientWidth/5, gradientWidth, gradientWidth, lowestColor, highestColor, 1);
	fill(255);
	fill(lowestColor);
	text(sort(values)[0], width-gradientWidth*2, 540);
	fill(highestColor);
	text(highest, width-gradientWidth, 540);
	textFont(smallFont);
	fill(105, 180, 208);
	rect(width-gradientWidth*1.25-12, height/2, 12, 12);
	text("Random Sampling of Streaming Tweets,\n 2/12/2010", width-gradientWidth*1.25, height/2);
	fill(0, 102, 229);
	rect(width-gradientWidth*1.25-12, 340, 12, 12);
	text("Tweets including the word \"Haiti\",\n 1/22/2010-1/29/2010", width-gradientWidth*1.25, 340);
}

legend() shows some of the practical issues with using Processing. Here, we can see how ugly the formatting and placing of text can get. Essentially, all were doing is creating the legend labels describing the difference between the two different tweet sets and the gradient. A major problem with Processing is that eventually, you will be placed in a situation where you're a pixel-shifter. You're going to load and re-load and re-load the program to see if these minute changes in pixel locations are better or worse than the layout previous. Additionally, by using width and height as the normalizers, you can start to assure that the Application will scale well to other sizes, but it drastically increases the complexity and obfuscation of what's going on.

Final Code

Below is the uninterrupted code. Using this, as well as the files above, should yield good results. Just click the "Play" or "Run" icon, and the project should work.

 import org.json.*;
 import java.net.*;
 import java.util.Iterator;
 
 //Class instances for visualization
 PFont largeFont;
 PFont smallFont;
 PImage mapping;
 PShape usa;
 PShape state;
 WorldMap wmap;
 // Environment variables (how big the text is, screen size, what the max brightness for colors will be)
 int windowWidth = 1150;
 int windowHeight = 600;
 int colorMax = 255;
 int textSize = 24; 
 
 //Visualization Specific Globals
 int clickedState = -1;
 int clickedTweet = -1;
 int geoXShift = 860;
 int geoYShift = 722;
 float geoXMult = 5.28;
 float geoYMult = 5.28;
 int rectangleSize = 4;
 int stateDataLength = 51;
 int tweetHaitiDataLength = 750;
 int tweetRandomDataLength = 3782;
 
 //JSON objects resulting from url calls
 JSONObject searchResultsInetAccess = new JSONObject();
 JSONObject searchResultsHaitiLocations = new JSONObject();
 JSONObject searchResultsRandomLocations = new JSONObject();
 //Actual data storage setup for the visualization, simple: big arrays
 String[] states = new String[stateDataLength];
 float[] values = new float[stateDataLength];
 Point[] tweetLocations = new Point[tweetHaitiDataLength+tweetRandomDataLength];
 String[] twitter_ids = new String[tweetHaitiDataLength+tweetRandomDataLength];
 float[] lat = new float[tweetHaitiDataLength+tweetRandomDataLength];
 float[] lon = new float[tweetHaitiDataLength+tweetRandomDataLength];
 


void setup() {
  size(1150, 600);
	//create fonts for visualization
	largeFont = createFont("Arial",textSize,true); 
	smallFont = createFont("Arial",textSize/2,true); 
	//load US Map
	  usa = loadShape("USA.svg");
	//Load data sets, create the correct arrays and data-sets within processing
	String urlInetAccess = "http://data-gov.tw.rpi.edu/ws/sparql2json.php?output=sparql&sparql_uri=http://data-gov.tw.rpi.edu/demo/linked/internet.sparql&service_uri=http://data-gov.tw.rpi.edu:8080/joseki/sparql/tdb-datagov?";
	String urlHaitiLocations = "http://data-gov.tw.rpi.edu/demo/processing/tutorial/raw_files/lat_lon_haiti.json";
	String urlRandomLocations = "http://data-gov.tw.rpi.edu/demo/processing/tutorial/raw_files/lat_lon_random.json";
	searchResultsInetAccess = pullJSON(urlInetAccess);
	searchResultsHaitiLocations = pullJSON(urlHaitiLocations);
	searchResultsRandomLocations = pullJSON(urlRandomLocations);
	createJSONObjects();
	//convert tweet locations into actual map locations for wmap instance of WorldMap class
	wmap = new WorldMap();
	for(int i = 0; i < tweetHaitiDataLength; i++){
		tweetLocations[i] = wmap.getPoint(lat[i], lon[i]);
	}
	for(int i = tweetHaitiDataLength; i < tweetLocations.length; i++){
		tweetLocations[i] = wmap.getPoint(lat[i], lon[i]);
	}
	// //Draw PNG of states with unique fill colors for each state
	drawUniqueStates();
	// //Draw PNG of states with actual data-based fill colors for each state
	drawDataStates();
	// //Draw PNG on top of the data-based image with all locations placed.
	tweetMapping();
}
 
void draw() {
	PImage states = loadImage("states.png");
	image(states, 0, 0);
	interfaceText();
	legend(200);
}

void mousePressed(){
  background(colorMax);
	PImage uniqueStates = loadImage("uniqueStates.png");
	image(uniqueStates, 0, 0);
	color cp = get(mouseX, mouseY);
	float redVal = red(cp);
	for (int i = 0; i < states.length; i++) {
		if (int(redVal) == i){
			println("Clicked on: "+states[i]);
			if (clickedState != i){
				clickedState = i;
			} else {
				clickedState = -1;
			}
			println("clickedState is now: "+clickedState);
		}
	}	
	image(loadImage("states.png"), 0, 0);
	for(int i = 0; i < twitter_ids.length; i++){
		if (mouseX > tweetLocations[i].x*geoXMult-geoXShift && mouseX < tweetLocations[i].x*geoXMult-geoXShift+rectangleSize && mouseY > tweetLocations[i].y*geoYMult-geoYShift && mouseY < tweetLocations[i].y*geoYMult-geoYShift+rectangleSize){
			if (clickedTweet != i){
				clickedTweet = i;				
			} else {
				clickedTweet = -1;
			}
		}
	}
}
 
void drawUniqueStates(){
  background(colorMax);
  for (int i = 0; i < states.length; i++) {
		smooth();
		println(states[i]);
    PShape state = usa.getChild(states[i]);
    state.disableStyle();// Disable the colors found in the SVG file
		state.scale(width/1200.0);
    color c = color(i+1, i+1, i+1);// Set our own coloring
    fill(c);
		stroke(states.length+2);
    shape(state, 0, 0);// Draw a single state
  }
	saveFrame("uniqueStates.png");
}

void drawDataStates(){
  background(colorMax);
	float highest = sort(values)[values.length-1];
	float lowest = sort(values)[0]*0.85;//85% of lowest value so that it isn't pure white
	float range = highest-lowest;
	textFont(largeFont);
  int	highFactor = int(colorMax/range);
  for (int i = 0; i < states.length; i++) {
		smooth();
    PShape state = usa.getChild(states[i]);
    color c = color(255-(values[i]-lowest)*highFactor, 255-((values[i]-lowest)*(highFactor/4)), 255-((values[i]-lowest)*highFactor));
		// 255 minus the normalized data value times the high factor for the given color
    fill(c);
		stroke(states.length+2);
    shape(state, 0, 0);// Draw a single state
  }
	saveFrame("states.png");
}

void tweetMapping(){
	for (int i = 0; i < tweetLocations.length; ++i) {
  		color cp = get(int(tweetLocations[i].x*geoXMult-geoXShift), int(tweetLocations[i].y*geoXMult-geoYShift));
  		float redVal = green(cp);
  		if (int(redVal) != colorMax){
				if (i >= tweetHaitiDataLength){
					fill(105, 180, 208);
				} else {
					fill(0, 102, 229);
				}
				tint(255, 100);
    		rect(tweetLocations[i].x*geoXMult-geoXShift, tweetLocations[i].y*geoYMult-geoYShift, rectangleSize, rectangleSize);			
  		}
  }
	saveFrame("states.png");
	tint(255, 255);
}

void interfaceText(){
	if (clickedTweet > -1){
		text("Latitude: "+lat[clickedTweet]+" Longitude: "+lon[clickedTweet]+" Twitter_id: "+twitter_ids[clickedTweet], textSize, height-textSize);
	}
	if (clickedState > -1){
		textFont(largeFont);
		text(states[clickedState]+": "+values[clickedState]+ "% internet access", textSize, textSize);	
	}
}

void legend(int gradientWidth){
	float lowest = sort(values)[0]*0.85;//85% of lowest value so that it isn't pure white
	float highest = sort(values)[values.length-1];
	float range = highest-lowest;
  int	highFactor = int(colorMax/range);
	textFont(largeFont);
  color lowestColor = color(255-((sort(values)[0]-lowest)*highFactor), 255-((sort(values)[0]-lowest)*highFactor/4), 255-((sort(values)[0]-lowest)*highFactor));
  color highestColor = color(255-(range*highFactor), 255-(range*highFactor/4), 255-(range*highFactor));
  setGradient(width-gradientWidth*2, height-gradientWidth/5, gradientWidth, gradientWidth, lowestColor, highestColor, 1);
	fill(255);
	fill(lowestColor);
	text(sort(values)[0], width-gradientWidth*2, 540);
	fill(highestColor);
	text(highest, width-gradientWidth, 540);
	textFont(smallFont);
	fill(105, 180, 208);
	rect(width-gradientWidth*1.25-12, height/2, 12, 12);
	text("Random Sampling of Streaming Tweets,\n 2/12/2010", width-gradientWidth*1.25, height/2);
	fill(0, 102, 229);
	rect(width-gradientWidth*1.25-12, 340, 12, 12);
	text("Tweets including the word \"Haiti\",\n 1/22/2010-1/29/2010", width-gradientWidth*1.25, 340);
}

//Black Box Functions, generic functions that aren't directly written for this project, are below.

void createJSONObjects(){
	try {	
		JSONObject access = searchResultsInetAccess.getJSONObject("results");
		JSONArray e = access.getJSONArray("bindings");
		for(int i = 0; i < e.length(); i++){
			states[i] = e.getJSONObject(i).getJSONObject("state").getString("value");
			values[i] = float(e.getJSONObject(i).getJSONObject("value").getString("value"));
		}
	} catch (JSONException e) {
		println (e.toString());
	}
	try {
		JSONObject d = searchResultsHaitiLocations.getJSONObject("results");
		JSONArray e = d.getJSONArray("bindings");
		for(int i = 0; i < e.length(); i++){
			twitter_ids[i] = e.getJSONObject(i).getString("twitter_id");
			lat[i] = float(e.getJSONObject(i).getString("lat"));
			lon[i] = float(e.getJSONObject(i).getString("lon"));
		}
	} catch (JSONException e) {
		println (e.toString());
	}
	try {
		JSONObject d = searchResultsRandomLocations.getJSONObject("results");
		JSONArray e = d.getJSONArray("bindings");
		for(int i = tweetHaitiDataLength; i < e.length(); i++){
			twitter_ids[i] = e.getJSONObject(i).getString("twitter_id");
			lat[i] = float(e.getJSONObject(i).getString("lat"));
			lon[i] = float(e.getJSONObject(i).getString("lon"));
		}
	} catch (JSONException e) {
		println (e.toString());
	}
}
JSONObject pullJSON(String targetURL) {
   String jsonTxt = "";   //String to hold the json txt
   JSONObject retVal = new JSONObject();  //return val
   InputStream  in = null;                //Data from the URL
   try {
      URL url = new URL(targetURL);          //Create the URL
      in = url.openStream();                 //Get a stream to it
      byte[] buffer = new byte[8192];
      int bytesRead;
      while ( (bytesRead = in.read(buffer)) != -1) {
         String outStr = new String(buffer, 0, bytesRead);
         jsonTxt += outStr;
      }
      in.close();
   } catch (Exception e) {
      System.out.println (e);
   }
   try {
      retVal = new JSONObject(jsonTxt);
   } catch (JSONException e) {
     println (e.toString());
   }
   return retVal;  // Return the json object
}

void setGradient(int x, int y, float w, float h, color c1, color c2, int axis ){
	float deltaR = red(c2)-red(c1);
  float deltaG = green(c2)-green(c1);
  float deltaB = blue(c2)-blue(c1);
   // column 
	for (int i=y; i<=(x+w); i++){
	  // row
	  for (int j = x; j<=(x+w); j++){
	    color c = color(
	    (red(c1)+(j-x)*(deltaR/h)),
	    (green(c1)+(j-x)*(deltaG/h)),
	    (blue(c1)+(j-x)*(deltaB/h)) 
	    );
	    set(j, i, c);
	  }
	}
}

Exporting

As an application

Applications are much easier to export; since it runs locally, it is assumed that the user of the computer can have a lot more control over their own files; thus, you can write/remove files, make any sort of remote call, etc...

To export, just click File -> Export Application, then select your settings, then click ok and a window should open pointing you to the location of your new app. Done!

As a web applet

Web applets are a bit harder. Since we can't actually save things, you have to prepare your project a bit differently. First off, since we can't write files, we obviously can't saveFrame, so remove all the calls to do so. Additionally, remove the functions that those are associated with; since we can't saveFrame, we might as well get rid of the code that would serve that function. Those functions are drawUniqueStates(), drawDataStates(), and tweetMapping(). Search the code to remove the actual calls for those functions. Instead of calling to save the images, we'll just use ones previously generated. A sample uniqueStates.png and states.png are located in the raw_files folder of the tutorial zip located at [2].

Facts about Processing TutorialRDF feed
Dcterms:created25 February 2010  +
Dcterms:descriptionTutorial on how to use Processing.
Dcterms:modified2010-2-25
Foaf:nameProcessing Tutorial
Skos:altLabelProcessing Tutorial  +, processing tutorial  +, and PROCESSING TUTORIAL  +
Personal tools
internal pages