Usage

A vector tile is composed by vector layers which represent different kind of data. Each layer is composed by features.

To start using django-vectortiles, you need GeoDjango models with geometries.

Then, you need to describe how your data will be embed in tiles.

Start by creating vector layers for your data…

# in your app models.py
from django.contrib.gis.db import models


class City(models.Model):
    name = models.CharField(max_length=250)
    city_code = models.CharField(max_length=10, unique=True)
    population = models.IntegerField(default=0)
    geom = models.MultiPolygonField(srid=4326)


# in a vector_layers.py file (for example)
from vectortiles import VectorLayer
from your_app.models import City


class CityVectorLayer(VectorLayer):
    model = City  # your model, as django conventions you can use queryset or get_queryset method instead)
    id = "cities"  # layer id in you vector layer. each class attribute can be defined by get_{attribute} method
    tile_fields = ('city_code', "name") # fields to include in tile
    min_zoom = 10 # minimum zoom level to include layer. Take care of this, as it could be a performance issue. Try to not embed data that will no be shown in your style definition.
    # all attributes available in vector layer definition can be defined

Well. your vector layer is ready. next step is to create a tile class and a view to serve it.

Simple layer tile view

# in your view file
from your_app.vector_layers import CityVectorLayer
from your_app.views import MVTView


class CityTileView(MVTView):
    layer_classes = [CityVectorLayer, CityCentroidVectorLayer]  # you can use get_layer_classes method, or directly get_layers instead

# in your urls file
from django.urls import path
from yourapp import views

urlpatterns = [
    ...
    views.CityTileView.get_url(),  # serve tiles at default /tiles/<int:z>/<int:x>/<int:y>
    ...
]

Multiple layer tile view

As vector tile layer permit it, you can embed multiple layers in your tile.

Let’s create a second layer.

# in your app models.py
class State(models.Model):
    name = models.CharField(max_length=250)
    state_code = models.CharField(max_length=10, unique=True)
    geom = models.MultiPolygonField(srid=4326)

# in vector_layers.py file
class StateVectorLayer(VectorLayer):
    model = State
    id = "states"
    tile_fields = ('state_code', "name")
    min_zoom = 3

# in your view file
class CityAndStateTileView(MVTView):
    layer_classes = [CityVectorLayer, StateVectorLayer]

# in your urls file
urlpatterns = [
    ...
    views.CityAndStateTileView.get_url(),  # serve tiles at default /tiles/<int:z>/<int:x>/<int:y>
    ...
]

Using TileJSON

It’s a good practice to use tilejson to tell to your map library how to gt your tiles and their defintion. django-vectortiles permit that.

TileJSON and tile views share some data, as vactor layers definition. So we need to factorize some things.

# in your view file

class CityAndStateBaseLayer:
    # mixin for your two views
    layer_classes = [CityVectorLayer, StateVectorLayer]
    prefix_url = 'city-and-states'  # as tilejson need to known tiles URL, we need to define a url prefix for our tiles

class CityAndStateTileView(CityAndStateBaseLayer, MVTView):
    pass


class CityAndStateTileJSON(CityAndStateBaseLayer, TileJSONView):
    pass


# in your urls file
urlpatterns = [
    ...
    views.CityAndStateTileView.get_url(),  # serve tiles at /city-and-states/<int:z>/<int:x>/<int:y>
    views.CityAndStateTileJSON.get_url(),  # serve tilejson at /city-and-states/tiles.json
    ...
]

Now you can use your tiles with a map library like MapLibre or Mapbox GL JS, directly wit hthe tileJSON provided.

Warning

By default, it’s your browser URL that will be used to generate tile url in tilejson. Take care about django and SSL configuration (django settings, web server headers) if you want to generate an URL with https://

Note

If your application is hosted on server with many workers, and you want to optimized tile loading, you can add several urls in your tilejson file.

# add in your settings.py file
ALLOWED_HOSTS = [
    "a.tiles.xxxx",
    "b.tiles.xxxx",
    "c.tiles.xxxx",
    ...
]

VECTOR_TILES_URLS = [
    "https://a.tiles.xxxx",
    "https://b.tiles.xxxx",
    "https://c.tiles.xxxx",
]

With these settings, each tilejson file will contain several urls, and your map library will be able to parallel load tiles at time.

More complex multiple layer tile view

You can customize geometry data embed in your tiles.

class CityCentroidVectorLayer(VectorLayer):
    queryset = City.objects.annotate(
        centroid=Centroid("geom"), # compute the city centroïd
        area=Area("geom"), # compute the city area
    )
    geom_field = "centroid"  # use the centroid field as geometry feature
    id = "city_centroids"
    tile_fields = ('name', 'city_code', 'area', 'population')  # add area and population properties in each tile feature
    min_zoom = 7  # let's show city name at zoom 7

Django Rest Framework

# in your views.py file
from vectortiles.rest_framework.renderers import MVTRenderer


class FeatureAPIView(BaseVectorTile, APIView):
    queryset = Feature.objects.all()
    id = "features"
    tile_fields = ('name', )
    queryset_limit = 100
    renderer_classes = (MVTRenderer, )

    def get(self, request, *args, **kwargs):
        return Response(self.get_tile(kwargs.get('x'), kwargs.get('y'), kwargs.get('z')))

