Maps with Django⁽¹⁾: GeoDjango, SpatiaLite & Leaflet

A quickstart guide to create a web map with the Python-based web framework Django using its module GeoDjango, the SQLite database with its spatial extension SpatiaLite and Leaflet, a JavaScript library for interactive maps.

© 2020 Paolo Melchiorre “Photo at the dawn of Monte Amaro trail sign in Abruzzo, Italy”
© 2020 Paolo Melchiorre “Photo at the dawn of Monte Amaro trail sign in Abruzzo, Italy”
Maps with Django (2 part series)
  1. Maps with Django⁽¹⁾: GeoDjango, SpatiaLite & Leaflet
  2. Maps with Django⁽²⁾: GeoDjango, PostGIS & Leaflet

Abstract

Keeping in mind the Pythonic principle that “simple is better than complex” we’ll see how to create a web map with the Python-based web framework Django using its GeoDjango module, storing geographic data in a SQLite database on which to run geospatial queries with SpatiaLite.

Introduction

A map in a website is the best way to make geographic data easily accessible to users because it represents, in a simple way, information relating to a specific geographical area and is used by many online services.

Implementing a web map can be complex and many adopt the strategy of using external services, but in most cases, this strategy turns out to be a major data and cost management problem.

In this guide we’ll see how to create a web map with the Python-based web framework Django using its GeoDjango module, storing geographic data in your file-based database on which to run geospatial queries.

Through this article, you can learn how to add on your website a complex and interactive web map based on this software:

Requirements

The requirements to create our map with Django are:

Python

A stable and supported version of Python 3 (tested with Python 3.8-3.11):

$ python3 --version
Python 3.11.4

Virtual environment

A Python virtual environment:

$ python3 -m venv ~/.mymap
$ source ~/.mymap/bin/activate

Django

The latest stable version of Django (tested with Django 3.1-4.2):

$ python3 -m pip install django~=4.2

Creating the mymap project

To create the mymap project I’ll switch to my projects directory:

$ cd ~/projects

and then use the startproject Django command:

$ python3 -m django startproject mymap

The basic files of our project will be created in the mymap directory:

$ tree --noreport mymap/
mymap/
├── manage.py
└── mymap
    ├── asgi.py
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

Creating the markers app

After switching to the mymap directory:

$ cd mymap

We can create our markers app with the Django startapp command:

$ python3 -m django startapp markers

Again, all the necessary files will be created for us in the markers directory:

$ tree --noreport markers/
markers/
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

Activating the markers app

Now, we have to activate our markers application by inserting its name in the list of the INSTALLED_APPS in the mymap settings file.

mymap/mymap/settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "markers",
]

Adding an empty web map

We’re going to add an empty web to the app:

Adding a template view

At this point, we can proceed to insert, in the views.py file, a new TemplateView for the page of our map.

mymap/markers/views.py

from django.views.generic.base import (
    TemplateView,
)


class MarkersMapView(TemplateView):
    template_name = "map.html"

Adding the map template

After switching to the markers directory:

$ cd markers

We have to add a templates/ directory in markers/:

$ mkdir templates

In the markers templates directory, we can now create a map.html template file for our map.

mymap/markers/templates/map.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Markers Map</title>
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
  </head>
  <body></body>
</html>

For now. we added only the usual boilerplate with a title but without body content.

Adding markers URLs

In the markers URL file, we must now add the path to view our map, using its template view.

mymap/markers/urls.py

from django.urls import path

from markers.views import MarkersMapView

app_name = "markers"

urlpatterns = [
    path(
        "map/", MarkersMapView.as_view()
    ),
]

Updating mymap URLs

As a last step, we include in turn the URL file of the marker app in that of the project.

mymap/mymap/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path(
        "markers/",
        include("markers.urls"),
    ),
]

We just made a first view in Django, but it will show a blank page.

Apply initial migration

Before starting our project for the first time we have to apply initial migration:

$ python3 -m manage migrate

Testing the blank map page

You can test the blank map page by running this command:

$ python3 -m manage runserver

Now that the server’s running, visit http://127.0.0.1:8000/markers/map/ with your Web browser.

You can open the browser at a specific URL using Python:

$ python -m webbrowser \
http://127.0.0.1:8000/markers/map/

You’ll see a working blank map page.

We can now let’s move on to something more challenging.

Leaflet

Leaflet logo
Leaflet logo

Updating the map template

