Creating the Map Maker app

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.

  1. Create a new package called map_maker inside your Chapter5 folder. To do this, right-click on the folder and chose New | Python Package.
  2. Move the my_datasource.py file to the map_make folder (drag and drop it).
  3. Copy the map_style.xml and map_functions.py files that are inside the mapnik_experiments folder to the map_maker folder.
  4. Rename map_style.xml to styles.xml.
  5. In the 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.

  6. In the 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.

  7. Copy the 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.

  8. Copy the 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.

Using PythonDatasource

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:

  1. Include this import at the beginning of the file:
    from map_maker.my_datasource import MapDatasource
  2. Modify the class __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.

  3. Add these imports to the beginning of the file:
    # coding=utf-8
    
    import platform
    import tempfile
    from models import BoundaryCollection, PointCollection
    import cv2
    import mapnik
  4. Now let's create a folder to hold our temporary files. Create a new folder named temp inside your Chapter5 folder.
  5. Modify the 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.

  6. In the 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.

  7. Make another change to the __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.

  8. Now edit the 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()

    Note

    Note how Mapnik is completely abstracted; we now only deal with the high-level functionality provided by our models and the app.

  9. Run the code; you should see an empty map and an output like this in the console:
    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.

  10. Let's start with the 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)
  11. Now edit 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>
  12. Now run the code again and look at the output. If you are using Windows, you should see a rendered map. If you are using Linux you should get an exception:
    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.

  13. Go to the 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)
  14. Run the code again; now you should see the rendered map in the output:
    Using PythonDatasource

Using the app with filtering

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:

  1. In the 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.

  2. Run the code and you should see a map containing only one country (zoom should be active), as in the following screenshot:
    Using the app with filtering
  3. Now try combining filters to show more than one 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()
  4. Run the code again and see the results.
    Using the app with filtering
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.136.17.12