Corona-Ampel - Session 2 (25.11.)

Corona-Ampel - Session 2: Wir integrieren die Covid-19 API für echte Daten. Dazu lernen wir den Umgang mit dem Angular HTTPClient und Ionic’s Geolocation Plugin.

Zeitrahmen

Zur Orientierung hier noch einmal der Zeithorizont:

Datum Grobes Ziel Link
18.11. Einführung und Tools kennenlernen -
25.11. API-Integration, GPS-Plugin und einfache Anzeige im UI Teil 2
2.12. Ampel-Anzeige, evtl. Umsetzen des Entwurfes vom Design-Team Teil 3
9.12. Finale Arbeiten Teil 4

Ziele

Die heutigen Ziele sind schnell zusammengefasst. Wir haben eine API bereitgestellt, die für eine gegebene Geolocation (GPS-Daten) Corona-Werte innerhalb Deutschlands zurückgibt. Diese soll in die App integriert werden.

Anschließend binden wir das Geolocation Plugin in die Ionic App ein, um die Position vom Telefon abzufragen und somit dem Nutzer die Daten zu seinem Standort anzeigen können.

Vorbereitung

Öffnet den CoronaAmpel Ordner auf eurem Desktop. Öffnet den Quellcode in Visual Studio Code, startet eine Kommandozeile (Rechtsklick > git bash here) und führt das Kommando npm start aus. Alternativ schaut euch die Beschreibung vom ersten Mal an.

Falls nicht mehr vorhanden, so ist hier der Code vom ersten Mal: Corona Ampel.zip. Nach dem Download entpacken und in der Kommandozeile npm install ausführen.

Anschließend zur Kontrolle den Browser öffnen und die App angucken. Es sollte das Ergebnis vom letzten Mal zu sehen sein.

Hier grob die Übersicht für heute:

  1. Die Covid-19 API
  2. API-Integration in der App
  3. Einfache Darstellung der Daten
  4. Geolocation Plugin

Die Covid-19 API

Zunächst wollen wir uns die API angucken, die euch zur Verfügung steht.

Eine API (Application Programming Interface) ist ein Programmteil, der von einem Softwaresystem anderen Programmen zur Anbindung an das System zur Verfügung gestellt wird. (Wikipedia)

In unserem Fall haben wir eine API zu den Covid-19-Daten gebaut. Die API ist über das Internet frei für alle verfügbar und kann beispielsweise über den Browser aufgerufen werden. Das wollen wir einmal ausprobieren. Ruft dazu folgende URL in eurem Browser auf:

https://covid-api-rki.vercel.app/api/v1/county-data-by-position?lat=54.083333&lng=12.133333

Das Ergebnis sollte so aussehen:

{"data":{"timestamp":"23.11.2020, 00:00 Uhr","county":{"name":"SK Rostock","cases":596,"deaths":6,"casesPer100KLast7Days":36.3304348657447,"inhabitants":209191},"state":{"name":"Mecklenburg-Vorpommern","inhabitants":1608138,"casesPer100KLast7Days":46.5134211118698}}}

Und etwas hübscher formatiert:

{
    "data": {
        "timestamp": "23.11.2020, 00:00 Uhr",
        "county": {
            "name": "SK Rostock",
            "cases": 596,
            "deaths": 6,
            "casesPer100KLast7Days": 36.3304348657447,
            "inhabitants": 209191
        },
        "state": {
            "name": "Mecklenburg-Vorpommern",
            "inhabitants": 1608138,
            "casesPer100KLast7Days": 46.5134211118698
        }
    }
}

Was sehen wir hier nun?