# in your urls file
urlpatterns = [
    ...
    path('features/tiles/<int:z>/<int:x>/<int:y>', FeatureAPIView.as_view(),
         name="feature-tile-drf"),
    ...
]

# or extending viewset

class FeatureViewSet(BaseVectorTile, viewsets.ModelViewSet):
    queryset = Feature.objects.all()
    id = "features"
    tile_fields = ('name', )
    queryset_limit = 100

    @action(detail=False, methods=['get'], renderer_classes=(MVTRenderer, ),
            url_path='tiles/(?P<z>\d+)/(?P<x>\d+)/(?P<y>\d+)', url_name='tile')
    def tile(self, request, *args, **kwargs):
        return Response(self.get_tile(x=int(kwargs.get('x')), y=int(kwargs.get('y')), z=int(kwargs.get('z'))))

# in your urls file
router = SimpleRouter()
router.register(r'features', FeatureViewSet, basename='features')

urlpatterns += router.urls

then use http://your-domain/features/tiles/{z}/{x}/{y}.pbf

MapLibre Example

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>MapBox / MapLibre example</title>
    <style>
        html, body {
            margin: 0;
            padding: 0;
        }
    </style>
    <link href='https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css' rel='stylesheet'/>
</head>
<body>
<div id="map" style="width: 100%; height: 100vh"></div>
<script src='https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js'></script>

<script>
    var map = new maplibregl.Map({
        container: 'map',
        hash: true,
        style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
        center: [1.77, 44.498], // starting position [lng, lat]
        zoom: 8 // starting zoom
    });
    var nav = new maplibregl.NavigationControl({visualizePitch: true});
    map.addControl(nav, 'top-right');
    var scale = new maplibregl.ScaleControl({
        maxWidth: 80,
        unit: 'metric'
    });
    map.addControl(scale);
    map.on('load', function () {
        map.addSource('layers', {
            'type': 'vector',
            'url': '{% url "city-tilejson" %}'
        });
        map.addLayer(
            {
                'id': 'background2',
                'type': 'background',
                'paint': {
                    'background-color': '#F8F4F0',
                }

            }
        );
        map.addLayer(
            {
                'id': 'cities',
                'type': 'line',
                'filter': ['==', ['geometry-type'], 'Polygon'],
                'source': 'layers',
                'source-layer': 'cities',
                'layout': {
                    'line-cap': 'round',
                    'line-join': 'round'
                },
                'paint': {
                    'line-opacity': 0.4,
                    'line-color': '#3636a8',
                    'line-width': 0.5,
                    'line-dasharray': [10, 10]
                }

            }
        );

        map.addLayer(
            {
                "id": "city-borders",
                "type": "symbol",
                "source": "layers",
                "source-layer": "cities",
                "minzoom": 13,
                "layout": {
                    "symbol-placement": "line",
                    "symbol-spacing": 350,
                    "text-field": "{nom}",
                    "text-font": ["Noto Sans Italic"],
                    "text-letter-spacing": 0.2,
                    "text-max-width": 5,
                    "text-rotation-alignment": "map",
                    "text-size": 10
                },
                "paint": {
                    "text-color": "#3636a8",
                    "text-halo-color": "rgba(255,255,255,0.7)",
                    "text-halo-width": 1
                }
            }
        );
        map.addLayer(
            {
                "id": "cities_marker",
                "type": "symbol",
                "source": "layers",
                "source-layer": "city-centroids",
                "minzoom": 10,
                "maxzoom": 12,
                "layout": {
                    "symbol-placement": "point",
                    "symbol-spacing": 350,
                    "text-field": "{nom}",
                    "text-font": ["Noto Sans Italic"],
                    "text-letter-spacing": 0.2,
                    "text-max-width": 5,
                    "text-rotation-alignment": "map",
                    "text-size": 14
                },
                "paint": {
                    "text-color": "#3636a8",
                    "text-halo-color": "rgba(255,255,255,0.7)",
                    "text-halo-width": 1.5
                }
            }
        );

        // Create a popup, but don't add it to the map yet.
        var popup = new maplibregl.Popup({
            closeButton: false,
            closeOnClick: false
        });

        map.on('mouseenter', 'cities_marker', function (e) {
            // Change the cursor style as a UI indicator.
            map.getCanvas().style.cursor = 'pointer';
            var coordinates = e.features[0].geometry.coordinates.slice();
            var description = `${e.features[0].properties.name} (${e.features[0].properties.population} hab.)`;

            // Ensure that if the map is zoomed out such that multiple
            // copies of the feature are visible, the popup appears
            // over the copy being pointed to.
            while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
                coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
            }

            // Populate the popup and set its coordinates
            // based on the feature found.
            popup.setLngLat(coordinates).setHTML(description).addTo(map);
        });

        map.on('mouseleave', 'cities_marker', function () {
            map.getCanvas().style.cursor = '';
            popup.remove();
        });
    }
);
</script>
</body>
</html>

Cache policy