Printing high-res world map posters with custom projection using TileMill and Python.
How is learned about map projections, map lovers and started to hate 180th Meridian.
Backstory
Plan was to get a huge poster map for the wall in our apartment. My SO was very specific with requirements.
- The land part should be white so we can use markers to write custom stuff.
- There should be no text on the map.
- Only borders of the countries are visible.
- Whole map should be in grey-scale.
- The map should be approximately 2m wide to fit our wall.
After brief research it was obvious that there are no pre-made maps that fit this description. It was time to make my own. I experimented with Google Maps and create a python script that accomplishes it. I wrote about it here.
Problems with my Google maps solution
- Questionable licensing.
- Limited amount of layers.(Wanted to add some more depth to the oceans)
- Output is not in vector format.(For huge prints vector is preferred)
- Projection. Google uses Web Mercator.
Why should I care about projection ?
As mentioned Google uses web Mercator projection. This is great for using in a web maps but not that great for making wall map out of it.
North is up everywhere, meridians are equally spaced vertical lines, but areas near the poles are greatly exaggerated.
If you wan’t to learn about projections there is XKCD with great summary, or more technical intro into projections.
My goal was to pick projection with square output (this limits my use of some cool projections out there) and reasonable size of areas close to the poles. For that reason I picked Gall Stereographic.
Solution
- Map creation
- Use Mapnik to change projection (Python)
- Generate PDF output (Python)
1. Map creation
After brief research I decided to use TileMill even though it’s not being actively developed. Main reason was ability to output Mapnik XML. As far as I searched I was not able to find any other tool that I can use to export file in SVG with custom projection.
Working with TileMill is straightforward but using Carto-CSS can be little tricky for the first time. I created 3 layers. Countries, Ocean(Bathymetry), Lakes. All layers are sourced from pre-configured TileMill sources. CSS altered to suit my requirements:
#countries {
line-width: 1;
polygon-fill: #ffffff;
line-color: #d4d4d4;
line-width: 3;
line-join: round;
line-clip: true;
}
#ocean {
polygon-opacity:1;
polygon-comp-op:color-burn;
polygon-fill:#f3f3f3;
}
#lakes {
polygon-opacity:1;
polygon-fill:#fcfcfc;
}
As TileMill supports only Web Mercator next step is moving model into the Mapnik and transforming it. Luckily TileMill offers export to Mapnik XML.
2. Use Mapnik to change projection
I used Python interface to change and export projection.( Jupyter Notebook examples included)
Install dependencies
!apt-get install python-cairocffi
!pip install mapnik
Then you have to take mapnik.xml and change projection to that of your own selection(Bold text). Make sure that you have correct path for shapefiles (Map data dependencies which are saved by TileMill). To find XML definition of projection use SpatialReference website.
%%writefile wallmap.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Map[]>
<Map srs="+proj=gall +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs">
<Parameters>
<Parameter name="bounds"> -180,-85.05112877980659,-180,85.05112877980659</Parameter>
<Parameter name="center">0,0,2</Parameter>
<Parameter name="format">png</Parameter>
<Parameter name="minzoom">0</Parameter>
<Parameter name="maxzoom">22</Parameter>
</Parameters>
<Style name="oceanfloor" filter-mode="first" >
<Rule>
<LineSymbolizer stroke="#c0ecff" stroke-width="0" stroke-linejoin="round" stroke-opacity="1" />
<PolygonSymbolizer fill-opacity="1" fill="#ececec" comp-op="color-burn" />
</Rule>
</Style>
<Layer name="oceanfloor"
srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over">
<StyleName>oceanfloor</StyleName>
<Datasource>
<Parameter name="file"><![CDATA[bathymetry.shp]]></Parameter>
<Parameter name="type"><![CDATA[shape]]></Parameter>
</Datasource>
</Layer>
<Style name="countries" filter-mode="first" >
<Rule>
<LineSymbolizer stroke-width="3" stroke="#d4d4d4" stroke-linejoin="round" />
<PolygonSymbolizer fill="#ffffff" />
</Rule>
</Style>
<Layer name="countries"
srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over">
<StyleName>countries</StyleName>
<Datasource>
<Parameter name="file"><![CDATA[10m-admin-0-countries.shp]]></Parameter>
<Parameter name="type"><![CDATA[shape]]></Parameter>
</Datasource>
</Layer>
</Map>
3. Generate PDF output (Python)
Exporting PDF is straightforward process.
import mapnik
import cairocffi as cairomap = mapnik.Map (1750 ,1185)
mapnik.load_map ( map ,'wallmap.xml')
map.zoom_all ()
map.zoom (1)
mapnik.render_to_file(map,"output.pdf","pdf")
Results
Sadly there is no way !(Please prove me wrong :) ) to export map over 180th meridian.(Timeline between US and Russia) Anytime you cross this line in an export it glitches into weird shapes all over the map. This means that I am unable to widen the map output.
I am however happy with the results :
Epilogue
You can find code that I used here. https://gist.github.com/kuboris/63cb0609d4cf2aa8cfd0333a3910090a
If you know how to cross 180th Meridian please let me know :)