Real-time markers plotten op Google maps

Inleiding
Voor ons ballonproject maken we gebruik van een gps-tracker die met behulp van een SIM-kaart zijn GPS-coördinaten via SMS naar specifieke telefoonnummers stuurt. Dit werkt op zich prima: je geeft op de tracker aan dat deze bijvoorbeeld elke minuut de coördinaten naar je iPhone stuurt, in die SMS staan de lengte- en breedtegraden en een linkje naar Google maps. Als je daar op klikt, krijg je de positie van de tracker te zien.

De sms'jes die je van de tracker krijgt.
De sms’jes die je van de tracker krijgt.

Hoewel dit op zich prima is, is het natuurlijk veel cooler om het pad dat de tracker aflegt real time op een kaart te plotten. Omdat dit met de SMS’jes zelf niet kan (en we het sowieso op een groot scherm willen zien) leek het me beter om de berichten te versturen naar een SIM-kaart die verbonden is met een Arduino, die op zijn beurt de berichten doorstuurt naar Processing die de coördinaten uit het bericht filtert en deze in een database zet. Deze architectuur is onderwerp van een andere blog (die later komt); nu ging het me er om hoe ik markers op een kaart kon plotten op het moment dat deze in de database terechtkomen.

Google Maps Markers

Een collega van me had me onlangs geattendeerd op Open Street Map; dit scheen mooier en beter te zijn dan Google maps. Maar na een (toegegeven korte) blik op de site en de API die hierbij geleverd wordt vond ik dit niet heel transparant, dus besloot ik het geheel toch maar eenvoudig in Google maps te maken.

Ik heb voor een ander projectje wel eens iets vergelijkbaars gedaan, maar die code bleek oud en had ook een live update: de data wordt hier gewoon uit de database gehaald op het moment dat de pagina geladen wordt en daar gebeurt verder niet zo veel mee. Voor een live verbinding tussen maps en de database heb ik even gezocht naar een bridge tussen Angular en maps, maar wat ik vond bleek niet heel vruchtbaar.

Omdat ik feitelijk een stateful verbinding tussen de client en de server wilde hebben, viel de keuze uiteindelijk op websockets en node.js. Zoals gebruikelijk bij dit soort projecten maakte ik gebruik van een blog van iemand anders die een minimal working example leverde. Dit voorbeeld was vrij redelijk, hoewel de code niet heel transparant is (niet in de laatste plaats omdat het onderscheid tussen users en clients hier niet duidelijk genoeg ingezet wordt). Na het bestuderen van een blog over de communicatie tussen node en mysql kreeg ik de database-verbinding uiteindelijk aan de praat.

