Skip to content

Printing Map Part III: Generate tiles with Mapnik instead of Qgis

September 14, 2011

Generating tiles with Qgis didn’t turn out too well. The main reason is that in Qgis there is no option to ignore labels that are too close to the edge and even if I’ve had all Labeling Engine Options set to 1:

script (modified for my specific needs) would still generate tiles with messed up labels. For example here:

Or I have made a mistake somewhere. Anyhow MAPNIK has a feature to ignore labels that are too close to the edge. A quick adoptation of QGIS script to check comfirmed that it would do exactly what I needed and it seemed to work well. I just had to redesign my map using MAPNIK and fully rewrite the quantumnik plugin to generate tiles.

Map design with MAPNIK using XML is very easy, so I will not post a lot info about it here.  In fact it is a lot easier — as software developer I enjoyed writing CSS design much more than doing it with user interface. Also I found it much quicker to style maps like this. Of course, to help me out, first I exported XML file generated by quantumnik and then redesigned it as I wanted.

See this PDF for full reference on MAPNIK XML Schema. You can see the final result in the end of this post as well as design rules I used for full-zoom scale.

Why did I use plugin to QGIS when I could just write a small script using mapnik API? At first that was what I’ve tried, but somehow it did not work with my CRS. However it did work on quantumnik. Anyway, lets see what I did to quantumnik so it would work for me.

First in imageexport.py find method

def render_thread(self,m,out,format):

and change it to this:

def render_thread(self,m,out,format):
    worky,result = render_wrapper.render_to_file(m,out,format)

then find method

def accept(self): 

This is where I have a loop for generating map tiles. Idea is the same as a script for QGIS, however now using MAPNIK API. Because code is so similar, I will not get into much detail and will only explain strange parts of code:

first I have hardcoded some stuff:

#hardcoded EST extents
xStart = 193503
yStart = 6374147
xEnd = 568711
yEnd = 6631764

#hardcoded other stuff
metersPerPixel = 32
w,h = 5120,5120

Comments and variable names indicate what it all means. It is not too hard to change all this for your own needs.

Then there is some strange code here:

envel = m.envelope()
strEnvelope = str(envel)
strEnvelope = strEnvelope.replace('Envelope(', '')
strEnvelope = strEnvelope.replace(')', '')
strBoundaries = strEnvelope.split(',')
xMinimum = float(strBoundaries[0])
yMinimum = float(strBoundaries[1])
xMaximum = float(strBoundaries[2])
yMaximum = float(strBoundaries[3])

I could not find (fast enough and I was really short on time) how to get bouding coordinates from envelope, so I have just taken them with some String methods. Stupid, ugly and not recommended, but eh, it worked ant at that point it was all I needed.

Here is a complete source code I used for accept method:

def accept(self):
    #hardcoded EST extents
    xStart = 193503
    yStart = 6374147
    xEnd = 568711
    yEnd = 6631764

    #hardcoded other stuff
    metersPerPixel = 32
    w,h = 5120,5120

    border = QgsVectorLayer('c:\\border_est.shp', 'border_est', 'ogr')
    border.invertSelection()
    borderFeatures = border.selectedFeatures()

    i = 0
    j = 0
    self.parent.canvas.width = w
    self.parent.canvas.height = h
    self.parent.mapnik_map.width = w
    self.parent.mapnik_map.height = h           

    if self.parent.from_mapfile:
        m = self.parent.mapnik_map
        easyCanvas = sync.EasyCanvas(self.parent,self.parent.canvas,90)
    else:
        easyCanvas = sync.EasyCanvas(self.parent,self.parent.canvas,90)
        m = easyCanvas.to_mapnik()

    e = self.parent.canvas.extent()

    currentMinX = xStart
    currentMinY = yStart

    bbox = mapnik.Envelope(xStart, yStart, xStart + w * metersPerPixel, yStart + h * metersPerPixel)

    format = str(self.format.currentText())

    while (currentMinY < yEnd):
        while (currentMinX < xEnd):
            m.zoom_to_box(bbox)
            mainScale = m.scale()
            envel = m.envelope()
            strEnvelope = str(envel)
            strEnvelope = strEnvelope.replace('Envelope(', '')
            strEnvelope = strEnvelope.replace(')', '')
            strBoundaries = strEnvelope.split(',')
            xMinimum = float(strBoundaries[0])
            yMinimum = float(strBoundaries[1])
            xMaximum = float(strBoundaries[2])
            yMaximum = float(strBoundaries[3])

            intersects = 0
            for borderFeature in borderFeatures:
                if (borderFeature.geometry().intersects(QgsRectangle(xMinimum, yMinimum, xMaximum, yMaximum))):
                    intersects = 1

            if intersects:
                #recalc bbox
                bbox = mapnik.Envelope(xMinimum, yMinimum, xMaximum, yMaximum)
                self.render_thread(m,"C:/" + str(metersPerPixel) +'m_' + str(i) + '_' + str(j) + ".png", format)

                f = open("c:\\" + str(metersPerPixel) +'m_' + str(i) + '_' + str(j) + ".pngw", 'w')
                f.write(str(mainScale) + '\n')
                f.write(str(0) + '\n')
                f.write(str(0) + '\n')
                f.write('-' + str(mainScale) + '\n')
                f.write(str(xMinimum + mainScale/2) + '\n')
                f.write(str(yMaximum - mainScale/2))
                f.close()

            bbox = mapnik.Envelope(xMaximum,yMinimum,xMaximum + w*metersPerPixel,yMaximum)
            currentMinX = xMaximum
            i += 1

        bbox = mapnik.Envelope(xStart, yMaximum, xStart + w * metersPerPixel, yMaximum + h*metersPerPixel)
        currentMinX = xStart
        currentMinY = yMaximum
        j += 1

