How-to for using mapnik to render osm map tiles using C.

How-to for using mapnik to render osm map tiles using C.
I have a slippy map program written in C++ that I use on a linux laptop to display maps for navigation in remote locations. That program relied on tiles previously archived on the local disk (bulk downloaded); which seemed to make tile server operators grouchy. So I added the capability to render tiles, using mapnik, as needed from osm.pbf data. I found nowhere anything that clearly said how to do this so here is my attempt to document the steps required to make an openstreetmap tile available on disk at the request of a C++ language computer program. I start this example on a virgin computer so as not to miss any install dependencies. Most likely some of the steps below will have already been completed on your machine.

  1. Install your favorite linux distribution. I used Fedora 20.
    I don’t suggest that you need to do this step.
  2. Bring your installation current.
    “sudo yum update”
    Fedora provides yum. Your distribution probably uses something else.
    Reboot. Just in case.
  3. Bring up the postgresql server. Mapnik expects postgre feature data
    I am user ‘RB’ that appears below
    “sudo yum install postgresql”
    “sudo yum install postgresql-server” this creates directory /var/lib/pgsql size 24 B
    “sudo yum install postgresql-contrib”
    “sudo yum install postgis”
    “sudo yum install kdeadmin” To set user parameters
    “sudo kuser” Add RB to postgres group and postgres to RB group
    “psql –version” returns 9.3.4 so postgre is up
    “sudo chmod 770 /var/lib/psql” Maybe ? I doubt this is necessary. Was reversed later.
    “sudo chmod 770 /home/RB”
    “su postgres”
    “postgresql-setup initdb”
    “/bin/initdb -U RB -D /usr/local/pgsql/data” This creates directory /usr/local/pgsql size 36K
    “postgres -D /usr/local/pgsql/data” Server now running
    “ps -A” and see 7 instances of postgres running
    “su RB”
    “createdb -O RB -U RB gis” ‘gis’ is the name of the database mapnik looks for
    “psql -d gis -c ‘CREATE EXTENSION hstore; CREATE EXTENSION postgis;’
    “psql gis” See database prompt gis=# \q to exit
  4. Fetch osm style sheets and make osm.xml file
    “sudo yum install git”
    “sudo yum install carto”
    Make a directory named whatever. I use /home/RB/Maps/MapWorking
    cd to that directory
    “git clone https://github.com/gravitystorm/openstreetmap-carto.git”
    This will make directory MapWorking/openstreetmap-carto.
    “cd MapWorking/openstreetmap-carto”
    “./get-shapefiles.sh” Time consuming
    “carto -l project.mml > osm.xml”
    This will make file ‘osm.xml’ which you will copy to another directory later.
  5. Fetch osm map data in .pbf format
    From http://download.geofabrik.de/ select a geographic area of interest and download the .osm.pbf version. For this exercise I select Nevada and Utah and place the requested data files into an arbitrarily named folder. I use “/home/RB/Maps/MapWorking/OSMfromGeofabrik”. These are 30+ MiB files.
  6. Place osm data into postgre database
    This uses the osm2pgsql program. Some examples I have come upon called for the “–slim” option. That will not work if wish to append to your database. Notice the –merc option. We are working with mercator data. The path to your data is shown as ~/

“sudo yum install osm2pgsql”
“su RB”
Hold on tight here
“osm2pgsql --create --cache 1000 --number-processes 2 --hstore --style ~/openstreetmap-carto/openstreetmap-carto.style --multi-geometry --merc ~/OSMfromGeofabrik/nevada-latest.osm.pbf”
“psql gis”
\d shows tables does not include table planet_osm_nodes
SELECT lat, lon FROM planet_osm_nodes ; Shows no points

“osm2pgsql --create --cache 1000 --number-processes 2 --hstore --style ~/openstreetmap-carto/openstreetmap-carto.style --multi-geometry --merc –slim ~/OSMfromGeofabrik/nevada-latest.osm.pbf” Notice –slim option
“psql gis”
\d shows tables now including planet_osm_nodes table
SELECT lat, lon FROM planet_osm_nodes ; Shows mercator points

