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 %}
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 %}
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.
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).