Getting data for geoMap
geoMap allows you to draw vector maps in Processing. Unlike images, vectors define the shape of each object in the map and so preserve their detail when magnified.
To draw such a map you first need to find some mapping data to use. geoMap reads shapefiles which are a standard format for digital mapping and GIS (geographic information systems) data. There are plenty of free sources of shapefiles including Natural Earth, Global Administrative Areas (GADM) and openstreetmap.
You may find that the shapefile you wish to use contains more geographic detail than you need, and as a consequence is slow to draw or the file size larger than you wish. A useful resource is mapShaper, which will allow you to simplify any shapefile, reducing the geographic detail but shrinking file size and speeding up drawing.
A shapefile actually consists of several files all with the same name, but with the extensions .shp (storing the geometry of the map), .dbf (storing the attributes) and optionally .shx (an index file). To use a shapefile with geoMap you need to place at least the .dbf and .shp files somewhere where your sketch can read them (e.g. by dragging them into your sketch).
The geoMap library comes with some sample shapefiles so you can build some sketches quickly - a world map (world.shp/dbf), a map of the US (usContinental.shp/dbf) and a map of London's boroughs (londonBoroughs.shp/dbf).
Hello world!
So let's build the simplest of geoMap sketches. Create a new sketch in Processing and drag the files world.shp and world.dbf into it (you can find these files in the data/ folder of the installed geoMap library). Then copy the following code:
import org.gicentre.geomap.*; GeoMap geoMap; // Declare the geoMap object. void setup() { size(800, 400); geoMap = new GeoMap(this); // Create the geoMap object. geoMap.readFile("world"); // Reads shapefile. } void draw() { geoMap.draw(); // Draw the entire map. }
To use geoMap you must first import the geoMap package (line 1). Because you will probably only need to read in data into geoMap once, but draw it many times, you should declare a geoMap object at the top of your sketch so it is visible to both the setup() and draw() methods.
The geoMap is created in setup() with the line new GeoMap(this) and then data from the shapefile are read into it with the readFile() method (in this example the sketch will look for a shapefile in its data/ folder called world.shp/world.dbf). Drawing the whole map is as simple as calling the draw() method of your geoMap object.
The appearance of the map will depend on your sketch's stroke and fill settings at the point that geoMap.draw() is called. In the example above the default black and white stroke and fill colours are a little boring, so let's enhance the sketch by setting some more attractive colours:
import org.gicentre.geomap.*; GeoMap geoMap; void setup() { size(800, 400); geoMap = new GeoMap(this); // Create the geoMap object. geoMap.readFile("world"); // Read shapefile. } void draw() { background(202, 226, 245); // Ocean colour fill(206,173,146); // Land colour stroke(0,40); // Boundary colour geoMap.draw(); // Draw the entire map. noLoop(); // Static map so no need to redraw. }
By default, calling the draw() method of a geoMap object will draw the map scaled to the width and height of your sketch. If you wish to draw the map in only part of your sketch window, you can specify the position of the map's top-left corner and its width and height when the object is created. So for example, the following line would create a small version of the map 250 pixels wide and 125 tall positioned at location (10,10):
geoMap = newGeoMap(10,10,250,125,this);
Interactive mapping
One of the advantages of vector-based mapping via shapefiles is that each item in the map is individually identifiable. We can exploit this to make our map interactive, highlighting the country under the current mouse position.
import org.gicentre.geomap.*; GeoMap geoMap; void setup() { size(800, 400); geoMap = new GeoMap(this); // Create the geoMap object. geoMap.readFile("world"); // Read shapefile. } void draw() { background(202, 226, 245); // Ocean colour stroke(0,40); // Boundary colour fill(206,173,146); // Land colour geoMap.draw(); // Draw the entire map. // Find the country at mouse position and draw in different colour. int id = geoMap.getID(mouseX, mouseY); if (id != -1) { fill(180, 120, 120); // Highlighted land colour. geoMap.draw(id); } }
Each item (a country in this example) in a geoMap object will always have its own unique numeric ID. There are a number of methods that can use this ID. The key line in the example above is the one that calls getID(mouseX, mouseY). Here geoMap will return the numeric ID of the map item at the given screen coordinates (mouse position in this example). We can then use this ID to draw, in a different colour, just the item selected. If there is no item at the given (x,y) coordinates, the method will return a -1, so we test for that to ensure we only draw when a map item has been found.
Querying the attribute table
We can take our interactive map one stage further by performing a query on the table of attributes associated with each map item. This table is stored inside the shapefile and the geoMap library provides some methods to make querying it easier.
The full attribute table can be retrieved by calling getAttributeTable() from the geoMap object. This will provide access to a Processing Table object that relates each numeric ID to one ore more attributes. If you wish to see what is contained in the attribute table you can all geoMap's writeAttributeAsTable() method, providing the number of rows of the table you would like to display. This will show the table in the Processing console. For example, the first five rows of the world.dbf attribute table is as follows:
------------------------------------------------------------ |id |ISO_A2|ISO_A3|NAME |ABBREV | ------------------------------------------------------------ |1 |AW |ABW |Aruba |Aruba | |2 |AF |AFG |Afghanistan |Afg. | |3 |AO |AGO |Angola |Ang. | |4 |AI |AIA |Anguilla |Ang. | |5 |AL |ALB |Albania |Alb. |
The first column in an attribute table is always the ID of each map object. So to display the name of the country associated with a given ID we would need to find the row in the table corresponding to the ID of the country of interest and then get the fourth column of that row. In our country example, we would use the line geoMap.getAttributeTable().findRow(str(id),0).getString("NAME") to do this where id has been found based on the mouse position and NAME is the header of the column we are interested in. The complete sketch to display the queried attribute is shown below:
import org.gicentre.geomap.*; // Simple interactive world map that queries the attributes // and highlights selected countries. GeoMap geoMap; void setup() { size(800, 400); geoMap = new GeoMap(this); geoMap.readFile("world"); // Set up text appearance. textAlign(LEFT, BOTTOM); textSize(18); // Display the first 5 rows of attributes in the console. geoMap.writeAttributesAsTable(5); } void draw() { background(202, 226, 245); // Ocean colour stroke(0, 40); // Boundary colour // Draw entire world map. fill(206, 173, 146); // Land colour geoMap.draw(); // Draw the entire map. // Query the country at the mouse position. int id = geoMap.getID(mouseX, mouseY); if (id != -1) { fill(180, 120, 120); geoMap.draw(id); String name = geoMap.getAttributeTable().findRow(str(id),0).getString("NAME"); fill(0); text(name, mouseX+5, mouseY-5); } }
Thematic mapping
Finally we will pull together querying of the attribute table and drawing of individual map elements by creating a thematic choropleth map that colours each country according to some data value associated with it.
The challenge here is that the data we wish to map, which in this case will be dental health, are not stored directly inside the shapefile. instead they have been downloaded separately from the gapminder web site and stored in the file badTeeth.csv. The first few rows of that file look like this:
Afghanistan,AFG,2.9 Albania,ALB,3.02 Algeria,DZA,2.3 Angola,AGO,1.7 Anguilla,AIA,2.5
It comprises three columns - the country name, its three-letter country code and the average number of unhealthy teeth per 12 year old girl.
We need a way to relate each map element ID to the data in this cdv file. One way of doing that is to use the three-letter country code, which is present in both tables, to relate them together. This is more reliable than using the full country name since it avoids mismatching different spellings (e.g. St Vincent vs Saint Vincent).
Using geoMap's getAttributeTable() method we can iterate through each of the map items in the attribute table extracting the three-letter country code:
for (int id : geoMap.getFeatures().keySet()) { // Extract attribute in 3rd column for each ID. String countryCode = geoMap.getAttributeTable().findRow(str(id),0).getString("ISO_A3"); }
We can then use Processing's Table class to find the row in which that country code occurs and extract the corresponding teeth data. There is a possibility that a country code in the geoMap attribute table will not be present in the tooth dataset, so we also need to check for 'null' rows returned by Processing's findRow() method.
We can create the choropleth map by setting a fill colour for each country according to the tooth value we have just extracted. A simple way of doing this is to scale the tooth data between 0-1 and then use Processing's lerpColor() to choose a colour between light and dark blue depending on that scaled data value.
The complete sketch is shown below.
import org.gicentre.geomap.*; // Draws a choropleth map of dental health data from gapminder.org. GeoMap geoMap; Table tabBadTeeth; color minColour, maxColour; float dataMax; void setup() { size(820, 440); // Read map data. geoMap = new GeoMap(10, 10, width-20, height-40, this); geoMap.readFile("world"); geoMap.writeAttributesAsTable(5); // Display first 5 rows of attribute table in console for checking. tabBadTeeth = loadTable("badTeeth.csv"); // Read dental health data. // Find largest data value so we can scale colours. dataMax = 0; for (TableRow row : tabBadTeeth.rows()) { dataMax = max(dataMax, row.getFloat(2)); } minColour = color(222, 235, 247); // Light blue maxColour = color(49, 130, 189); // Dark blue. } void draw() { background(255); stroke(255); strokeWeight(0.5); // Draw countries for (int id : geoMap.getFeatures().keySet()) { String countryCode = geoMap.getAttributeTable().findRow(str(id),0).getString("ISO_A3"); TableRow dataRow = tabBadTeeth.findRow(countryCode, 1); if (dataRow != null) // Table row matches country code { float normBadTeeth = dataRow.getFloat(2)/dataMax; fill(lerpColor(minColour, maxColour, normBadTeeth)); } else // No data found in table. { fill(250); } geoMap.draw(id); // Draw country } // Draw title text fill(50); textAlign(LEFT, TOP); text("Number of bad teeth per 12 year-old child", 10, height-20); noLoop(); // Static map so no need to redraw. }