“osm2pgsql --append --cache 1000 --number-processes 2 --hstore --style ~/openstreetmap-carto/openstreetmap-carto.style --multi-geometry --merc –slim ~/OSMfromGeofabrik/utah-latest.osm.pbf” fails for duplicate key

“osm2pgsql --create --cache 1000 --number-processes 2 --hstore --style ~/openstreetmap-carto/openstreetmap-carto.style --multi-geometry --merc ~/OSMfromGeofabrik/nevada-latest.osm.pbf” Notice –slim dropped
“psql gis”
\d shows tables Still includies planet_osm_nodes table
SELECT lat, lon FROM planet_osm_nodes ; Shows mercator points

“osm2pgsql --append --cache 1000 --number-processes 2 --hstore --style ~/openstreetmap-carto/openstreetmap-carto.style --multi-geometry --merc ~/OSMfromGeofabrik/utah-latest.osm.pbf”
Now completes successfully with Nevada and Utah loaded into postgresql
I don’t know anything about most other parameters except they were there to copy
7. Install mapnik I doubt this was necessary as mapnik was configured and built later on
“sudo yum install mapnik” See folder /usr/lib64/mapnik appear
“sudo yum install mapnik-demo”
“sudo yum install wget”
in directory MapWorking
“wget http://svn.openstreetmap.org/applications/rendering/mapnik/get-coastlines.sh”
“chmod 777 get-coastlines.sh”
“get-coastlines.sh”
In /usr/share/doc/mapnik-demo/python
“python rundemo.py” See in demo.png Ottawa map
8. Install development environment
The goal of this exercise is to use mapnik from a C language program. Kdevelop is used here to build that C program. This is a QT graphics project. The need for installs here may have been discovered in later steps and grouped here for convenience. If I did an install, but it did not fix what I was working on then I do not know if it was necessary.
“sudo yum install kdevelop”
“sudo yum install gcc”
“sudo yum install gcc-c++”
“sudo yum install qt-devel”
“sudo yum install boost-system” Not sure about this one
“sudo yum install boost”
“sudo yum install boost-devel”
“sudo yum install python-devel” Not sure about this one
“sudo yum install libxml2-devel”
“sudo yum install libtiff-devel”
“sudo yum install libpqxx”
“sudo yum install libpqxx-devel”
download mapnik-v2.2.0.tar.bz2and extract to ~/Maps/MapMaker/MapnikBuild
In /MapnikBuild
“./configure” Completes with noted omissions
“make” Time consuming
“sudo make install”
“kdevelop” See kdevelop come up and compile the “hello world” project
9. Bring up the application
The development location will be ~/projects/MapMaker
The execution location will be ~/projects/MapMaker/build
Copy in or create a real project that uses mapnik. When you get your kdevelop project right it will create or recognize the structure above.
Copy the osm.xml file created in Step 4 above to ~/projects/MapMaker/build
edit this file at line 18 to replace fontset-0 ‘DejaVu sans book’ with ‘DejaVu sans oblique’
(I could not find the ‘book’ font anyplace I looked)
In ~/projects/MapMaker/build
“ln -s /home/RB/Maps/MapWorking/openstreetmap-carto/symbols symbols”
With kdevelop build and execute the application disclosed below.
10. The application
This application will cause the file 24920.png to appear in directory /home/RB/Maps/Mapworking. You will recognizs that as part of the Spaghetti Bowl in Reno, Nv.

