mapnik / python-mapnik

Python bindings for mapnik
GNU Lesser General Public License v2.1
160 stars 91 forks source link

MemoryDatasource with xml styling #159

Closed mrterry closed 7 years ago

mrterry commented 7 years ago

I'm trying to upgrade to Mapnik 3, but am having issues with rendering text. In 2.2 I could use the Python API to specify text styling, however in 3.x these bindings are broken / no longer exist. In #126 it was recommended to use load_map & an xml stylesheet. I"m attempting to follow that guidance, but I haven't been able to figure out how to get anything to render using a MemoryDatasource on the Python side and specifying styling information in the xml file. Any help would be greatly appreciated.

The following snippets are Mapnik 2.2, but should work in Mapnik 3 with minor changes.

import mapnik                                                                   
from mapnik import Color                                                        
from mapnik import Layer                                                        
from mapnik import Style                                                        

wkt = 'POLYGON ((0.06 0.05, 0.050.04, 0.04 0.05, 0.05 0.06, 0.06 0.05))'

map_obj = mapnik.Map(256, 256, '+init=epsg:3857')                               

STYLESHEET_PATH = 'min_style.xml'
mapnik.load_map(map_obj, STYLESHEET_PATH)                                       

map_obj.background = Color(0, 0, 0, 0)                                          

layer = Layer('my_layer', '+init=epsg:4326')                                    

ds = mapnik.MemoryDatasource()                                                  
context = mapnik.Context()                                                      

feature = mapnik.Feature(context, 1)                                            
feature.add_geometries_from_wkt(wkt)                              
ds.add_feature(feature)                                                         
layer.datasource = ds                                                           

py_style = False  # I'm attempting to move the styling specification form the Python API to the XML file
if py_style:                                                                        
    style = Style()                                                             
    ls = mapnik.LineSymbolizer(Color('#f03b20'), 3.)                            
    rule = mapnik.Rule()                                                        
    rule.symbols.append(ls)                                                     
    style.rules.append(rule)                                                    
    map_obj.append_style('style_agency_geometry', style)                        
    layer.styles.append('style_agency_geometry')                                

    map_obj.layers.append(layer)                                                

bbox_web_extent = (4452.779631730666, 4452.779993435518, 6679.1694475959985, 6679.170668350084)
map_obj.zoom_to_box(mapnik.Box2d(*bbox_web_extent))                             

image = mapnik.Image(256, 256)                                                   
mapnik.render(map_obj, image)                                                   
image.save('img.png', 'png32:z=1')

I'm using the following xml style.

<Map background-color="transparent" srs="+init=epsg:3857">                      
  <Style name="style_agency_geometry">                                          
    <Rule>                                                                      
      <LineSymbolizer stroke="#f03b20" stroke-opacity="0.75" stroke-width="3." />
    </Rule>                                                                     
  </Style>                                                                      

  <Layer name="my_layer" srs="+init=epsg:4326">                                 
    <StyleName>style_agency_geometry</StyleName>                                
    <StyleName>text_style</StyleName>                                           
    <Datasource>                                                                
      <Parameter name="type">???</Parameter>                                    
    </Datasource>                                                               
  </Layer>                                                                      
</Map>
springmeyer commented 7 years ago

I think you are on the right path trying to do as much in XML as possible, since the python API is not viable for accessing all styling options or for adding in-memory data. Like I've posted at https://github.com/mapnik/python-mapnik/issues/126#issuecomment-326819222 I would recommend using python to form up an XML file that includes an inline geojson string. Fully create the XML with the geojson data and the styling you need, then call load_map.

mrterry commented 7 years ago

Thanks! This is the route we ended up taking.

We had to do a bit of refactoring to remove all the Python-level conditionals from our styling and replace them with Mapnik styling rules. We then used mapnik.save_map() to create the initial XML file. I couldn't get the ogr/geojson datasource to work inline in Mapnik 2.2, but inline geojson/geojson datasource works just fine in Mapnik 3.

Perf with the inline-xml geojson datasource seems asymptotically little faster than the wkb PythonMemory datasource. Our 95th percentile latency dropped about 20ms after the switch. Lots of other things were happening at the time (take with a grain of salt, YMMV, IANAL, etc), but even with the extra serialize-to-a-geojson-string + mapnik-parsing-that-string, perf is no worse.

daumann commented 7 years ago

@springmeyer what is the recommended approach to add a mvt datasource with a xml stylesheet for styling? Decoding it to inline geojson is unfortunately not performant enough.

springmeyer commented 7 years ago

@daumann Right, I would not recommend decoding a vector tile into inline geojson in order render with mapnik. Vector Tiles are designed to be much more efficient and lower memory to parse than GeoJSON so that would defeat the purpose of them. Mapnik has a datasource.next() iterator that Mapnik's internal rendering processor calls. The optimal design has just the minimal part of a vector tile decoded per call to datasource.next(). This support is implemented in node-mapnik via node-mapnik's integration of https://github.com/mapbox/mapnik-vector-tile. The API works like:

1) Create Vector Tile 2) Create Mapnik XML 3) Pass XML to Vector tile be be rendered (http://mapnik.org/documentation/node-mapnik/3.6/#VectorTile.render)

But this is not supported in python-mapnik. The reason I've not implemented it is that me and my team at Mapbox are exclusively using nodejs not python.

I recommend anyone wanting the performance of vector tiles either use node-mapnik, or if you are interested in writing C++, you could have a go at adding this functionality to the python-mapnik bindings. There are also other libraries, besides mapnik, that can render Vector Tiles, like https://github.com/mapbox/mapbox-gl-native which can be bound from a variety of languages. See https://github.com/mapbox/awesome-vector-tiles/ for more details.