Waterman Butterfly Map

Table of Contents

Introduction
Haacker
Generating Height Levels
Layers
Shaded Relief
Appendix

Introduction

The source of this burst of inspiration, as well as of most others, was an xkcd comic. Specifically comic 977 which describes what your favourite map projection says about you. The comic emphasises the beauty of a specific projection: The Waterman Butterfly Projection. Inspired, I set out to make one.

Looking about, I came across a wonderful blog post describing how to construct one (as you just have) by Fabian Ehmel & Boris Müller entitled Shaping a butterfly. I followed along and was able to reconstruct the butterfly for the most part. However, the process was non-trivial for myself. Thus this post is intended as an addendum; to hopefully fill in the small technical details. Before you continue, I would encourage you to read Fabian and Boris’ blog.

Note: This post doesn’t cover labeling the map.

Prerequisites/Specs

Some processes are computationally intensive so here are my computer’s specs so you can scale and hopefully make an estimate about how long processes will take on your own computer.

  • OS: Arch Linux 5.11.2
  • CPU: Intel Core i5-10500 (12×4.5Ghz)
  • GPU: GTX 660 (2GB)
  • RAM: 16GB

Note that this tutorial is intended for Linux.

We require the following software

  • Node.js (and npm)
  • Python
  • GDAL
  • Git
  • D3/D3-Geo and Haacker (see next section)
  • ArcGIS
  • Imagemagick (or any other image manipulation software)
  • Blender
Overview

This process is separated into several steps:

  1. Generate height layers in the form of a GeoJSON file for each height level;
  2. Colour each layer and project using the waterman butterfly;
  3. Composite each layer to form a rendered and coloured map;
  4. Generate a monochrome version to create a 3D model in Blender;
  5. Use the 3D model to shade the coloured map;
  6. Print.

Haacker

Haacker is a collection of js scripts written by Fabian Ehmel and hosted on github which we are fortunate enough to have and make use of to generate our level data and render our layers.

Downloading/Installing

Navigate to an appropriate directory and clone the repository:

git clone https://github.com/fabianehmel/haacker.git

We also require a couple js packages which we can install with npm

npm install <PackageName>
  • bezier-easing
  • canvas
  • d3
  • d3-geo
  • d3-geo-projection

We also require GDAL to generate the GeoJSON level files. On Arch you can install both GDAL and the Python bindings with:

pacman -S gdal && pacman -S python-gdal
D3-Geo

D3 is the software that will do the actual projection and colouring of the map. You can read more about it and browse more projections here:

https://github.com/d3/d3-geo
https://github.com/d3/d3-geo-projection

Generating Height Levels

In this step we download the ETOPO1 relief data and then process it to form a GeoJSON file for every level we desire. For example a 20m resolution from -10,000m to 9,000m will result in 951 GeoJSON files.

Fortunately this process is automated for us. The Haacker script process-etopo1-data will both download the ETOPO1 data and process it.

To set the resolution and height range edit the haacker/process-etopo1-data/main.sh file from line 19-25:

# desired level range (in meter)
# default is -10000 to 9000
startLevel=-10000
endLevel=9000

# Resolution: desired distance between levels in meter
resolution=20

Then the script can be run with

npm run process-etopo1-data

Note: This process can take some time. A 20m resolution took 2 days to run. Try first experimenting with a rougher resolution e.g 100m-1000m.

When this is done you should hopefully have a directory haacker/data/levels containing all the GeoJSON files named after their corresponding height e.g. -100.geojson.

Layers

This step revolves around converting GeoJSON files into coloured PNG files of the appropriate projection using the render-map-layer script within Haacker.

Haacker provides an example config file haacker/render-map-layer/config.example.js which I suggest you copy and edit. Name your copy config.js. This is the file we will be editing in this section.

Selecting the projection

The first thing we can select is which projection we would like to use. For the Waterman Butterfly we can select

projection: {
 name: "geoPolyhedralWaterman",
 settings: {
  rotate: [20, 0],
  precision: 0.1
 }

[...]

}

Note: The default precision: 0 almost works except for some strange artifacts. This was a significant headache.

More Projections

For other projections you can browse both the d3-geo and d3-geo-projection repositories.

For even more see d3-geo-polygon. If you decide to use d3-geo-polygon make sure to install the package with npm and you’ll also have to edit haacker/utils.js to append the requirement near the top of the file. Add

require("d3-geo-polygon")

amongst the other requirements.

Choosing Layers

Haacker allows us to choose subsets of our GeoJSON files and colour/project them separately. Thus we will construct the map in 6 layers which we will the composite to form a complete map.

  • -10,000m – 0m
  • Land Area
  • 0m-6000m
  • 6000m-9000m
  • Rivers and Deltas
  • Glacial Areas

We make the map in several layers as each layer needs to be coloured according to a different gradient.