#include
#include
#include “/home/RB/projects/MapMaker/MapMaker.h”
#define RAD_TO_DEG (360.0 / 2.0 / M_PI)
//
// Driver program (the application)
// Calls interface with zoom level and x/y tile numbers to render and destination
// for the rendered map. Note that ‘typeMap’ is unused here
//
bool MapMakerDo(int zoom, int xTileLon,int yTileLat, QString destPath, QString typeMap);
MapMaker::MapMaker(QApplication *App)
{
int zoom = 16;
int xTileLon = 10961;
int yTileLat = 24920;
QString destPath = “/home/RB/Maps/MapWorking”;
QString typeMap = “openstreetmap”;
MapMakerDo(zoom, xTileLon, yTileLat, destPath, typeMap);
}
//
// The mapnik interface
//
bool MapMakerDo(int zoom, int xTileLon,int yTileLat, QString destPath, QString typeMap)
{
int i;
mapnik::datasource_cache::instance().register_datasources(“/usr/local/lib/mapnik/input”);
i=mapnik::freetype_engine::register_font(“/usr/local/lib/mapnik/fonts/DejaVuSans-Oblique.ttf”); i=mapnik::freetype_engine::register_font(“/usr/local/lib/mapnik/fonts/DejaVuSans-Bold.ttf”);
i=mapnik::freetype_engine::register_fonts(“/usr/local/lib/mapnik/fonts/DejaVuSans-Oblique.ttf”);
i=mapnik::freetype_engine::face_names().size();

double mapWid = 256;
mapnik::Map m(mapWid, mapWid);
mapnik::load_map(m, "osm.xml");

double 	powZoom = exp2(zoom);
double 	lon;
double latMax = atan(sinh(M_PI * (1 - 2 * (yTileLat) / powZoom))) * RAD_TO_DEG;	//NW corner
double latMin = atan(sinh(M_PI * (1 - 2 * (yTileLat+1) / powZoom))) * RAD_TO_DEG;	//SE corner
lon = (xTileLon / powZoom) * 360.0 - 180.0;
double lonDegPerPix = 360.0 / (mapWid * powZoom);
double lonMin = lon;
double lonMax = lon + lonDegPerPix * mapWid;

mapnik::projection merc = mapnik::projection("+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");
mapnik::projection longlat = mapnik::projection("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs");
mapnik::box2d<double> bbox (lonMin, latMin, lonMax, latMax);
mapnik::proj_transform tr(longlat,merc);
bool ret = tr.forward(bbox);		//Alters bbox to merc
m.zoom_to_box(bbox);
mapnik::image_32 buf (m.width(), m.height());
mapnik::agg_renderer<mapnik::image_32> r(m, buf);
r.apply();

QString outFname = destPath + "/"  + QString("%1").arg(yTileLat) + ".png";	/destPath must exist
mapnik::save_to_file<mapnik::image_data_32>(buf.data(), outFname.toStdString(), "png");
return(true);		//Only return value as path constrained to be present

}
11. PS
This has been posted to both the openstreetmap forum and the mapnik google group Comments posted on either group may add insight to what was presented above.

I was never able to find a source for the ’ DejaVu sans book’ font. Like “DejaVuSans-Book.ttf” Do you know where to look?

Thank you to all who posted the useful examples scattered about the web that I have used here.
-RickBrown in Reno, NV

Hi Rick !
First of all I got to thank you for your Tutorial. There aren’t a lot of Tutorials on the net for Mapnik with C/C++.
I am currently working on a Project for University with Mapnik ( just started ). My Prof wants me to develop an Android App
entirely written in QML/C++ using the Qt Framework and Mapnik for rendering Maps ( with the premiss of not using Python ).
Since this App should run on a Android device I don’t want to use a PostgreSql db - I think that a SpatiaLite db should work on
such a Device better. So i did not follow your Tutorial entirely.
But what i did is i downloaded the shapefiles from Berlin for test purposes and made a little Stylesheet for that with TileMill.
Now my question is: I don’t understand where did you get yTileLat = 10961 and xTileLon = 24920 from ?
I looked up the latitude and longitude of the Reno spaghetti bowl and it does not match ( apart from that the lat and lon would be double values and not int’s)
Could you explain what you meant by that ?

