Code, Maps

Using shp2stl to Convert Maps to 3D Models

I’ve been working on a utility called shp2stl that converts geographic data in shapefiles to 3D models, suitable for 3D printing. The code is published as a NodeJS package, available on npm and GitHub.

You can control the height of each shape by specifying an attribute of your data to use. Each shape will be placed along the z-axis based on the shape’s value relative to the max range in the data. Additionally, if you want more detailed control you can specify a function to use to extrude each shape.

Examples

South Napa Earthquake

Here’s an example using the recent South Napa earthquake, first as the source shapefile:

Then converted to a 3D model using shp2stl:

And finally printed with a 3D printer:

epicenter_angled-696x420

All the files and source code for generating the model above are available on github. See more about my 3D print of the 2014 Napa earthquake.

San Francisco Population

The map below shows the population of San Francisco broken down by census tract.

And the same data converted to a 3D model:

All the files and source code for generating the model above are available on github.

How to use it

shp2stl is a NodeJS package you can install via npm. You can install it like any npm package by doing

npm install shp2stl

If you’re new to NodeJS then you’ll also have to download Node (which comes bundled with npm). shp2stl is not a standalone program that you run, you have to use it in your own NodeJS code.

The easiest way to understand how to use the package is via an example:

var fs = require('fs');
var shp2stl = require('shp2stl');

var file = 'SanFranciscoPopulation.shp';

shp2stl.shp2stl(file, 
    {
        width: 100, //in STL arbitrary units, but typically 3D printers use mm
        height: 10,
        extraBaseHeight: 0,
        extrudeBy: "Pop_psmi",
        simplification: .8,

        binary: true,
        cutoutHoles: false,
        verbose: true,
        extrusionMode: 'straight'
    },
    function(err, stl) {
        fs.writeFileSync('SanFranciscoPopulation.stl',  stl);
    }
);

That will produce an STL model with each polygon sized by the Pop_psmi attribute. You should customize the example above to point to your own shapefile and specify a valid attribute within that shapefile to use for the height.

More detailed documentation covering all the available options is in the shp2stl README.

How it works

The TLDR version: convert from shapefile to geojson, then to topojson, then to a threeJS model, then to STL.

shp2stl takes advantage of the rich NodeJS ecosystem and uses a number of third party packages available on npm. The key packages that are used are shapefile, topojson, and three (in addition deepcopy, point-in-polygon, and ogr2ogr are used).

A brief word on manifold meshes: it’s especially important for 3D printing that all your 3D meshes are “manifold”. You can think about that in terms of producing a clean water-tight model that’s perfectly hollow on the inside. Here are some good examples of non-manifold problematic meshes. This becomes very important when we figure out how to covert the polygons in our shapefile to a 3D model.

Step 1: Convert to GeoJSON

The shapefile is first read with Mike Bostock’s shapefile package, which converts the geo data into GeoJSON. This will be converted in the shapefile’s source projection, but if you want to reproject the data before converting it you can using the sourceSRS and destSRS options, which uses the ogr2ogr package to do the projection (this requires ogr2ogr already installed separately).

Step 2: Convert GeoJSON to TopoJSON

One of the key issues we encounter when trying to convert polygons to a 3D model has to do with shapes that share borders. Shapefiles (and GeoJSON) represent bordering shapes entirely independently. That means that two shapes that share a border have two distinct sets of line segments that define that border. You can certainly attempt to create a simple 3D model by treating each polygon separately like this, which will produce distinct 3D meshes for each poly. That’s likely fine for 3D rendering on a computer, but it’s not ideal for 3D printing. We need a single manifold mesh, not a bunch of distinct ones that are all touching each other. What we’re hoping to create is the full combined outer shell.

The key to converting to a single unified mesh is to convert the GeoJSON to TopoJSON. Mike Bostock’s TopoJSON format encodes topology, which means that it figures out all the unique line segments that make up the polys. If two shapes share a border that border only gets converted to a single line segment instead of two, and both shapes reference the same segment. This allows you to reconstruct each poly, but it also tells you an important piece of information: which line segments are shared between which polygons. That’s the key to creating a nice single outer shell for our polygon.

Step 3: Convert to ThreeJS

ThreeJS is a full-featured 3D library for JavaScript. It can do a ton of stuff, but for my purposes I’m only really interested in a couple things: triangulating polygons (to convert the polys), creating simple faces (to make the sides of the model), and easily iterating over all triangles in a model (to convert to an STL file).

shp2stl will create ThreeJS planes for each polygon in your shapefile, and extrude each based on an attribute you specify. The built-in triangulation method of ThreeJS is used to convert the shape polygons into triangle faces.

The distinct planes are then connected by creating connecting faces along the z-axis. Since the TopoJSON format tells us which polygons share which borders, we’re able to connect the faces that touch by manually creating sides that start at the edge of the lower face and extend up to the height of the upper face.

Step 4: Print!

You should now have a nice manifold mesh ready to be sent to your 3D printer of choice. I’ve had a lot of success printed the models created by shp2stl with the Afinia Series H.

Grab shp2stl on github or from npm and I’d love to hear about things you’ve printed.



Related:

  • Two weeks ago on the night of August 24, 2014 I was shaken awake by the 6.0 magnitude earthquake that struck Napa, CA. It was the largest earthquake to hit the Bay Area since the 1989 World Series quake. Where I was in San Francisco wasn't close enough to the…
  • This is a map of Moore, Oklahoma. On May 20, 2013 an EF5 tornado struck this city of 58,000 people, killing 24 and injuring 377 others. The destruction within the direct path of the storm was near complete. This piece focuses on the city boundary of Moore and the destruction…
  • I've been playing with different ways of representing data (see my previous night lights example) and I decided to venture into 3D representations. I've used a full year of crime data for San Francisco from 2009 to create these maps. The full dataset can be download from the city's DataSF…
Standard