Maps with Django (part 1): GeoDjango, SpatiaLite and Leaflet

pauloxnet

Paolo Melchiorre

Posted on December 10, 2020

Maps with Django (part 1): GeoDjango, SpatiaLite and 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 SpaliaLite and Leaflet , a JavaScript library for interactive maps.

Introduction

In this guide we will see how to create a minimal web map using Django (the Python-based web framework), starting from its default project, writing a few lines of code and with the minimum addition of other software:

  • GeoDjango, the Django geographic module

  • SpatiaLite, the SQLite spatial extension

  • Leaflet, a JavaScript library for interactive maps.

Requirements

  • The only python package required is Django.

  • We’ll assume you have Django installed already.

  • This guide is tested with Django 3.1 and Python 3.8.

Creating a project

You need to create the basic project in your workspace directory with this command:

$ django-admin startproject mymap
Enter fullscreen mode Exit fullscreen mode

That’ll create a directory mymap, which is laid out like this:

mymap/
├── manage.py
└── mymap
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
Enter fullscreen mode Exit fullscreen mode

Creating the Markers app

To create your app, make sure you’re in the same directory as manage.py and type this command:

$ python manage.py startapp markers
Enter fullscreen mode Exit fullscreen mode

That’ll create a directory markers, which is laid out like this:

markers/
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py
Enter fullscreen mode Exit fullscreen mode

Activating the Markers app

Modify the INSTALLED_APPS setting

Append markers to the INSTALLED_APPS in mymap/settings.py

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

Adding an empty web map

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

Adding a template view

We have to add a TemplateView in views.py:

"""Markers view."""

from django.views.generic.base import TemplateView


class MarkersMapView(TemplateView):
    """Markers map view."""

    template_name = "map.html"
Enter fullscreen mode Exit fullscreen mode

Adding a map template

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

$ mkdir templates
Enter fullscreen mode Exit fullscreen mode

And a map.html template in markers/templates/:

{% 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@1.7.1/dist/leaflet.css">
  <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
</head>
<body>
  <div id="map"></div>
  <script src="{% static 'map.js' %}"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Adding javascript and css files

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

$ mkdir static
Enter fullscreen mode Exit fullscreen mode

Add a map.css stylesheet in markers/static/:

html, body {
  height: 100%;
  margin: 0;
}
#map {
  width: 100%;
  height: 100%;
}
Enter fullscreen mode Exit fullscreen mode

Add a map.js stylesheet in markers/static/:

const attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
const map = L.map('map')
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: attribution }).addTo(map);
map.fitWorld();
Enter fullscreen mode Exit fullscreen mode

Adding a new URL

Add an urls.py files in markers/:

"""Markers urls."""

from django.urls import path

from .views import MarkersMapView

app_name = "markers"

urlpatterns = [
    path("map/", MarkersMapView.as_view()),
]
Enter fullscreen mode Exit fullscreen mode

Modify urls.py in mymap/ :

"""mymap URL Configuration."""

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

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

Testing the web map

Now you can test the empty web map running this command:

$ python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

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!

An empty web map page.

An empty web map page.

Adding geographic features

Installing SpatiaLite

We need to install the SQLite spatial extension SpatiaLite:

  • on Debian-based GNU/Linux distributions (es: Debian, Ubuntu, ...):
$ apt install libsqlite3-mod-spatialite
Enter fullscreen mode Exit fullscreen mode
  • on macOS using Homebrew:
$ brew install spatialite-tools
Enter fullscreen mode Exit fullscreen mode

Changing the database engine

Modify the DATABASES default engine in mymap/settings.py

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

Activating GeoDjango

Modify the INSTALLED_APPS setting

Append GeoDjango to the INSTALLED_APPS in 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",
]
Enter fullscreen mode Exit fullscreen mode

Adding some markers

Now we can add some markers in the map.

Adding the Marker model

We're going to add a Marker model in markes/models.py:

"""Markers models."""

from django.contrib.gis.db.models import PointField
from django.db import models


class Marker(models.Model):
    """A marker with name and location."""

    name = models.CharField(max_length=255)
    location = PointField()
Enter fullscreen mode Exit fullscreen mode

Now we have to create migrations for the new model:

$ python manage.py makemigrations
Enter fullscreen mode Exit fullscreen mode

And then we'll apply this migration to the SQLite database:

$ python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Activating the Marker admin

To insert new marker we have to add a Marker admin in markes/admin.py

"""Markers admin."""

from django.contrib.gis import admin

from .models import Marker


@admin.register(Marker)
class MarkerAdmin(admin.OSMGeoAdmin):
    """Marker admin."""

    list_display = ("name", "location")
Enter fullscreen mode Exit fullscreen mode

Testing the admin

We have to create an admin user to login and test it:

$ python manage.py createsuperuser
Enter fullscreen mode Exit fullscreen mode

Now you can test the admin running this command:

$ python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

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 a 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.

Showing all markers in the web 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 markes/views.py:

"""Markers view."""

import json

from django.core.serializers import serialize
from django.views.generic.base import TemplateView

from .models import Marker


class MarkersMapView(TemplateView):
    """Markers map view."""

    template_name = "map.html"

    def get_context_data(self, **kwargs):
        """Return the view context data."""
        context = super().get_context_data(**kwargs)
        context["markers"] = json.loads(serialize("geojson", Marker.objects.all()))
        return context
Enter fullscreen mode Exit fullscreen mode

The value of the markes key in the context dictionary we'll something like that:

{
    "type": "FeatureCollection",
    "crs": {
        "type": "name",
        "properties": {
            "name": "EPSG:4326"
        }
    },
    "features": [
        {
            "type": "Feature",
            "properties": {
                "name": "Monte Amaro 2793m \ud83c\uddee\ud83c\uddf9",
                "pk": "1"
            },
            "geometry": {
                "type": "Point",
                "coordinates": [
                    14.08591836494682,
                    42.08632592463349
                ]
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Inserting the GeoJSON in the template

Using json_script built-in filter we can safely outputs the Python dict 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@1.7.1/dist/leaflet.css">
  <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
</head>
<body>
  {{ markers|json_script:"markers-data" }}
  <div id="map"></div>
  <script src="{% static 'map.js' %}"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

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 attribution = '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
const map = L.map('map')
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: attribution }).addTo(map);
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] });
Enter fullscreen mode Exit fullscreen mode

Testing the populated 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.

Now you can test the populated web map running this command:

$ python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

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 of the highest or lowest points I've visited in the world with the opened popup of the latest peak I climbed.

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.

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

License

This article and related presentation is released with Creative Commons Attribution ShareAlike license (CC BY-SA)

Original

Originally posted on my blog:

https://www.paulox.net/2020/12/08/maps-with-django-part-1-geodjango-spatialite-and-leaflet/

💖 💪 🙅 🚩
pauloxnet
Paolo Melchiorre

Posted on December 10, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related