Zum Inhalt

Verarbeitung

Ein Wettbewerb benötigt eine Auswertung, eine Evaluation. Diese besteht aus drei Javascript Funktionen, welche den Lebenszyklus des Wettbewerbs beschreiben. Um deren Funktionsweise zu verstehen ist es sinnvoll, zuerst die grundlegende Funktionsweise zu verstehen.

Grundlagen

Während eines Wettbewerbs liefern die Geräte jedes Teilnehmers einen endlosen Strom von Geräte-Events, welche zum Sport-Server gesendet werden. Ein Teilnehmer ist also aus Sicht des Systems nicht anderes als ein Datensatz mit Namen, Avatar, … und vor allem: Einen endlosen Strom von Wegpunkten.

Alleine die Zeitmessung legt fest, welche dieser Wegpunkte relevant sind und welche ignoriert werden können. Es gibt mehrere Zeitstempel, welche wichtig sind:

  • Der ganze Wettbewerb hat einen Beginn- und Ende-Zeitstempel. Nur Ereignisse die in diesem Zeitraum liegen sind überhaupt relevant.
  • Jeder Teilnehmer selbst wird mehrere Zeitmessungen unterzogen. Nur Ereignisse, die während einer aktiven Messung des Teilnehmers erfolgen, sind auch relevant.

Zu jedem Wettbewerb wird pro Teilnehmer ein Ranking-Datensatz gespeichert. Dieser Datensatz wird bei jeden eingehenden Event, welcher während einer Zeitmessung erfolgt neu berechnet. Welche Daten in diesem Datensatz gespeichert werden ist vollkommen frei vom Entwickler des Reducers zu entscheiden. Am Anfang des Wettbewerbs (bzw. bei der ersten Zeitmessung) wird ein initialer Datensatz angelegt und bei jeden eingehenden Geräte-Event wird dieser Datensatz geändert.

In regelmässigen Abständen evaluiert der Sports-Server diese Datensätze um ein Ranking zu errechnen und dieses Ranking anzuzeigen.

Ein Wettbewerb sieht also so aus:

  • Initialisierung eines Datensatzes pro Teilnehmer
  • Pro eingehenden Geräte-Event wird dieser Datensatz mutiert indem eine benutzerdefinierte Funktion aufgerufen wird, welcher der aktuelle Datensatz und der aktuelle Event als Parameter übergeben werden.
  • In regelmässigen Abständen/Intervallen wird ein neues Ranking berechnet und publiziert

Code

Im folgenden soll ein Beispielcode entwickelt werden, welcher als Grundlage dienen kann um eigene Reducer zu entwickeln. Das zu entwickelnde System soll ein Ranking darstellen, welches den Teilnehmer mit der höchsten Geschwindigkeit zum Gewinner kürt.

Initializierung

Die Initialisierungsfunktion muss den Namen initValue besitzen und eine Datenstruktur zurückgeben, welche als Initialwert für einen Teilnehmer dient.

1
2
3
4
5
function initValue() {
    return {
        maxspeed: 0
    }
}

Der Code liefert ein Objekt zurück, welches das Feld maxspeed bereitstellt und mit dem Wert 0 initialisiert, d.h. für jeden Teilnehmer wird ein Datensatz der Form

1
2
3
{
  maxspeed: 0
}
angelegt.

Reducer

Liefert das Gerät des Teilnehmers nun ein Signal, so wird die Funktion reduce aufgerufen. Als Paramter bekommt diese Funktion den aktuellen Datensatz data und das Geräte-Signal event als Parameter.

