Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Maps with Django - EuroPython 2022

Maps with Django - EuroPython 2022

-- A talk I gave at EuroPython 2022

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 your local database on which to run geospatial queries.

https://www.paulox.net/2022/07/14/europython-2022/

Paolo Melchiorre

July 13, 2022
Tweet

More Decks by Paolo Melchiorre

Other Decks in Technology

Transcript

  1. MAPS WITH DJANGO
    PAOLO MELCHIORRE ~ @pauloxnet

    View Slide

  2. Paolo Melchiorre ~ @pauloxnet
    2

    View Slide

  3. Paolo Melchiorre ~ @pauloxnet
    @pauloxnet
    • CTO @ 20tab
    • Software engineer
    • Python developer
    • Django contributor
    • Conference speaker
    Paolo Melchiorre
    3
    DjangoCon Europe 2019 - Bartek Pawlik (CC BY-NC-SA)

    View Slide

  4. Paolo Melchiorre ~ @pauloxnet
    1.883 m
    0 km 27 km
    0 km
    4
    Paolo Melchiorre (CC BY-SA)
    Adriatic Sea from the Italian Apennines

    View Slide

  5. Paolo Melchiorre ~ @pauloxnet
    • Static or Dynamic
    • Interactive or view-only
    • Raster or Vector tiles
    • Spatial databases
    • Javascript library
    Web maps features
    5

    View Slide

  6. Paolo Melchiorre ~ @pauloxnet
    6
    Web mapping
    “… process of using the maps
    delivered by Geographic Information Systems (GIS)
    on the Internet …”
    — “Web mapping”, Wikipedia

    View Slide

  7. Paolo Melchiorre ~ @pauloxnet
    django
    7
    William Gottlieb (Public domain)
    Django Reinhardt at the Aquarium jazz club in New York, NY 1946

    View Slide

  8. Paolo Melchiorre ~ @pauloxnet
    8
    Requirements
    $ python3 --version
    Python 3.10.4
    $ python3 -m venv ~/.mymap
    $ source ~/.mymap/bin/activate
    $ python3 -m pip install django~=4.0

    View Slide

  9. Paolo Melchiorre ~ @pauloxnet
    9
    Creating the ‘mymap’ project
    $ cd ~/projects
    $ python3 -m django startproject mymap
    mymap
    ├── manage.py
    └── mymap
    ├── asgi.py
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

    View Slide

  10. Paolo Melchiorre ~ @pauloxnet
    10
    Creating the ‘markers’ app
    $ cd mymap
    $ python3 -m django startapp markers
    markers
    ├── admin.py
    ├── apps.py
    ├── __init__.py
    ├── migrations
    │ └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py

    View Slide

  11. Paolo Melchiorre ~ @pauloxnet
    11
    Activating the ‘markers’ app
    # mymap/settings.py
    INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "markers",
    ]

    View Slide

  12. Paolo Melchiorre ~ @pauloxnet
    12
    Adding a template view
    # markers/views.py
    from django.views.generic import TemplateView
    class MarkersMapView(TemplateView):
    template_name = "map.html"

    View Slide

  13. Paolo Melchiorre ~ @pauloxnet
    13
    Adding the ‘map’ template




    Markers Map





    View Slide

  14. Paolo Melchiorre ~ @pauloxnet
    14
    Adding ‘markers’ urls
    # markers/urls.py
    from django.urls import path
    from markers.views import MarkersMapView
    app_name = "markers"
    urlpatterns = [
    path("map/", MarkersMapView.as_view()),
    ]

    View Slide

  15. Paolo Melchiorre ~ @pauloxnet
    15
    Updating ‘mymap’ urls
    # 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")),
    ]

    View Slide

  16. Paolo Melchiorre ~ @pauloxnet
    2.068 m
    0 km 27 km
    2 km
    16
    Paolo Melchiorre (CC BY-SA)
    Sunrise panorama on the Blockhaus

    View Slide

  17. Paolo Melchiorre ~ @pauloxnet
    • JavaScript library for maps
    • Free Software
    • Desktop & Mobile friendly
    • Light (~42 KB of gzipped JS)
    • Well documented
    Leaflet
    17

    View Slide

  18. Paolo Melchiorre ~ @pauloxnet
    18
    Updating the ‘map’ template

    {% load static %}



    Markers Map










    View Slide

  19. Paolo Melchiorre ~ @pauloxnet
    19
    Adding the ‘map’ CSS
    /* markers/static/map.css */
    html,
    body {
    height: 100%;
    margin: 0;
    }
    #map {
    height: 100%;
    width: 100%;
    }

    View Slide

  20. Paolo Melchiorre ~ @pauloxnet
    20
    Adding the ‘map’ JavaScript
    // markers/static/map.js
    const copy = '© OpenStreetMap contributors'
    const url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    const osm = L.tileLayer(url, { attribution: copy })
    const map = L.map('map', { layers: [osm] })
    map.fitWorld();

    View Slide

  21. Paolo Melchiorre ~ @pauloxnet
    21
    Show the empty web map
    $ python3 -m manage runserver
    $ python3 -m webbrowser -t localhost:8000/markers/map

    View Slide

  22. Paolo Melchiorre ~ @pauloxnet
    22

    View Slide

  23. Paolo Melchiorre ~ @pauloxnet
    2.117 m
    0 km 27 km
    6 km
    23
    Paolo Melchiorre (CC BY-SA)
    Trail signs near the Sella Acquaviva fountain

    View Slide

  24. Paolo Melchiorre ~ @pauloxnet
    24
    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)

    View Slide

  25. Paolo Melchiorre ~ @pauloxnet
    • OSGeo library
    • Free Software
    • Read/Write geospatial data
    • Raster/Vector formats
    • Command line interface
    GDAL
    25

    View Slide

  26. Paolo Melchiorre ~ @pauloxnet
    26
    Installing GDAL
    $ sudo apt install gdal-bin

    View Slide

  27. Paolo Melchiorre ~ @pauloxnet
    27
    Activating GeoDjango
    # 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",
    ]

    View Slide

  28. Paolo Melchiorre ~ @pauloxnet
    • SQLite spatial extension
    • Vector geodatabase functions
    • Free Software
    • Simple architecture
    • Single file
    SpatiaLite
    28

    View Slide

  29. Paolo Melchiorre ~ @pauloxnet
    29
    Installing SpatiaLite
    $ sudo apt install libsqlite3-mod-spatialite

    View Slide

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

    View Slide

  31. Paolo Melchiorre ~ @pauloxnet
    31
    Adding the Marker model
    # 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

    View Slide

  32. Paolo Melchiorre ~ @pauloxnet
    32
    Adding the Marker admin
    # 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")

    View Slide

  33. Paolo Melchiorre ~ @pauloxnet
    33
    Adding some markers
    $ python3 -m manage makemigrations
    $ python3 -m manage migrate
    $ python3 -m manage createsuperuser
    $ python3 -m manage runserver
    $ python3 -m webbrowser -t localhost:8000/admin

    View Slide

  34. Paolo Melchiorre ~ @pauloxnet
    34

    View Slide

  35. Paolo Melchiorre ~ @pauloxnet
    35
    Updating the view
    # 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)
    markers = Marker.objects.all()
    context["markers"] = json.loads(serialize("geojson", markers))
    return context

    View Slide

  36. Paolo Melchiorre ~ @pauloxnet
    36
    Inserting markers in the template

    {% load static %}



    Markers Map






    {{ markers|json_script:"markers-data" }}




    View Slide

  37. Paolo Melchiorre ~ @pauloxnet
    37
    Generated GeoJSON
    <br/>{<br/>"type": "FeatureCollection",<br/>"crs": { "type": "name", "properties": { "name": "EPSG:4326" } },<br/>"features": [<br/>{<br/>"type": "Feature",<br/>"properties": { "name": "Monte Amaro (2793m)", "pk": "1" },<br/>"geometry": {<br/>"type": "Point",<br/>"coordinates": [14.08591836494682, 42.08632592463349]<br/>}<br/>}<br/>]<br/>}<br/>

    View Slide

  38. Paolo Melchiorre ~ @pauloxnet
    38
    Rendering all markers in the map
    // markers/static/map.js
    const copy = '© OpenStreetMap contributors'
    const url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    const osm = L.tileLayer(url, { attribution: copy })
    const map = L.map('map', { layers: [osm], minZoom: 5 })
    const markers = JSON.parse(document.getElementById('markers-data').textContent)
    const features = L.geoJSON(markers).bindPopup(layer => layer.feature.properties.name)
    map.addLayer(features).fitBounds(feature.getBounds())

    View Slide

  39. Paolo Melchiorre ~ @pauloxnet
    39
    Show the populated web map
    $ python3 -m manage runserver
    $ python3 -m webbrowser -t localhost:8000/markers/map

    View Slide

  40. Paolo Melchiorre ~ @pauloxnet
    40

    View Slide

  41. Paolo Melchiorre ~ @pauloxnet
    2.450 m
    0 km 27 km
    7 km
    41
    Paolo Melchiorre (CC BY-SA)
    Trail signs near the Fusco bivouac

    View Slide

  42. Paolo Melchiorre ~ @pauloxnet
    • PostgreSQL extension
    • Best GeoDjango backend
    • Spatial data types
    • Spatial indexing
    • Spatial functions
    PostGIS
    42

    View Slide

  43. Paolo Melchiorre ~ @pauloxnet
    43
    Installing PostgreSQL C client library
    $ sudo apt install libpq5

    View Slide

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

    View Slide

  45. Paolo Melchiorre ~ @pauloxnet
    45
    Requirements
    # requirements.txt
    django~=4.0.0
    psycopg2~=2.9.0
    djangorestframework~=3.13.0
    djangorestframework-gis~=1.0.0
    django-filter~=22.1.0

    View Slide

  46. Paolo Melchiorre ~ @pauloxnet
    46
    Installing requirements
    $ python3 -m pip install -r requirements.txt

    View Slide

  47. Paolo Melchiorre ~ @pauloxnet
    47
    Activating Django REST Framework
    # 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",
    "rest_framework",
    "rest_framework_gis",
    "markers",
    ]

    View Slide

  48. Paolo Melchiorre ~ @pauloxnet
    48
    Adding the Marker serializer
    # markers/serializers.py
    from rest_framework_gis import serializers
    from markers.models import Marker
    class MarkerSerializer(serializers.GeoFeatureModelSerializer):
    class Meta:
    fields = ("id", "name")
    geo_field = "location"
    model = Marker

    View Slide

  49. Paolo Melchiorre ~ @pauloxnet
    49
    Adding the Marker viewset
    # markers/api_views.py
    from rest_framework import viewsets
    from rest_framework_gis import filters
    from markers.models import Marker
    from markers.serializers import MarkerSerializer
    class MarkerViewSet(viewsets.ReadOnlyModelViewSet):
    bbox_filter_field = "location"
    filter_backends = (filters.InBBoxFilter,)
    queryset = Marker.objects.all()
    serializer_class = MarkerSerializer

    View Slide

  50. Paolo Melchiorre ~ @pauloxnet
    50
    Adding API ‘markers’ urls
    # markers/api_urls.py
    from rest_framework import viewsets
    from rest_framework import routers
    from markers.api_views import MarkerViewSet
    router = routers.DefaultRouter()
    router.register(r"markers", MarkerViewSet)
    urlpatterns = router.urls

    View Slide

  51. Paolo Melchiorre ~ @pauloxnet
    51
    Updating ‘mymap’ urls
    # 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_urls")),
    path("markers/", include("markers.urls")),
    ]

    View Slide

  52. Paolo Melchiorre ~ @pauloxnet
    52
    Trying to locate the user
    // markers/static/map.js
    const copy = '© OpenStreetMap contributors'
    const url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
    const osm = L.tileLayer(url, { attribution: copy })
    const map = L.map('map', { layers: [osm], minZoom: 5 })
    map.locate()
    .on('locationfound', e => map.setView(e.latlng, 8))
    .on('locationerror', () => map.setView([0, 0], 5))
    // …

    View Slide

  53. Paolo Melchiorre ~ @pauloxnet
    53
    Rendering markers incrementally
    // markers/static/map.js
    async function load_markers() {
    const markers_url = `/api/markers/?in_bbox=${map.getBounds().toBBoxString()}`
    const response = await fetch(markers_url)
    const geojson = await response.json()
    return geojson
    }
    async function render_markers() {
    const markers = await load_markers()
    L.geoJSON(markers).bindPopup(layer => layer.feature.properties.name).addTo(map)
    }
    map.on('moveend', render_markers)

    View Slide

  54. Paolo Melchiorre ~ @pauloxnet
    54

    View Slide

  55. Paolo Melchiorre ~ @pauloxnet
    55

    View Slide

  56. Paolo Melchiorre ~ @pauloxnet
    2.793 m
    0 km 27 km
    14 km
    56
    Paolo Melchiorre (CC BY-SA)
    Summit of Monte Amaro (2.793 meters)

    View Slide

  57. Paolo Melchiorre ~ @pauloxnet
    57
    What’s next
    • Markers customization
    • Relational filtering
    • Clustering frontend/backend
    • Geocoding services
    • …

    View Slide

  58. Paolo Melchiorre ~ @pauloxnet
    58
    Tips
    • docs in djangoproject.com
    • details in postgis.net
    • source code in github.com
    • questions in gis.stackexchange.com

    View Slide

  59. Paolo Melchiorre ~ @pauloxnet
    59
    License
    CC BY-SA 4.0
    This work is licensed under
    a Creative Commons
    Attribution-ShareAlike 4.0
    International License.

    View Slide

  60. Paolo Melchiorre ~ @pauloxnet
    @20tab
    20tab
    20tab
    [email protected]
    20tab.com
    60

    View Slide

  61. Paolo Melchiorre ~ @pauloxnet
    @pauloxnet
    paolomelchiorre
    pauloxnet
    [email protected]
    paulox.net
    61

    View Slide