To use Leaflet, we need to link its JavaScript and CSS modules in our template. We also need a DIV tag with map as ID.

In addition, using the “static” template tag, we’ll also link our custom JavaScript and CSS files, which we’ll now create.

mymap/markers/templates/map.html

{% load static %}
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Markers Map</title>
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <link
      rel="stylesheet"
      type="text/css"
      href="{% static 'map.css' %}"
    />
    <link
      rel="stylesheet"
      type="text/css"
      href="https://unpkg.com/leaflet/dist/leaflet.css"
      crossorigin=""
    />
    <script
      src="https://unpkg.com/leaflet/dist/leaflet.js"
      crossorigin=""
    ></script>
  </head>
  <body>
    <div id="map"></div>
    <script
      src="{% static 'map.js' %}"
      defer
    ></script>
  </body>
</html>

Creating the static directory

We have to add a static/ directory in markers/:

$ mkdir static

Adding the map CSS

We add our map.css file in the static directory and, inside it, we add only the basic rules to show a full-screen map.

mymap/markers/static/map.css

html,
body {
  height: 100%;
  margin: 0;
}
#map {
  height: 100%;
  width: 100%;
}

Adding the map JavaScript

In our map.js file we add the code to view our map.

mymap/markers/static/map.js

const copy =
  "&copy; <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a>";
const url =
  "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const layer = L.tileLayer(url, {
  attribution: copy,
});
const map = L.map("map", {
  layers: [layer],
});
map.fitWorld();

Using the defined variables, we initialize an OpenStreetMap layer and hook it to our map.

The last statement sets a map view, that mostly contains the whole world, with the maximum zoom level possible.

Show the empty web map

We can now start our Django project with the runserver command

$ python3 -m manage runserver

Now that the server’s running, visit http://127.0.0.1:8000/markers/map/ with your Web browser. You’ll see a “Markers map” page, with a full-page map. It worked!

We just created an empty map with Django and the result is pretty much what you see now.

An empty web map
A map, without markers, showing the whole world.

GeoDjango

It’s time to get to know and activate GeoDjango, the Django geographic module.

Django added geographic functionality a few years ago (v1.0 ~2008), in the django.contrib.gis, with specific fields, multiple database backends, spatial queries, and also admin integration.

Since then, many new useful features have been added every year, until the latest version:

Before activating it we need to install some requirements.

GDAL

A mandatory GeoDjango requirement is GDAL.

GDAL logo
GDAL logo

Installing GDAL

We need to install the GDAL (Geospatial Data Abstraction Library) ^:

$ sudo apt install gdal-bin

^ For other platform-specific instructions read the Django documentation.

Activating GeoDjango

We can now activate GeoDjango by adding the django.contrib.gis module to the INSTALLED_APPS, in our project settings.

mymap/mymap/settings.py

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.gis",
    "markers",
]

SpatiaLite

And now let’s start using SpatiaLite as a new backend engine.

SpatiaLite logo
SpatiaLite logo

Installing SpatiaLite

To use `SpatiaLite“ as a database backend, we need to install it ^:

$ sudo apt install \
libsqlite3-mod-spatialite

^ For other platform-specific instructions read the Django documentation.

Activating SpatiaLite

We’ll modify the project database settings, adding the SpatiaLite engine and leaving the other parameters as default:

mymap/mymap/settings.py

DATABASES = {
    "default": {
        "ENGINE": "django.contrib.gis.db.backends.spatialite",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

Adding some markers

Now we can add some markers to the map.

Adding the Marker model

We can now define our Marker model to store a location and a name.

mymap/markers/models.py

from django.contrib.gis.db import models


class Marker(models.Model):
    name = models.CharField(
        max_length=255
    )
    location = models.PointField()

    def __str__(self):
        return self.name

Our two fields are both mandatory, the location is a simple PointField, and we’ll use the name to represent the model.

Updating the database

We can now generate a new database migration and then apply it to our database.

$ python3 -m manage makemigrations
$ python3 -m manage migrate

Activating the Marker admin

To easily insert new markers in the map we use the Django admin interface.

mymap/markers/admin.py

from django.contrib.gis import admin

from markers.models import Marker


@admin.register(Marker)
class MarkerAdmin(admin.GISModelAdmin):
    list_display = ("name", "location")

We define a Marker admin class, by inheriting the GeoDjango admin class, which uses the OpenStreetMap layer in its widget.

Testing the admin

We have to create an admin user to log in and test it:

$ python3 -m manage createsuperuser

Now you can test the admin running this command:

$ python3 -m manage runserver

Now that the server’s running, visit http://127.0.0.1:8000/admin/markers/marker/add/ with your Web browser. You’ll see a “Markers” admin page, to add new markers with a map widget. I added a marker to the latest peak I climbed: “Monte Amaro 2793m”

Adding a marker in the admin page.

Adding a marker in the admin page.

Note: In this version of the Marker admin you have to manually navigate on the map and pin the marker to your desired location.

Showing markers on the map

Adding all markers in the view

We can add with a serializer all markers as a GeoJSON in the context of the MarkersMapView in markers/views.py:

import json

from django.core.serializers import (
    serialize,
)
from django.views.generic.base import (
    TemplateView,
)

from markers.models import Marker


class MarkersMapView(TemplateView):
    template_name = "map.html"

    def get_context_data(
        self, **kwargs
    ):
        context = (
            super().get_context_data(
                **kwargs
            )
        )
        context["markers"] = json.loads(
            serialize(
                "geojson",
                Marker.objects.all(),
            )
        )
        return context

The value of the markers key in the context dictionary we’ll something like this:

{
  "type": "FeatureCollection",
  "crs": {
    "type": "name",
    "properties": {
      "name": "EPSG:4326"
    }
  },
  "features": [
    {
      "id": 1,
      "type": "Feature",
      "properties": {
        "name": "Monte Amaro 2793m",
        "pk": "1"
      },
      "geometry": {
        "type": "Point",
        "coordinates": [
          14.085911, 42.086332
        ]
      }
    }
  ]
}

Inserting the GeoJSON in the template

Using json_script built-in filter we can safely output the Python dictionary with all markers as GeoJSON in markers/templates/map.html:

{% load static %}
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Markers Map</title>
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <link
      rel="stylesheet"
      type="text/css"
      href="{% static 'map.css' %}"
    />
    <link
      rel="stylesheet"
      type="text/css"
      href="https://unpkg.com/leaflet/dist/leaflet.css"
      crossorigin=""
    />
    <script
      src="https://unpkg.com/leaflet/dist/leaflet.js"
      crossorigin=""
    ></script>
  </head>
  <body>
    {{
    markers|json_script:"markers-data"
    }}
    <div id="map"></div>
    <script src="{% static 'map.js' %}"></script>
  </body>
</html>

Rendering all markers in the map

We can render the GeoJSON with all markers in the web map using Leaflet in markers/static/map.js:

const copy =
  "&copy; <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a>";
const url =
  "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const layer = L.tileLayer(url, {
  attribution: copy,
});
const map = L.map("map", {
  layers: [layer],
});
// map.fitWorld();
const markers = JSON.parse(
  document.getElementById(
    "markers-data"
  ).textContent
);
let feature = L.geoJSON(markers)
  .bindPopup(function (layer) {
    return layer
      .feature.properties.name;
  })
  .addTo(map);
map.fitBounds(feature.getBounds(), {
  padding: [100, 100],
});

Testing the populated map

And finally here is our complete map.

I populated the map with other markers of the highest or lowest points I’ve visited in the world to show them on my map.

You can test the populated web map by running this command:

$ python3 -m manage runserver

Now that the server’s running, visit http://127.0.0.1:8000/markers/map/ with your Web browser. You’ll see the “Markers map” page, with a full-page map and all the markers. It worked!

A map with some markers
A map with some markers of the highest or lowest points I’ve visited in the world with the opened popup of the latest peak I climbed.

Curiosity

If you want to know more about my latest hike to the Monte Amaro peak you can see it on my Wikiloc account: Round trip hike from Rifugio Pomilio to Monte Amaro 🔗.

Conclusion

We have shown an example of a fully functional map, trying to use the least amount of software, without using external services.

This map is enough to show a few points in a simple project using SQLite and Django templates.

In future articles we will see how to make this map even more advanced using Django Rest Framework, PostGIS, etc … to render very large numbers of markers in an even more dynamic way.

Stay tuned.

— Paolo


Resources


Updates

  • 2023-09-12: update dependencies version and supported system libraries

  • 2023-02-03: update dependencies version and reformat sections

  • 2021-03-09: added missing “Installing GDAL section

  • 020-12-09: added missing “Activating the Markers app” section and fixed some typos