Of course, I had to do some transformations for the printed files, but they are irrelevant here.

Then in file sync.py

for

class EasyCanvas(object):

I have changed methods
def to_mapnik(self,m=None):

So it would not take canvas from user interface, but rather use my hardcoded pixel count:

#m = mapnik.Map(*self.dimensions)
m = mapnik.Map(5120, 5120)

and

def __init__(self,parent,canvas,resolution=90.714):

as

def __init__(self,parent,canvas,resolution=90.714):
    canvas.width = 5120
    canvas.height = 5120
    self.parent = parent
    self.canvas = canvas
    self.resolution = resolution
    #self.width = canvas.width()
    #self.height = canvas.height()
    self.width = 5120
    self.height = 5120
    self.normal_pixel = 90.714
    self.base_path =  tempfile.gettempdir()
    # TODO - expose as user options...
    self.merge_duplicate_layers = True

And that’s about it. Don’t forget to recompile and reload the plugin!

Here is the final result:

Styles I used for full zoom:

<Style name="Highway_labels">
    <Rule>
        <TextSymbolizer avoid_edges="1"
            allow_overlap="0"
            dy="6.720000000000001"
            face_name="DejaVu Sans Bold"
            fill="rgb(0,0,0)"
            halo_fill="rgb(0, 225, 0)"
            halo_radius="20"
            min_distance="500"
            name="E_MNT_NR"

            size="50"
            spacing="50"
            vertical_alignment="bottom"/>
    </Rule>
</Style>
<Style name="highway-border">
    <Rule>
        <LineSymbolizer>
            <CssParameter name="stroke">rgb(192,160,64)</CssParameter>
            <CssParameter name="stroke-width">25</CssParameter>
            <CssParameter name="stroke-linejoin">round</CssParameter>
            <CssParameter name="stroke-linecap">round</CssParameter>
        </LineSymbolizer>
    </Rule>
<Style name="highway-fill">
    <Rule>
        <LineSymbolizer>
            <CssParameter name="stroke">rgb(225,212,173)</CssParameter>
            <CssParameter name="stroke-width">10</CssParameter>
            <CssParameter name="stroke-linejoin">round</CssParameter>
            <CssParameter name="stroke-linecap">round</CssParameter>
        </LineSymbolizer>
    </Rule>
</Style>

<Style name="boatRoadStyle">
    <Rule>
        <LineSymbolizer>
            <CssParameter name="stroke">rgb(100,100,100)</CssParameter>
            <CssParameter name="stroke-opacity">0.5</CssParameter>
            <CssParameter name="stroke-width">10</CssParameter>
            <CssParameter name="stroke-dasharray">30,15</CssParameter>
            <CssParameter name="stroke-linejoin">round</CssParameter>
            <CssParameter name="stroke-linecap">round</CssParameter>
        </LineSymbolizer>
    </Rule>
</Style>
<Style name="Districts_fill">
    <Rule>
        <PolygonSymbolizer>
            <CssParameter name="fill">rgb(245,245,230)</CssParameter>
            <CssParameter name="gamma">1</CssParameter>
        </PolygonSymbolizer>
    </Rule>
</Style>
<Style name="Districts_Border">
    <Rule>
        <LineSymbolizer>
            <CssParameter name="stroke">rgb(200,200,200)</CssParameter>
            <CssParameter name="stroke-width">5</CssParameter>
            <CssParameter name="stroke-linejoin">round</CssParameter>
            <CssParameter name="stroke-linecap">round</CssParameter>
        </LineSymbolizer>
    </Rule>
</Style>
<Style name="Lake_style">
    <Rule>
        <PolygonSymbolizer>
            <CssParameter name="fill">rgb(128,202,255)</CssParameter>
            <CssParameter name="gamma">1</CssParameter>
        </PolygonSymbolizer>
    </Rule>
</Style>
<Style name="Forest_style">
    <Rule>
        <PolygonSymbolizer>
            <CssParameter name="fill">rgb(209,255,179)</CssParameter>
            <CssParameter name="gamma">1</CssParameter>
        </PolygonSymbolizer>
    </Rule>
</Style>
No comments yet

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: