Slide 1

Slide 1 text

MAPS WITH DJANGO PAOLO MELCHIORRE ~ www.paulox.net © 2020 Paolo Melchiorre (CC BY-SA)

Slide 2

Slide 2 text

2

Slide 3

Slide 3 text

Paolo Melchiorre ~ www.paulox.net 🌐 www.paulox.net 🐍 PSF fellow 🦄 DSF member 󰠁 Django Girls+ coach 🚀 Djangonaut navigator 󰏢 PyCon Italia organizer 🐬 Python Pescara founder Paolo Melchiorre 3 © 2022 Bartek Pawlik (CC BY-NC-SA)

Slide 4

Slide 4 text

4 © 2020 Paolo Melchiorre (CC BY-SA)

Slide 5

Slide 5 text

5 © 2020 Paolo Melchiorre (CC BY-SA) 1.883 m 6.178 ft 0 km 0 mi 27 km 17 mi 0 km 0 mi

Slide 6

Slide 6 text

Paolo Melchiorre ~ www.paulox.net • Static or Dynamic • Interactive or view-only • Raster or Vector tiles • Spatial databases • Javascript library Web maps 6

Slide 7

Slide 7 text

Paolo Melchiorre ~ www.paulox.net 7 Web mapping “… process of using the maps delivered by Geographic Information Systems (G.I.S.) on the Internet …” — “Web mapping”, Wikipedia

Slide 8

Slide 8 text

django 8 © 1946 William Gottlieb (Public Domain)

Slide 9

Slide 9 text

Paolo Melchiorre ~ www.paulox.net 9 Requirements $ python3 --version Python 3.12.3 $ python3 -m venv ~/.mymap $ source ~/.mymap/bin/activate $ python -m pip install django ... Successfully installed asgiref-3.8.1 django-5.1.1 sqlparse-0.5.1

Slide 10

Slide 10 text

Paolo Melchiorre ~ www.paulox.net 10 Creating the ‘mymap’ project $ cd ~/Projects $ python -m django startproject mymap $ tree --noreport mymap/ mymap/ ├── manage.py └── mymap ├── asgi.py ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py

Slide 11

Slide 11 text

Paolo Melchiorre ~ www.paulox.net 11 Creating the ‘markers’ app $ cd mymap $ python -m django startapp markers $ tree --noreport markers/ markers/ ├── admin.py ├── apps.py ├── __init__.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py └── views.py

Slide 12

Slide 12 text

Paolo Melchiorre ~ www.paulox.net 12 Activating the ‘markers’ app # 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", ]

Slide 13

Slide 13 text

Paolo Melchiorre ~ www.paulox.net 13 Adding the ‘map’ template Markers Map

Slide 14

Slide 14 text

Paolo Melchiorre ~ www.paulox.net 14 Adding ‘markers’ urls # mymap/markers/urls.py from django.urls import path from django.views.generic import TemplateView urlpatterns = [ path( "map/", TemplateView.as_view(template_name="map.html") ), ]

Slide 15

Slide 15 text

Paolo Melchiorre ~ www.paulox.net 15 Updating ‘mymap’ urls # mymap/mymap/urls.py from django.contrib import admin from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), path("", include("markers.urls")), ]

Slide 16

Slide 16 text

16 © 2020 Paolo Melchiorre (CC BY-SA)

Slide 17

Slide 17 text

17 © 2020 Paolo Melchiorre (CC BY-SA) 2.068 m 6.784 ft 0 km 0 mi 27 km 17 mi 2 km 1,25 mi

Slide 18

Slide 18 text

Paolo Melchiorre ~ www.paulox.net • JavaScript library for maps • Free Software • Desktop & Mobile friendly • Light (~42 KB of gzipped JS) • Well documented Leaflet 18

Slide 19

Slide 19 text

Paolo Melchiorre ~ www.paulox.net 19 Updating the ‘map’ template - header {% load static %}

Slide 20

Slide 20 text

Paolo Melchiorre ~ www.paulox.net 20 Updating the ‘map’ template - body {% load static %}

Slide 21

Slide 21 text

Paolo Melchiorre ~ www.paulox.net 21 Adding the ‘map’ CSS /* mymap/markers/static/map.css */ html, body { height: 100%; margin: 0; } #map { height: 100%; width: 100%; }

Slide 22

Slide 22 text

Paolo Melchiorre ~ www.paulox.net 22 Adding the ‘map’ JavaScript // mymap/markers/static/map.js const osm = "https://www.osm.org/copyright"; const copy = `© OpenStreetMap`; const url = "https://{s}.tile.osm.org/{z}/{x}/{y}.png"; const layer = L.tileLayer(url, {attribution: copy}); const map = L.map("map", {layers: [layer]}); map.fitWorld();

Slide 23

Slide 23 text

Paolo Melchiorre ~ www.paulox.net 23 Show the empty web map $ python -m manage runserver Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). September 24, 2024 - 11:38:00 Django version 5.1, using settings 'mymap.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.

Slide 24

Slide 24 text

24

Slide 25

Slide 25 text

25 © 2020 Paolo Melchiorre (CC BY-SA)

Slide 26

Slide 26 text

26 © 2020 Paolo Melchiorre (CC BY-SA) 2.117 m 6.945 ft 0 km 0 mi 27 km 17 mi 6 km 3,75 mi

Slide 27

Slide 27 text

Paolo Melchiorre ~ www.paulox.net 27 🌍 GeoDjango django.contrib.gis (v1.0 ~2008) Fields, backends, queries, admin, … 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)

Slide 28

Slide 28 text

Paolo Melchiorre ~ www.paulox.net • OSGeo library • Free Software • Read/Write geospatial data • Raster/Vector formats • Command line interface GDAL 28

Slide 29

Slide 29 text

Paolo Melchiorre ~ www.paulox.net 29 Installing GDAL $ # Ubuntu (20.04 - 24.04) / Debian (11 - 12) $ sudo apt install gdal-bin FROM python:3.12-slim-bookworm RUN apt-get install --assume-yes --no-install-recommends \ libgdal32

Slide 30

Slide 30 text

Paolo Melchiorre ~ www.paulox.net 30 Activating GeoDjango # 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", ]

Slide 31

Slide 31 text

Paolo Melchiorre ~ www.paulox.net • SQLite spatial extension • Vector geodatabase functions • Free Software • Simple architecture • Single file SpatiaLite 31

Slide 32

Slide 32 text

Paolo Melchiorre ~ www.paulox.net 32 Installing SpatiaLite $ # Ubuntu (20.04 - 24.04) / Debian (10 - 12) $ sudo apt install libsqlite3-mod-spatialite FROM python:3.12-slim-bookworm RUN apt-get install --assume-yes --no-install-recommends \ libsqlite3-mod-spatialite

Slide 33

Slide 33 text

Paolo Melchiorre ~ www.paulox.net 33 Activating SpatiaLite # mymap/settings.py ... DATABASES = {"default": { "ENGINE": "django.contrib.gis.db.backends.spatialite", "NAME": BASE_DIR / "db.sqlite3", }}

Slide 34

Slide 34 text

Paolo Melchiorre ~ www.paulox.net 34 Adding the Marker model # 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

Slide 35

Slide 35 text

Paolo Melchiorre ~ www.paulox.net 35 Adding the Marker admin # 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")

Slide 36

Slide 36 text

Paolo Melchiorre ~ www.paulox.net 36 Creating migrations $ python -m manage makemigrations Migrations for 'markers': markers/migrations/0001_initial.py + Create model Marker

Slide 37

Slide 37 text

Paolo Melchiorre ~ www.paulox.net 37 Applying migrations $ python -m manage migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, markers, sessions Running migrations: ... Applying markers.0001_initial... OK ...

Slide 38

Slide 38 text

Paolo Melchiorre ~ www.paulox.net 38 Creating a superuser $ python -m manage createsuperuser Username (leave blank to use 'paulox'): Email address: Password: Password (again): Superuser created successfully.

Slide 39

Slide 39 text

Paolo Melchiorre ~ www.paulox.net 39 Running the project $ python -m manage runserver Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). September 24, 2024 - 11:45:00 Django version 5.1, using settings 'mymap.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.

Slide 40

Slide 40 text

40

Slide 41

Slide 41 text