Nun gehen, wir das ganz Stück für Stück durch. Das Ergbenis ist ein JSON-Datensatz - ein durchaus übliches Format zum Datenaustausch im Internet. Der Datensatz enthält verschiedene Einträge und ist verschachtelt - wichtig ist vor allem alles innerhalb von data. Darin finden wir ein JavaScript- Object (zu erkennen an den geschweiften Klammern {}). Dieses hat verschiedene Attribute, wie timestamp, county und auch state. Innerhalb von county bzw. state finden wir interessante Daten zu Covid-19, z.B. das wir uns am Standort Rostock befinden und dieser 596 Fälle hat. Die Inzidenzzahl je 100.000 Einwohner der letzten 7 Tage (casesPer100KLast7Days) beträgt ca 36. Etwas ähnliches finden wir für das Bundesland (state) vor.

Vorher weiß die API aber wo wir uns befinden? Dazu schauen wir uns die URL erneut an - insbesondere den Teil hinter dem Fragezeichen:

?lat=54.083333&lng=12.133333

An dieser Stelle geben wir unserer API die geografische Breite und Länge - Englisch Latitude (Breite), abgekürzt mit lat bzw. Longitude (Länge) und hier abgekürzt mit lng. Rostock liegt ca auf dem Breitengrad 54.083333°N. und 12.133333°O.

Tauscht die beiden Werte (lat und lng) doch mal gegen andere Orte in Deutschland aus. Wie wäre es mit Wismar (lat = 53.9037952, lng = 11.4168494)? Die Breiten- und Längengrade findet ihr unter anderem bei Wikipedia - einfach nach dem gewünschten Ort suchen und oben rechts auf die Position klicken. Anschließend seht ihr ein Fenster mit der Karte und den beiden Koordinaten.

PS: Den Quellcode zur API findet ihr auf Gitlab. Das solltet ihr euch aber erst angucken nachdem die weiteren Aufgaben und Schritte vollzogen worden sind. 😉

API-Integration in der App

Nachdem wir nun wissen was die API kann, wollen wir sie in unsere App einbinden. Als Vorbereitung legen wir im Ordner Home eine neue Datei namens models.ts an und fügen folgenden Inhalt hinein:

export interface ICovid19Response {
  timestamp: string;
  county: {
    name: string;
    cases: number;
    deaths: number;
    casesPer100KLast7Days: number;
    inhabitants: number;
  };
  state: {
    name: string;
    casesPer100KLast7Days: number;
    inhabitants: number;
  };
}

Nun öffnet ihr die Datei home.page.ts. An dieser Stelle setzen wir ein paar Punkte aus der 1. Session voraus. Schaut da im Zweifelsfall rein oder fragt!

Zunächst importieren wir das so eben erstellte Interface ICovid19Response. Dazu fügt ihr unter die bestehenden import Statements die Zeile

import { ICovid19Response } from './models';

Sollte etwas nicht stimmen, z.B. der Dateipfad, wird Visual Studio Code meckern. In der Klasse HomePage benötigen wir eine neue public Variable, nennen wir sie covidData und initialisieren wir sie mit dem Wert null. Zusätzlich wollen wir ihren Typ explizit setzen. Das machen wir durch einen Doppelpunkt: variablen_name: typ - für unsere Variable als covidData: ICovid19Response.

Als nächsten Schritt benötigen wir den HttpClient von unserem genutzten Framework (Angular). Diesen können wir aus dem Paket @angular/common/http importieren. Fügt dazu also folgendes Statement zu den anderen imports:

import { HttpClient } from '@angular/common/http';

Über die sogenannte Dependency Injection (darauf gehen wir nicht weiter ein), können wir unserer HomePage-Klasse diesen HttpClient nun zur Verfügung stellen. Dazu fügen wir dem constructor ein neues Argument hinzu:

  constructor(
    private http: HttpClient,
  ) { }

Nun können wir mittels this.http überall innerhalb unserer Klasse darauf zugreifen.

Warum brauchen wir eigentlich diesen HttpClient? Nun damit wollen wir innerhalb unseres Codes die API aufrufen. Da dies schlussendlich über das Protokoll HTTP läuft, das zum Austausch von Daten im Internet dient, benötigen wir eine Stück Software, die dieses Protokoll spricht. Der HttpClient ist selber eine Klasse mit verschiedenen Methoden, beispielsweise get. Diese führt einen HTTP-Aufruf aus und entspricht im Grunde dem was eurer Browser macht, wenn ihr in der Adresszeile eine URL eingebt.