Uiteindelijk moest ik de voorbeeldcode van Gianluca Guarini helemaal uitkleden tot een bare minimum om te ontdekken wat er bij mij mis ging. Het opzetten van de node server is eenvoudig genoeg, maar de client had wat meer voeten in de aarde. Het fascinerende van de opzet is dat je de browser een request laat doen naar een specifieke poort van de server waar node op staat te draaien (in dit geval http://localhost:8000/); node stuurt vervolgens de hele client naar de browser, die vervolgens een socket opent met de server. De server stuurt vervolgens op gezette tijden een message naar de clients, met een string (‘notification’ in het voorbeeld hieronder) om de verschillende berichten van elkaar te kunnen scheiden:

var updateClients = function(data) {
  data.time = new Date();
  connectionsArray.forEach(function(tmpSocket){
    tmpSocket.emit('notification' , data);
  });
};

Deze wordt dan in de client opgevangen:

var socket = io.connect('http://localhost:8000');
var html = $('#container').html();
var tmp = '';
socket.on('notification', function (data) {
  $.each(data.latlng, function(index, latlng) {
  tmp += "<br/>Latitude: " +latlng.lat+ ", 
      longitude:" +latlng.lng;

});

$("#container").html(tmp);
});

Met deze code wordt de div ‘container’ uiteraard gevuld met steeds dezelfde data. Om dat te voorkomen kunnen we beter aan de server-kant alleen de meest recente coördinaten versturen, anders gaat er steeds veel te veel over de lijn en moet de browser veel te veel werk doen. Om deze reden heb ik de tabel in mysql voorzien van een timestamp: we kunnen dan eenvoudig alleen de coördinaten versturen waarvan de timestamp voorbij de laatste broadcast ligt.

Testsite’je
Om de ontwikkeling wat te vereenvoudigen maakte ik even een testscriptje die om data in de database te zetten. Ik heb hele stapel coördinaten uit mijn grafvelden-onderzoekje die ik daar wel voor kon gebruiken. Dit betreft feitelijk statische test-data, dus die zette ik direct in javascript (geen database-connectie nodig). Een eenvoudig server-side scriptje dat kan worden aangeroepen vanuit een webpagina, zodat ik in eerste instantie controle houd over de input in de database, maar dat niet de hele tijd met de hand hoef in te voeren. Dus twee knoppen: één om een nieuwe locatie in de database in te voeren en één om de tabel weer te legen.

Site'je om testdata in de database te stoppen.
Site’je om testdata in de database te stoppen.

Ik moest even nadenken over de juiste volgorde: een nieuwe client meldt zich aan en moet dan alle punten tot dusver krijgen. Daarna krijgen alle clients alleen de nieuwe punten. Dus aan de serverkant moeten we bijhouden wanneer de laatste update is geweest en een separate methode maken voor wanneer clients zich aanmelden. Die methode is er al (io.sockets.on( ‘connection’,)), ik moet alleen dan alle coördinaten naar die nieuwe client pushen.

Timestamp
Een timestamp in mysql is in ISO8601; gelukkig is er een javascript-functie die een datum om kan zetten in zo’n string. Maar die bleek wel heel secuur:

bart$ node -e 'var d = new Date();console.log(d.toISOString());'
2016-05-10T16:12:04.626Z
bart$

Het grootste probleem van deze functie bleek dat deze timestamps omzet naar UTC en voorziet van een tijdzone. Dit doet mysql niet (die neemt gewoon de server-tijd over), dus als ik die timestamp zou willen gebruiken zou ik hier een hoop gedoe mee krijgen. Dus maar pragmatisch zijn en gewoon een auto_increment id aan de tabel toevoegen. Uiteindelijk wordt het maar één proces dat hierin data gaat invoeren (namelijk die tracker) dus er zullen geen race conditions ontstaan. Het bijhouden van de laatste id aan de server-kant wordt dan redelijk triviaal (regel 2 in de code hieronder):

.on('result', function(data) {
  if (data.id > lastID) lastID = data.id;
  rv.push(data);
})

.on('end', function() {
  if (connectionsArray.length) {
    console.log('time: ' +pollingTimer);
    pollingTimer = setTimeout(getLatLng, POLLING_INTERVAL, 1);
    if (rv.length) updateClients({latlng:rv});
  }
});
Nu komen de coördinaten binnen (onderkant scherm)
Nu komen de coördinaten binnen (onderkant scherm)

Het laatste punt dat nog geadresseerd moest worden is dat een nieuwe client alle coördinaten krijgt en alle clients alleen de meest recente. Op zich was dit niet heel lastig: ik kon gewoon een parameter aan die methode meegeven aan de hand waarvan hij alle of alleen de nieuwste uit de database haalde:

function getLatLng (onlynew) {
  var where = onlynew ? " where id >;" +lastID : " ";
  var query=database.query("select * from latlng" 
            +where+ " order by id desc");
//
}

Alleen bleek er toen ineens van alles niet meer te werken. Toen ik hiermee aan de slag ging, bedacht ik me dat de hele architectuur van de blog van Guarini helemaal verkeerd was: alle clients krijgen een update wanneer een nieuwe client zich aanmeldt en hij maakt gebruik van een setTimeOut die wordt aangemaakt wanneer de sql-query afgerond is. Dit kan beter veranderd worden in een setInterval die wordt aangemaakt wanneer de server is opgestart:

app.listen(8000);
setInterval(getLatLng, POLLING_INTERVAL, 1);

Volatile
Opvallend was dat de client nu alleen nog maar de nieuwe coördinaten binnenkreeg, terwijl tests op alle mogelijke posities uitwezen dat alles wel degelijk werd verstuurd. Nadat ik een hele zondag hieraan had verspild was het enige dat ik kon bedenken dat het versturen zelf (dus vanuit node) niet goed werd uitgevoerd. Guarini maakt in zijn blog gebruik van socket.volatile.emit() om de data naar de client te pushen. Op stackoverflow zei iemand hierover : ‘Essentially, if you don’t care if the client receives the data, then send it as volatile’. Voor mij is het echter wel van belang dat de clients de data ontvangen. Toen ik dat volatile had weggehaald werkte alles eindelijk naar behoren.

De hele cirkel compleet.
De hele cirkel compleet.

Nu is het zaak om de coördinaten niet via het test-script, maar via Processing in de database te krijgen.

Leave a Reply

Your email address will not be published. Required fields are marked *