Paolo Melchiorre ~ www.paulox.net 41 Updating the view - part 1 # markers/views.py import json from django.core.serializers import serialize from django.views.generic import ListView from markers.models import Marker class MapView(ListView): context_object_name = "markers" model = Marker template_name = "map.html"

Slide 42

Slide 42 text

Paolo Melchiorre ~ www.paulox.net 42 Updating the view - part 2 # markers/views.py class MapView(ListView): ... def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["markers"] = json.loads( serialize("geojson", context["markers"]) ) return context

Slide 43

Slide 43 text

Paolo Melchiorre ~ www.paulox.net 43 Adding ‘markers’ urls # mymap/markers/urls.py from django.urls import path from markers.views import MapView urlpatterns = [ path("map/", MapView.as_view()), ]

Slide 44

Slide 44 text

Paolo Melchiorre ~ www.paulox.net 44 Inserting markers in the template {% load static %}
{{ markers|json_script:"markers-data" }}

Slide 45

Slide 45 text

Paolo Melchiorre ~ www.paulox.net 45 Generated GeoJSON { "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.0, 42.0]} }] }

Slide 46

Slide 46 text

Paolo Melchiorre ~ www.paulox.net 46 Rendering all markers in the map // markers/static/map.js // ... // map.fitWorld(); const data = document.getElementById("markers-data"); const markers = JSON.parse(data.textContent) let feature = L.geoJSON(markers) .bindPopup(function (layer) { return layer.feature.properties.name; }) .addTo(map); map.fitBounds(feature.getBounds());

Slide 47

Slide 47 text

Paolo Melchiorre ~ www.paulox.net 47 Show the populated web map $ python -m manage runserver Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). September 24, 2024 - 11:45:00 Django version 5.1, using settings 'mymap.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.

Slide 48

Slide 48 text

48

Slide 49

Slide 49 text

49 © 2020 Paolo Melchiorre (CC BY-SA)

Slide 50

Slide 50 text

50 © 2020 Paolo Melchiorre (CC BY-SA) 2.450 m 8.038 ft 0 km 0 mi 27 km 17 mi 7 km 4,35 mi

Slide 51

Slide 51 text

Paolo Melchiorre ~ www.paulox.net 51 GeoDjango Compatibility Table ● Spatial Lookups (18/31) ● Database functions (15/32) ● Aggregate Functions (0/5) MariaDB 49% ● Spatial Lookups (31/31) ● Database functions (32/32) ● Aggregate Functions (5/5) PostGIS 100% ● Spatial Lookups (22/31) ● Database functions (30/32) ● Aggregate Functions (4/5) Spatialite 82% ● Spatial Lookups (18/31) ● Database functions (16/32) ● Aggregate Functions (0/5) MySQL 50%

Slide 52

Slide 52 text

Paolo Melchiorre ~ www.paulox.net • PostgreSQL extension • Best* GeoDjango backend • Spatial data types • Spatial indexing • Spatial functions PostGIS 52

Slide 53

Slide 53 text

Paolo Melchiorre ~ www.paulox.net 53 Requirements # requirements.txt django~=5.1.0 django-filter~=24.3.0 djangorestframework~=3.15.0 djangorestframework-gis~=1.1.0 psycopg[binary]~=3.2.0

Slide 54

Slide 54 text

Paolo Melchiorre ~ www.paulox.net 54 Installing requirements $ python -m pip install -r requirements.txt ... Successfully installed django-filter-24.3 djangorestframework-3.15.2 djangorestframework-gis-1.1 psycopg-3.2.2 psycopg-binary-3.2.2 typing-extensions-4.12.2

Slide 55

Slide 55 text

Paolo Melchiorre ~ www.paulox.net 55 Activating PostGIS # mymap/mymap/settings.py DATABASES = { "default": { "ENGINE": "django.contrib.gis.db.backends.postgis", "HOST": "database", "NAME": "mymap", "PASSWORD": "password", "PORT": 5432, "USER": "postgres", } }

Slide 56

Slide 56 text

Paolo Melchiorre ~ www.paulox.net Applying migrations to PostgreSQL 56 $ python -m manage migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, markers, sessions Running migrations: ... Applying markers.0001_initial... OK ...

