Having seen a visualization by Robert Kosara of EagerEyes a loooong time ago, I wanted to try to reproduce it for Switzerland using Processing. This is the first installment of a two-parts post covering this project, in which I will describe how to arrive at an intermediate result. I’ll do that in some detail, maybe this is helpful to somebody.
The visualization is called the ZIPScribbleMap: “ZIP” for postal codes, “Scribble” for rather obvious reasons (as in “it looks like what I doodle while on the phone!”):
Data
First of all I needed a dataset of Swiss postal codes. Ideally this dataset would contain the place names along with the postal codes (for error/quality checking) and would be geocoded (that is, contain geographic coordinates). However, if the postal codes were not geocoded, one could resort to using a (for example, Google’s) geocoding service to hopefully obtain coordinates for the postal codes).
Luckily, I could avoid this extra step, because after a brief search it turned out that Geonames does offer datasets of geocoded postal codes on a per-country basis for download: http://download.geonames.org/export/zip (scroll down for a readme; entry page is here with reference to postal codes in first paragraph).
The data is CC-BY-3.0-licensed, which means we can use it to our ends (even commercial ones) provided we attribute it to Geonames. The Readme accompanying the data says a link to the Geonames website is okay for this purpose.
Basemap
Next I felt I needed a nice looking base map to plot the postal code data on, rather than just an empty canvas. In order to prepare one I decided to use TileMill by MapBox (see this post). TileMill is available for Mac OS and Ubuntu. So I fired up my Ubuntu desktop computer and had TileMill installed. TileMill runs in the browser.
I edited an existing stylesheet to get a very reduced basemap with bright colours. The basemap contains country outlines and major lakes. I exported the basemap as a PNG file for use in Processing.
Note: When you do the export, you have to pay attention to note down the extent coordinates (north, east, south and west boundary coordinates). You need these later on in order to get the spatial reference in Processing right: Processing has to project geographically referenced data (basemap and postal codes) consistently into the screen spatial reference system.
Side-note: While figuring out the design of the basemap I exported different versions several times, because I forgot to take note of the extent coordinates. Later I dropped defining an extent by mouse but entered the extent coordinates numerically into TileMill instead.
Finally: This is what my basemap of Switzerland looked like:
The Mechanics of Part I
Time to fire up Processing. I’m using version 1.5.1 on Ubuntu:
Let’s summarise: By now I had my basemap as well as my postal code data ready. The latter is saved in a text file. Both files have to be stored in the data folder of the Processing sketch. You can move them there manually or use the Sketch > Add File…dialog in Processing.
For using the basemap in Processing, you best refer to the Simple static map example by Till Nagel. He offers a nifty MercatorMap class for download, which one can use for handling the projection from geographic coordinates to screen coordinates. Download and save it as MercatorMap.pde in the Processing sketch folder (not the data folder).
Using the MercatorMap class I could load the basemap in Processing as follows (this is where you need the extent coordinates, which you noted when you exported the basemap from TileMill):
[sourcecode language=”java” highlight=”12″]
PImage img;
MercatorMap mercatorMap;
void setup() {
smooth();
noLoop();
// Set up canvas
size(1024, 673);
// Load image
img = loadImage(“mapSWI_bright.png”);
// Initiate mercator map to handle projection
mercatorMap = new MercatorMap(width, height, 47.9, 45.7, 5.78, 10.67);
[/sourcecode]
Outside the setup() method I defined a PImage object in order to store the PNG file of the basemap and a MercatorMap object for handling the projection. In the setup() method I define the canvas size, load the basemap file and (in line 12) instantiate the MercatorMap using the sketch width and height as well as the extent coordinates.
In a second step I added some font definitions (the fonts can be created using Tools > Create Font… in Processing) and the functionality to read the postal code data from the Geonames text file:
[sourcecode language=”java” highlight=”2,3,5,6,17,18,19,20,21,22″]
PImage img;
PFont fontSmall, fontBig, fontBold;
PVector pnt;
MercatorMap mercatorMap;
String[] lines;
int index = 0;
void setup() {
smooth();
noLoop();
// Set up canvas
size(1024, 673);
// Load image
img = loadImage(“mapSWI_bright.png”);
// Initiate mercator map to handle projection
mercatorMap = new MercatorMap(width, height, 47.9, 45.7, 5.78, 10.67);
// Set up fonts for labelling
fontSmall = loadFont(“DejaVuSansCondensed-10.vlw”);
fontBig = loadFont(“DejaVuSerifCondensed-25.vlw”);
fontBold = loadFont(“DejaVuSerifCondensed-Bold-25.vlw”);
// Load zip code data
lines = loadStrings(“zipcodes_SWI.txt”);
[/sourcecode]
Line 22 reads the postal code from the Geonames text file and stores it in a String array lines. Since I had defined the array outside the setup() method, I can access it now inside the draw() method:
[sourcecode language=”java” highlight=”7,8,9,10,11,12,13,14″]
void draw() {
// Draw basemap
image(img, 0, 0);
// Draw zip code locations
noStroke();
fill(250, 150, 0, 80);
while (index < lines.length) {
String[] row = split(lines[index], ‘t’);
float lat = float(row[9]);
float lon = float(row[10]);
pnt = mercatorMap.getScreenLocation(new PVector(lat, lon));
ellipse(pnt.x, pnt.y, 6, 6);
index = index + 1;
}
[/sourcecode]
Let’s look at what happens here in a bit more detail: The following part is inside a loop which iterates through the entries in the String array lines:
[sourcecode language=”java” firstline=”8″]
String[] row = split(lines[index], ‘t’);
float lat = float(row[9]);
float lon = float(row[10]);
[/sourcecode]
Line 8 takes the current entry of the lines array and splits it wherever it finds a tabulator (or ‘t’ in Java). The result is stored in a new String array row. Lines 9 and 10 pick the correct entries from the array, convert them to a floating point number and store it in the variables lat (latitude) and lon (longitude). Thus, now we have the geographic coordinates of a postal code right there.
The next part is still in the loop which iterates through the postal code data. Line 11 creates a new PVector (a Processing object) from the two coordinates I just extracted. This PVector object is subjected to the getScreenLocation() method of Till Nagel’s MercatorMap object.
[sourcecode language=”java” firstline=”11″]
pnt = mercatorMap.getScreenLocation(new PVector(lat, lon));
ellipse(pnt.x, pnt.y, 6, 6);
index = index + 1;
[/sourcecode]
The getScreenLocation() method is pretty self-explanatory: It takes geographic coordinates and using the extent coordinates from when I set up the MercatorMap object, converts them into screen coordinates of this Processing sketch. Then, using the x and y coordinates of the Object pnt we draw an ellipse of size (6, 6), that is, a circle, before we advance the index and continue to iterate through the postal code data (as long as there is data left to read).
Result
Finally, the result is here (click on the figures to see them bigger). I produced versions with different basemaps and with/without additional, labelled cities:
On the way to the ZIPScribble Map: Switzerland these are only intermediate results. Yet I find them nice and insightful enough. Bigger cities (with several postal codes) and agglomerations are quite clearly visible. Also the mountainous regions of Switzerland such as the Alps are discernible in the picture. In alpine regions municipalities and thus postal code regions tend to be bigger than in the rather densely populated lowlands. Overall the maps are certainly not bad approximations of population density.
Any feedback or questions are welcome, feel free to sound off in the comment section or shot me an e-mail.
Below I include the Processing source code of the last version of the Postal Code map:
[sourcecode language=”java” collapse=”true” gutter=”false”]</span>
<pre>// Ralph Straumann
// visurus.wordpress.com
// 2011-10-16
// CC-BY-NC
// Thanks to:
// http://download.geonames.org/export/zip/CH.zip
// http://tillnagel.com/wp-content/uploads/2011/06/MercatorMap.java via http://tillnagel.com/2011/06/tilemill-for-processing
PImage img;
PFont fontSmall, fontBig, fontBold;
PVector pnt;
MercatorMap mercatorMap;
String[] lines;
int index = 0;
boolean placeLabels = true;
void setup() {
smooth();
noLoop();
// Set up canvas
size(1024, 673);
// Load image – either bright or dark basemap
img = loadImage(“mapSWI_bright.png”);
//img = loadImage(“mapSWI_dark.png”);
// Load zip code data
lines = loadStrings(“zipcodes_SWI.txt”);
// Set up fonts for labelling
fontSmall = loadFont(“DejaVuSansCondensed-10.vlw”);
fontBig = loadFont(“DejaVuSerifCondensed-25.vlw”);
fontBold = loadFont(“DejaVuSerifCondensed-Bold-25.vlw”);
// Initiate mercator map to handle projection
mercatorMap = new MercatorMap(width, height, 47.9, 45.7, 5.78, 10.67);
}
void draw() {
// Draw basemap
image(img, 0, 0);
// Draw zip code locations
noStroke();
fill(250, 150, 0, 80);
while (index < lines.length) {
String[] row = split(lines[index], ‘t’);
float lat = float(row[9]);
float lon = float(row[10]);
pnt = mercatorMap.getScreenLocation(new PVector(lat, lon));
ellipse(pnt.x, pnt.y, 6, 6);
index = index + 1;
}
if (placeLabels == true) {
fill(0);
textFont(fontSmall);
pnt = mercatorMap.getScreenLocation(new PVector(46.9167,7.4667));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Berne”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.5842,7.5876));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Basle”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.2095,6.1438));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Geneva”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.0833,8.2667));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Lucerne”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.4236,9.3622));
ellipse(pnt.x, pnt.y, 4, 4);
text(“St. Gallen”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.2295,7.3568));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Sion”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.7151,8.622));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Schaffhausen”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.1889,9.0247));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Bellinzona”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.85,9.5));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Chur”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.1324,7.2441));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Bienne”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.7554,7.6236));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Thun”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.0083,8.9495));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Lugano”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.4916,8.7295));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Winterthur”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.7787,6.6339));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Yverdon-les-Bains”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.3667,8.55));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Zurich”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.5534,6.6971));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Lausanne”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.289,7.5458));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Sierre”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.3433,7.9052));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Olten”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(47.0543,9.4488));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Sargans”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.4855,9.8335));
ellipse(pnt.x, pnt.y, 4, 4);
text(“St. Moritz”, pnt.x+5, pnt.y-5);
pnt = mercatorMap.getScreenLocation(new PVector(46.7783,9.879));
ellipse(pnt.x, pnt.y, 4, 4);
text(“Davos”, pnt.x+5, pnt.y-5);
}
// Add labels
fill(180);
textFont(fontBig);
text(“Postal Codes”, 30, 45);
textFont(fontBold);
text(“Switzerland”, 30, 73);
textFont(fontSmall);
fill(130);
text(“Postal codes CC-BY”, 30, 632);
text(“www.geonames.org”, 30, 645);
textAlign(RIGHT);
text(“CC-BY-NC”, 990, 632);
text(“visurus.wordpress.com”, 990, 645);
save(“PostalCodesSWI.png”);
}
[/sourcecode]
Thanks to Robert Kosara, TileMill, Till Nagel and Geonames!
Great description of your process, and nice looking results!
For the next version, one idea would be to get cities and their geolocations from some API directly, e.g. Geoname’s cities method ( http://api.geonames.org/cities?north=47.9&south=45.7&east=10.67&west=5.7&lang=de&maxRows=15&username=demo ). Simply filter out all non-Swiss cities, and you’re good.
Thank you for your comment, Till!
I must admit I had a only a faint idea that Geonames has an API (haven’t used it yet, ever). I suppose you’re suggesting using the API for labelling major cities on the map? Or is there also a query which would yield postal codes along with the city names?
In any case, thank you for your work and your instructional blog posts!