Während die Struktur des data Parameter dem Entwickler bekannt sein sollte (der Initialwert wird ja mit der initValue erzeugt), hat ein Event folgende Struktur:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "imei": "123123123123",
  "sent": "2022-02-20T14:05:15.68896Z",
  "received": "2022-02-20T14:05:15.68896Z",
  "code": "POSITION",
  "deviceCode": 49,
  "deviceEvent": "",
  "position": {
    "invalid": false,
    "lat": 58.03461323789451,
    "lng": 21.576161484035282,
    "alt": 0,
    "height": 584,
    "gpsfix": 3,
    "numsatellites": 5,
    "course": 0,
    "speedkmh": 20,
    "verticalspeedms": 0,
    "motionless": 0,
    "hdop": 0,
    "vdop": 0
  },
  "battery": {
    "loadpercent": 87,
    "low": false,
    "loadvoltage": 0
  },
  "type": "alive",
  "source": "socket"
}

Um bei der Rangliste den Teilnehmer mit der höchsten Geschwindigkeit als Gewinner zu küren, muss die reduce Funktion sich die höchste Geschwindigkeit merken:

1
2
3
4
5
6
7
8
9
function reduce(data, event) {
    // some devices can send events without a legal position, so use ?.
    const speed = event?.position?.speedkmh || 0;
    if (speed > data.maxspeed) {
        data.maxspeed = speed;
        jslog("new maxspeed found", "event", event, "data", data);
    }
    return data;
}

Es wird hier nur geprüft, ob die im Geräte-Datensatz enthaltene Geschwindigkeit grösser ist, als die im Teilnehmer-Datensatz gespeicherte. Wenn dem so ist, wird diese Geschwindigkeit als neue maximale Geschwindigkeit gespeichert. Die Funktion jslog kann genutzt werden um Logausgaben zu erzeugen, welche in der Sports-App angezeigt bzw. verfolgt werden können.

Jeder Datensatz, welcher vom Gerät gesendet wird, wird mit Hilfe der reduce Funktion auf einen Teilnehmer-Datensatz reduziert, d.h. der eingehende Strom von Events wird auf einen einzigen Datensatz komprimiert. In diesem einfachen Beispiel ist es einfach die maximale Geschwindigkeit, aber natürlich sind hier endlos viele Möglichkeiten denkbar.

Ranking

Letzendlich wird jedoch ein Rangliste benötigt, welche von der Sports-App auch sortiert werden muss. Damit diese Sortierung und Darstellung unabhängig von der benutzerdefinierten Datenstruktur erfolgt muss noch eine dritte Funktion zur Verfügung gestell werden, die rankValue.

1
2
3
4
5
6
7
function rankValue (data) {
    return [
        parseInt(data.maxspeed * 100, 10),
        0,
        0
    ]
}

Diese Funktion liefert einen Triple mit drei ganzen Zahlen zurück. Die Sortierung der Teilnehmer erfolgt dann in der Art, dass derjenige mit dem höchsten Score am Index 0 der Gewinner ist. Gibt es mehrere Teilnehmer mit dem gleichen Wert, wird nach dem Index 1 sortiert und ist auch dieser gleich, wird der Index 2 verwendet. Sind alle drei Werte gleich, so gelten die Teilnehmer als punktgleich.

Im obigen Beispiel wird die gespeicherte Geschwindigkeit mit 100 multipliziert, da es sich um eine Kommazahl handelt, welche die Geschwindigkeit in km/h angibt. Dieser Wert wird dann (mit der JS-Standardfunktion parseInt) zu einem Integer umgewandelt. Die beiden anderen Werten werden als 0 zurückgeliefert und können in diesem Fall auch weggelassen werden, d.h. es würde auch reichen einfach nur einen Array mit einem einzigen Element zurückzugeben.

Debugger

Zur Entwicklung der reducer und ranking Funktionalitäten bietet das System einen integrierten Debug-Mechanismus bereit. Dazu können die Funktionen mit fest hinterlegten Events durchgespielt werden. Jedes Event kann als Einzelschritt oder automatisch abgefeuert werden.

Generator

