156a7590f4
This plugin allows you to write data sources in the Python programming language. This is useful if you want to rapidly prototype a plugin, perform some custom manipulation on data or if you want to bind mapnik to a datasource which is most conveniently accessed through Python. The plugin may be used from the existing mapnik Python bindings or it can embed the Python interpreter directly allowing it to be used from C++, XML or even JavaScript. Mapnik already has excellent Python bindings but they only directly support calling *into* mapnik *from* Python. This forces mapnik and its input plugins to be the lowest layer of the stack. The role of this plugin is to allow mapnik to call *into* Python itself. This allows mapnik to sit as rendering middleware between a custom Python frontend and a custom Python datasource. This increases the utility of mapnik as a component in a larger system. There already exists MemoryDatasource which can be used to dynamically create geometry in Python. It suffers from the problem that it does not allow generating only the geometry which is seen by a particular query. Similarly the entire geometry must exist in memory before rendering can progress. By using a custom iterator object or by using generator expressions this plugin allows geometry to be created on demand and to be destroyed after use. This can have a great impact on memory efficiency. Since geometry is generated on-demand as rendering progresses there can be arbitrarily complex 'cleverness' optimising the geometry generated for a particular query. Obvious examples of this would be generating only geometry within the query bounding box and generating geometry with an appropriate level of detail for the output resolution.
158 lines
5.4 KiB
Python
158 lines
5.4 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import os
|
|
import math
|
|
import mapnik
|
|
import sys
|
|
from utilities import execution_path
|
|
from nose.tools import *
|
|
|
|
try:
|
|
from shapely.geometry import Point
|
|
have_shapely = True
|
|
except ImportError:
|
|
print('Shapely is required for python data source test.')
|
|
have_shapely = False
|
|
|
|
def setup():
|
|
# All of the paths used are relative, if we run the tests
|
|
# from another directory we need to chdir()
|
|
os.chdir(execution_path('.'))
|
|
|
|
class PointDatasource(mapnik.PythonDatasource):
|
|
def __init__(self):
|
|
super(PointDatasource, self).__init__(
|
|
envelope = mapnik.Box2d(0,-10,100,110)
|
|
)
|
|
|
|
def features(self, query):
|
|
return mapnik.PythonDatasource.wkb_features(
|
|
keys = ('label',),
|
|
features = (
|
|
( Point(5,6).wkb, { 'label': 'foo-bar'} ),
|
|
( Point(60,50).wkb, { 'label': 'buzz-quux'} ),
|
|
)
|
|
)
|
|
|
|
def box2d_to_shapely(box):
|
|
import shapely.geometry
|
|
return shapely.geometry.box(box.minx, box.miny, box.maxx, box.maxy)
|
|
|
|
class ConcentricCircles(object):
|
|
def __init__(self, centre, bounds, step=1):
|
|
self.centre = centre
|
|
self.bounds = bounds
|
|
self.step = step
|
|
|
|
class Iterator(object):
|
|
def __init__(self, container):
|
|
self.container = container
|
|
|
|
centre = self.container.centre
|
|
bounds = self.container.bounds
|
|
step = self.container.step
|
|
|
|
if centre.within(bounds):
|
|
self.radius = step
|
|
else:
|
|
self.radius = math.ceil(centre.distance(bounds) / float(step)) * step
|
|
|
|
def next(self):
|
|
circle = self.container.centre.buffer(self.radius)
|
|
self.radius += self.container.step
|
|
|
|
# has the circle grown so large that the boundary is entirely within it?
|
|
if circle.contains(self.container.bounds):
|
|
raise StopIteration()
|
|
|
|
return ( circle.wkb, { } )
|
|
|
|
def __iter__(self):
|
|
return ConcentricCircles.Iterator(self)
|
|
|
|
class CirclesDatasource(mapnik.PythonDatasource):
|
|
def __init__(self, centre_x=-20, centre_y=0, step=10):
|
|
super(CirclesDatasource, self).__init__(
|
|
geometry_type=mapnik.DataGeometryType.Polygon
|
|
)
|
|
|
|
# note that the plugin loader will set all arguments to strings and will not try to parse them
|
|
centre_x = int(centre_x)
|
|
centre_y = int(centre_y)
|
|
step = int(step)
|
|
|
|
self.centre_x = centre_x
|
|
self.centre_y = centre_y
|
|
self.step = step
|
|
|
|
def features(self, query):
|
|
# Get the query bounding-box as a shapely bounding box
|
|
bounding_box = box2d_to_shapely(query.bbox)
|
|
centre = Point(self.centre_x, self.centre_y)
|
|
|
|
return mapnik.PythonDatasource.wkb_features(
|
|
keys = (),
|
|
features = ConcentricCircles(centre, bounding_box, self.step)
|
|
)
|
|
|
|
if 'python' in mapnik.DatasourceCache.instance().plugin_names() and have_shapely:
|
|
# make sure we can load from ourself as a module
|
|
sys.path.append(execution_path('.'))
|
|
|
|
def test_python_point_init():
|
|
ds = mapnik.Python(factory='python_plugin_test:PointDatasource')
|
|
e = ds.envelope()
|
|
|
|
assert_almost_equal(e.minx, 0, places=7)
|
|
assert_almost_equal(e.miny, -10, places=7)
|
|
assert_almost_equal(e.maxx, 100, places=7)
|
|
assert_almost_equal(e.maxy, 110, places=7)
|
|
|
|
def test_python_circle_init():
|
|
ds = mapnik.Python(factory='python_plugin_test:CirclesDatasource')
|
|
e = ds.envelope()
|
|
|
|
assert_almost_equal(e.minx, -180, places=7)
|
|
assert_almost_equal(e.miny, -90, places=7)
|
|
assert_almost_equal(e.maxx, 180, places=7)
|
|
assert_almost_equal(e.maxy, 90, places=7)
|
|
|
|
def test_python_circle_init_with_args():
|
|
ds = mapnik.Python(factory='python_plugin_test:CirclesDatasource', centre_x=40, centre_y=7)
|
|
e = ds.envelope()
|
|
|
|
assert_almost_equal(e.minx, -180, places=7)
|
|
assert_almost_equal(e.miny, -90, places=7)
|
|
assert_almost_equal(e.maxx, 180, places=7)
|
|
assert_almost_equal(e.maxy, 90, places=7)
|
|
|
|
def test_python_point_rendering():
|
|
m = mapnik.Map(512,512)
|
|
mapnik.load_map(m,'../data/good_maps/python_point_datasource.xml')
|
|
m.zoom_all()
|
|
im = mapnik.Image(512,512)
|
|
mapnik.render(m,im)
|
|
actual = '/tmp/mapnik-python-point-render1.png'
|
|
expected = 'images/support/mapnik-python-point-render1.png'
|
|
im.save(actual)
|
|
expected_im = mapnik.Image.open(expected)
|
|
eq_(im.tostring(),expected_im.tostring(),
|
|
'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
|
|
|
|
def test_python_circle_rendering():
|
|
m = mapnik.Map(512,512)
|
|
mapnik.load_map(m,'../data/good_maps/python_circle_datasource.xml')
|
|
m.zoom_all()
|
|
im = mapnik.Image(512,512)
|
|
mapnik.render(m,im)
|
|
actual = '/tmp/mapnik-python-circle-render1.png'
|
|
expected = 'images/support/mapnik-python-circle-render1.png'
|
|
im.save(actual)
|
|
expected_im = mapnik.Image.open(expected)
|
|
eq_(im.tostring(),expected_im.tostring(),
|
|
'failed comparing actual (%s) and expected (%s)' % (actual,'tests/python_tests/'+ expected))
|
|
|
|
if __name__ == "__main__":
|
|
setup()
|
|
[eval(run)() for run in dir() if 'test_' in run]
|