SlideShare a Scribd company logo
Django GeoTracker
About me
• My name is Jani Tiainen
• I work for company called Keypro
• Django and GeoDjango user since 2008
• Individual member of DSF
• jtiai on #django IRC channel
What we will build?
• A web site that takes advantage of geolocation service.
• A responsive layout with map and few buttons to record GPS location
from mobile device.
• A tool to convert individual tracked points to a linestring.
• A tool that can show linestring on a map.
App skeleton
• Can be cloned with git from https://github.com/jtiai/dceu-geotracker
• Contains base layout.
• Index page with locate-button.
• Used to verify that all essential parts do work.
Geolocation service
• Can query geolocation from browser. Most usable with mobile
devices with GPS.
• Works pretty well.
• Lately browsers have been requiring SSL (HTTPS) when using
geolocation.
• Serveo or ngrok can solve problem in small scale testing and development
First step
• Let’s make sure that everyone has skeleton app up and running and
can locate themselves on a map using locate-button.
• Make sure that migrations are done.
• Make sure that superuser is created to access admin ui.
• There are bugs in instructions. 
• docker-compose run –rm web pipenv run python manage.py migrate
• docker-compose run –rm web pipenv run python manage.py
createsuperuser
• And a bug in the code…. 
• start.sh script had CRLF-line-endings. Not good for *nix. 
Record location
• Let’s add a new button to store location to database.
• Each ”track” has it’s own (unique) name.
Index.html
<div class="form-group">
<label for="tracking_name">Tracking name</label>
<input type="text" maxlength="200" id="tracking_name" class="form-control" name="name" placeholder="Name of the tracking" required>
</div>
<button type="button" class="btn btn-primary" onclick="single_locate()">Locate</button>
<button type="button" class="btn btn-primary" onclick="start_tracking()">Start tracking</button>
Record location JS
let watchId = null;
let currentLocationMarker = null;
let trackingMarkers = new Array(5);
let trackingMarkerIndex = 0; // current counter
function start_tracking() {
if (watchId) {
alert("You're already tracking. Stop it first to restart");
} else {
let tracking_name = document.getElementsByName('name')[0];
if (!tracking_name.value) {
alert("Please set tracking name first.");
return;
}
tracking_name.disabled = true;
watchId = navigator.geolocation.watchPosition(function (position) {
console.log("Tracked new position", position);
if (trackingMarkers[trackingMarkerIndex]) {
// Remove oldest markeer
myMap.removeLayer(trackingMarkers[trackingMarkerIndex]);
}
trackingMarkers[trackingMarkerIndex] = L.marker([position.coords.latitude, position.coords.longitude], {icon: violetIcon});
trackingMarkers[trackingMarkerIndex].addTo(myMap);
trackingMarkerIndex++;
if (trackingMarkerIndex >= trackingMarkers.length) {
trackingMarkerIndex = 0; // Rollover
}
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
// Handle error, in case of successful we don't care
};
xhttp.open("POST", tracking_point_url);
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
let data = new URLSearchParams();
data.append('name', tracking_name.value);
data.append('timestamp', position.timestamp.toString());
data.append('altitude', position.coords.altitude == null ? "" : position.coords.altitude.toString());
data.append('altitude_accuracy', position.coords.altitudeAccuracy == null ? "" : position.coords.altitudeAccuracy.toString());
data.append('accuracy', position.coords.accuracy.toString());
data.append('latitude', position.coords.latitude.toString());
data.append('longitude', position.coords.longitude.toString());
xhttp.send(data);
}, null, {timeout: 5000, enableHighAccuracy: true});
}
}
Record location view code
@method_decorator(csrf_exempt, name="dispatch")
class TrackingPointAPIView(View):
def post(self, request):
form = TrackingPointForm(request.POST)
if form.is_valid():
tp = TrackedPoint()
# Timestamp is in milliseconds
tp.name = form.cleaned_data["name"]
tp.timestamp = datetime.datetime.fromtimestamp(
form.cleaned_data["timestamp"] / 1000
)
tp.location = Point(
form.cleaned_data["longitude"], form.cleaned_data["latitude"]
)
tp.save()
return JsonResponse({"successful": True})
return JsonResponse({"succesful": False, "errors": form.errors})
Don’t forget to add required imports…
Record location url config
And let’s test we everything works as expected…
urlpatterns = [
path('admin/', admin.site.urls),
path('tracking-point/', views.TrackingPointAPIView.as_view(), name='tracking-point-api’),
path('', views.IndexView.as_view(), name='tracking-index'),
]
Testing and verifying
• Run devserver
• docker-compose up
• Route https to local machine (serveo or ngrok)
• Note with serveo – it doesn’t redirect http to https.
• Open index page and store point(s) to database.
Make tracking to stop
• Add stop-tracking button
<button type="button" class="btn btn-primary" onclick="stop_tracking()">Stop tracking</button>
function stop_tracking() {
if (!watchId) {
alert("You're not tracking yet. Start tracking first.");
} else {
navigator.geolocation.clearWatch(watchId);
watchId = null;
let tracking_name = document.getElementsByName('name')[0];
tracking_name.disabled = false;
}
}
List tracked points
• Simple list with name of tracking and number of points in it.
• A button to convert points to a linestring.
{% extends "geotracker/base.html" %}
{% block contents %}
<form action="{% url "route-create" %}" method="post">
{% csrf_token %}
<table class="table">
<thead>
<tr>
<th scope="col">Tracking name</th>
<th scope="col">Number points</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for r in track_names %}
<tr>
<td>{{ r.name }}</td>
<td>{{ r.num_points }}</td>
<td><button class="btn btn-primary btn-sm" name="name" value="{{ r.name }}">Create Track</button></td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endblock %}
List tracked points, the view
class TrackingPointsListView(View):
def get(self, request):
track_names = (
TrackedPoint.objects.values("name")
.distinct()
.annotate(num_points=Count("name"))
.values("name", "num_points")
)
return render(
request,
"geotracker/tracked_points_list.html",
{"track_names": track_names, "tracked_page": " active"},
)
path('tracking-points-list/', views.TrackingPointsListView.as_view(), name='tracking-points-list'),
A view to create a linestring
class RouteCreateView(View):
def post(self, request):
name = request.POST["name"]
qs = TrackedPoint.objects.filter(name=name)
# Create line
points = [tp.location for tp in qs]
linestring = LineString(points)
RouteLine.objects.create(name=name, location=linestring)
return redirect(reverse("routes-list"))
path('route-create/', views.RouteCreateView.as_view(), name='route-create'),
Add page to list lines
• Add a list and show on map button
{% extends "geotracker/base.html" %}
{% load staticfiles %}
{% block extra_head %}
<script src="{% static "js/geotracker.js" %}"></script>
<style>
#map {
height: 350px;
margin-top: 16px;
margin-bottom: 16px;
}
</style>
{% endblock %}
{% block onloadcall %}line_startup();{% endblock %}
{% block contents %}
<div id="map"></div>
<table class="table">
<thead>
<tr>
<th scope="col">Tracking name</th>
<th scope="col">Length (m)</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for r in lines %}
<tr>
<td>{{ r.name }}</td>
<td>{{ r.route_length|floatformat:2 }}</td>
<td><button class="btn btn-primary btn-sm" onclick="show_line_on_map({{ r.get_geojson_feature }})">Show on map</button></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
…template continued…
Add a view to provide list of linestrings
class RoutesListView(View):
def get(self, request):
lines = RouteLine.objects.all()
return render(
request,
"geotracker/tracked_routes.html",
{"lines": lines, "tracked_lines_page": " active"},
)
path('routes/', views.RoutesListView.as_view(), name="routes-list"),
Aaaand… Does it work?
• If it does, good.
• If not, let’s try to figure out what’s wrong.
Working with geometries
• Distance calculations
• Length calculations
• Proximity queries
Smoothing GPS data
• We’re not going to do that… 
• GPS data jumps around due inaccuracies.
• Smoothing algorithms do exist.
• Based on predictions of next point and statistics of previous points.
• Quite common solution is to use is Kalman filter (or some derivation
from that).
Questions, comments?