Grundlage für den Debugger ist ein Generator. Hier handelt es sich um eine Entität welche einen Strom von Events erzeugen kann. Es gibt hierzu drei verschiedene Generator-Typen:

  • Stringbasiert
    In einem Edit-Bereich wird ein JSON-Array eingegeben, welcher dem Event-Format der Plattform entspricht. Es kann zwar die Syntax geprüft werden; ob jedoch auch die gewünschten Events erzeugt werden, zeigt sich erst zur Laufzeit.

  • Datenbankbasiert
    Es können auch Events direkt aus der Datenbank selektiert werden. Hierzu werden zwei Zeitstempel (Begin, Ende) sowie die Quelle (Comeptition, Team, Participant) ausgewählt. Das System holt sich die entsprechenden Events und speichert diese redundant in zeitlicher Reihenfolge sortiert. Achtung: Bei grossen Zeiträumen kann das dazu führen, dass es länger dauert bis alle Daten gepuffert sind. Ausserdem wird viel Platz in der Datenbank belegt, d.h. grosse DB-Generatoren sollten nur für Replay’s genutzt und dann wieder gelöscht werden.

  • GPX basiert
    Hier können verschiedene GPX-Daten hochgeladen werden aus dem Tracking-Daten extrahiert werden. Zu jeder Daten wird noch eine IMEI sowie ein Zeitstempel und ein Interval benötigt. Das System erzeugt dann aus den Positionen, der IMEI und den Zeitangaben eine Liste von Events. Wenn gewünscht, kann diese Liste dann in einen String-basierten Generator umgewandelt werden, so dass dort dann einzelne Attribute der Events noch manuell angepasst werden können.

Ist ein passender Generator angelegt, so kann dieser zum Debuggen einer Evaluation genutzt werden.

Debuggerdaten

Der Debugger erwartet drei Felder, die befüllt werden können/müssen:

Metadaten

  • Start Zeitstempel
    In einem reducer kann auf sogenannte measurements zugegriffen werden. Das sind Zeitmessungen, die beim Starten der Messung angelegt werden. Benötigt der reducer ein Measurement, so kann durch die Angabe des Zeitstempels eine Zeitmessung angelegt werden, die dann im Code abgefragt werden kann. Benötigt der Code dieses nicht, kann das Feld leer gelassen werden.
  • Competition
    In der Liste werden alle aktuell verfügbaren Competitions angezeigt. Der Debugger benötigt diese um ein passenden Ranking für alle Teilnehmer anzulegen, welches dann vom reducer mit Daten befüllt wird.
  • Generator
    Der Generator liefert einen Strom von Events.

Initialisierung

Das starten der Debug-Session erfolgt mit einem Klick auf Reset

Initialisierung

Das Sytem lädt die Events aus dem Generator und zeigt einen Ausschnitt daraus an. Der gelb hinterlegte ist immer der Datensatz, welcher als nächster zur Ausführung kommt.

Mit den Knöpfen 1x, 2x… kann ein, zwei, … Events gegen den Reducer ausgeführt werden.

Direkt unter der Liste der Events ist ein Ausgabebereich für die Logs des reducers und darunter sind die Laufzeitdaten der Competition. Durch die Selektion eines Teilnehmers in der Dropdown Liste kann die Anzeige der Daten auf diesen Teilnehmer beschränkt werden. Mit dem PLUS Knopf können mehrere solcher Anzeigebereiche angelegt werden bei dem jeder ggf. einen spezifischen Teilnehmer anzeigt.

Ist eine Debug-Session durchgelaufen (alle Events), so kann diese durch Reset erneut gestartet werden.

Schrittweises Debuggen

Nach einem Debug-Schritt, zeigt die Liste der Events den neuen (zukünftigen) Record gelb, die Ausgabe zeigt eventuell erfolgte Logs und der Datenbereich zeigt den Datenbestand der selektierten Teilnehmer:

Debug Schritt

Der Datenbereich besteht immer aus einer Struktur mit folgenden Werten:

  • value1value3
    Diese Werte sind die Ergebnisse der Rank-Funktion

  • userdata
    Im Benutzerdatenbereich befinden sich die Daten, welche der Reducer nach jedem Schritt zurückliefert.