Das wollen wir auch gleich mal ausprobieren. Legt eine neue Methode an, nennen wir sie loadCovidData. Innerhalb des Methodenkörpers (denkt an die {}) fügen wir eine Variable url hinzu und weisen ihr den Wert unserer Covid-API zu. Innerhalb von Methoden deklarieren wir Variablen mittels der Stichworte let oder const. Ersteres nutzen wir, falls die Variable veränderbar sein soll und letzteres, falls sich ihr Wert nicht mehr ändern darf. Nutzen wir zunächst const:

const url = 'https://covid-api-rki.vercel.app/api/v1/county-data-by-position?lat=54.083333&lng=12.133333';

Diesen Wert wollen wir nun in der get Methode des HttpClient einsetzen:

this.http.get<{ data: ICovid19Response }>(url)
  .toPromise()
  .then(body => {
    // Hier weisen wir unserer Klassen-Variable covidData den Wert body.data zu
  })
  .catch(err => {
    console.error('Fehler beim Abrufen der Daten', err);
  })

Hui, in dieser Zeile passiert nun recht viel. Nehmen wir das Keyword für Keyword auseinander:

  • <{ data: ICovid19Response }> setzt den expliziten Typ der Variable body im then-Block
  • toPromise wandelt das Ergebis der get Methode in ein Promise um. Ein Promise ist entweder
    • erfolgreich, sodass der Block .then ausgeführt wird oder
    • fehlerhaft, sodass der catch Block ausgeführt wird

Ein Promise kann ähnlich wie ein try..catch Block aus anderen Sprachen (Java, C, Python) gesehen werden. Wir führen eine Methode aus (hier get). Ist diese erfolgreich, dann führe then aus, ansonsten fange die Fehler im catch Block. Innerhalb unseres catch-Blockes loggen wir einfach erstmal die Fehlermeldung und gebe diese auf der Konsole aus.

Den then-Block müsst ihr nun ausfüllen. Innerhalb setzen wir den Wert der Klassenvariable covidData. Die Variable body (siehe oben) hat den folgenden Typ { data: ICovid19Response }. Schreiben wir das Interface ICovid19Response aus, dann sehen wir recht schnell, dass body dem entspricht, das unsere Covid-19-API zurückliefert.

Im nächten Schritt wollen wir die Variable covidData in unserer HTML Datei nutzen und die Ergebniswerte anzeigen. Schaut dazu weiter im Schritt Einfache Darstellung der Daten.

Der Code für die API-Integration

Solltet ihr an einer Stelle steckenbleiben, könnt ihr hier immer mal nachgucken (auf den Pfeil klicken).
// Datei home.page.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { ICovid19Response } from './models';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  public districtMapUrl = '';
  public covidData: ICovid19Response = null;

  constructor(
    private http: HttpClient,
  ) { }

  public loadMap() {
    this.districtMapUrl = 'https://rki-covid-api.now.sh/api/districts-map';
  }

  public loadCovidData() {
    const url = `https://covid-api-rki-6746boneu.vercel.app/api/v1/county-data-by-position?lat=54.083333&lng=12.133333`

    this.http.get<{ data: ICovid19Response }>(url)
      .toPromise()
      .then(body => {
        this.covidData = body.data;
      })
      .catch(err => {
        console.error(err);
      });
  }
}

Einfache Darstellung der Daten

Wir haben nun schon einiges vorbereitet. In der Theorie, können wir Daten laden. In der Oberfläche wollen wir dem Nutzer nun eine Möglichkeit geben die Daten abzurufen. Das gestaleten wir relativ ähnlich wie im ersten Teil am 18.11.: der Nutzer klickt auf den Button, unsere soeben geschrieben Methode wird ausgeführt und wir nutzen unsere covidData Variable, um die Daten anzuzeigen.

