This example loads a map from an SVG file and adds it to the web page so that it can be controlled by your p5.js sketch. Inside the SVG file, each boundary will be named. For instance, in the world maps, it'll be the 2-digit country code for that area.
This part of the code takes care of loading and sizing a world map that uses the Robinson projection:
let worldMap;
functionsetup() {
createCanvas(720, 480);
background(224);
// If you change the dimensions, the aspect ratio will stay the same. // The browser will size the map to use as much of the width/height as possible.let mapWidth = width * 0.8; // use 80% of the sketch sizelet mapHeight = height * 0.8;
// Center the map on the screen. The mapX and mapY // coordinates are relative to the sketch location. let mapX = (width - mapWidth) / 2;
let mapY = (height - mapHeight) / 2;
let mapPath = "data/world-robinson.svg";
//let mapPath = "data/world-equirectangular.svg";//let mapPath = "data/us-counties.svg";//let mapPath = "data/us-states.svg";// This will create a new SVG map from the 'robinson.svg' file in the data folder.// Once the map has finished loading, the mapReady() function will be called.
worldMap = new SimpleSVG(mapPath, mapX, mapY, mapWidth, mapHeight, mapReady);
}
The mapReady() function is optional, but gives us a way to find out more information about the map, and tell the map what to do when the user clicks on a shape, or the mouse moves over a shape. It also lists all the names of the shapes that it found in the file, so you can see that the map is working like you expect.
// this function is called when the map loadsfunction mapReady() {
// show a list of all the shapes by name (i.e. all the 2-digit country codes)print(worldMap.listShapes());
// call the function named 'mapClick' whenever a shape is clicked
worldMap.onClick(mapClick);
// handle mouseover (hover) events, and mouseout (the opposite of hover)
worldMap.onMouseOver(mapOver);
worldMap.onMouseOut(mapOut);
}
In the map files, there are some sections that aren't countries or states: they might be boundary lines or the ocean. Since we don't want these to light up on hover (or be clickable), we create a function that checks whether we can ignore that particular area:
// returns 'true' if this shape should be ignored// i.e. if it's the ocean or it's the boundary lines between statesfunction ignoreShape(name) {
return (name === 'ocean' || name.startsWith('lines-'));
}
When a country on the map is clicked, this code checks to see if its something to be ignored, and if not, sets the fill color to red. It also prints the id (name) of the shape to the console:
function mapClick(shape) {
if (!ignoreShape(shape.id)) {
worldMap.setFill(shape, 'red');
}
print(`click ${shape.id}`);
}
Mouse hovers (also called rollovers) are handled in a similar way. We check whether it's a shape to be ignored, and if not, set its fill color:
function mapOver(shape) {
if (!ignoreShape(shape.id)) {
worldMap.setFill(shape, '#666');
}
print(`over ${shape.id}`);
}
function mapOut(shape) {
if (!ignoreShape(shape.id)) {
worldMap.setFill(shape, '#ccc');
}
print(`out ${shape.id}`);
}
These handlers use the setFill() function that's part of SimpleSVG. Unfortunately, it's not as simple as saying shape.fill = 'red' because each country shape might have several sub-shapes to it. The SimpleSVG code hides this complexity and just takes care of it for you.
Other Maps
There are several maps included in the data folder of this sketch:
world-robinson.svg – The map we've been using so far, with countries named with the 2-digit ISO country code, plus an element named ocean. (original source)
world-equirectangular.svg - Similar to the Robinson map, but uses an equirectangular projection. Not the prettiest, but the lat/lon math is very basic, and this can sometimes be helpful for mixing with other code. (original source)
us-states.svg - A map of states, each named by their two-letter state abbreviation (in all caps). (original source)
us-counties.svg - A map of states and counties. Each county is named FIPS_NNNNN where NNNNN is the 5-digit FIPS code for that county. There's also a <title> sub-element on each that gives the name of the county and its state abbreviation, if you want to get into the DOM a little more. (original source)
Each of these have been modified slightly to rearrange layers or rename elements so that they're consistent with one another, and work well with SimpleSVG.
They take up quite a bit of space, so remove the ones that you're not using.
Most of the work is done inside the SimpleSVG.js file. You can see the id for each shape by looking at the SVG inside Adobe Illustrator, where each path/group will be named in the layers palette. The SimpleSVG code will only pay attention to the first layer of named elements in the file, so make sure that's where your named entries are. You may need to move things around a bit.
To map a set of points, create a CSV or TSV file with a columns for latitude and longitude. This example uses postal codes and their lat/lon location. To read the file:
functionsetup() {
createCanvas(720, 452);
// load the table of zip codes and call the tableLoaded() function when readyloadTable("data/zips.tsv", "header", "tsv", tableLoaded);
}
Once the file is ready, it will call the tableLoaded() function, which goes through the file line by line, and creates an array of "places", each with an x/y position and a name:
function tableLoaded(t) {
// go through each row of the zip codes filefor (let row = 0; row < t.getRowCount(); row++) {
let place = { };
place.lat = t.getNum(row, "lat");
place.lon = t.getNum(row, "lon");
place.name = t.getString(row, "name");
place.zip = t.getString(row, "zip");
// now calculate specific locationlet projected = projectAlbers(place.lon, place.lat);
// or you can use the Mercator version://var projected = projectMercator(place.lon, place.lat);
place.x = projected.x;
place.y = projected.y;
placeList.push(place);
}
// print out how many points were found//print(placeList.length);
findMinMax();
}
At the end of that function, it calls findMinMax() which figures out the minimum and maximum coordinates, which are needed to draw the shapes to the screen:
// figure minimum and maximum values for the x- and y-coordinatesfunction findMinMax() {
// start by setting the min/max to the first point in the list
minX = maxX = placeList[0].x;
minY = maxY = placeList[0].y;
// then check each of the other points in the listfor (let i = 1; i < placeList.length; i++) {
var place = placeList[i];
if (place.x > maxX) {
maxX = place.x;
}
if (place.x < minX) {
minX = place.x;
}
if (place.y > maxY) {
maxY = place.y;
}
if (place.y < minY) {
minY = place.y;
}
}
}
Finally, drawing to the screen is just a matter of iterating over the placeList array and drawing each point:
functiondraw() {
background(250);
stroke(128);
// draw each of the points on the screen
placeList.forEach(function(place) {
let x = map(place.x, minX, maxX, 0, width);
let y = map(place.y, maxY, minY, 0, height);
point(x, y);
});
}
This example builds on the previous but uses mouse input to handle hovers or clicking individual points. This is done by rewriting the draw() method so that we can keep track of what dot is closest to the mouse. We use the dist() function to calculate the distance from the mouse to each point, and if it's less than the last value, closestPlace is set to that place entry.
// the place currently under the mousevar closestPlace = null;
functiondraw() {
background(250);
stroke(128);
// for rollovers/selection, keep track of the point nearest the mousecursor(CROSS); // better for selectionlet closestDist = 5; // minimum distance
closestPlace = null;
placeList.forEach(function(place) {
let x = map(place.x, minX, maxX, 0, width);
let y = map(place.y, maxY, minY, 0, height);
point(x, y);
// check if the distance from the mouse to this point is the closest so far let mouseDist = dist(mouseX, mouseY, x, y);
if (mouseDist < closestDist) {
closestPlace = place;
closestDist = mouseDist;
}
});
if (closestPlace !== null) {
let x = map(closestPlace.x, minX, maxX, 0, width);
let y = map(closestPlace.y, maxY, minY, 0, height);
noStroke();
fill(255, 0, 0);
ellipse(x, y, 5, 5);
fill(0);
textAlign(CENTER);
text(closestPlace.name, x, y - 5);
}
}
functionmousePressed() {
if (closestPlace !== null) {
// do something with 'closestPlace' hereprint(`${closestPlace.name} clicked`);
}
}
After drawing all the points, the code checks if (closestPlace !== null), and if so, it'll re-draw that point using an ellipse, and write its name above it.
We also add a mousePressed() function, which you can use to trigger something more interesting when clicking on a point. Because closetPlace is set inside draw(), that'll be the place being clicked.
Projections
Over in the projections.js file, there's code to convert latitude and longitude points to an Albers Equal-Area Conic or to Mercator coordinates. Albers is what's used most often for the US, while Mercator is easy to calculate and gets used more often than it should. You needn't worry about that code unless you want to use other kinds of projections, or you're curious about how they work.