Maps with Django (3 part series)
- Maps with Django⁽¹⁾: GeoDjango, SpatiaLite & Leaflet
- Maps with Django⁽²⁾: GeoDjango, PostGIS & Leaflet
- Maps with Django⁽³⁾: GeoDjango, Pillow & GPS
Abstract
Keeping in mind the Pythonic principle that “simple is better than complex” we’ll see how to create a web map with images, using the Python-based Django web framework, leveraging its GeoDjango module, and extracting GPS information directly from images with Pillow, the Python imaging library.
Note
You can find a more detailed step-by-step description of the first part of this guide in my previous article “Maps with Django⁽¹⁾: GeoDjango, SpatiaLite & Leaflet”, which I recommend you read if you are new to maps and Django.
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
- Pillow, a Python imaging library
Requirements
The requirements to create our map with Django are:
Python
A stable and supported version of Python 3:
Virtual environment
A Python virtual environment:
$ python3.13 -m venv ~/.mymap
$ source ~/.mymap/bin/activate
Django
The latest stable version of black ^ and django:$ python -m pip install black django
Collecting black
Using cached black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl.metadata (81 kB)
Collecting django
Using cached Django-5.2-py3-none-any.whl.metadata (4.1 kB)
Collecting click>=8.0.0 (from black)
Using cached click-8.1.8-py3-none-any.whl.metadata (2.3 kB)
Collecting mypy-extensions>=0.4.3 (from black)
Using cached mypy_extensions-1.0.0-py3-none-any.whl.metadata (1.1 kB)
Collecting packaging>=22.0 (from black)
Using cached packaging-24.2-py3-none-any.whl.metadata (3.2 kB)
Collecting pathspec>=0.9.0 (from black)
Using cached pathspec-0.12.1-py3-none-any.whl.metadata (21 kB)
Collecting platformdirs>=2 (from black)
Using cached platformdirs-4.3.7-py3-none-any.whl.metadata (11 kB)
Collecting asgiref>=3.8.1 (from django)
Using cached asgiref-3.8.1-py3-none-any.whl.metadata (9.3 kB)
Collecting sqlparse>=0.3.1 (from django)
Using cached sqlparse-0.5.3-py3-none-any.whl.metadata (3.9 kB)
Using cached black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl (1.8 MB)
Using cached Django-5.2-py3-none-any.whl (8.3 MB)
Using cached asgiref-3.8.1-py3-none-any.whl (23 kB)
Using cached click-8.1.8-py3-none-any.whl (98 kB)
Using cached mypy_extensions-1.0.0-py3-none-any.whl (4.7 kB)
Using cached packaging-24.2-py3-none-any.whl (65 kB)
Using cached pathspec-0.12.1-py3-none-any.whl (31 kB)
Using cached platformdirs-4.3.7-py3-none-any.whl (18 kB)
Using cached sqlparse-0.5.3-py3-none-any.whl (44 kB)
Installing collected packages: sqlparse, platformdirs, pathspec, packaging, mypy-extensions, click, asgiref, django, black
Successfully installed asgiref-3.8.1 black-25.1.0 click-8.1.8 django-5.2 mypy-extensions-1.0.0 packaging-24.2 pathspec-0.12.1 platformdirs-4.3.7 sqlparse-0.5.3
^ The Python files created by startproject, startapp and makemigrations are formatted using the black command if it is present on your PATH.
GDAL
In order to use GeoDjango, we need to install the GDAL (Geospatial Data Abstraction Library) ^ library.
On Debian-based GNU/Linux distributions (e.g., Debian 12, Ubuntu 25.04, …) we can install the gdal-bin system package, which contains utility programs, useful for development purpose:
$ sudo apt install gdal-bin
A minimal GDAL installation to have only the libgdal.so file will be (e.g., Ubuntu 25.04, …):
$ sudo apt install libgdal36
Or, if you are using a Debian-based Docker image (e.g., python:3.13-slim):
RUN apt-get update \
&& apt-get install --yes --no-install-recommends \
libgdal32 \
&& rm -rf /var/lib/apt/lists/*
^ For other platform-specific instructions, read the Django documentation.
SpatiaLite
To use `SpatiaLite“ as a database backend, we need to install its loadable module ^:
On Debian-based GNU/Linux distributions (e.g. Debian 12, Ubuntu 25.04, …) a minimal installation to have the mod_spatialite.so file will be:
$ sudo apt install libsqlite3-mod-spatialite
^ For other platform-specific instructions, read the Django documentation.
Creating the mymap project
To create the mymap project, I’ll switch to my Projects directory:
and then use the startproject Django command:
$ python -m django startproject mymap
The basic files of our project will be created in the mymap directory.
Creating the markers app
After switching to the mymap directory:
We can create our markers app with the Django startapp command:
$ python -m django startapp markers
Again, all the necessary files will be created for us in the markers directory.
Activating apps
We can now activate GeoDjango and our Markers module by adding django.contrib.gis and markers 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",
]
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()
location = models.PointField()
def __str__(self):
return str(self.name)
Our two fields are both mandatory, the location is a PointField, and we’ll use the name to represent the model.
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.
Updating the database
We can now generate a new database migration:$ python -m manage makemigrations
Migrations for 'markers':
markers/migrations/0001_initial.py
+ Create model Marker
Then we can apply all the migrations to our database:$ python -m manage migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, markers, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying markers.0001_initial... OK
Applying sessions.0001_initial... OK
Adding some markers
We have to create an admin user to log in and test it:$ python -m manage createsuperuser
Username (leave blank to use 'paulox'):
Email address:
Password:
Password (again):
Superuser created successfully.
Now you can test the admin running this command:$ python -m manage runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
Django version 5.2, using settings 'mymap.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
For more information on production servers see: https://docs.djangoproject.com/en/5.2/howto/deployment/
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: you have to manually navigate on the map and pin the marker to your desired location.
Adding a web map
We’re going to add a web map to the app.
Adding the map template
We have to add the templates/ directory in markers/:
$ mkdir markers/templates
In the markers templates directory, we can now create a map.html template file for our map.
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 the HTML, we add only the basic CSS rules to show a full-screen map.
In addition, using the “static” template tag, we’ll also link our custom JavaScript file, which we’ll now create.
Using json_script built-in filter, we can safely output the Python dictionary with all markers as GeoJSON in markers/templates/map.html:
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"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="" />
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<style>
body {margin: 0; padding: 0;}
html, body, #map {height: 100%; width: 100vw;}
</style>
</head>
<body>
<div id="map"></div>
{{ markers|json_script:"markers" }}
<script defer src="{% static 'map.js' %}"></script>
</body>
</html>
Creating the static directory
We have to add a static/ directory in markers/:
Adding the map JavaScript
In our map.js file, we add the code to view our map.
mymap/markers/static/map.js
const href = "https://www.openstreetmap.org/copyright";
const link = `© <a href='${href}'>OpenStreetMap</a>`;
const tiles = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const layer = L.tileLayer(tiles, { attribution: link });
const map = L.map("map", { layers: [layer] });
const feature = L.geoJSON(
JSON.parse(document.getElementById("markers").textContent),
)
.bindPopup((layer) => layer.feature.properties.name)
.addTo(map);
map.fitBounds(feature.getBounds());
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.
Showing markers on the map
Adding a view with all markers
We will create a new markers/views.py file, where we can add a custom MapView class, inheriting from the TemplateView, to which to assign the markers as an extra context.
We can add with a serializer all markers as a GeoJSON in the context of a MapView in markers/views.py:
mymap/markers/views.py
import json
from django.core.serializers import serialize
from django.views.generic import TemplateView
from markers.models import Marker
class MapView(TemplateView):
template_name = "map.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["markers"] = json.loads(
serialize(
"geojson",
Marker.objects.all(),
)
)
return ctx
Updating map URLs
In the markers URL file, we must now add the path to our map view.
mymap/markers/urls.py
from django.urls import path
from markers.views import MapView
urlpatterns = [
path("map/", MapView.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("", include("markers.urls")),
]
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:$ python -m manage runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
Django version 5.2, using settings 'mymap.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
For more information on production servers see: https://docs.djangoproject.com/en/5.2/howto/deployment/
Now that the server’s running, visit http://127.0.0.1:8000/map/ with your Web browser. You’ll see the “Markers map” page, with a full-page map and all the markers. It worked!
Images
It would also be nice to be able to associate the points that we load into our system with an image that represents the place to be displayed directly on the map, and perhaps avoid having to manually navigate the map to select the point.
We could extract this information automatically from an image taken at the point we want to represent, since recent devices store GPS information in the image EXIF metadata.
Django has always supported image fields, but to use them, a dependency must be installed.
Pillow
The Python Imaging Library adds image processing capabilities to your Python interpreter.

Requirements file
The Python requirements of our project are increasing, and therefore a good practice is to create a requirements file, with the package list.
We’ll use the Python Imaging Library in addition to the already installed Django and black packages.
mymap/requirements.txt
black==25.1
django==5.2
pillow==11.1
Installing requirements
We install all the Python requirements, using the Python package installer module.$ python -m pip install -r requirements.txt
Requirement already satisfied: black==25.1 in /home/paulox/.mymap/lib/python3.13/site-packages (from -r requirements.txt (line 1)) (25.1.0)
Requirement already satisfied: django==5.2 in /home/paulox/.mymap/lib/python3.13/site-packages (from -r requirements.txt (line 2)) (5.2)
Collecting pillow==11.1 (from -r requirements.txt (line 3))
Using cached pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl.metadata (9.1 kB)
Requirement already satisfied: click>=8.0.0 in /home/paulox/.mymap/lib/python3.13/site-packages (from black==25.1->-r requirements.txt (line 1)) (8.1.8)
Requirement already satisfied: mypy-extensions>=0.4.3 in /home/paulox/.mymap/lib/python3.13/site-packages (from black==25.1->-r requirements.txt (line 1)) (1.0.0)
Requirement already satisfied: packaging>=22.0 in /home/paulox/.mymap/lib/python3.13/site-packages (from black==25.1->-r requirements.txt (line 1)) (24.2)
Requirement already satisfied: pathspec>=0.9.0 in /home/paulox/.mymap/lib/python3.13/site-packages (from black==25.1->-r requirements.txt (line 1)) (0.12.1)
Requirement already satisfied: platformdirs>=2 in /home/paulox/.mymap/lib/python3.13/site-packages (from black==25.1->-r requirements.txt (line 1)) (4.3.7)
Requirement already satisfied: asgiref>=3.8.1 in /home/paulox/.mymap/lib/python3.13/site-packages (from django==5.2->-r requirements.txt (line 2)) (3.8.1)
Requirement already satisfied: sqlparse>=0.3.1 in /home/paulox/.mymap/lib/python3.13/site-packages (from django==5.2->-r requirements.txt (line 2)) (0.5.3)
Using cached pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl (4.5 MB)
Installing collected packages: pillow
Successfully installed pillow-11.1.0
Since we want to allow the user to upload images to our system we need to update our settings to define where these images will be stored and what path they will be served from. To do that we need to configure the MEDIA_ROOT and MEDIA_URL variables in the mymap settings file, respectively.
mymap/mymap/settings.py
STATIC_URL = "static/"
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media/"
Serve images during development
During development, you can serve user-uploaded images from MEDIA_ROOT, you can do this by adding the following snippet to your ROOT_URLCONF:
mymap/mymap/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("markers.urls")),
] + static(
settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT,
)
NOTE: this is not suitable for production use! For some common deployment strategies, see How to deploy static files.
Add an image field to the Marker model
We can now update our Marker model to add an “image” field.
mymap/markers/models.py
from django.contrib.gis.db import models
from django.core import validators
class Marker(models.Model):
name = models.CharField()
location = models.PointField()
image = models.ImageField(
help_text="Allowed only JPEG images.",
null=True,
upload_to="images/markers/",
validators=[
validators.FileExtensionValidator(
allowed_extensions=["jpg", "jpeg"]
),
],
)
def __str__(self):
return str(self.name)
We have added a validator to the field to ensure that only JPEG images that are most likely to contain GPS information are uploaded.
Updating the Marker admin
To see the image field, we use update 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", "image")
Updating the database
We can now generate a new database migration:$ python -m manage makemigrations
Migrations for 'markers':
markers/migrations/0002_marker_image.py
+ Add field image to marker
We can see the SQL code that this migration will execute in SQLite:$ python -m manage sqlmigrate markers 0002
BEGIN;
--
-- Add field image to marker
--
ALTER TABLE "markers_marker"
ADD COLUMN "image" varchar(100) NULL;
COMMIT;
Then we can apply all the migrations to our database:$ python -m manage migrate markers 0002
Operations to perform:
Target specific migration: 0002_marker_image, from markers
Running migrations:
Applying markers.0002_marker_image... OK
Rendering markers images in the map
We can render the GeoJSON with markers images in the web map using Leaflet:
mymap/markers/static/map.js
const href = "https://www.openstreetmap.org/copyright";
const link = `© <a href='${href}'>OpenStreetMap</a>`;
const tiles = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const layer = L.tileLayer(tiles, { attribution: link });
const map = L.map("map", { layers: [layer] });
// map.fitWorld();
const feature = L.geoJSON(
JSON.parse(document.getElementById("markers").textContent),
)
.bindPopup(function (layer) {
return '<figure>' +
'<img src="/media/' + layer.feature.properties.image + '">' +
'<figcaption>' + layer.feature.properties.name + '</figcaption>' +
'</figure>';
})
.addTo(map);
map.fitBounds(feature.getBounds());
Update the template to render the markers image
Update CSS styles in markers/templates/map.html for rendering images in pop-ups :
{% 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"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="" />
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<style>
body {margin: 0; padding: 0;}
html, body, #map {height: 100%; width: 100vw;}
figure {margin: 0; padding: 0; text-align: center;}
img {max-width: 100%; height: auto;}
</style>
</head>
<body>
<div id="map"></div>
{{ markers|json_script:"markers" }}
<script defer src="{% static 'map.js' %}"></script>
</body>
</html>
Testing the images in the map
You can test the images in the web map by running the command:$ python -m manage runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
Django version 5.2, using settings 'mymap.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
For more information on production servers see: https://docs.djangoproject.com/en/5.2/howto/deployment/
Now that the server’s running, visit http://127.0.0.1:8000/map/ with your Web browser. You’ll see the “Markers map” page, with a full-page map and all the markers.
To avoid searching and selecting the location manually, we can extract the location information directly from the image we are going to upload.
Retrieving GPS information from Image’s Exif
With Pillow, we can selectively extract the GPS information of the image, reading its Exif data.
The result is a dictionary with numeric keys and different values that represent the latitude or longitude in DMS format, plus other values such as direction, dates, times, or bytes.
Here is an example of extracting GPS information from the Exif data of the image that I used as the header of this article:
>>> from PIL import Image
... from PIL.ExifTags import IFD
... img = "IMG_20200919_110740.jpg"
... Image.open(img).getexif().get_ifd(IFD.GPSInfo)
{0: b'\x02\x02\x00\x00',
1: 'N',
2: (42.0, 5.0, 10.76),
3: 'E',
4: (14.0, 5.0, 9.27),
5: b'\x00',
6: 2843.5,
7: (9.0, 7.0, 38.0),
11: 6.529,
27: b'ASCII\x00\x00\x00fused',
29: '2020:09:19'}
NOTE: as you may have guessed the float corresponding to the
6key represents the altitude where the photo was taken. However, in my experience it always turns out to be much less accurate than the real altitude, so I decided not to use it in this article.
DMS to DD Conversion
Geographic information is stored in images in degrees, minutes, and seconds (DMS) format, but as we saw earlier we need to provide GeoDjango with points in decimal degree (DD) format.
We can define a utility function to perform this conversion.
mymap/markers/utils.py
def dms_to_dd(degrees, minutes, seconds, ref):
REFS = {"N": 1, "S": -1, "E": 1, "W": -1}
try:
return (
float(degrees)
+ (float(minutes) / 60)
+ (float(seconds) / 3600)
) * REFS.get(ref, 0)
except ZeroDivisionError:
return 0
We will now limit ourselves to extracting geographic information only from uploaded images, which contain metadata in Exif format, returning a point.
mymap/markers/utils.py
from PIL import Image
from PIL.ExifTags import GPS, IFD
from django.contrib.gis.geos import Point
# ...
def get_point(image):
gpsinfo = (
Image.open(image)
.getexif()
.get_ifd(IFD.GPSInfo)
)
longitude = dms_to_dd(
*gpsinfo.get(GPS.GPSLongitude, (0, 0, 0)),
gpsinfo.get(GPS.GPSLongitudeRef, "E"),
)
latitude = dms_to_dd(
*gpsinfo.get(GPS.GPSLatitude, (0, 0, 0)),
gpsinfo.get(GPS.GPSLatitudeRef, "N"),
)
return Point(longitude, latitude)
Add validator to check Image GPS info
We use the image location extraction function to create a validator that verifies user uploaded images.
mymap/markers/validators.py
from django.core import exceptions
from django.contrib.gis.geos import Point
from markers.utils import get_point
def validate_gpsinfo(image):
if get_point(image) == Point(0, 0):
raise exceptions.ValidationError(
"Missing GPS info in Image EXIF."
)
Save location from image
We can now add the new validator to the Marker image field and customize the save method to save the point extracted from the image in the location field.
We make the location field blankable to avoid manually populating it, since we are going to extract the GPS information directly from the image.
mymap/markers/models.py
from django.contrib.gis.db import models
from django.core import validators
from markers.utils import get_point
from markers.validators import validate_gpsinfo
class Marker(models.Model):
name = models.CharField()
location = models.PointField(blank=True)
image = models.ImageField(
help_text="Allowed only JPEG images.",
null=True,
upload_to="images/markers/",
validators=[
validators.FileExtensionValidator(
allowed_extensions=["jpg", "jpeg"]
),
validate_gpsinfo,
],
)
def __str__(self):
return str(self.name)
def save(self, *args, **kwargs):
self.location = get_point(self.image)
super().save(*args, **kwargs)
Updating the Marker admin
We then edit the marker admin so that the location field is read-only and not manually editable. Even if we left it editable, it would always be overwritten with the location extracted from the image, creating only confusion.
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", "image")
readonly_fields = ("location",)
Updating the database
We can now generate a new database migration:$ python -m manage makemigrations
Migrations for 'markers':
markers/migrations/0003_alter_marker_image_alter_marker_location.py
~ Alter field image on marker
~ Alter field location on marker
Then we can apply all the migrations to our database:$ python -m manage migrate markers 0003
Operations to perform:
Target specific migration: 0003_alter_marker_image_alter_marker_location, from markers
Running migrations:
Applying markers.0003_alter_marker_image_alter_marker_location... OK
Testing the images in the map
The process of manually uploading markers into the admin is much faster because the GPS information is extracted directly from the imagery and there is no need to manually search and select it in the map widget. I have so easily added some markers to my local map by simply uploading images of other elevation points I have reached near my home.
You can test the images in the web map by running the command:$ python -m manage runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
Django version 5.2, using settings 'mymap.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
WARNING: This is a development server. Do not use it in a production setting. Use a production WSGI or ASGI server instead.
For more information on production servers see: https://docs.djangoproject.com/en/5.2/howto/deployment/
Now that the server’s running, visit http://127.0.0.1:8000/map/ with your Web browser. You’ll see the “Markers map” page, with a full-page map and all the markers.
Fun fact
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
As we have seen, once a basic web map has been initialized with GeoDjango, there are many evolutions and functions that we can add.
The positioning of markers by reading GPS information from images is just a demonstration.
There are many other geographic information that can be extracted and used, we will use them in the next articles.
Stay tuned!
— Paolo