Fangen wir also an. Wir werden hier primär in der home.page.html arbeiten. Zunächst entfernen wir das Bild (<img>) aus der der Datei - einfach der Übersicht halber. Im nächsten Schritt registrieren wir unsere Methode loadCovidData am click Handler des Buttons. Den Text können wir auch gleich zu “Lade Daten” ändern. Passt besser.

Lasst uns kurz in die home.page.ts Datei wechseln und überprüfen, ob der click Handler funktioniert. Ein rudimentärer Weg dafür ist eine einfache Konsolenausgabe. Innerhalb des then Blocks, könnt ihr mit console.log(body) den Wert ausgeben.

Wechselt zum Browser und eurer Anwendung. Sofern alles richtig gelaufen ist, sollte der Button nun “Lade Daten” anzeigen. Öffnet eure Browser Dev-Tools mit F12 und wechselt auf den Reiter Konsole. Hier erscheinen alle Ausgaben die eure Anwendung auf die Konsole schreibt. Klickt nun auf den Button und nach einer kurzen Ladezeit sollte eine Ausgabe auftauchen {data: {…}} home.page.ts:29. Klickt auf den kleinen Pfeil, um das Objekt aufzuklappen. Es sollte dem entsprechen was wir schon beim Ausprobieren der API-URL im Browser gesehen haben.

Nun gut, wechseln wir zurüc zur home.page.html, um die Daten nun Anzuzeigen. Dazu fügen wir beispielsweise einen Paragraphen <p> hinzu:

<p>
 Fälle der letzten 7 Tage je 100k Einwohner.
</p>

Mit Hilfe des Punktes . greifen wir auf die einzelnen Attribute eines Objektes zu. Da die Daten verschachtelt sind, müssen wir dies hier zwei mal machen.

Aufgabe: Fügt nun weitere Paragraphen für die Uhrzeit, sowie den Namen des Landkreises (county) hinzu.

Das ganze sieht dann in etwa so aus:

Daten in der UI

Habt ihr noch die Konsole offen? Schaut mal rein! Da sollte es derzeit ein paar Fehler (roter Text) geben mit dem Wortlaut ERROR TypeError: Cannot read property 'county' of null. Ist erstmal nicht problematisch, aber wir wollen uns das trotzdem kurz angucken und beheben.

Woher kommt der Fehler?

Nun ja, dort steht, dass der Interpreter (der euren Code ausführt) county nicht lesen bzw. nicht darauf zureifen kann. Das liegt daran dass covidData in unserer HomePage Klasse mit dem Wert null initializiert wird. Es hat also gar kein Attribut mit dem Namen county. Dies ist erst möglich nachdem wir die Daten von der API geladen und den Wert von covidData neu gesetzt haben. Die Fehlermeldung können wir beheben, indem wir um unsere <p> Elemente ein Containerelement (div) packen und dieses nur Anzeigen lassen, wenn covidData Daten enthält. Dazu:

<div *ngIf="covidData">
  <!-- Die <p> Blöcke entsprechen den Paragrahphen, die ihr zuvor hinzugefügt habt.-->
  <p>...</p>
  <p>...</p>
  <p>...</p>
</div>

Abschließend noch einmal zur Oberfläche.

Was hier auffällt: die Zahl der Fälle hat ziemlich viele Kommastellen. Lasst uns das noch kurz begrenzen. Dazu nutzen wir Angular DecimalPipe. Diese erlaubt es uns relativ einfach Zahlen zu begrenzen. Dazu sind zwei Schritte notwendig.

  1. Registrieren der Pipe in der app.module.ts.
import { DecimalPipe } from '@angular/common';

providers: [
  // ... hier stehen die anderen Werte
  DecimalPipe,
]
  1. Nutzen der Pipe in unserer HTML Datei.
