{"id":2061,"date":"2014-12-30T08:30:40","date_gmt":"2014-12-30T16:30:40","guid":{"rendered":"http:\/\/dougmccune.com\/blog\/?p=2061"},"modified":"2017-01-03T16:05:00","modified_gmt":"2017-01-04T00:05:00","slug":"using-shp2stl-to-convert-maps-to-3d-models","status":"publish","type":"post","link":"https:\/\/dougmccune.com\/blog\/2014\/12\/30\/using-shp2stl-to-convert-maps-to-3d-models\/","title":{"rendered":"Using shp2stl to Convert Maps to 3D Models"},"content":{"rendered":"<p>I&#8217;ve been working on a utility called <a href=\"https:\/\/github.com\/dougmccune\/shp2stl\">shp2stl<\/a> that converts geographic data in shapefiles to 3D models, suitable for 3D printing. The code is published as a NodeJS package, available on <a href=\"https:\/\/www.npmjs.org\/package\/shp2stl\">npm<\/a> and <a href=\"https:\/\/github.com\/dougmccune\/shp2stl\">GitHub<\/a>. <\/p>\n<p>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&#8217;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.<\/p>\n<h1>Examples<\/h1>\n<h2>South Napa Earthquake<\/h2>\n<p>Here&#8217;s an example using the recent South Napa earthquake, first as the source shapefile:<\/p>\n<div class=\"overlay\" onClick=\"style.pointerEvents='none'\" style=\"background:transparent; position:relative; width:696px; height:420px; top:420px; margin-top:-420px;\"><\/div>\n<p><script src=\"https:\/\/embed.github.com\/view\/geojson\/dougmccune\/3dmaps\/master\/napa2014Epicenter\/pgv.topojson?width=696\"><\/script><\/p>\n<p>Then converted to a 3D model using shp2stl:<\/p>\n<div class=\"overlay\" onClick=\"style.pointerEvents='none'\" style=\"background:transparent; position:relative; width:696px; height:420px; top:420px; margin-top:-420px;\"><\/div>\n<p><script src=\"https:\/\/embed.github.com\/view\/3d\/dougmccune\/3dmaps\/master\/napa2014Epicenter\/pgv.stl?width=696\"><\/script><\/p>\n<p>And finally printed with a 3D printer:<\/p>\n<p><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/dougmccune.com\/blog\/wp-content\/uploads\/2014\/12\/epicenter_angled-696x420.jpg?resize=696%2C420&#038;ssl=1\" alt=\"epicenter_angled-696x420\" width=\"696\" height=\"420\" class=\"alignnone size-full wp-image-2161\" srcset=\"https:\/\/i0.wp.com\/dougmccune.com\/blog\/wp-content\/uploads\/2014\/12\/epicenter_angled-696x420.jpg?resize=696%2C420&amp;ssl=1 696w, https:\/\/i0.wp.com\/dougmccune.com\/blog\/wp-content\/uploads\/2014\/12\/epicenter_angled-696x420.jpg?resize=300%2C181&amp;ssl=1 300w\" sizes=\"auto, (max-width: 696px) 100vw, 696px\" \/><\/p>\n<p><!--more--><\/p>\n<p>All the files and source code for generating the model above are <a href=\"https:\/\/github.com\/dougmccune\/3dmaps\/tree\/master\/napa2014Epicenter\">available on github<\/a>. See more about my <a href=\"https:\/\/dougmccune.com\/blog\/2014\/09\/09\/2014-south-napa-earthquake-3d-print\/\">3D print of the 2014 Napa earthquake<\/a>.<\/p>\n<h2>San Francisco Population<\/h2>\n<p>The map below shows the population of San Francisco broken down by census tract.<\/p>\n<div class=\"overlay\" onClick=\"style.pointerEvents='none'\" style=\"background:transparent; position:relative; width:696px; height:420px; top:420px; margin-top:-420px;\"><\/div>\n<p><script src=\"https:\/\/embed.github.com\/view\/geojson\/dougmccune\/3dmaps\/master\/sfPopulation\/SanFranciscoPopulation.topojson?width=696\"><\/script><\/p>\n<p>And the same data converted to a 3D model:<\/p>\n<div class=\"overlay\" onClick=\"style.pointerEvents='none'\" style=\"background:transparent; position:relative; width:696px; height:420px; top:420px; margin-top:-420px;\"><\/div>\n<p><script src=\"https:\/\/embed.github.com\/view\/3d\/dougmccune\/3dmaps\/master\/sfPopulation\/SanFranciscoPopulation.stl?width=696\"><\/script><\/p>\n<p>All the files and source code for generating the model above are <a href=\"https:\/\/github.com\/dougmccune\/3dmaps\/tree\/master\/sfPopulation\">available on github<\/a>.<\/p>\n<h1>How to use it<\/h1>\n<p>shp2stl is a NodeJS package you can install via npm. You can install it like any npm package by doing<\/p>\n<p>[js]npm install shp2stl[\/js]<\/p>\n<p>If you&#8217;re new to NodeJS then you&#8217;ll also have to <a href=\"http:\/\/nodejs.org\/\">download Node<\/a> (which comes bundled with npm). shp2stl is not a standalone program that you run, you have to use it in your own NodeJS code.<\/p>\n<p>The easiest way to understand how to use the package is via an example:<\/p>\n<p>[js]<br \/>\nvar fs = require(&#8216;fs&#8217;);<br \/>\nvar shp2stl = require(&#8216;shp2stl&#8217;);<\/p>\n<p>var file = &#8216;SanFranciscoPopulation.shp&#8217;;<\/p>\n<p>shp2stl.shp2stl(file,<br \/>\n    {<br \/>\n        width: 100, \/\/in STL arbitrary units, but typically 3D printers use mm<br \/>\n        height: 10,<br \/>\n        extraBaseHeight: 0,<br \/>\n        extrudeBy: &#8220;Pop_psmi&#8221;,<br \/>\n        simplification: .8,<\/p>\n<p>        binary: true,<br \/>\n        cutoutHoles: false,<br \/>\n        verbose: true,<br \/>\n        extrusionMode: &#8216;straight&#8217;<br \/>\n    },<br \/>\n    function(err, stl) {<br \/>\n        fs.writeFileSync(&#8216;SanFranciscoPopulation.stl&#8217;,  stl);<br \/>\n    }<br \/>\n);<br \/>\n[\/js]<\/p>\n<p>That will produce an STL model with each polygon sized by the <code>Pop_psmi<\/code> 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.<\/p>\n<p>More detailed documentation covering all the available options is in the <a href=\"https:\/\/github.com\/dougmccune\/shp2stl\">shp2stl README<\/a>.<\/p>\n<h1>How it works<\/h1>\n<p>The TLDR version: <strong>convert from shapefile to geojson, then to topojson, then to a threeJS model, then to STL<\/strong>. <\/p>\n<p>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 <a href=\"https:\/\/www.npmjs.org\/package\/shapefile\">shapefile<\/a>, <a href=\"https:\/\/www.npmjs.org\/package\/topojson\">topojson<\/a>, and <a href=\"https:\/\/www.npmjs.org\/package\/three\">three<\/a> (in addition <a href=\"https:\/\/www.npmjs.org\/package\/deepcopy\">deepcopy<\/a>, <a href=\"https:\/\/www.npmjs.org\/package\/point-in-polygon\">point-in-polygon<\/a>, and <a href=\"https:\/\/www.npmjs.org\/package\/ogr2ogr\">ogr2ogr<\/a> are used).<\/p>\n<p>A brief word on manifold meshes: it&#8217;s especially important for 3D printing that all your 3D meshes are &#8220;manifold&#8221;. You can think about that in terms of producing a clean water-tight model that&#8217;s perfectly hollow on the inside. Here are some <a href=\"http:\/\/www.shapeways.com\/tutorials\/fixing-non-manifold-models\">good examples of non-manifold problematic meshes<\/a>. This becomes very important when we figure out how to covert the polygons in our shapefile to a 3D model.<\/p>\n<h2>Step 1: Convert to GeoJSON<\/h2>\n<p>The shapefile is first read with Mike Bostock&#8217;s <a href=\"https:\/\/www.npmjs.org\/package\/shapefile\">shapefile<\/a> package, which converts the geo data into GeoJSON. This will be converted in the shapefile&#8217;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 <a href=\"https:\/\/www.npmjs.org\/package\/ogr2ogr\">ogr2ogr<\/a> package to do the projection (this requires ogr2ogr already installed separately).<\/p>\n<h2>Step 2: Convert GeoJSON to TopoJSON<\/h2>\n<p>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&#8217;s likely fine for 3D rendering on a computer, but it&#8217;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&#8217;re hoping to create is the full combined outer shell.<\/p>\n<p><strong>The key to converting to a single unified mesh is to convert the GeoJSON to <a href=\"https:\/\/github.com\/mbostock\/topojson\/wiki\">TopoJSON<\/a>.<\/strong> Mike Bostock&#8217;s TopoJSON format encodes topology, which means that it figures out all the <strong>unique<\/strong> 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&#8217;s the key to creating a nice single outer shell for our polygon.<\/p>\n<h2>Step 3: Convert to ThreeJS<\/h2>\n<p><a href=\"http:\/\/threejs.org\/\">ThreeJS<\/a> is a full-featured 3D library for JavaScript. It can do a ton of stuff, but for my purposes I&#8217;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).<\/p>\n<p>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. <\/p>\n<p>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&#8217;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.<\/p>\n<p><img data-recalc-dims=\"1\" height=\"412\" width=\"696\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/dougmccune.com\/blog\/wp-content\/uploads\/2014\/09\/model_4.png?resize=696%2C412&#038;ssl=1\" alt=\"\" \/><\/p>\n<h2>Step 4: Print!<\/h2>\n<p>You should now have a nice manifold mesh ready to be sent to your 3D printer of choice. I&#8217;ve had a lot of success printed the models created by shp2stl with the Afinia Series H.<\/p>\n<p><img data-recalc-dims=\"1\" height=\"463\" width=\"696\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/dougmccune.com\/blog\/wp-content\/uploads\/2014\/09\/all_tiles_angled1.jpg?resize=696%2C463&#038;ssl=1\" \/><\/p>\n<p>Grab <a href=\"https:\/\/github.com\/dougmccune\/shp2stl\">shp2stl on github<\/a> or from <a href=\"https:\/\/www.npmjs.com\/package\/shp2stl\">npm<\/a> and I&#8217;d love to hear about things you&#8217;ve printed.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I&#8217;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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2040,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[94,34],"tags":[],"class_list":["post-2061","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-code-2","category-maps"],"aioseo_notices":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/dougmccune.com\/blog\/wp-content\/uploads\/2014\/09\/model_4.png?fit=2574%2C1524&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/posts\/2061","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/comments?post=2061"}],"version-history":[{"count":35,"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/posts\/2061\/revisions"}],"predecessor-version":[{"id":2495,"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/posts\/2061\/revisions\/2495"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/media\/2040"}],"wp:attachment":[{"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/media?parent=2061"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/categories?post=2061"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dougmccune.com\/blog\/wp-json\/wp\/v2\/tags?post=2061"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}