Slide 57

Slide 57 text

Paolo Melchiorre ~ www.paulox.net 57 Activating Django REST Framework # mymap/settings.py INSTALLED_APPS = [ ... "rest_framework", "rest_framework_gis", "markers", ]

Slide 58

Slide 58 text

Paolo Melchiorre ~ www.paulox.net 58 Adding the Marker serializer # mymap/markers/serializers.py from rest_framework_gis import serializers from markers.models import Marker class MarkerSerial(serializers.GeoFeatureModelSerializer): class Meta: fields = ("id", "name") geo_field = "location" model = Marker

Slide 59

Slide 59 text

Paolo Melchiorre ~ www.paulox.net 59 Adding the Marker viewset # mymap/markers/viewsets.py from rest_framework import viewsets from rest_framework_gis import filters from markers.models import Marker from markers.serializers import MarkerSerial class MarkerViewSet(viewsets.ReadOnlyModelViewSet): bbox_filter_field = "location" filter_backends = [filters.InBBoxFilter] queryset = Marker.objects.all() serializer_class = MarkerSerial

Slide 60

Slide 60 text

Paolo Melchiorre ~ www.paulox.net 60 Adding API ‘markers’ urls # mymap/markers/api.py from rest_framework import routers from markers.viewsets import MarkerViewSet router = routers.DefaultRouter() router.register(r"markers", MarkerViewSet) urlpatterns = router.urls

Slide 61

Slide 61 text

Paolo Melchiorre ~ www.paulox.net 61 Updating ‘mymap’ urls # mymap/mymap/urls.py from django.contrib import admin from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), path("api/", include("markers.api")), path("", include("markers.urls")), ]

Slide 62

Slide 62 text

Paolo Melchiorre ~ www.paulox.net 62 Trying to locate the user // mymap/markers/static/map.js // ... // map.fitWorld(); map.locate() .on("locationfound", (e) => map.setView(e.latlng, 8)) .on("locationerror", () => map.setView([0, 0], 5)); const layerGroup = L.layerGroup().addTo(map); // ...

Slide 63

Slide 63 text

Paolo Melchiorre ~ www.paulox.net 63 Fetch required markers // mymap/markers/static/map.js // ... async function load_markers() { const bounds = map.getBounds().toBBoxString(); const markers_url = `/api/markers/?in_bbox=${bounds}`; const response = await fetch(markers_url); const geojson = await response.json(); return geojson; } // ...

Slide 64

Slide 64 text

Paolo Melchiorre ~ www.paulox.net 64 Rendering markers incrementally // mymap/markers/static/map.js // ... async function render_markers() { const markers = await load_markers(); layerGroup.clearLayers(); L.geoJSON(markers) .bindPopup((layer) => layer.feature.properties.name) .addTo(layerGroup); } map.on("moveend", render_markers);

Slide 65

Slide 65 text

Paolo Melchiorre ~ www.paulox.net 65 Show the populated web map $ python -m manage runserver Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). September 24, 2024 - 11:55:00 Django version 5.1, using settings 'mymap.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.

Slide 66

Slide 66 text

66

Slide 67

Slide 67 text

67

Slide 68

Slide 68 text

68 © 2020 Paolo Melchiorre (CC BY-SA)

Slide 69

Slide 69 text

69 © 2020 Paolo Melchiorre (CC BY-SA) 2.793 m 9.163 ft 0 km 0 mi 27 km 17 mi 13,5 km 8,5 mi

Slide 70

Slide 70 text

Paolo Melchiorre ~ www.paulox.net 70 What’s next • Markers customization • Relational filtering • Clustering frontend/backend • Geocoding services • ...

Slide 71

Slide 71 text

Paolo Melchiorre ~ www.paulox.net 71 Tips • docs in djangoproject.com • details in postgis.net • source code in github.com • questions in gis.stackexchange.com

Slide 72

Slide 72 text

Paolo Melchiorre ~ www.paulox.net 72 License CC BY-SA 4.0 This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. CC

Slide 73

Slide 73 text

Paolo Melchiorre ~ www.paulox.net @pauloxnet /paolomelchiorre @pauloxnet www.paulox.net 73 @[email protected]