Note: This post doesn’t cover labeling the map which would result in more layers.

The presence of some layers is obvious: We wish to colour the sea (-10,000m – 0m) differently from land (0m – 9,000m). Rivers and Deltas cannot be recovered solely from height data and neither can Glacial Areas. Thus we need to produce GeoJSON files describing these areas. We can download the shape files for the corresponding areas and union and export them as GeoJSON files in QGIS.

However, we still require one more level. That is Land Area. The areas not covered by ocean cannot be inferred solely from height data. One third of the Netherlands lies below sea level. Thus we want a GeoJSON file describing land area which we can render on top of our sea.

Rendering Layers

Haacker provides three ways of rendering GeoJSON files

  • filled
  • stroked
  • levels

Filled will take a GeoJSON file and render all the area the file outlines.
Stroked will render the lines within a GeoJSON file.
Levels will fill several GeoJSON files with optional gradient.

Because our Rivers/Deltas/Countries are a single GeoJSON file each, they will be rendered will filled. The remaining layers are composited level data and will be rendered with levels.

  • (levels) -10,000m – 0m
  • (filled) Land Area
  • (levels) 0m-6000m
  • (levels) 6000m-9000m
  • (filled) Rivers and Deltas
  • (filled) Glacial Areas
Rendering “Levels” Layers

The levels layer has three important properties:

  • filters
  • path
  • style

filters has a js-like syntax that will specify which layers are to be rendered by number.

filters: [level => <LEVEL EXPRESSION>]

where <LEVEL EXPRESSION> is some boolean expression that describes which levels are to be rendered. For example to render only levels between 0m and 6,000m one would use

filters: [level => level >= 0 && level <= 6000]

path is the file path to where your level GeoJSONs are stored. For the level data you downloaded from ETOPO1 this will be

path: "levels"

styles has a single parameter color with two sections: easing and scale. The latter describes the start and end colours of your gradient in hex whereas the former contains the parameters of a bezier curve. The easing describes how the colours will be distributed along your gradient; perhaps you would like more colour distinction in the first few layers of your render or vise-versa. You can play around different curves at https://cubic-bezier.com/.

Rendering “Filled” Layers

filled layers work exactly the same as levels layers except your path is replaced with a data parameter that describes the path to a single GeoJSON file and styles only contains a color parameter. Moreover, a filters parameter is not necessary as we’re only rendering a single file.

An example config.js file may look like this:

Note: Smaller rivers aren’t defined as areas and instead as strokes and thus require a stroked layer. However, a 1px stroke looks enormous on a image only a couple thousand pixels wide. Hence I opted not to render them. Have a look at your river/deltas shape data to see which rivers you want to render.

Shaded Relief

The next step is to create a bump map in Blender for which I refer you a brilliant tutorial. The only thing you require before you are sent off is a height map which you can get by rendering all ETOPO1 layers with a simple linear gradient from black (#000000) to white (#ffffff).

Appendix

Example config.js:
exports.config = {
  dimensions: {
    height: 6000,
    width: 10000
  },
  projection: {
    name: "geoPolyhedralWaterman",
    settings: {
      rotate: [20, 0],
      precision: 0.1
    }
  },
  filepaths: {
    data: "data",
    export: "export"
  },
  layers: [
    {
      type: "levels",
      properties: {
        filters: [level => level < 0],
        path: "levels20m",
        style: {
          color: {
            easing: [0.6, 0, 1.0, 1.0],
            scale: ["#02163b", "#7ac3c7"]
          }
        }
      }
    },

    {
      type: "levels",
      properties: {
        path: "countries",
        style: {
          color: {
            easing: [1.0, 1.0, 1.0, 1.0],
            scale: ["#000e1f", "#000e1f"]
          }
        }
      }
    },

    {
      type: "levels",
      properties: {
        filters: [level => level >= 0 && level <= 6000],
        path: "levels20m",
        style: {
          color: {
            easing: [0, 0.3, 1.0, 1.0],
            scale: ["#000e1f", "#fed0b0"]
          }
        }
      }
    },

    {
      type: "levels",
      properties: {
        filters: [level => level > 6000],
        path: "levels20m",
        style: {
          color: {
            easing: [1.0, 1.0, 1.0, 1.0],
            scale: ["#fed0b0", "#fffaf6"]
          }
        }
      }
    },

    {
      type: "levels",
      properties: {
        path: "rivers",
        style: {
          color: {
            easing: [1.0, 1.0, 1.0, 1.0],
            scale: ["#7ac3c7", "#7ac3c7"]
          }
        }
      }
    },

    {
      type: "levels",
      properties: {
        path: "ice",
        style: {
          color: {
            easing: [1.0, 1.0, 1.0, 1.0],
            scale: ["#ffffff", "#ffffff"]
          }
        }
      }
    }
    ]
};

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create your website with WordPress.com
Get started
%d bloggers like this: