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.
Maps with Django (2 part series)
- Maps with Django⁽¹⁾: GeoDjango, SpatiaLite & Leaflet
- 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:
- GeoDjango, the Django geographic module
- SpatiaLite, the SQLite spatial extension
- Leaflet, a JavaScript library for interactive maps
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” is one of the most used JavaScript libraries for web maps
- It’s a Free Software
- It’s desktop and mobile-friendly
- “Leaflet” is very light (~42 KB of gzipped JS)
- It has a very good documentation
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.
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:
- Spatialite backend (v1.1 ~2009)
- Multiple backends (v1.2 ~2010)
- OpenLayers-based widgets (v1.6 ~2013)
- GeoJSON serializer (v1.8 ~2015)
- GeoIP2 Geolocation (v1.9 ~2016)
Before activating it we need to install some requirements.
GDAL
A mandatory GeoDjango requirement is GDAL.
- It’s an OS/Geo library for reading and writing raster and vector geospatial data formats,
- It’s released with a free software license
- It has a variety of useful command lines for data translation and processing.
Installing GDAL
We need to install the GDAL
(Geospatial Data Abstraction Library) ^:
- on Debian-based GNU/Linux distributions (e.g. Debian 10-12, Ubuntu 20.04-23.04, …):
$ 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 is a SQLite extension, and it’s only one of the database backends for GeoDjango.
Installing SpatiaLite
To use `SpatiaLite“ as a database backend, we need to install it ^:
- on Debian-based GNU/Linux distributions (e.g. Debian 10-12, Ubuntu 20.04-23.04, …):
$ 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”
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!
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