Here is the code that I tried so far:

    
try {
        std::cout << " running demo ... \n";
        std::string mapnik_dir(argv[1]);
        std::cout << " looking for 'shape.input' plugin in... " << mapnik_dir << "/lib/mapnik/input/" << "\n";
        datasource_cache::instance().register_datasources(mapnik_dir + "/lib/mapnik/input/");
        std::cout << " looking for DejaVuSans font in... " << mapnik_dir << "/lib/mapnik/fonts/DejaVuSans.ttf" << "\n";
        freetype_engine::register_font(mapnik_dir + "/lib/mapnik/fonts/DejaVuSans.ttf");
        
        double mapWid = 256;

        Map m(mapWid, mapWid);
        load_map(m, "Berlin.xml");
        
        
        //m.set_srs(srs_merc);
        
        // layers
        // Highways  polygons
        {
            parameters p;
            p["type"]="shape";
            p["file"]="demo/data/berlin/berlin.osm-line.shp";
            p["encoding"]="latin1";
            
            layer lyr("berlin-lines");
            lyr.set_datasource(datasource_cache::instance().create(p));

            lyr.add_style("berlin-lines");
            m.addLayer(lyr);
        }
        
        // Polygons
        {
            parameters p;
            p["type"]="shape";
            p["file"]="demo/data/berlin/berlin.osm-polygon.shp";


            layer lyr("berlin-polygons");
            lyr.set_datasource(datasource_cache::instance().create(p));

            lyr.add_style("berlin-polygons");
            m.addLayer(lyr);
        }

        int zoom = 10;
        int yTileLat = 10961;
        int xTileLon = 24920;


        double powZoom = exp2(zoom);
        double lon;

        double latMax = atan(sinh(M_PI * (1 - 2 * (yTileLat) / powZoom))) * RAD_TO_DEG;	//NW corner
        double latMin = atan(sinh(M_PI * (1 - 2 * (yTileLat+1) / powZoom))) * RAD_TO_DEG;	//SE corner

        std::cerr << "latMax: " << latMax << std::endl;
        std::cerr << "latMin: " << latMin << std::endl;

        lon = (xTileLon / powZoom) * 360.0 - 180.0;

        double lonDegPerPix = 360.0 / (mapWid * powZoom);
        double lonMin = lon;
        double lonMax = lon + lonDegPerPix * mapWid;

        std::cerr << "lonMax: " << lonMax << std::endl;
        std::cerr << "lonMin: " << lonMin << std::endl;

        mapnik::projection merc = mapnik::projection("+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");
        mapnik::projection longlat = mapnik::projection("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs");
       
       //That does not work yet - need to set xTileLon and yTileLat correct. 
       //mapnik::box2d<double> bbox (lonMin, latMin, lonMax, latMax);

        // Got the parameter from OpenStreetMap.org
        mapnik::box2d<double> bbox (13.3003, 52.5442, 52.4740, 13.5001);


        mapnik::proj_transform tr(longlat,merc);
        bool ret = tr.forward(bbox);	//Alters bbox to merc
        m.zoom_to_box(bbox);
        mapnik::image_32 buf (m.width(), m.height());
        mapnik::agg_renderer<mapnik::image_32> r(m, buf);
        r.apply();

        mapnik::save_to_file(buf, "berlin.png", "png");
}

You probably will recognize the code :slight_smile:
It would be very kind of you if you could help me !

Best regards from Germany !
Konstantin H.

Sorry for being slow to answer your question.
" I don’t understand where did you get yTileLat = 10961 and xTileLon = 24920 from ? "
Those are the tile coordinates generated thusly. lat and lon is query point
“”“”“”“”"
for (int zoom = 1; zoom <= 16; zoom++)
{
powZoom = exp2(zoom);
double lat_rad = lat * TO_RADIANS;
xtilef = (lon + 180.0) / 360.0 * powZoom;
double lonW = int(xtilef)*360.0/powZoom - 180;
double lonE = int(xtilef+1)*360.0/powZoom - 180;
ytilef = ((1.0 - log(tan(lat_rad) + (1 / cos(lat_rad))) / PI) / 2.0 * powZoom);
double latN = atan(sinh(PI * (1 - 2 * int(ytilef) / powZoom))) / TO_RADIANS;
double latS = atan(sinh(PI * (1 - 2 * int(ytilef+1) / powZoom))) / TO_RADIANS;
bldStr.sprintf(“%4d %8d %8d %12.6f %12.6f %12.6f %12.6f”, zoom, int(xtilef), int(ytilef),lonW, lonE, latN, latS);
“”“”“”“”"

Also from Section 11: the ““edit this file at line 18 to replace fontset-0 ‘DejaVu sans book’ with ‘DejaVu sans oblique’ issue was caused by failing to register that font like:
i=mapnik::freetype_engine::register_font(”/usr/local/lib/mapnik/fonts/DejaVuSans.ttf”); In the MapMaker routine
-RickBrown in Reno