Now we will prepare an environment that is capable of using this data source. We are going to adapt the previous experiments into building blocks for the application and put them inside an application class, just as we did with the Geocaching app.
First let's organize the folder and files.
map_maker
inside your Chapter5
folder. To do this, right-click on the folder and chose New | Python Package.my_datasource.py
file to the map_make
folder (drag and drop it).map_style.xml
and map_functions.py
files that are inside the mapnik_experiments
folder to the map_maker
folder.map_style.xml
to styles.xml
.Chapter5
root, create a file named map_maker_app.py
. The complete tree structure should look like this:Chapter5 │ geocaching_app.py | map_maker_app.py │ models.py │ ├───mapnik_experiments │ ├───map_maker │ __init__.py │ my_datasource.py | styles.xml | map_functions.py │ ├───utils
Now we create the class that represents the application.
map_maker_app.py
file, create this new class and its __init__
method:# coding=utf-8 import cv2 import mapnik class MapMakerApp(object): def __init__(self, output_image=None): """Application class.""" self.output_image = output_image
output_image
will be the image that the app will write to the maps. It's not private because we may want to change it during the application execution.
display_map
function from the map_functions.py
file, and adapt it to work as a method of our new class:class MapMakerApp(object): def __init__(self, output_image=None): """Application class.""" self.output_image = output_image def display_map(self): """Opens and displays a map image file. :param image_file: Path to the image. """ image = cv2.imread(self.output_image) cv2.imshow('image', image) cv2.waitKey(0) cv2.destroyAllWindows()
This function now uses the output_image
property to display the map and takes no arguments apart from the class instance (self
) when called.
Next, let's work on the create_map
function.
create_map
function from the map_functions.py
file and make the following changes to the class:# coding=utf-8 import cv2 import mapnik class MapMakerApp(object): def __init__(self, output_image="map.png", style_file="map_maker/styles.xml", map_size=(800, 600)): """Application class. :param output_image: Path to the image output of the map. :param style_file: Mapnik XML file containing only the style for the map. :param map_size: Size of the map in pixels. """ self.output_image = output_image self.style_file = style_file self.map_size = map_size def display_map(self): """Opens and displays a map image file.""" image = cv2.imread(self.output_image) cv2.imshow('image', image) cv2.waitKey(0) cv2.destroyAllWindows() def create_map(self): """Creates a map and writes it to a file.""" map = mapnik.Map(*self.map_size) mapnik.load_map(map, self.style_file) layers = map.layers map.zoom_all() mapnik.render_to_file(map, self.output_image)
As we did with display_map
, now the create_map
function takes no arguments (except for self
) and all parameters come from the instance attributes, the ones that were added to the __init__
method. We also improved the default values for those arguments.
All the layer and data source definitions were removed from create_map
because in the next steps we will plug in the PythonDatasource
that we created earlier.
To use this type of data source and implement the ability to display any number of data sources on the map, we will make our app class take control of the organization of the layers and the data that they use, always following the premise that the application should have a high level of abstraction:
from map_maker.my_datasource import MapDatasource
__init__
method and create an add_layer
method, as follows:class MapMakerApp(object): def __init__(self, output_image="map.png", style_file="map_maker/styles.xml", map_size=(800, 600)): """Application class. :param output_image: Path to the image output of the map. :param style_file: Mapnik XML file containing only the style for the map. :param map_size: Size of the map in pixels. """ self.output_image = output_image self.style_file = style_file self.map_size = map_size self._layers = {} def display_map(self):... def create_map(self):... def add_layer(self, geo_data, name, style='style1'): """Add data to the map to be displayed in a layer with a given style. :param geo_data: a BaseGeoCollection subclass instance. """ data source = mapnik.Python(factory='MapDatasource', data=geo_data) layer = {"data source": data source, "data": geo_data, "style": style} self._layers[name] = layer
What we did here is use a private attribute (_layers
) to keep track of the layers that we will use by their names. The add_layer
method is responsible for instantiating the MapDatasource
class and passing to it the data.
The data that we will use here is a subclass of BaseGeoCollection
that we used in the previous chapters. With this, we will manipulate the map using only high-level objects, and also get all their functionality for free.
As we said before, Python Datasource does not work on Windows, so we need to create a workaround to make things work despite the operating system. What we are going to do is save the data to a temporary file and then use Mapnik's GeoJSON plugin to create a data source.
# coding=utf-8 import platform import tempfile from models import BoundaryCollection, PointCollection import cv2 import mapnik
temp
inside your Chapter5
folder.add_layer
method to include the workaround:#... def add_layer(self, geo_data, name, style='style1'): """Add data to the map to be displayed in a layer with a given style. :param geo_data: a BaseGeoCollection subclass instance. """ if platform.system() == "Windows": print("Windows system") temp_file, filename = tempfile.mkstemp(dir="temp") print temp_file, filename geo_data.export_geojson(filename) data source = mapnik.GeoJSON(file=filename) else: data source = mapnik.Python(factory='MapDatasource', data=geo_data) layer = {"data source": data source, "data": geo_data, "style": style} self._layers[name] = layer
Here, we used platform.system()
to detect whether the operating system is Windows. If so, instead of creating a Python DataSource, it creates a temporary file and exports geo_data
to it. Then we use the GeoJSON plugin to open that file, creating a DataSource.
Now that the workaround is complete, we need to go back to the MapDatasource
definition and make it accept the data that we are passing to it.
my_datasource.py
file, include the following __init__
method in the MapDatasource
class:class MapDatasource(mapnik.PythonDatasource): """Implementation of Mapinik's PythonDatasource.""" def __init__(self, data): super(MapDatasource, self).__init__(envelope, geometry_type, data_type) self.data = data def features(self, query=None): raise NotImplementedError
Our subclass of PythonDatasource
now takes one obligatory data
argument. Since we are increasing the level of abstraction, we will make the MapDatasource
class define all the other arguments automatically by inspecting the data it receives; with this change, we won't need to worry about the geometry type or data type.
__init__
method:class MapDatasource(mapnik.PythonDatasource): """Implementation of Mapinik's PythonDatasource.""" def __init__(self, data): data_type = mapnik.DataType.vector if isinstance(data, PointCollection): geometry_type = mapnik.GeometryType.Point elif isinstance(data, BoundaryCollection): geometry_type = mapnik.GeometryType.Polygon else: raise TypeError super(MapDatasource, self).__init__( envelope=None, geometry_type=geometry_type, data_type=data_type) self.data = data def features(self, query=None): raise NotImplementedError
Here, isinstance()
checks which type is data
, and for each of the possible types it defines the corresponding geometry_type
to be passed to the parent __init__
method.
For now, we only have one data type: the vector. Anyway, we will make this definition explicit (data_type = mapnik.DataType.vector
) because in the next chapter, the raster type will be introduced.
Before we go any further, let's test the app as it is.
if __name__ == '__main__':
block at the end of the file:if __name__ == '__main__': world_borders = BoundaryCollection( "../data/world_borders_simple.shp") map_app = MapMakerApp() map_app.add_layer(world_borders, 'world') map_app.create_map() map_app.display_map()
File imported: ../data/world_borders_simple.shp Windows system File exported: geopyChapter5 emp mpfqv9ch
The map is empty because two points are still missing: the features
method, which is the glue between our geo data and the Mapnik data source, and making the create_map
function use the layers that we have defined.
create_map
method. Change its code so it can iterate over our layers and add them to the map:#... def create_map(self): """Creates a map and writes it to a file.""" map = mapnik.Map(*self.map_size) mapnik.load_map(map, self.style_file) layers = map.layers for name, layer in self._layers.iteritems(): new_layer = mapnik.Layer(name) new_layer.datasource = layer["data source"] new_layer.stylers.append(layer['style']) layers.append(new_layer) map.zoom_all() mapnik.render_to_file(map, self.output_image)
styles.xml
in order to remove the extent restriction from it:<Map background-color="white"> <Style name="style1"> <Rule> <PolygonSymbolizer fill="#f2eff9" /> <LineSymbolizer stroke="red" stroke-width="1.0" smooth="0.5" /> <TextSymbolizer face-name="DejaVu Sans Book" size="10" fill="black" halo-fill= "white" halo-radius="1" placement="interior" allow-overlap="false">[NAME] </TextSymbolizer> </Rule> </Style> <Style name="style2"> <Rule> <PointSymbolizer file="marker.svg" transform="scale(0.3)"/> </Rule> </Style> </Map>
Traceback (most recent call last): File … in <module> raise NotImplementedError NotImplementedError Process finished with exit code 1
If you got this exception (in Linux), it is because everything went fine and Mapnik called our unimplemented features method.
So now let's implement this method.
my_datasource.py
file and edit our class:class MapDatasource(mapnik.PythonDatasource): """Implementation of Mapinik's PythonDatasource.""" def __init__(self, data): data_type = mapnik.DataType.Vector if isinstance(data, PointCollection): geometry_type = mapnik.GeometryType.Point elif isinstance(data, BoundaryCollection): geometry_type = mapnik.GeometryType.Polygon else: raise TypeError super(MapDatasource, self).__init__( envelope=None, geometry_type=geometry_type, data_type=data_type) self.data = data def features(self, query=None): keys = ['name',] features = [] for item in self.data.data: features.append([item.geom.wkb, {'name': item.name}]) return mapnik.PythonDatasource.wkb_features(keys, features)
Since the BaseGeoCollection
class has filtering capabilities that were implemented before, it's possible to filter the data before passing it to the map.
Let's try some examples:
map_maker_app.py
file, edit the if __name__ == '__main__':
block:if __name__ == '__main__': world_borders = BoundaryCollection( "../data/world_borders_simple.shp") my_country = world_borders.filter('name', 'Brazil') map_app = MapMakerApp() map_app.add_layer(my_country, 'countries') map_app.create_map() map_app.display_map()
Here, we are using the filter
function of the BaseGeoCollection
class to filter the countries by name; feel free to try to filter by your country.
if __name__ == '__main__': world_borders = BoundaryCollection( "../data/world_borders_simple.shp") countries = world_borders.filter('name', 'China') + world_borders.filter('name', 'India') + world_borders.filter('name', 'Japan') map_app = MapMakerApp() map_app.add_layer(countries, 'countries') map_app.create_map() map_app.display_map()
3.145.125.51