I have a large, framed map of Tokyo on my wall. It’s made up of only the streets within its 23 special wards, and some rivers and other bodies of water to add some color.

On several occasions I’ve been asked how I created this map, and since I’ve had it for several years now, couldn’t quite remember how I did it myself - so I decided to retrace my steps with the files I still had laying around.

If you’re just interested in the final result, skip to the bottom of this post.

Gathering the Data

The visible part of this map uses street and water body data from the OpenStreetMap project. There are various sources that offer downloads for the raw data - I chose the data extracts provided by Geofrabrik.

For the first set of data points, we need the shape files for the Kantō region, available as kanto-latest-free.shp.zip. From that ZIP file we need the following files:

  • gis_osm_roads_free_1.* - information about all sorts of roads and pathways
  • gis_osm_water_a_free_1.* - data about rivers and bodies of water

Since we only want to show the roads within Tokyo’s 23 special wards, we also need information about their administrative boundaries. Additionally, the shape files for water above do not include larger bodies of water, for example Tokyo Bay.

This information is available from GADM by country, in our case for Japan, again as a set of shape files (as of this post gadm36_JPN_shp.zip). From that file we need:

  • gadm36_JPN_2.*

Preparing the Database

I needed a way to select only the parts of the data that was needed for the image and filter out the rest, and PostgreSQL with the PostGIS extension ticked all those boxes.

Conveniently, the PostGIS Docker image also comes with the tools needed to import the data we’ve downloaded above.

# Start the container, mounting the current directory as /data
docker run -d --name=postgis -p 5432:5432 -v "${PWD}:/data" \
  -e POSTGRES_PASSWORD=notsecret postgis/postgis:13-3.0-alpine

# Grab a bash shell inside the container
docker exec -it -u postgres postgis bash

# Prepare the database
psql -c 'CREATE DATABASE gis;'
psql gis -c 'CREATE EXTENSION postgis;'

# Convert and import water, boundaries and roads
# -s 4326 because the files use the WGS 84 (aka EPSG:4326) coordinate system
shp2pgsql -I -s 4326 /data/gis_osm_water_a_free_1 water | psql -q gis
shp2pgsql -I -s 4326 /data/gadm36_JPN_2 boundaries | psql -q gis
# This one takes a while, remember to hydrate
shp2pgsql -I -s 4326 /data/gis_osm_roads_free_1 roads | psql -q gis

Messing with the Data

We now have a database named gis that contains three tables we care about: roads, water and boundaries, containing data about roads, bodies of water and administrative boundaries respectively.

Here’s the roads table:

screenshot of roads table contents

The most interesting column in this table is the fclass one, which has some info about what kind of road we’re dealing with. We’ll be using this one later to filter out some roads so the image isn’t too busy. More information about what the types mean can be found on the OSM Wiki.

The boundaries table looks like this:

screenshot of boundaries table contents

Of note here are the columns type_2 or engtype_2, which contain the value Special Ward for our 23 wards.

We can now use those tables to query for roads that intersect with any of the 23 special wards in Tokyo using PostGIS’ ST_Intersects function:

SELECT roads.gid,
       roads.fclass,
       roads.geom
FROM roads,
     boundaries
WHERE boundaries.name_1 = 'Tokyo'
  AND boundaries.type_2 = 'Special Ward'
  AND ST_Intersects(roads.geom, boundaries.geom);

In order to save some CPU cycles later, we can create a new table that contains only the filtered data:

CREATE TABLE roads_filtered AS
SELECT roads.gid,
       roads.fclass,
       roads.geom
FROM roads,
     boundaries
WHERE boundaries.name_1 = 'Tokyo'
  AND boundaries.type_2 = 'Special Ward'
  AND ST_Intersects(roads.geom, boundaries.geom);

CREATE INDEX roads_filtered_geom_idx
    ON roads_filtered USING GIST (geom);

Making Maps

So far the whole process hasn’t been very visual, but this is going to change now. Using TileMill, an open source map design studio, we can basically take the map geometry data and apply CSS to it. I’m using the last released version for Windows when it was still supported by Mapbox, which is v0.10.1.

First we’ll need to create a new project, choosing a name and disabling the inclusion of some default data.

screenshot of new project window

After opening the project, there’ll be a couple buttons on the bottom left, one of which lets us add a new layer. Since the data is in Postgres, we choose PostGIS, enter the appropriate connection parameters and choose the boundaries table for now. As the unique key field we can use the gid column, the geometry data is in the geom column. The SRS projection is WSG84, the text field next it gets automatically populated.

screenshot of add layer window

The default map background is blue, which will be our water. The boundaries layer will be our white layer for the majority of the background, containing the entire landmass. Here’s the style to add to make it all white:

#boundaries {
  line-color:#fff;
  polygon-fill:#fff;
}

The result should look something like this:

screenshot of base layer

Next, we’ll add our water layer. We do mostly the same as we did for the boundaries layer, but this time we’re using the water table. All other options should be the same as before.

To fill just the polygon area with the same background color we’re using on the map:

#water {
  polygon-fill:#b8dee6;
}

Just from the water layer alone we can now guess where some parts of Tokyo are, for example the moat around the imperial palace is now visible:

screenshot of water layer

Finally, we add the roads_filtered table. This is the style I’ve applied:

#roads_filtered {
  line-width: 1;
  line-color: #000;
  line-join: round;
  
  [fclass = 'footway'],
  [fclass = 'steps'] {
    line-width: 0;
  }
  
  [fclass = 'service'],
  [fclass = 'pedestrian'] {
    line-width: 0.5;
  }
}

The result should look something like this:

screenshot of roads layer

Using the [fclass = 'x'] selector, we can apply different styles based on values in the table columns. For example I’ve hidden roads entries where the fclass column contains footway or steps. By digging around in the data and hiding or applying different styles to various road types we can clean up the result a bit.

Our roads may appear thicker or thinner depending on the zoom level we’re at, which also applies to the final export. Here’s an example of how the same style looks in zoom levels 12, 13 and 14:

example of difference between zoom levels 12 to 14

Once we’ve found a style and zoom level we’re happy with, we can choose export at the top right. Shift+drag to set bounds of the image, then zoom in to the desired zoom level. I chose svg and a zoom level of 14, with an export size of roughly 4000x4000 pixels. The result is a 50MB svg file that imported into Photoshop as a 60cm wide image looks like this:

map export result

Final Result

All that’s left to do is just cleaning up of the edges, trimming off a few longer roads and outlying grids (choosing looks over technical accuracy) and cutting the water layer so that the whole metropolis stands on its own in a white background, maybe adding Hokusai’s Great Wave in there, but upside down for some reason…

Here’s the final result. It’s the original file I sent to be printed, so there will be some subtle differences in both looks and street layouts between the result and the export above. Good luck finding them all ;)

final result

Some Lessons Learned

I’ve tried a few other towns and cities close to where I live, but the result looks best in densely populated areas with many roads, since those make up all of the shape. Sadly this means smaller villages or cities may not look as good with more empty space.

Of course availability of OSM data is also a factor and might vary globally.