<p>
   Fälle der letzten 7 Tage je 100k Einwohner.
</p>

Der Code für die Darstellung

Solltet ihr an einer Stelle steckenbleiben, könnt ihr hier immer mal nachgucken (auf den Pfeil klicken).
<!-- Date home.page.html -->
<ion-header [translucent]="true">
  <ion-toolbar>
    <ion-title>
      Corona-Ampel
    </ion-title>
  </ion-toolbar>
</ion-header>

<ion-content [fullscreen]="true">
  <ion-header collapse="condense">
    <ion-toolbar>
      <ion-title size="large">Blank</ion-title>
    </ion-toolbar>
  </ion-header>

  <div id="container">
    <ion-button (click)="loadCovidData()">Lade Daten</ion-button>
    <div *ngIf="covidData">
      <p>
        {{ covidData.county.casesPer100KLast7Days | number: '1.1-1' }} Fälle der letzten 7 Tage je 100k Einwohner.
      </p>
      <p>
        Ort: {{ covidData.county.name }}
      </p>
      <p>
        <small>Datum: {{ covidData.timestamp }}</small>
      </p>
    </div>
  </div>
</ion-content>

Geolocation Plugin

Wenn ihr hier angekommen seid, dann habt ihr schon ‘ne ganze Menge geschafft! Ich hoffe es ist noch genug Motivation und auch Zeit vorhanden für den letzten Schritt. Bisher zeigt die App ja lediglich die Daten für Rostock an. Das bringt einem nur nicht viel, wenn man sich dort gar nicht aufhält.

Da Smartphones (fast) alle einen GPS-Empfänger eingebaut haben, wollen wir uns diesen zunutze machen. Glücklicherweise bietet die Welt rund um Ionic eine Erweiterung an, die den Zugriff auf die GPS-Daten erleichtert: das Geolocation Plugin.

Für die Einbindung importieren wir das Modul zunächst und registrieren es in unserer Klasse - recht ähnlich zum HttpClient im Kapitel API-Integration in der App:

import { Geolocation } from '@ionic-native/geolocation/ngx';
export class HomePage {
  constructor(
    private http: HttpClient,
    private geolocation: Geolocation,
  ) { }
}

Nun kommt der spannende Teil. Das geolocation Objekt bietet eine Methode names getCurrentPosition, die einfach für die aktuelle Position Daten innerhalb eines Promise zurückliefert. Diese wollen wir in unserer loadCovidData Funktion nutzen beim Aufrufen der API-URL. Zur Erinnerung: ein Promise behandeln wir mit then (Erfolg) und catch (Fehler) Blöcken. In diesem Falle sieht das aus wie folgt:

this.geolocation.getCurrentPosition()
  .then(pos => {
    console.log(pos);
    // Zugriff auf latitude und longitude wie folgt:
    // pos.coords.latitude;
    // pos.coords.longitude;
  });

Anschließend müssen wir das ganze mit dem HttpClient und dem get Aufruf verknüpfen. Hier wird’s bestimmt ein bisschen tricky, daher gebe ich euch ein bisschen was vor:

public loadCovidData() {
  const baseUrl = `https://covid-api-rki-6746boneu.vercel.app/api/v1/county-data-by-position`;

  this.geolocation.getCurrentPosition()
    .then(pos => {
      console.log(pos);
      
      const url = /* das müsst ihr hier nun erfüllen */
      return this.http.get<{ data: ICovid19Response }>(url)
        .toPromise();
    })
    .then(body => {
      console.log(body)
      this.covidData = body.data;
    })
    .catch(err => {
      console.error(err);
    });
}

Ok, was sehen wir da? Zunächst haben wir die Konstante url umbenannt in baseUrl und den Text hinter dem Fragezeichen entfernt. Die Wert für lat und lng sind schließlich variabel und müssen zur Laufzeit gesetzt werden. Anschließend fragen wir die aktuelle Position ab und nutzen die Werte für den get-Aufruf. Das Ergebnis des get-Promise geben wir zurück und nutzen dann wie bisher die dessen Rückgabewert zum Setzen von covidData. Das Verknüpfen von mehreren Promise-then-Blöcken nennt man Chaining - nur so am Rande bemerkt.