More Related Content

RTF
Database scripts
PDF
Gmaps Railscamp2008
PDF
Delivering a Responsive UI
KEY
Django quickstart
PDF
Session on "The Screenplay Pattern: Better Interactions for Better Automation...
PDF
Building Persona: federated and privacy-sensitive identity for the Web (Open ...
PDF
Eddystone beacons demo
Database scripts
Gmaps Railscamp2008
Delivering a Responsive UI
Django quickstart
Session on "The Screenplay Pattern: Better Interactions for Better Automation...
Building Persona: federated and privacy-sensitive identity for the Web (Open ...
Eddystone beacons demo

What's hot (15)

PDF
Building Persona: federated and privacy-sensitive identity for the Web (LCA 2...
PDF
Beyond the DOM: Sane Structure for JS Apps
KEY
JQuery In Rails
PPTX
PPTX
course js day 3
PPTX
jQuery Data Manipulate API - A source code dissecting journey
PDF
Курсы по мобильной разработке под iOS. 4 лекция. Возможности телефона
PPTX
How to Bring Common UI Patterns to ADF
PDF
The web beyond "usernames & passwords"
PDF
Android Animation (in tamil)
PDF
Functionality Focused Code Organization
PDF
How te bring common UI patterns to ADF
PDF
Sequence diagrams
PPTX
Eddystone Beacons - Physical Web - Giving a URL to All Objects
PDF
3D Touch: Preparando sua app para o futuro do iOS
Building Persona: federated and privacy-sensitive identity for the Web (LCA 2...
Beyond the DOM: Sane Structure for JS Apps
JQuery In Rails
course js day 3
jQuery Data Manipulate API - A source code dissecting journey
Курсы по мобильной разработке под iOS. 4 лекция. Возможности телефона
How to Bring Common UI Patterns to ADF
The web beyond "usernames & passwords"
Android Animation (in tamil)
Functionality Focused Code Organization
How te bring common UI patterns to ADF
Sequence diagrams
Eddystone Beacons - Physical Web - Giving a URL to All Objects
3D Touch: Preparando sua app para o futuro do iOS
Ad

Similar to Django GeoTracker (20)

KEY
@Anywhere
KEY
How Quick Can We Be? Data Visualization Techniques for Engineers.
PPTX
Angular Workshop_Sarajevo2
PDF
Clean Javascript
PPTX
How data rules the world: Telemetry in Battlefield Heroes
PPTX
Big Data for each one of us
PPT
Html and i_phone_mobile-2
PDF
Gis SAPO Hands On
PDF
Sapo GIS Hands-On
PPTX
Geo django
PPTX
Designing and developing mobile web applications with Mockup, Sencha Touch an...
PDF
Building Real Time Systems on MongoDB Using the Oplog at Stripe
TXT
Vidéo approche en immobilier
PDF
Geolocation and Mapping
PDF
mobl
PDF
JavaScript Refactoring
PDF
Android Best Practices
KEY
#NewMeetup Performance
DOC
Baitap tkw
KEY
Seti 09
@Anywhere
How Quick Can We Be? Data Visualization Techniques for Engineers.
Angular Workshop_Sarajevo2
Clean Javascript
How data rules the world: Telemetry in Battlefield Heroes
Big Data for each one of us
Html and i_phone_mobile-2
Gis SAPO Hands On
Sapo GIS Hands-On
Geo django
Designing and developing mobile web applications with Mockup, Sencha Touch an...
Building Real Time Systems on MongoDB Using the Oplog at Stripe
Vidéo approche en immobilier
Geolocation and Mapping
mobl
JavaScript Refactoring
Android Best Practices
#NewMeetup Performance
Baitap tkw
Seti 09
Ad

Recently uploaded (20)

PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
PDF
Design an Analysis of Algorithms I-SECS-1021-03
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 41
PDF
SAP S4 Hana Brochure 3 (PTS SYSTEMS AND SOLUTIONS)
PDF
Nekopoi APK 2025 free lastest update
PDF
medical staffing services at VALiNTRY
PDF
Which alternative to Crystal Reports is best for small or large businesses.pdf
PPT
Introduction Database Management System for Course Database
PDF
top salesforce developer skills in 2025.pdf
PPTX
Reimagine Home Health with the Power of Agentic AI​
PPTX
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
PPTX
assetexplorer- product-overview - presentation
PDF
EN-Survey-Report-SAP-LeanIX-EA-Insights-2025.pdf
PDF
System and Network Administraation Chapter 3
PDF
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
PDF
Wondershare Filmora 15 Crack With Activation Key [2025
PDF
wealthsignaloriginal-com-DS-text-... (1).pdf
PPTX
Agentic AI Use Case- Contract Lifecycle Management (CLM).pptx
PPTX
Computer Software and OS of computer science of grade 11.pptx
PDF
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
Design an Analysis of Algorithms I-SECS-1021-03
Internet Downloader Manager (IDM) Crack 6.42 Build 41
SAP S4 Hana Brochure 3 (PTS SYSTEMS AND SOLUTIONS)
Nekopoi APK 2025 free lastest update
medical staffing services at VALiNTRY
Which alternative to Crystal Reports is best for small or large businesses.pdf
Introduction Database Management System for Course Database
top salesforce developer skills in 2025.pdf
Reimagine Home Health with the Power of Agentic AI​
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
assetexplorer- product-overview - presentation
EN-Survey-Report-SAP-LeanIX-EA-Insights-2025.pdf
System and Network Administraation Chapter 3
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
Wondershare Filmora 15 Crack With Activation Key [2025
wealthsignaloriginal-com-DS-text-... (1).pdf
Agentic AI Use Case- Contract Lifecycle Management (CLM).pptx
Computer Software and OS of computer science of grade 11.pptx
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool

Django GeoTracker

  • 2. About me • My name is Jani Tiainen • I work for company called Keypro • Django and GeoDjango user since 2008 • Individual member of DSF • jtiai on #django IRC channel
  • 3. What we will build? • A web site that takes advantage of geolocation service. • A responsive layout with map and few buttons to record GPS location from mobile device. • A tool to convert individual tracked points to a linestring. • A tool that can show linestring on a map.
  • 4. App skeleton • Can be cloned with git from https://github.com/jtiai/dceu-geotracker • Contains base layout. • Index page with locate-button. • Used to verify that all essential parts do work.
  • 5. Geolocation service • Can query geolocation from browser. Most usable with mobile devices with GPS. • Works pretty well. • Lately browsers have been requiring SSL (HTTPS) when using geolocation. • Serveo or ngrok can solve problem in small scale testing and development
  • 6. First step • Let’s make sure that everyone has skeleton app up and running and can locate themselves on a map using locate-button. • Make sure that migrations are done. • Make sure that superuser is created to access admin ui. • There are bugs in instructions.  • docker-compose run –rm web pipenv run python manage.py migrate • docker-compose run –rm web pipenv run python manage.py createsuperuser • And a bug in the code….  • start.sh script had CRLF-line-endings. Not good for *nix. 
  • 7. Record location • Let’s add a new button to store location to database. • Each ”track” has it’s own (unique) name. Index.html <div class="form-group"> <label for="tracking_name">Tracking name</label> <input type="text" maxlength="200" id="tracking_name" class="form-control" name="name" placeholder="Name of the tracking" required> </div> <button type="button" class="btn btn-primary" onclick="single_locate()">Locate</button> <button type="button" class="btn btn-primary" onclick="start_tracking()">Start tracking</button>
  • 8. Record location JS let watchId = null; let currentLocationMarker = null; let trackingMarkers = new Array(5); let trackingMarkerIndex = 0; // current counter
  • 9. function start_tracking() { if (watchId) { alert("You're already tracking. Stop it first to restart"); } else { let tracking_name = document.getElementsByName('name')[0]; if (!tracking_name.value) { alert("Please set tracking name first."); return; } tracking_name.disabled = true; watchId = navigator.geolocation.watchPosition(function (position) { console.log("Tracked new position", position); if (trackingMarkers[trackingMarkerIndex]) { // Remove oldest markeer myMap.removeLayer(trackingMarkers[trackingMarkerIndex]); } trackingMarkers[trackingMarkerIndex] = L.marker([position.coords.latitude, position.coords.longitude], {icon: violetIcon}); trackingMarkers[trackingMarkerIndex].addTo(myMap); trackingMarkerIndex++; if (trackingMarkerIndex >= trackingMarkers.length) { trackingMarkerIndex = 0; // Rollover } let xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function () { // Handle error, in case of successful we don't care }; xhttp.open("POST", tracking_point_url); xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); let data = new URLSearchParams(); data.append('name', tracking_name.value); data.append('timestamp', position.timestamp.toString()); data.append('altitude', position.coords.altitude == null ? "" : position.coords.altitude.toString()); data.append('altitude_accuracy', position.coords.altitudeAccuracy == null ? "" : position.coords.altitudeAccuracy.toString()); data.append('accuracy', position.coords.accuracy.toString()); data.append('latitude', position.coords.latitude.toString()); data.append('longitude', position.coords.longitude.toString()); xhttp.send(data); }, null, {timeout: 5000, enableHighAccuracy: true}); } }
  • 10. Record location view code @method_decorator(csrf_exempt, name="dispatch") class TrackingPointAPIView(View): def post(self, request): form = TrackingPointForm(request.POST) if form.is_valid(): tp = TrackedPoint() # Timestamp is in milliseconds tp.name = form.cleaned_data["name"] tp.timestamp = datetime.datetime.fromtimestamp( form.cleaned_data["timestamp"] / 1000 ) tp.location = Point( form.cleaned_data["longitude"], form.cleaned_data["latitude"] ) tp.save() return JsonResponse({"successful": True}) return JsonResponse({"succesful": False, "errors": form.errors}) Don’t forget to add required imports…
  • 11. Record location url config And let’s test we everything works as expected… urlpatterns = [ path('admin/', admin.site.urls), path('tracking-point/', views.TrackingPointAPIView.as_view(), name='tracking-point-api’), path('', views.IndexView.as_view(), name='tracking-index'), ]
  • 12. Testing and verifying • Run devserver • docker-compose up • Route https to local machine (serveo or ngrok) • Note with serveo – it doesn’t redirect http to https. • Open index page and store point(s) to database.
  • 13. Make tracking to stop • Add stop-tracking button <button type="button" class="btn btn-primary" onclick="stop_tracking()">Stop tracking</button> function stop_tracking() { if (!watchId) { alert("You're not tracking yet. Start tracking first."); } else { navigator.geolocation.clearWatch(watchId); watchId = null; let tracking_name = document.getElementsByName('name')[0]; tracking_name.disabled = false; } }
  • 14. List tracked points • Simple list with name of tracking and number of points in it. • A button to convert points to a linestring. {% extends "geotracker/base.html" %} {% block contents %} <form action="{% url "route-create" %}" method="post"> {% csrf_token %} <table class="table"> <thead> <tr> <th scope="col">Tracking name</th> <th scope="col">Number points</th> <th scope="col"></th> </tr> </thead> <tbody> {% for r in track_names %} <tr> <td>{{ r.name }}</td> <td>{{ r.num_points }}</td> <td><button class="btn btn-primary btn-sm" name="name" value="{{ r.name }}">Create Track</button></td> </tr> {% endfor %} </tbody> </table> </form> {% endblock %}
  • 15. List tracked points, the view class TrackingPointsListView(View): def get(self, request): track_names = ( TrackedPoint.objects.values("name") .distinct() .annotate(num_points=Count("name")) .values("name", "num_points") ) return render( request, "geotracker/tracked_points_list.html", {"track_names": track_names, "tracked_page": " active"}, ) path('tracking-points-list/', views.TrackingPointsListView.as_view(), name='tracking-points-list'),
  • 16. A view to create a linestring class RouteCreateView(View): def post(self, request): name = request.POST["name"] qs = TrackedPoint.objects.filter(name=name) # Create line points = [tp.location for tp in qs] linestring = LineString(points) RouteLine.objects.create(name=name, location=linestring) return redirect(reverse("routes-list")) path('route-create/', views.RouteCreateView.as_view(), name='route-create'),
  • 17. Add page to list lines • Add a list and show on map button {% extends "geotracker/base.html" %} {% load staticfiles %} {% block extra_head %} <script src="{% static "js/geotracker.js" %}"></script> <style> #map { height: 350px; margin-top: 16px; margin-bottom: 16px; } </style> {% endblock %}
  • 18. {% block onloadcall %}line_startup();{% endblock %} {% block contents %} <div id="map"></div> <table class="table"> <thead> <tr> <th scope="col">Tracking name</th> <th scope="col">Length (m)</th> <th scope="col"></th> </tr> </thead> <tbody> {% for r in lines %} <tr> <td>{{ r.name }}</td> <td>{{ r.route_length|floatformat:2 }}</td> <td><button class="btn btn-primary btn-sm" onclick="show_line_on_map({{ r.get_geojson_feature }})">Show on map</button></td> </tr> {% endfor %} </tbody> </table> {% endblock %} …template continued…
  • 19. Add a view to provide list of linestrings class RoutesListView(View): def get(self, request): lines = RouteLine.objects.all() return render( request, "geotracker/tracked_routes.html", {"lines": lines, "tracked_lines_page": " active"}, ) path('routes/', views.RoutesListView.as_view(), name="routes-list"),
  • 20. Aaaand… Does it work? • If it does, good. • If not, let’s try to figure out what’s wrong.
  • 21. Working with geometries • Distance calculations • Length calculations • Proximity queries
  • 22. Smoothing GPS data • We’re not going to do that…  • GPS data jumps around due inaccuracies. • Smoothing algorithms do exist. • Based on predictions of next point and statistics of previous points. • Quite common solution is to use is Kalman filter (or some derivation from that).