Autoplay

Mit Hilfe des automatischen debuggens können die Events aus dem Generator in zeitlicher Abfolge durschritten werden. Achtung: Die Events beinhalten alle einen Sent Zeitstempel, d.h. die Geschwindigkeit mit der die Events abgearbeitet werden sollte die Logik des Reducers nicht beeinflussen.

Wenn der Debugger im Automatikmodus läuft kann jederzeit auf auf den Pause-Knopf gedrückt werden und dann im Einzelschrittmodus weigergemacht werden.

Bereitgestelte Objekte

Wie im Beispiel zu sehen ist, wird der reducer Funktion immer der aktuelle Datensatz und der aktuelle Event übergeben. Neben diesen Daten stehen in der Funktion aber noch andere Daten zur Verfügung.

Logging

So gibt es eine Funktion jslog mit welcher Logausgaben durchgeführt werden können. Die Signatur der Funktion lautet:

1
function jslog(message, key1, value1, key2, value2, ...)

Der erste Parameter ist also eine Logmessage, danach erfolgen immer Key/Value Paare, wobei der Key ein String sein muss und der Value frei definierbar ist. Im obigen Beispiel war folgender Aufruf enthalten:

1
  jslog("new maxspeed found", "event", event, "data", data);

Hier ist new maxspeed found die Logmessage; dann folgen die Key/Value Paare ("event", event) und ("data",data).

Mapdaten

Grundlegende Daten zur angezeigten Map sind in der Variablen mapdata enthalten.

  • mapdata.name
    Der Name des Wettbewerbs
  • mapdata.baselayer
    Der Name des eingestellten Layers
  • mapdata.opacity
    Der Wert der eingestellten Deckkraft
  • mapdata.zoomlevel
    Der Wert des eingestellten Zoomlevels
  • mapdata.location
    Die Position des Wettbewerbsmarkers

Ausserdem enthält mapdata noch jeweils die gespeicherten Kurse und Regionen mit den Namen, welche im System angegeben wurden. Ein Kurs ist eine Sammlung von Linien und der Kurs besitzt eine Funktion intersects, bspw.

  • mapdata.Finish.intersects(pos1, pos2)
    Wenn es einen Kurs mit dem Namen Finish gibt, kann mit dieser Funktion erfragt werden, ob sich diese Kurs mit einer Verbindungslinie aus den beiden übergebenen Positionen schneidet (Es können auch direkt Events übergeben werden; das System extrahiert dort selbständig die Position).

Bei einer Region gibt es eine Funktion contains welcher nur eine einzige Position (bzw. ein Event) übergeben wird. Ist diese Position in der Region enthalten, so liefert diese Funktion den Wert true.

Messung

Ein Teilnehmer kann mehrere Zeitmessungen haben. Um diese zu erfragen, dient das measurement Objekt. Dieses stellt eine Funktion getAt zur Verfügung, welche die Zeitmessung für den angegebenen Geräte-Event liefert.

1
2
const m = measurement.getAt(event);
let firstTS = m.start;

Dieser Code fragt das System nach dem Beginn der Zeitmessung für das aktuelle Geräte-Event. Das Attribut start ist ein Javascript-Daten Objekt mit dem Zeitstempel. Dieses kann genutzt werden, wenn ein Rennen bspw. mit einem fest definierten Massenstart und einem klaren Signal gestartet wird.

Race

Über das race Objekt ist es möglich, die Zeitmessung zu stoppen; dazu dient die Funktion stop(evt) die als Parameter einen Event benötigt. Wird diese Funktion aufgerufen, wird die Zeitmessung für das im Event enthaltene Geräte beendet und der sent Zeitstempel als Ende der Zeitmessung genutzt. Bsp.:

1
2
3
4
    // stop measurement if 10 laps are driven ...
    if (data.lapcount > 10) {
        race.stop(event)
    }