Ihr müsst nun nur noch den Wert von url bestimmen und dazu die latitude und longitude Werte aus dem pos-Objekt nutzen. url soll ein String sein, bestehend aus verschiedenen Werten. Strings können wir mit Hilfe von + zusammenfügen, also 'Hallo' + 'Welt' ergibt den String 'Hallo Welt'. Vergesst nicht baseUrl mit zu nutzen. Der Anfang sieht also aus wie const url = baseUrl + '?lat=' + .... Beachtet, dass das Ergebnis den gleichen Aufbau wie di ursprüngliche URL hat. Ihr könnt euch den Wert von url auch jederzeit auf der Konsole ausgeben lassen und es so leicht überprüfen.

Beim ersten Betätigen des Buttons wird euch der Browser, wie auch später die Smartphones fragen, ob ihr den Zugriff auf die Standortinformationen erlaubt. Das müssen wir hier natürlich machen. Die Abfrage im Browser sieht so aus:

Browser Location Request

Nachdem der Zugriff erlaubt ist, wird die App die Daten laden. Da ihr voraussichtlich ebenfalls für den Standort Rostock ermittelt werdet, wird die Anzeige in der App nicht so viel anders aussehen als vorher. Dennoch ist es nun möglich den Standort zu wechseln und die örtlichen Daten abzufragen!

Der Code für die Einbindung des Plugins

Solltet ihr an einer Stelle steckenbleiben, könnt ihr hier immer mal nachgucken (auf den Pfeil klicken).
// home.page.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Geolocation } from '@ionic-native/geolocation/ngx';

import { ICovid19Response } from './models';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage {
  public districtMapUrl = '';
  public covidData: ICovid19Response = null;

  constructor(
    private http: HttpClient,
    private geolocation: Geolocation,
  ) { }

  public loadMap() {
    this.districtMapUrl = 'https://rki-covid-api.now.sh/api/districts-map';
  }

  public loadCovidData() {
    const baseUrl = `https://covid-api-rki-6746boneu.vercel.app/api/v1/county-data-by-position`;

    this.geolocation.getCurrentPosition()
      .then(pos => {
        console.log(pos);
        const lat = pos.coords.latitude;
        const lng = pos.coords.longitude;
        const url = `${baseUrl}?lat=${lat}&lng=${lng}`;
        return this.http.get<{ data: ICovid19Response }>(url)
          .toPromise();
      })
      .then(body => {
        console.log(body)
        this.covidData = body.data;
      })
      .catch(err => {
        console.error(err);
      });
  }

}
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { DecimalPipe } from '@angular/common';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { Geolocation } from '@ionic-native/geolocation/ngx';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule, IonicModule.forRoot(), HttpClientModule, AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    Geolocation, // <--- das hier!
    DecimalPipe,
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Abschluss

Solltet ihr es bis hier hin schaffen, dann habt ihr ganz schön was geleistet und könnt stolz auf euch sein. Heute haben wir eine Menge gelernt. Die Grundlagen, um Daten über das Internet mittels HttpClient abzufragen. Wir lernten was eine API ist und haben mittels Geolocation Plugins dynamisch die Position ermittelt. Lasst das erstmal sacken und beim nächsten Mal kümmern wir uns um eine Ampel-Übersicht innerhalb der App.

Zum nächsten Teil geht’s hier lang: Corona-Ampel - Session 3 (02.12.)

Sonstiges

Für die Daten der Covid-19 API nutzen wir

Der Quellcode für diesen Schritt ist auf dem Rostock School Covid 19 Repository auf Gitlab auf Branch step-2 zu finden.

Written on November 24, 2020
Source code on Gitlab: /covid19-app-school-project-part-2.md